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 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!
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 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:
Setting the owner of the contract, and making sure that some functions are limited to its address (
ownerAddr
)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
.

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).
Open 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 is
keyword:
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:
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
:
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:
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
:
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 BaseStorage
contract's 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 createUser
:
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
!
Open TweetStorage.sol
, make sure that the contract inherits from BaseStorage
, and add the onlyController
modifier to the createTweet
function:
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
You need a Ludu account in order to ask the instructor a question or post a comment.
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?