Discover Ethereum & Solidity

Back to All Courses

Lesson 8

Inheritance & modifiers

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:

  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

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.