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
function createTweet(uint _userId, string memory _text) public returns(uint) {
latestTweetId++;
tweets[latestTweetId] = Tweet(latestTweetId, _text, _userId, now);
return latestTweetId;
}
We also don't do any checks in our createUser
function to see if the username is taken:
// contracts/users/UserStorage.sol
function createUser(bytes32 _username) public returns(uint) {
latestUserId++;
profiles[latestUserId] = Profile(latestUserId, _username);
return latestUserId;
}
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
pragma solidity ^0.5.10;
contract TweetController {
}
// contracts/users/UserController.sol
pragma solidity ^0.5.10;
contract UserController {
}
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
// ...
contract UserStorage {
// ...
function createUser(bytes32 _username) public returns(uint) {
// Add this line at the start of the function:
require(msg.sender == controllerAddr);
// ...
}
}
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
// ...
contract UserStorage {
// ...
address controllerAddr;
function setControllerAddr(address _controllerAddr) public {
controllerAddr = _controllerAddr;
}
// ...
}
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
// ...
contract UserStorage {
// ...
address ownerAddr;
address controllerAddr;
function setControllerAddr(address _controllerAddr) public {
require(msg.sender == ownerAddr)
controllerAddr = _controllerAddr;
}
// ...
}
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
.
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
pragma solidity ^0.5.10;
contract BaseStorage {
address public controllerAddr;
function setControllerAddr(address _controllerAddr) public {
require(msg.sender == ownerAddr)
controllerAddr = _controllerAddr;
}
}
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
pragma solidity ^0.5.10;
import '../helpers/BaseStorage.sol';
contract UserStorage is BaseStorage {
// ...
}
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
pragma solidity ^0.5.10;
contract Owned {
address public ownerAddr;
constructor() public {
ownerAddr = msg.sender;
}
function transferOwnership(address _newOwner) public {
// Only the current owner can set a new ownerAddr:
require(msg.sender === ownerAddr);
// The new address cannot be null:
require(_newOwner != address(0));
ownerAddr = _newOwner;
}
}
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
pragma solidity ^0.5.10;
// Import the Owned contract:
import './Owned.sol';
// Make BaseStorage inherit from Owned:
contract BaseStorage is 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:
// contracts/helpers/BaseStorage.sol
// ...
contract BaseStorage is Owned {
// ...
function setControllerAddr(address _controllerAddr) public {
require(msg.sender == ownerAddr) // <-- This line
// ...
}
}
// contracts/helpers/Owned.sol
// ...
contract Owned {
// ...
function transferOwnership(address _newOwner) public {
require(msg.sender === ownerAddr); // <-- This line
// ...
}
}
// contracts/users/UserStorage.sol
// ...
contract UserStorage is BaseStorage {
// ...
function createUser(bytes32 _username) public returns(uint) {
require(msg.sender == controllerAddr); // <-- This line
// ...
}
}
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
pragma solidity ^0.5.10;
contract Owned {
// ...
modifier onlyOwner() {
require(msg.sender == 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):
// contracts/helpers/Owned.sol
// ...
contract Owned {
// ...
function transferOwnership(address _newOwner) public onlyOwner {
require(_newOwner != address(0));
ownerAddr = _newOwner;
}
}
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
pragma solidity ^0.5.10;
// ...
contract BaseStorage is Owned {
// ...
function setControllerAddr(address _controllerAddr) public onlyOwner {
controllerAddr = _controllerAddr;
}
}
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
// ...
contract BaseStorage is Owned {
// ...
modifier onlyController() {
require(msg.sender == controllerAddr);
_;
}
}
And now we can update createUser
:
// contracts/users/UserStorage.sol
// ...
contract UserStorage is BaseStorage {
// ...
// Add the "onlyController" modifier:
function createUser(bytes32 _username) public onlyController returns(uint) {
latestUserId++;
profiles[latestUserId] = Profile(latestUserId, _username);
return latestUserId;
}
}
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
pragma solidity ^0.5.10;
contract Owned {
address public ownerAddr;
constructor() public {
ownerAddr = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == ownerAddr);
_;
}
function transferOwnership(address _newOwner) public onlyOwner {
require(_newOwner != address(0));
ownerAddr = _newOwner;
}
}
// contracts/helpers/BaseStorage.sol
pragma solidity ^0.5.10;
import './Owned.sol';
contract BaseStorage is Owned {
address public controllerAddr;
modifier onlyController() {
require(msg.sender == controllerAddr);
_;
}
function setControllerAddr(address _controllerAddr) public onlyOwner {
controllerAddr = _controllerAddr;
}
}
// contracts/users/UserStorage.sol
pragma solidity ^0.5.10;
import '../helpers/BaseStorage.sol';
contract UserStorage is BaseStorage {
mapping(uint => Profile) profiles;
struct Profile {
uint id;
bytes32 username;
}
uint latestUserId = 0;
function createUser(bytes32 _username) public onlyController returns(uint) {
latestUserId++;
profiles[latestUserId] = Profile(latestUserId, _username);
return latestUserId;
}
}
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
// ...
// Import the BaseStorage contract
import '../helpers/BaseStorage.sol';
// Make sure TweetStorage inherits from it
contract TweetStorage is BaseStorage {
// ...
// Add the "onlyController" modifier:
function createTweet(uint _userId, string _text) public onlyController returns(uint _newTweetId) {
// ...
}
}
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.