Customizing A Proposal
Overview
Extending FPS for accommodate Arbitrum Governance
/// @notice set eth fork id function setEthForkId(uint256 _forkId) public { ethForkId = _forkId; }/// @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); }/// @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"); } }/// @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 ); }/// @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; }/// @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
Proposal Simulation
Setting Up the Addrsses JSON Files
Running the Proposal
Last updated