Securing Your Smart Contract

Shield up with modifiers

Hello fellow Ethereum enthusiasts! In this article, we'll walk you through how to secure your smart contract which uses the Telegraph bridge for crosschain transactions. Specifically, we're going to add a modifier called onlyBridge to restrict who can call the portMessage function. This will ensure that only the bridge contract can call this function, thereby enhancing the security of our smart contract.

Moreover, we will also incorporate an authorization mechanism for senders using a mapping. This mechanism will further filter transactions and only allow those coming from authorized senders. Ready? Let's dive right in!

What's in a Modifier?

In Solidity, a modifier is a special type of function that can be used to change the behavior of other functions, in a declarative way. We can use it to check certain conditions before executing a function, for instance.

Adding the onlyBridge Modifier

To start, let's define our modifier. We're going to name it onlyBridge, and its purpose will be to ensure that the function it is applied to can only be called by the bridge contract. Here's how you'd define it:

address public bridgeAddress;
    
modifier onlyBridge() {
    require(msg.sender == bridgeAddress, "Caller is not the bridge");
    _;
}

In this snippet, bridgeAddress is the Ethereum address of the bridge contract. We'll need to set this in the constructor or with a separate setter function. The require function inside the modifier ensures that the sender of the message (msg.sender) is equal to the bridgeAddress. If it's not, the function will revert and an error message will be emitted. The underscore (_) at the end of the modifier tells Solidity where the rest of the function's code will go.

We can now use the onlyBridge modifier in our portMessage function like so:

function portMessage(
    address[] memory addresses, 
    uint256[] memory numbers, 
    string[] memory strings, 
    bool[] memory bools, 
    string sender) public onlyBridge {
    // function code here...
}

By adding onlyBridge after the public keyword, we're telling Solidity that this function should be modified by our onlyBridge modifier.

Implementing Sender Authorization

Now let's add an authorization mechanism to ensure that only transactions from authorized senders are processed. We'll create a mapping called authorizedSenders where the keys are the sender addresses (as strings) and the values are booleans indicating whether the sender is authorized.

Here's what the mapping might look like:

mapping(string => bool) public authorizedSenders;

To authorize a sender, we'd just need to set their corresponding value in the authorizedSenders mapping to true. We can create a function authorizeSender to handle this:

function authorizeSender(string memory sender) public {
    // Here you should add some code to restrict who can authorize new senders. 
    // For instance, you could require that msg.sender is a specific address.
    authorizedSenders[sender] = true;
}

Now we can update our portMessage function to only process the transaction if the sender is authorized:

function portMessage(address[] memory addresses, uint256[] memory numbers, string[] memory strings, bool[] memory bools, string sender) public onlyBridge {
    require(authorizedSenders[sender], "Sender is not authorized");
    // function code here...
}

This require statement checks that the sender is authorized before executing the rest of the function. If the sender is not authorized, the function will revert and an error message will be emitted.

By incorporating these security measures, your contract becomes more robust and resistant to unauthorized manipulations. But we don't just have to imagine what this final, more secure contract will look like, because here it is in all its glory:

pragma solidity ^0.8.0;

contract HelloWorld {
    string public message;
    event NewMessage(string message);
    address public bridgeAddress;
    mapping(string => bool) public authorizedSenders;

    constructor(address _bridgeAddress) public {
        message = "Hello, World!";
        bridgeAddress = _bridgeAddress;
    }
    
    modifier onlyBridge() {
        require(msg.sender == bridgeAddress, "Caller is not the bridge");
        _;
    }
    
    function authorizeSender(string memory sender) public {
        // Here you should add some code to restrict who can authorize new senders. 
        // For instance, you could require that msg.sender is a specific address.
        authorizedSenders[sender] = true;
    }

    function portMessage(address[] memory addresses, uint256[] memory numbers, string[] memory strings, bool[] memory bools, string sender) public onlyBridge {
        require(authorizedSenders[sender], "Sender is not authorized");
        require(addresses.length > 0, "Address array empty");
        require(strings.length > 0, "String array empty");
        emit NewMessage(strings[0]);
    }
    
    function sendMessage() public returns (bool) {
        address[] memory addresses = new address[](1);
        uint256[] memory integers = new uint256[](1);
        string[] memory strings = new string[](1);
        bool[] memory bools = new bool[](1);

        strings[0] = "Hello World";
        // send the message to the bridge
        bridge.outboundMessage(
            address(this), 
            0x0000000000000000000000000000000000000000, 
            addresses, 
            integers, 
            strings, 
            bools, 
            "ETH-Rinkeby");
        return true;
    }
}

This contract now has a specific onlyBridge modifier for portMessage, and a mapping system that allows transactions only from authorized senders. This makes it much more secure and resilient.

These safety measures ensure that your contract behaves as expected and that your users' assets are secure. Now, you can write your contract with confidence, knowing that it's both robust and secure. Keep coding, and keep exploring the fascinating world of blockchain development!

Last updated