Arbitrum Timelock
Overview
This example showcases how FPS can be utilized for simulating proposals for the Arbitrum timelock on mainnet. Specifically, it upgrades the WETH gateway on L1. The proposal involves deploying a new implementation contract ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION
and a governance action contract ARBITRUM_GAC_UPGRADE_WETH_GATEWAY
. Then, the timelock employs upgradeExecutor
to upgrade the WETH gateway. The proposer for the L1 timelock should always be the Arbitrum bridge.
The relevant contract can be found in the mocks folder.
Let's review each of the overridden functions:
name()
: Defines the name of the proposal.function name() public pure override returns (string memory) { return "ARBITRUM_L1_TIMELOCK_MOCK"; }
description()
: Provides a detailed description of the proposal.function description() public pure override returns (string memory) { return "Mock proposal for upgrading the WETH gateway"; }
deploy()
: This function demonstrates the deployment of a new MockUpgrade, which will be used as the new implementation for the WETH Gateway Proxy and a new GAC contract for the upgrade.function deploy() public override { // Deploy new WETH gateway implementation if not already deployed if ( !addresses.isAddressSet("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION") ) { // In a real case, this function would be responsible for // deploying a new implementation contract instead of using a mock address l1NFTBridgeImplementation = address(new MockUpgrade()); addresses.addAddress( "ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION", l1NFTBridgeImplementation, true ); } // Deploy new GAC contract for gateway upgrade if not already deployed if (!addresses.isAddressSet("ARBITRUM_GAC_UPGRADE_WETH_GATEWAY")) { address gac = address(new GovernanceActionUpgradeWethGateway()); addresses.addAddress( "ARBITRUM_GAC_UPGRADE_WETH_GATEWAY", gac, true ); } }
preBuildMock()
: Post-deployment mock actions, such as setting a newoutBox
forArbitrum bridge
usingvm.store
foundry cheatcode.function preBuildMock() public override { // Deploy new mockOutBox address address mockOutbox = address(new MockOutbox()); // This is a workaround to replace the mainnet outBox with the newly deployed one for testing purposes only vm.store( addresses.getAddress("ARBITRUM_BRIDGE"), bytes32(uint256(5)), bytes32(uint256(uint160(mockOutbox))) ); }
build()
: Add actions to the proposal contract. In this example,ARBITRUM_L1_WETH_GATEWAY_PROXY
is upgraded to the new implementation. The actions should be written in solidity code and in the order they should be executed. Any calls (except to the Addresses object) will be recorded and stored as actions to execute in the run function. Thecaller
address is passed intobuildModifier
; it will call the actions inbuild
. The caller is the Arbitrum timelock in this example. ThebuildModifier
is a necessary modifier for thebuild
function and will not work without it. For further reading, see the build function.function build() public override buildModifier(address(timelock)) { /// STATICCALL -- not recorded for the run stage // Get upgrade executor address IUpgradeExecutor upgradeExecutor = IUpgradeExecutor( addresses.getAddress("ARBITRUM_L1_UPGRADE_EXECUTOR") ); /// CALLS -- mutative and recorded // Upgrade WETH gateway using GAC contract to the newly deployed implementation upgradeExecutor.execute( addresses.getAddress("ARBITRUM_GAC_UPGRADE_WETH_GATEWAY"), abi.encodeWithSelector( GovernanceActionUpgradeWethGateway.upgradeWethGateway.selector, addresses.getAddress("ARBITRUM_L1_PROXY_ADMIN"), addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_PROXY"), addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION") ) ); }
run()
: Sets up the environment for running the proposal, and executes all proposal actions. This setsaddresses
,primaryForkId
, andtimelock
and callssuper.run()
to run the entire proposal. In this example,primaryForkId
is set tomainnet
and the fork for running the proposal is selected. Next, theaddresses
object is set by reading the JSON file. The timelock contract to test is set usingsetTimelock
. This will be used to check onchain calldata and simulate the proposal. For further reading, see the run function.function run() public override { // Create and select the mainnet fork for proposal execution primaryForkId = vm.createFork("mainnet"); vm.selectFork(primaryForkId); uint256[] memory chainIds = new uint256[](1); chainIds[0] = 1; // Set the addresses object by reading addresses from the json file addresses = new Addresses( vm.envOr("ADDRESSES_PATH", string("./addresses")), chainIds ); // Set the timelock. This address is used for proposal simulation and checking on-chain proposal state setTimelock(addresses.getAddress("ARBITRUM_L1_TIMELOCK")); // Call the run function of the parent contract 'Proposal.sol' super.run(); }
simulate()
: Executes the proposal actions outlined in thebuild()
step. This function performs a call to_simulateActions
from the inheritedTimelockProposal
contract. Internally,_simulateActions()
simulates a call to Timelock scheduleBatch and executeBatch with the calldata generated from the actions set up in the build step.function simulate() public override { // Proposer must be the Arbitrum bridge address proposer = addresses.getAddress("ARBITRUM_BRIDGE"); // Executor can be anyone address executor = address(1); // Simulate the actions in the `build` function _simulateActions(proposer, executor); }
validate()
: Validates that the implementation is upgraded correctly.function validate() public override { // Get proxy address IProxy proxy = IProxy( addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_PROXY") ); // Ensure implementation is upgraded to the newly deployed implementation require( proxy.implementation() == addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION"), "Proxy implementation not set" ); }
Running the Proposal
forge script mocks/MockTimelockProposal.sol:MockTimelockProposal --fork-url mainnet
All required addresses should be in the JSON file, including DEPLOYER_EOA
address, which will deploy the new contracts. If these do not align, the script execution will fail.
The script will output the following:
== Logs ==
--------- Addresses added ---------
{
"addr": "0x714CB817EfD08fEe91558b07A924a87C3587F3C1",
"isContract": true,
"name": "ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION"
},
{
"addr": "0x56a0dFA59fD02284D1b39327CfE92251051Da6bb",
"isContract": true,
"name": "ARBITRUM_GAC_UPGRADE_WETH_GATEWAY"
}
---------------- Proposal Description ----------------
Mock proposal that upgrades the weth gateway
------------------ Proposal Actions ------------------
1). calling ARBITRUM_L1_UPGRADE_EXECUTOR @0x3ffFbAdAF827559da092217e474760E2b2c3CeDd with 0 eth and 0x1cff79cd00000000000000000000000056a0dfa59fd02284d1b39327cfe92251051da6bb0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006409b461c10000000000000000000000009ad46fac0cf7f790e5be05a0f15223935a0c0ada000000000000000000000000d92023e9d9911199a6711321d1277285e6d4e2db000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c100000000000000000000000000000000000000000000000000000000 data.
target: ARBITRUM_L1_UPGRADE_EXECUTOR @0x3ffFbAdAF827559da092217e474760E2b2c3CeDd
payload
0x1cff79cd00000000000000000000000056a0dfa59fd02284d1b39327cfe92251051da6bb0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006409b461c10000000000000000000000009ad46fac0cf7f790e5be05a0f15223935a0c0ada000000000000000000000000d92023e9d9911199a6711321d1277285e6d4e2db000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c100000000000000000000000000000000000000000000000000000000
----------------- Proposal Changes ---------------
ARBITRUM_L1_UPGRADE_EXECUTOR @0x3ffFbAdAF827559da092217e474760E2b2c3CeDd:
State Changes:
Slot: 0x0000000000000000000000000000000000000000000000000000000000000097
- 0x0000000000000000000000000000000000000000000000000000000000000001
+ 0x0000000000000000000000000000000000000000000000000000000000000002
Slot: 0x0000000000000000000000000000000000000000000000000000000000000097
- 0x0000000000000000000000000000000000000000000000000000000000000002
+ 0x0000000000000000000000000000000000000000000000000000000000000001
ARBITRUM_L1_WETH_GATEWAY_PROXY @0xd92023E9d9911199a6711321D1277285e6d4e2db:
State Changes:
Slot: 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
- 0x0000000000000000000000004b8e9b3f253e68837bf719997b1eeb9e8f1960e2
+ 0x000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c1
------------------ Schedule Calldata ------------------
0x8f2a0bb000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000050deb3e0ef55ff1976003bef5ca1a251beebbeb0d17ef15e6340ea825bbfe8e8000000000000000000000000000000000000000000000000000000000003f48000000000000000000000000000000000000000000000000000000000000000010000000000000000000000003fffbadaf827559da092217e474760e2b2c3cedd000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e41cff79cd00000000000000000000000056a0dfa59fd02284d1b39327cfe92251051da6bb0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006409b461c10000000000000000000000009ad46fac0cf7f790e5be05a0f15223935a0c0ada000000000000000000000000d92023e9d9911199a6711321d1277285e6d4e2db000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
------------------ Execute Calldata ------------------
0xe38335e500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000050deb3e0ef55ff1976003bef5ca1a251beebbeb0d17ef15e6340ea825bbfe8e800000000000000000000000000000000000000000000000000000000000000010000000000000000000000003fffbadaf827559da092217e474760e2b2c3cedd000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e41cff79cd00000000000000000000000056a0dfa59fd02284d1b39327cfe92251051da6bb0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006409b461c10000000000000000000000009ad46fac0cf7f790e5be05a0f15223935a0c0ada000000000000000000000000d92023e9d9911199a6711321d1277285e6d4e2db000000000000000000000000714cb817efd08fee91558b07a924a87c3587f3c10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
It is crucial to note that two new addresses have been added to the Addresses.sol
storage. These addresses are not included in the JSON files when proposal is run without the DO_UPDATE_ADDRESS_JSON
flag set to true.
Last updated