Let’s get practical
In the previous post, Understanding how DAOs interact with external contracts, we talked about how Governance can interact with external contracts. Towards the bottom of that post, it was a bit hypothetical, so in this post we will walk through exactly what needs to be done from a technical perspective.
In this post we will step through the creation of a proposal to change a variable on an external contract written in the previous post.
Also, since then we have launched the Rootstock Collective DAO and have learned a lot about how these interactions should happen.
Deployed contracts
To interact with the contracts, they need to be deployed. In this article, everything will be done on Rootstock Testnet using the addresses below.
Contract Name | Rootstock Testnet Address |
---|---|
Governor.sol | 0x2109ff4a9d5548a21f877ca937ac5847fde49694 |
TimeLockController.sol | 0x2c4B5481C935Eb96AD9c8693DAf77131Dce607d9 |
NumberKeeper.sol | 0xe82ea99325bcbc46cc29de266b6f3b487da6b725 |
Setup our environment in Remix
We will use the Rootstock Collective Testnet Deployment to update the number. Unfortunately, the UX of the Rootstock Collective’s proposal section does not allow for custom proposals yet, but we can create it via the smart contracts. The best way to do this would be to use Remix.
How to use Remix could be a post on its own, so for this article I will skim over it. If there is interest, I can write up a full post about it.
In Remix, create a new file called Governor.abi
. In that file, we will paste in the Governor’s ABI, which can be found here. Then, under the “Deploy and Run transactions” tab, we will connect MetaMask, which should already be connected to Rootstock Testnet, and under the “Contract” section, paste in the Governor address under “At Address”.
Create the proposal
The Governor’s propose()
method takes four parameters the first three of which are arrays. Remix shows the four parameters that are needed:
A proposal can target multiple contracts, which is the reason for the arrays.
title | description |
---|---|
targets | An array of addresses to be interacted with |
values | An array of amounts of Ether (in wei) to be sent to each of the addresses |
calldata | An array of data to be sent to each address |
description | A title and a description for the proposal |
Let’s go through each property one by one and set the value.
targets array
On a successful vote of our proposal, we want to trigger a single contract, our NumberKeeper contract:
["0xe82ea99325bcbc46cc29de266b6f3b487da6b725"]
values array
We will not be sending any value to the NumberKeeper contract, so this value will be an array with a zero.
["0"]
calldata array
This is byte data that will get executed on the contract above. Normally, we would use our UX or a library to create this data, but we will do it by hand to learn what is happening. The calldata is comprised of the method we want to call with the data, basically setNumber(162)
:
The calldata is broken up into parts, the first four bytes is the method to be called as a Keccak-256 hash.
Keccak256("setNumber(uint256)") = 0x9cf27f37
The next part is the arguments we will pass to the method in hexidecimnal form, which is a2
. However, the ABI requires that the arguments be 32 bytes long so this value needs to be padded with zeros.
description | decoded | encoded |
---|---|---|
Keccak256 hash of the method to be called | setNumber(uint256) | 0x9cf27f37 |
padded zeros to fill out 32 bytes | padded zeros | 000…000 |
argument(s) to be passed | 162 | a2 |
The final calldata is the following:
["0x9cf27f3700000000000000000000000000000000000000000000000000000000000000a2"]
description
Finally, a title and description separated by a semicolon that will show up in the dapp.
Custom proposal to change the number;Proposal to change the number from 155 to 42.
Hit the transact
button and watch it get mined (explorer hash). In a few minutes, it will show up as a proposal on the Rootstock Collective Dapp here.
Grant access
While we wait for the proposal to be voted on, we can grant the TimeLockController access to change the number in our NumberKeeper contract. The TimeLockController is the one in the end that will execute the transaction, not the Governor.
In Remix, in our NumberKeeper contract, under changeAccess
, add the TimeLockController address and set access to true
:
Now we wait for the voting period.
Now we have to wait for the proposal to pass, approximately seven days. Then, the proposal needs to be queued to execute, which is a few more days of waiting. Finally, after a week and a half or so, we can execute the transaction via the Rootstock Collective Dapp.
[!] As of November 18th 2024, the proposal is still in an active voting state. The final steps and image below were done on a local test environment that has quicker governance times.
Execute the transaction
After the propsal has passed and qued for execution, it can be executed.
Because the TimeLockController contract has permissions to interact with our contract, the proposal passes, the transaction is executed, and the number is updated.
Back in Remix, or in the Contract Interaction tab of the Rootstock Explorer, you can see when you click on getNumber()
, it returned 162.
If you get an error in the Explorer, you need to connect your wallet first.
Cleanup
This is a simple example, but it should show how the Governor contract can interact with external contracts. This was all done with the basic implementation of OpenZeppelin’s Governor contract and didn’t require any modifications to it. The modifications that are needed are the access control in our NumberKeeper contract to allow access.