Before contract function code executes, it’s a good idea to validate who triggered it and what inputs are given.
Here we build on the Solidity documentation and our own practice to demonstrate a few methods for validating caller and data of inter-contract communication, in both contract-to-contract and user-to-contract calls.
Restricting access is a common pattern for contracts. Note that you can never restrict any human or computer from reading the content of your transactions or your contract’s state. [..] You can restrict read access to your contract’s state by other contracts.
Furthermore, you can restrict who can make modifications to your contract’s state or call your contract’s functions. The use of function modifiers makes these restrictions highly readable.
For call authorisation we can start with a simple scenario where the creator of the contract is the only one we’d like to allow to make certain restricted ‘owner-only’ calls, e.g. changing the owner, writing to storage, or killing the contract.
The Ownable.sol contract below defines the basis of implementing such contract ‘ownership’ and owner-only function modifier.
Any contract which inherits Ownable will have owner set to the caller at the time of its creation, and any of its functions implementing onlyOwner modifier will not accept calls from another account. Note that the creator can be a user or another contract.
To put that in context, we use the sample contract code from our previous post on implementing upgradable contracts — specifically the main two contracts: ‘Parent’ and ‘Organisation’. To recap those, Parent contract is used to create, store, retrieve and upgrade Organisation instances. Organisation interacts with an instance of EternalStorage via ProposalsLibrary and also with an ITokenLedger implementation instance.
We’ll demonstrate how to secure Organisation contract’s storage to allow owner-only write permissions.
Securely writing to Storage
EternalStorage contract is used to read and write values of different types to storage. Every Organisation contract has an instance of EternalStorage to which only it should be able to write. In this case the owner of EternalStorageshould be a contract (Organisation) rather than a user. We do not want users to be interacting directly with storage as all the business rules for data are abstracted away from it.
For the implementation we simply inherit Ownable and decorate all storage write functions with the onlyOwner modifier, e.g. setUIntValue, setStringValue etc.
We need to ensure the owner of EternalStorage instance is the Organisationcontract. The easiest way to ensure that, is to let it create it. See constructor below.
Here msg.sender in EternalStorage is the Organisation contract which then becomes the only address allowed to write to storage.
Note on Libraries: We use a library contract (ProposalsLibrary) to attach a set of functions to EternalStorage, so effectively message calls flow through the following chain: Organisation -> ProposalsLibrary -> EternalStorage.
However, since libraries in Solidity pass on the msg.value and msg.sendervalues, calls to EternalStorage have msg.sender as the Organisation contract address and not ProposalsLibrary address.
Securely writing to Storage — extended example
Let’s extend the simplified example above to cover our sample scenario of using a Parent contract to create and manage Organisations. We would also like to remove the Organisation contract dependency on EternalStorage just to make our contract lighter. For this we have delegated the role of creating EternalStorage and Organisation to Parent contract, which are both created via the Parent.createOrganisation(bytes32) function. When Parent calls new EternalStorage() it becomes the owner of it as well, so it needs to call Ownable.changeOwner(address) to adjust ownership to the newly created Organisation.
This is an example of where EternalStorage ownership change is required due to the more complex initiation process of our contracts.
Support for multiple admin accounts
Since msg.sender only allows for a single owner per contract, to implement support for multiple admins we can make use of storage to manage a collection of user admin account addresses.
Below is a helper library which implements the basic functionality of adding and removing admin accounts for a given contract (address).
The isUserAdmin function can then be used in a modifier, just like onlyOwner in our previous example. For instance if we wanted to allow multiple admins in Organisation we can implement this as follows.
In addition to authorising the call sender, modifiers can be also used to validate the data provided with the call. A sample library of useful modifiers is below, implementing checks for various types, e.g. empty address or zero value integers.
Putting it all together
With the armour of authorisation and validation modifiers you can decorate your functions as necessary to ensure that before you execute a function, you have subjected the caller and call data to the utmost scrutiny.
Example of such use is in our Organisation.addProposal(bytes32) function which should only allow Organisation admins to create proposals. Additionally we ensure no ether is sent with the request and that we get a non empty key for the proposal being created.