Lesson 8

Inheritance & modifiers

4

Our contracts are starting to take shape, but you might have noticed that they're still quite flawed at this point. For example, in our current TweetStorage contract, anyone can create a tweet on behalf of any user simply by passing their user ID as a parameter:

contracts/tweets/TweetStorage.sol

We also don't do any checks in our createUser function to see if the username is taken:

contracts/users/UserStorage.sol

This is obviously not good. However, as we mentioned earlier, it's risky to add too much logic in our storage contracts, since they cannot be updated after being deployed without also clearing all their stored data. Therefore, we want to keep them as simple as possible, and instead let upgradable controller contracts handle the bulk of the logic.

Remember our model from earlier?

Using this model, our test logic will change a little bit. When we want to get data, we'll interact with a storage contract directly (as we've done so far), but when we add or change data, we should always go via the controller contract!

Working with permissions

Let's create two new files so that we can start working on our new structure. In the tweets folder, we'll create a file called TweetController.sol, and in users we'll create a file called UserController.sol:

contracts/tweets/TweetController.sol
contracts/users/UserController.sol

So how do we actually make sure that the createUser function in UserStorage only gets accessed through the UserController?

Here's the idea: Solidity has a special variable that's accessible in all contract functions, called msg.sender. It represents the Ethereum address that's calling the contract. So for the createUser function in UserStorage, we should simply make sure that msg.sender is equal to the address that UserController is deployed to!

How we want our users to call the "createUser" function.

We can use Solidity's require function to make sure that a condition is met before proceeding to the next line of code. It the requirement fails, it will throw an error:

contracts/users/UserStorage.sol

Naturally the next question is – how does UserStorage know what the controllerAddr is? For that, we need a new function that manually sets it as a storage variable (using the special address data type).

contracts/users/UserStorage.sol

But obviously, this new function can't be accessible to anyone, or else everything falls apart! We need to make sure that only the owner of the contract can change it:

contracts/users/UserStorage.sol

Are you still following? As you can see, this is getting pretty complicated, and we're introducing state variables like ownerAddr and controllerAddr which don't really have anything to do with our users. To make all this a little less complex, let's extract this new logic into some Solidity helper libraries instead!

Helper libraries and inheritance

Similarly to classic oriented object programming languages, Solidity makes it possible for contracts to inherit properties from other contracts. This is very handy if you think a contract is getting too long, and you want to extract some of the logic into another file, or if you have logic that should be duplicated across contracts.

In our UserStorage contract above, we have two features that could be inherited from a more general contract library:

  1. Setting the owner of the contract, and making sure that some functions are limited to its address (ownerAddr)

  2. Setting the controller of the contract, and making sure that some functions are limited to its address (controllerAddr)

Sounds good, let's get to work! Inside your contracts folder, create a new folder called helpers and add two files in it: Owned.sol and BaseStorage.sol.

What your folder structure should look like.

The idea is that our UserStorage inherits from BaseStorage (which sets the controllerAddr for the storage contract), and BaseStorage itself inherits from Owned (which sets the ownerAddr for the storage contract).

The chain of inheritance.

Open BaseStorage.sol and fill it with the following code:

contracts/helpers/BaseStorage.sol

As you can see, we've now extracted the logic for setting the contract's controller address into its own contract. In order for UserStorage to inherit these properties, we simply import the contract into UserStorage.sol and use the is keyword:

contracts/users/UserStorage.sol

Note that we don't have to deploy BaseStorage separately from UserStorage when it's used as a library. Instead, UserStorage will simply copy all the logic that it needs from BaseStorage at compilation time.

Now that we have this, we can remove controllerAddr and setControllerAddr from the UserStorage contract, since we're inheriting them instead.

Next, we want BaseStorage to inherit from Owned, since it's expecting to find an ownerAddr state variable in its setControllerAddr function. Here's what Owned.sol should look like:

contracts/helpers/Owned.sol

Notice how we have a special constructor function inside the Owned contract? This function runs only once, when the contract is deployed, and then never again.

By getting the msg.sender inside the constructor, we are getting the address that's deploying the contract for the very first time. This is a very common way of setting the initial ownerAddr securely.

We've also added a transferOwnership function just in case we need to change the owner at some point in the future. As you can see, we've added some require functions to make sure that only the owner can call this function, and that the new address isn't empty (address(0) is the same as the empty address 0x0).

Now we can go back to BaseStorage.sol and make sure that the BaseStorage contract inherits from Owned:

contracts/helpers/BaseStorage.sol

Voila! Thanks to this, we have made it possible for UserStorage to access all the state variables needed for checking permissions (ownerAddr and controllerAddr) without polluting the contract code with irrelevant functions.

Using modifiers

There's one last improvement that we could use in our contracts before we move on to making our tests pass. You might have noticed that we have a lot of logic involving checking who the msg.sender is. For example:

contracts/helpers/BaseStorage.sol
contracts/helpers/Owned.sol
contracts/users/UserStorage.sol

We can actually extract this logic so that it's easier to use, using a custom modifier. Modifiers are used to "wrap" some additional functionality around a function, and are similar to decorators in object-oriented programming.

Here's how we would write a modifier for checking that the msg.sender is equal to the ownerAddr:

contracts/helpers/Owned.sol

The _ symbol indicates where the rest of the code should be "injected". The image below might help you understand this concept better:

We can take advantage of this in our transferOwnership function simply by adding onlyOwner right after the public modifier (and removing the require(msg.sender === ownerAddr); line):

contracts/helpers/Owned.sol

This will still work in exactly the same way as previously! Pretty cool, huh? Since we're also using the same logic in our BaseStorage contract's setControllerAddr, we can update that function as well:

contracts/helpers/BaseStorage.sol

We would also like to create an onlyController modifier. This way we can easily set the right permissions for functions in the storage contract that should only be accessed through the controller (like createUser). Again, this is easily done:

contracts/helpers/BaseStorage.sol

And now we can update createUser:

contracts/users/UserStorage.sol

Sweet! Just to make sure that you've followed everything so far, this is what your final files should look like:

contracts/helpers/Owned.sol
contracts/helpers/BaseStorage.sol
contracts/users/UserStorage.sol

Duplicating the logic to TweetStorage

Thanks to all the work we've done in extracting some of our logic into libraries, it is now trivial to adopt the same kind of controller-storage pattern for our TweetStorage!

Open TweetStorage.sol, make sure that the contract inherits from BaseStorage, and add the onlyController modifier to the createTweet function:

contracts/tweets/TweetStorage.sol

And we're done! Good thing we're using libraries, right?

Unfortunately, in the process of restructuring our app we've also completely broken our tests...

But don't panic! In the next chapter, we're going to populate our controller contracts, and see how we can get the tests working again.

Comments

Phaedrus Raznikov

I think there's a typo when we first write the transferOwnership function (before extracting the isOwner modifier). Solidity doesn't support the triple-equals operator, right?