Customizing A Proposal
Overview
The framework is designed to be flexible and loosely coupled, as explained at the end of proposal functions. However, in some cases, additional customization may be required. Currently, FPS supports four types of proposals, each of which can be further customized to meet specific requirements. This guide will explore an example where the OZ Governor proposal type is customized to implement Arbitrum cross-chain proposals. Arbitrum's governance process involves the use of an OZ governor with a timelock extension on Arbitrum L2 and a simple timelock on L1. The proposal's path is determined by whether it is targeting L1 or L2. Regardless of whether its final destination is an L2 contract, every Arbitrum proposal must go through a settlement process on Layer 1.
The following steps from 1 to 5 are equal no matter which chain the proposal final contract target is deployed:
Proposal is created on L2 Governor
Proposal is voted on and passes
Proposal is queued on L2 timelock
Proposal is executed on L2 timelock after the delay, and therefore initiates a bridge request to L1 inbox by calling the ArbSys precompiled contract
After the bridge delay of 1 week, anyone can call the Bridge contract on L1 using the merkle proof generated for the proposal calldata, effectively scheduling the proposal on the L1 Timelock
Once the proposal is scheduled on the L1 Timelock, there is a three-day delay before it becomes executable. When executed, it can follow two different paths. If the target is an L1 contract, the proposal follows the standard OpenZeppelin Timelock path. For L2 proposals, identified by the target being a Retryable Ticket Magic address, a call to the L1 inbox generates the L2 ticket. Once it is bridged to L2, anyone can execute the ticket. The ticket is responsible for calling the final contract target, which, for proposals that are Arbitrum contract upgrades, will be the Arbitrum Upgrade Executor Contract.
Read more about Arbitrum Governance here.
It is worth noting that the Arbitrum governance process has its own specifications and is not a straightforward implementation of the OZ Governor contract. Therefore, it is not possible to directly use the OZ Governor proposal type to simulate an Arbitrum proposal. Customization is needed to accommodate FPS for creating and testing Arbitrum proposals. Thanks to FPS flexibility, this is possible without much extra effort.
Extending FPS for accommodate Arbitrum Governance
The Arbitrum Proposal Type demonstrates the framework's flexibility. Let's go through each of its functions:
setEthForkId(uint256)
: used to create the Ethereum fork using Foundry as Arbitrum Governance is cross-chain. This function will be set by the child proposal./// @notice set eth fork id function setEthForkId(uint256 _forkId) public { ethForkId = _forkId; }
preBuildMock()
: deploys theMockOutBox
contract and points on theArbitrum Bridge
with the help of thevm.store
foundry cheatcode. Additionally, it embeds theMockArbSys
bytecode at theArbitrum sys
contract address. As previously mentioned in the overview section, the calldata for the L2 proposal needs to be a call to thesendTxToL1
on the ArbSys precompiled contract. This call requires the L1 timelock contract address as the first parameter and the L1 timelock schedule calldata as the second parameter. TheMockArbOutbox
is employed to replicate off-chain actions. The bridge ensures that the schedule was initiated by the L2 timelock by callingl1ToL2Sender
in the outbox contract. This ensures that the bridge verification is successful, even though no genuine off-chain actions have been executed. These two mock functionalities are crucial for the arbitrum proposal flow, and here, it can be observed how FPS simplifies the simulation of such a complex proposal flow./// @title MockArbSys /// @notice a mocked version of the Arbitrum pre-compiled system contract, add additional methods as needed contract MockArbSys { uint256 public ticketId; function sendTxToL1( address _l1Target, bytes memory _data ) external payable returns (uint256) { (bool success, ) = _l1Target.call(_data); require(success, "Arbsys: sendTxToL1 failed"); return ++ticketId; } } // @title MockArbOutbox // @notice Mock arbitrum outbox to return L2 timelock on l2ToL1Sender call contract MockArbOutbox { function l2ToL1Sender() external pure returns (address) { return 0x34d45e99f7D8c45ed05B5cA72D54bbD1fb3F98f0; } } /// @notice mock arb sys precompiled contract on L2 /// mock outbox on mainnet function preBuildMock() public override { // switch to mainnet fork to mock arb outbox vm.selectFork(ethForkId); address mockOutbox = address(new MockArbOutbox()); vm.store( addresses.getAddress("ARBITRUM_BRIDGE"), bytes32(uint256(5)), bytes32(uint256(uint160(mockOutbox))) ); vm.selectFork(primaryForkId); address arbsys = address(new MockArbSys()); vm.makePersistent(arbsys); vm.etch(addresses.getAddress("ARBITRUM_SYS"), address(arbsys).code); }
_validateActions()
: Validates proposal actions. An arbitrum proposal should have a single action. This method checks that there is only one action in the proposal and the target contract is not the zero address. Furthermore, it also checks that there are no actions without arguments and value, and if the execution chain isL2
, noETH
is transferred (value = 0)./// @notice Arbitrum proposals should have a single action function _validateActions() internal view override { uint256 actionsLength = actions.length; require( actionsLength == 1, "Arbitrum proposals must have a single action" ); require(actions[0].target != address(0), "Invalid target for proposal"); /// if there are no args and no eth, the action is not valid require( (actions[0].arguments.length == 0 && actions[0].value > 0) || actions[0].arguments.length > 0, "Invalid arguments for proposal" ); // Value is ignored on L2 proposals if (executionChain == ProposalExecutionChain.ARB_ONE) { require(actions[0].value == 0, "Value must be 0 for L2 execution"); } }
getScheduleTimelockCaldata()
: This function returns calldata to schedule proposal actions on the L1 timelock. Calldata is generated based on the execution chain where the proposal will be executed. If the execution chain is the L1, then the build target is the target contract but if the execution chain is L2 chain then target isRETRYABLE_TICKET_MAGIC
contract and the build target is encoded in the calldata along with the inbox contract address./// @notice get the calldata to schedule the timelock on L1 /// the L1 schedule calldata must be the calldata for all arbitrum proposals function getScheduleTimelockCaldata() public view returns (bytes memory scheduleCalldata) { // address only used if is a L2 proposal address inbox; if (executionChain == ProposalExecutionChain.ARB_ONE) { inbox = arbOneInbox; } else if (executionChain == ProposalExecutionChain.ARB_NOVA) { inbox = arbNovaInbox; } scheduleCalldata = abi.encodeWithSelector( ITimelockController.schedule.selector, // if the action is to be executed on l1, the target is the actual // target, otherwise it is the magic value that tells that the // proposal must be relayed back to l2 executionChain == ProposalExecutionChain.ETH ? actions[0].target : RETRYABLE_TICKET_MAGIC, // target actions[0].value, // value executionChain == ProposalExecutionChain.ETH ? actions[0].arguments : abi.encode( // these are the retryable data params // the inbox contract used, should be arb one or nova inbox, addresses.getAddress("ARBITRUM_L2_UPGRADE_EXECUTOR"), // the upgrade executor on the l2 network 0, // no value in this upgrade 0, // max gas - will be filled in when the retryable is actually executed 0, // max fee per gas - will be filled in when the retryable is actually executed actions[0].arguments // calldata created on the build function ), bytes32(0), // no predecessor keccak256(abi.encodePacked(description())), // salt is prop description minDelay // delay for this proposal ); }
getProposalActions()
: This function returns proposal actions to propose on arbitrum governor. Arbitrum proposals must have a single action which must be a call to ArbSys address with the L1 timelock schedule calldata./// @notice get proposal actions /// @dev Arbitrum proposals must have a single action which must be a call /// to ArbSys address with the l1 timelock schedule calldata function getProposalActions() public view override returns ( address[] memory targets, uint256[] memory values, bytes[] memory arguments ) { _validateActions(); // inner calldata must be a call to schedule on L1Timelock bytes memory innerCalldata = getScheduleTimelockCaldata(); targets = new address[](1); values = new uint256[](1); arguments = new bytes[](1); bytes memory callData = abi.encodeWithSelector( MockArbSys.sendTxToL1.selector, addresses.getAddress("ARBITRUM_L1_TIMELOCK", 1), innerCalldata ); // Arbitrum proposals target must be the ArbSys precompiled address targets[0] = addresses.getAddress("ARBITRUM_SYS"); values[0] = 0; arguments[0] = callData; }
simulate()
: Executes the proposal actions outlined in thebuild()
step. Initial steps to simulate a proposal are the same assimulate()
method in OZ Governor Proposal as Arbitrum also uses OZ Governor with timelock for L2 governance.super.simulate()
is called at the start of the method. Next, further steps for the proposal simulation are added. These steps can be understood by going through the overview section of this guide and the code snippet below with inline comments./// @notice override the OZGovernorProposal simulate function to handle /// the proposal L1 settlement function simulate() public override { // First part of Arbitrum Governance proposal path follows the OZ // Governor with TimelockController extension super.simulate(); // Second part of Arbitrum Governance proposal path is the proposal // settlement on the L1 network bytes memory scheduleCalldata = getScheduleTimelockCaldata(); // switch fork to mainnet vm.selectFork(ethForkId); // prank as the bridge vm.startPrank(addresses.getAddress("ARBITRUM_BRIDGE")); address l1TimelockAddress = addresses.getAddress( "ARBITRUM_L1_TIMELOCK" ); ITimelockController timelock = ITimelockController(l1TimelockAddress); address target; uint256 value; bytes memory data; bytes32 predecessor; { // Start recording logs so we can create the execute calldata using the // CallSchedule log data vm.recordLogs(); // Call the schedule function on the L1 timelock l1TimelockAddress.functionCall(scheduleCalldata); // Stop recording logs Vm.Log[] memory entries = vm.getRecordedLogs(); // Get the execute parameters from schedule call logs (target, value, data, predecessor, ) = abi.decode( entries[0].data, (address, uint256, bytes, bytes32, uint256) ); // warp to the future to execute the proposal vm.warp(block.timestamp + minDelay); } vm.stopPrank(); { // Start recording logs so we can get the TxToL2 log data vm.recordLogs(); // execute the proposal timelock.execute( target, value, data, predecessor, keccak256(abi.encodePacked(description())) ); // Stop recording logs Vm.Log[] memory entries = vm.getRecordedLogs(); // If is a retriable ticket, we need to execute on L2 if (target == RETRYABLE_TICKET_MAGIC) { // entries index 2 is TxToL2 // topic with index 2 is the l2 target address address to = address(uint160(uint256(entries[2].topics[2]))); bytes memory l2Calldata = abi.decode(entries[2].data, (bytes)); // Switch back to primary fork, must be either Arb One or Arb Nova vm.selectFork(primaryForkId); // Perform the low-level call vm.prank(addresses.getAddress("ARBITRUM_ALIASED_L1_TIMELOCK")); bytes memory returndata = to.functionCall(l2Calldata); if (DEBUG && returndata.length > 0) { console.log("Target %s called on L2 and returned:", to); console.logBytes(returndata); } } } }
Proposal contract
Two proposals were added to the fps-example-repo. First, ArbitrumPorposal_01 which is an Arbitrum proposal that executes on L2. Second, ArbitrumPorposal_02 which is an arbitrum proposal that executes on L1. Let's first have a look at ArbitrumPorposal_02
by going through the code snippets:
constructor()
: Execution chain is set to ethereum mainnet in constructor.constructor() { executionChain = ProposalExecutionChain.ETH; }
name()
: Defines the name of the proposal.function name() public pure override returns (string memory) { return "ARBITRUM_PROPOSAL_02"; }
description()
: Provides a detailed description of the proposal.function description() public pure override returns (string memory) { return "This proposal upgrades the L1 weth gateway"; }
deploy()
: This function deploys any necessary contracts. In this example the new weth gateway implementation contract and the GAC contract are deployed on L1. Once deployed, these contracts are added to theAddresses
contract by callingaddAddress()
.function deploy() public override { if ( !addresses.isAddressSet("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION") ) { address mockUpgrade = address(new MockUpgrade()); addresses.addAddress( "ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION", mockUpgrade, true ); } if (!addresses.isAddressSet("PROXY_UPGRADE_ACTION")) { address gac = address(new MockProxyUpgradeAction()); addresses.addAddress("PROXY_UPGRADE_ACTION", gac, true); } }
build()
: Add the needed actions to update the Arbitrum WETH Gateway on L1. For more in-depth information on how this process operates behind the scenes, please refer to the build function.function build() public override buildModifier(addresses.getAddress("ARBITRUM_L1_TIMELOCK", 1)) { // select etherem mainnet fork as this proposal upgrades weth gateway on L1 vm.selectFork(ethForkId); IUpgradeExecutor upgradeExecutor = IUpgradeExecutor( addresses.getAddress("ARBITRUM_L1_UPGRADE_EXECUTOR") ); upgradeExecutor.execute( addresses.getAddress("PROXY_UPGRADE_ACTION"), abi.encodeWithSelector( MockProxyUpgradeAction.perform.selector, addresses.getAddress("ARBITRUM_L1_PROXY_ADMIN"), addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_PROXY"), addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION") ) ); vm.selectFork(primaryForkId); }
run()
: Serves as the entrypoint for executing the proposal byforge script
. In this example, 'primaryForkId' is configured as 'Arbitrum' to execute the proposal on L2. Subsequently, 'ethForkId' is also set as every Arbitrum proposal must undergo a settlement process on L1, regardless of the final execution chain target. The address of the Arbitrum L2 governor's contract is set using 'setGovernor' to simulate the proposal. This address will be used later to verify on-chain calldata and simulate the proposal. For further reading, see the run function.function run() public override { string memory addressesFolderPath = "./addresses"; uint256[] memory chainIds = new uint256[](2); chainIds[0] = 1; chainIds[1] = 42161; addresses = new Addresses(addressesFolderPath, chainIds); vm.makePersistent(address(addresses)); setPrimaryForkId(vm.createFork("arbitrum")); setEthForkId(vm.createFork("ethereum")); /// select arbitrum fork to set governor address vm.selectFork(primaryForkId); setGovernor(addresses.getAddress("ARBITRUM_L2_CORE_GOVERNOR")); /// select ethereum mainnet fork as contracts are deployed on ethereum mainnet vm.selectFork(ethForkId); super.run(); }
validate()
: This final step validates the system in its post-execution state. It ensures that gateway proxy is upgraded to new implementation on L1. Only the proxy owner can callimplementation()
method to check its implementation address.function validate() public override { vm.selectFork(ethForkId); IProxy proxy = IProxy( addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_PROXY") ); // implementation() caller must be the owner vm.startPrank(addresses.getAddress("ARBITRUM_L1_PROXY_ADMIN")); require( proxy.implementation() == addresses.getAddress("ARBITRUM_L1_WETH_GATEWAY_IMPLEMENTATION"), "Proxy implementation not set" ); vm.stopPrank(); vm.selectFork(primaryForkId); }
Now let's have a look at ArbitrumProposal_02
:
constructor()
: Execution chain is set to arbitrum one chain in constructor.constructor() { executionChain = ProposalExecutionChain.ARB_ONE; }
name()
: Defines the name of the proposal.function name() public pure override returns (string memory) { return "ARBITRUM_PROPOSAL_01"; }
description()
: Provides a detailed description of the proposal.function description() public pure override returns (string memory) { return "This proposal upgrades the L2 weth gateway"; }
deploy()
: This function deploys any necessary contracts. In this example the new weth gateway implementation contract and the GAC contract are deployed on L2. Once deployed, these contracts are added to theAddresses
contract by callingaddAddress()
.function deploy() public override { if ( !addresses.isAddressSet("ARBITRUM_L2_WETH_GATEWAY_IMPLEMENTATION") ) { address mockUpgrade = address(new MockUpgrade()); addresses.addAddress( "ARBITRUM_L2_WETH_GATEWAY_IMPLEMENTATION", mockUpgrade, true ); } if (!addresses.isAddressSet("PROXY_UPGRADE_ACTION")) { address gac = address(new MockProxyUpgradeAction()); addresses.addAddress("PROXY_UPGRADE_ACTION", gac, true); } }
build()
: Add the needed actions to update the Arbitrum WETH Gateway on L2. For more in-depth information on how this process operates behind the scenes, please refer to the build function.function build() public override buildModifier(addresses.getAddress("ARBITRUM_ALIASED_L1_TIMELOCK")) { IUpgradeExecutor upgradeExecutor = IUpgradeExecutor( addresses.getAddress("ARBITRUM_L2_UPGRADE_EXECUTOR") ); upgradeExecutor.execute( addresses.getAddress("PROXY_UPGRADE_ACTION"), abi.encodeWithSelector( MockProxyUpgradeAction.perform.selector, addresses.getAddress("ARBITRUM_L2_PROXY_ADMIN"), addresses.getAddress("ARBITRUM_L2_WETH_GATEWAY_PROXY"), addresses.getAddress("ARBITRUM_L2_WETH_GATEWAY_IMPLEMENTATION") ) ); }
run()
: Serves as the entrypoint for executing the proposal byforge script
. In this example, 'primaryForkId' is configured as 'Arbitrum' to execute the proposal on L2. Subsequently, 'ethForkId' is also set as every Arbitrum proposal must undergo a settlement process on L1, regardless of the final execution chain target. The address of the Arbitrum L2 governor's contract is set using 'setGovernor' to simulate the proposal. This address will be used later to verify on-chain calldata and simulate the proposal. For further reading, see the run function.function run() public override { string memory addressesFolderPath = "./addresses"; uint256[] memory chainIds = new uint256[](2); chainIds[0] = 1; chainIds[1] = 42161; addresses = new Addresses(addressesFolderPath, chainIds); vm.makePersistent(address(addresses)); setPrimaryForkId(vm.createFork("arbitrum")); setEthForkId(vm.createFork("ethereum")); vm.selectFork(primaryForkId); setGovernor(addresses.getAddress("ARBITRUM_L2_CORE_GOVERNOR")); super.run(); }
validate()
: This final step validates the system in its post-execution state. It ensures that gateway proxy is upgraded to new implementation. Only proxy owner can callimplementation()
method to check implementation.function validate() public override { IProxy proxy = IProxy( addresses.getAddress("ARBITRUM_L2_WETH_GATEWAY_PROXY") ); // implementation() caller must be the owner vm.startPrank(addresses.getAddress("ARBITRUM_L2_PROXY_ADMIN")); require( proxy.implementation() == addresses.getAddress("ARBITRUM_L2_WETH_GATEWAY_IMPLEMENTATION"), "Proxy implementation not set" ); vm.stopPrank(); }
Proposal Simulation
Setting Up the Addrsses JSON Files
Copy all address arbitrum address from 1.json. Your 1.json
file should follow this structure:
[
{
"addr": "0xE6841D92B0C345144506576eC13ECf5103aC7f49",
"name": "ARBITRUM_L1_TIMELOCK",
"isContract": true
},
{
"addr": "0x3ffFbAdAF827559da092217e474760E2b2c3CeDd",
"name": "ARBITRUM_L1_UPGRADE_EXECUTOR",
"isContract": true
},
{
"addr": "0x9aD46fac0Cf7f790E5be05A0F15223935A0c0aDa",
"name": "ARBITRUM_L1_PROXY_ADMIN",
"isContract": true
},
{
"addr": "0xd92023e9d9911199a6711321d1277285e6d4e2db",
"name": "ARBITRUM_L1_WETH_GATEWAY_PROXY",
"isContract": true
},
{
"addr": "0x8315177aB297bA92A06054cE80a67Ed4DBd7ed3a",
"name": "ARBITRUM_BRIDGE",
"isContract": true
},
{
"addr": "0x2c9c0F10E3F8820544522df210dFb0A2BbC75147",
"name": "DEPLOYER_EOA",
"isContract": false
}
]
Copy all arbitrum address from 42161.json. Your 42161.json
file should follow this structure:
[
{
"addr": "0x34d45e99f7D8c45ed05B5cA72D54bbD1fb3F98f0",
"name": "ARBITRUM_L2_TIMELOCK",
"isContract": true
},
{
"addr": "0xCF57572261c7c2BCF21ffD220ea7d1a27D40A827",
"name": "ARBITRUM_L2_UPGRADE_EXECUTOR",
"isContract": true
},
{
"addr": "0xd570aCE65C43af47101fC6250FD6fC63D1c22a86",
"name": "ARBITRUM_L2_PROXY_ADMIN",
"isContract": true
},
{
"addr": "0x6c411aD3E74De3E7Bd422b94A27770f5B86C623B",
"name": "ARBITRUM_L2_WETH_GATEWAY_PROXY",
"isContract": true
},
{
"addr": "0xf07DeD9dC292157749B6Fd268E37DF6EA38395B9",
"name": "ARBITRUM_L2_CORE_GOVERNOR",
"isContract": true
},
{
"addr": "0x0000000000000000000000000000000000000064",
"name": "ARBITRUM_SYS",
"isContract": true
},
{
"addr": "0xf7951d92b0c345144506576ec13ecf5103ac905a",
"name": "ARBITRUM_ALIASED_L1_TIMELOCK",
"isContract": false
},
{
"addr": "0x2c9c0F10E3F8820544522df210dFb0A2BbC75147",
"name": "DEPLOYER_EOA",
"isContract": false
}
]
Running the Proposal
forge script src/proposals/arbitrum/Arbitrum_Proposal_01.sol --sender -vvvv
The script will output the following:
== Logs ==
--------- Addresses added ---------
{
"addr": "0x6801E4888A91180238A8c36594EC65797eC2dDDf",
"isContract": true,
"name": "ARBITRUM_L2_WETH_GATEWAY_IMPLEMENTATION"
},
{
"addr": "0xA98deC0C8e0326756C956033bbF091081986d0eD",
"isContract": true,
"name": "PROXY_UPGRADE_ACTION"
}
---------------- Proposal Description ----------------
This proposal upgrades the L2 weth gateway
------------------ Proposal Actions ------------------
1). calling ARBITRUM_L2_UPGRADE_EXECUTOR @0xCF57572261c7c2BCF21ffD220ea7d1a27D40A827 with 0 eth and 0x1cff79cd000000000000000000000000a98dec0c8e0326756c956033bbf091081986d0ed00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064e17f52e9000000000000000000000000d570ace65c43af47101fc6250fd6fc63d1c22a860000000000000000000000006c411ad3e74de3e7bd422b94a27770f5b86c623b0000000000000000000000006801e4888a91180238a8c36594ec65797ec2dddf00000000000000000000000000000000000000000000000000000000 data.
target: ARBITRUM_L2_UPGRADE_EXECUTOR @0xCF57572261c7c2BCF21ffD220ea7d1a27D40A827
payload
0x1cff79cd000000000000000000000000a98dec0c8e0326756c956033bbf091081986d0ed00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064e17f52e9000000000000000000000000d570ace65c43af47101fc6250fd6fc63d1c22a860000000000000000000000006c411ad3e74de3e7bd422b94a27770f5b86c623b0000000000000000000000006801e4888a91180238a8c36594ec65797ec2dddf00000000000000000000000000000000000000000000000000000000
----------------- Proposal Changes ---------------
ARBITRUM_L2_UPGRADE_EXECUTOR @0xCF57572261c7c2BCF21ffD220ea7d1a27D40A827:
State Changes:
Slot: 0x0000000000000000000000000000000000000000000000000000000000000097
- 0x0000000000000000000000000000000000000000000000000000000000000001
+ 0x0000000000000000000000000000000000000000000000000000000000000002
Slot: 0x0000000000000000000000000000000000000000000000000000000000000097
- 0x0000000000000000000000000000000000000000000000000000000000000002
+ 0x0000000000000000000000000000000000000000000000000000000000000001
ARBITRUM_L2_WETH_GATEWAY_PROXY @0x6c411aD3E74De3E7Bd422b94A27770f5B86C623B:
State Changes:
Slot: 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
- 0x000000000000000000000000806421d09cdb253aa9d128a658e60c0b95effa01
+ 0x0000000000000000000000006801e4888a91180238a8c36594ec65797ec2dddf
------------------ Proposal Calldata ------------------
0x7d5e81e2000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000004c00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000344928c169a000000000000000000000000e6841d92b0c345144506576ec13ecf5103ac7f49000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000002c401d5062a000000000000000000000000a723c008e76e379c55599d2e4d93879beafda79c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000009c33e8e47a5438b554b45b782bed73248b78e26754b37292265f0b4a3ede7874000000000000000000000000000000000000000000000000000000000003f48000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000004dbd4fc535ac27206064b68ffcf827b0a60bab3f000000000000000000000000cf57572261c7c2bcf21ffd220ea7d1a27d40a82700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e41cff79cd000000000000000000000000a98dec0c8e0326756c956033bbf091081986d0ed00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064e17f52e9000000000000000000000000d570ace65c43af47101fc6250fd6fc63d1c22a860000000000000000000000006c411ad3e74de3e7bd422b94a27770f5b86c623b0000000000000000000000006801e4888a91180238a8c36594ec65797ec2dddf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a546869732070726f706f73616c20757067726164657320746865204c322077657468206761746577617900000000000000000000000000000000000000000000
A DAO member can check whether the calldata proposed on the governance matches the calldata from the script exeuction. 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.
The proposal script will deploy the contracts in deploy()
method and will generate actions calldata for each individual action along with proposal calldata for the proposal. The proposal can be proposed manually using cast send
with the calldata generated above.
Last updated