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”.

Remix

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:

Propose Method

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.

Rootstock Collective

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:

Grant access

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.

Grant access

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.

Read Contract

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.