The deployment orchestra
To make our storage and controller contracts work together, we now need to make sure that we deploy them in a very specific order.
The reason for this is that some functions in our storage contracts use the
onlyController modifier. Therefore, they obviously need to be aware of what address their respective controllers have been uploaded to.
Based on the code, here's what we should do:
TweetStoragecontracts (as we do now)
Get the deployed instances of
TweetStorage(so that we can interact with them in our migration files)
controllerAddrin the deployed
Let's do this step by step. The first one is already done, so we'll start by creating a new migration file where we deploy our
TweetController contracts. We can call it
By using an array inside the
deployer.deploy function, we get a resolved promise once they're both done:
After that, we want to get the deployed versions of our storage contracts. We can do this by calling the
.deployed() method on the contracts:
Finally, we call the
setControllerAddr function on the two storage contracts instances. In order to get the deployed addresses for
TweetStorage, we simply type
address property becomes available on the contract object after it's been deployed.
This is what the final code should look like:
To see if the deployment works, you can try running
truffle test again:
Handling errors in tests
Believe it or not, not all of our failing tests are actually bad. If a user tries to call
createTweet by calling the storage contract directly for example, we should expect an error, since they're not going through the controller like we want them to.
To test this behaviour, let's create a new test at the very top of
"can't create user without controller". For now, we'll just log the returned error message to get an idea of how we can identify it:
truffle test and look for the newly added test.
Aha, so it seems like we get an error object containing the string
"VM Exception". Not exactly the easiest thing to test, but we can work with it. Let's look for that in our test by creating a function called
Note that if
storage.createUser(username) doesn't throw an error, we intentionally make the test fail with
Great! Next up, we want to do the same thing in
Before adding the
"can't create tweet without controller" test, we'll first remove the
before()-part of the test for now. We will re-add the same functionality later by calling the controller instead.
Since we want to use an
assertVMException function here too, let's extract it into its own file – that way, we can easily import it into any test in the future. We'll call the file
utils.js and place it right in the
Then we just import the function into
Run the test again, and you'll see that we now have 2 tests passing!
Adding a contract manager
Since our controllers are supposed to call functions in our storage contracts, they obviously need to know what address these storage contracts are deployed to.
To make this work, we'll build a
ContractManager that keeps track of all contracts' addresses.
The functions needed in the contract manager are the following:
Add new key-value records, with a string (ex:
"UserStorage") pointing to an address (ex:
Get an address based on the string key.
Delete the address of a string key.
All these functions should obviously only be available to the owner of the contract, so we'll first of all make our contract inherit from
Then we simply add three functions (
deleteAddress), which all use the
Rethinking our deployment strategy
As we've said earlier, immutability is an important aspect of the Ethereum ecosystem, so it's essential to plan ahead when writing Solidity contracts. This goes for migration files too.
In Truffle, we ideally want to separate our migration files so that we can run just one of them in isolation if some aspect of the code changes.
Some migration files, such as
2_deploy_storage.js will never be run more than once (since that would reset its stored data). When it comes to our controllers however, the best case scenario would be if when we update, say, our
UserController, we could run just a single migration file which takes care of replacing our contract, set the new controller address in the storage and updating the contract manager – all in one sweep.
Knowing this, you might see why having a migration file called
3_deploy_controllers.js isn't really as modular as we want it to be. Let's reorganise our migration files!
You can safely delete the
3_deploy_controllers.js file so that we can instead create one called
3_deploy_manager.js. Similarly to
2_deploy_storage.js, this one should only have to be deployed once.
Next, we'll create two deployment files for our controllers – one for the
UserController and one for the
In these migrations, we need to make sure that our controllers have the address of the deployed
ContractManager. If they have that address, then they can get the address of every other deployed contract too.
For this, we'll first create a library called
BaseController that our
TweetController will inherit from:
As you can see, all
BaseController does is set the
managerAddr state variable using the
setManagerAddr function. Now we just need to make sure that
UserController inherit from it:
We're ready to write our two migration files! These should:
Deploy their dedicated controller contract
ContractManager's address in the controller
Set the controller's address in the
Set the controller's address in the storage contract that goes with it
truffle test once more to make sure that the contracts are being deployed as expected (there should still be only 2 passing tests though).
This is a huge step forward for our migration architecture. If we later find a bug in our TweetController contract, we can simply edit it and run
truffle migrate -f 5 (
-f meaning "force" and
5 referring to the fifth migration file). That way, Truffle will only execute
5_deploy_tweetcontroller.js without touching the rest of your migration files.
We're almost there! Now we just need to make all our existing tests pass.
Updating our unit tests
First, we're going to focus on our unit tests. Remember, our unit tests run our contracts in isolation, so there's no interaction between our controller contracts and storage contracts. In other words, we can't make our controller contract call a function on the deployed storage contract in a unit test.
This problem becomes very apparent in
TestUserStorage. We're trying to call the
createUser function, even though we've explicitly specified that that function is
controllerOnly, making it inaccessible to
The solution is to create a brand new instance of our
UserStorage inside the test's constructor, and manually call
setControllerAddr on it. That way, we can set the
controllerAddr to the test contract's own address, which allows us to call functions like
createUser without getting an error.
TestTweetStorage, we're going to do exactly the same thing – create a new instance (this time of
TweetStorage) and manually set the controller address before running the test functions:
Building our controllers
To round this up, we need to actually make our controllers work by adding some functions to them that will forward data to their respective storage contracts. We'll start with the user controller.
First of all, we'll go to the users' integration test file, remove the test called
"can create user" and replace it with one called
"can create user with controller".
To make this pass, all we need to do is create the
createUser function in the
UserController. The function will fetch the deployed
UserStorage instance through the
ContractManager, and send its arguments to that instance's own
Finally, we're going to do exactly the same thing with our
TweetController and the
We now have a nice mix of unit and integration tests that make sure our contracts are working as expected.
At first glance, it might seem overly tedious to separate your code into separate contracts like this with controllers, storages and a contract manager. As you continue to add new features, you'll probably be happy about it though, since you'll be able to swap out and upgrade parts of your application without losing precious data!