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:
We also don't do any checks in our
createUser function to see if the username is taken:
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.
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
So how do we actually make sure that the
createUser function in
UserStorage only gets accessed through the
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!
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:
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).
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:
Are you still following? As you can see, this is getting pretty complicated, and we're introducing state variables like
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.
UserStorage contract above, we have two features that could be inherited from a more general contract library:
Setting the owner of the contract, and making sure that some functions are limited to its address (
Setting the controller of the contract, and making sure that some functions are limited to its address (
Sounds good, let's get to work! Inside your
contracts folder, create a new folder called
helpers and add two files in it:
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).
BaseStorage.sol and fill it with the following code:
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
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
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:
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
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
Now we can go back to
BaseStorage.sol and make sure that the
BaseStorage contract inherits from
Voila! Thanks to this, we have made it possible for
UserStorage to access all the state variables needed for checking permissions (
controllerAddr) without polluting the contract code with irrelevant functions.
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:
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
_ 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):
This will still work in exactly the same way as previously! Pretty cool, huh? Since we're also using the same logic in our
setControllerAddr, we can update that function as well:
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:
And now we can update
Sweet! Just to make sure that you've followed everything so far, this is what your final files should look like:
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.sol, make sure that the contract inherits from
BaseStorage, and add the
onlyController modifier to the
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.