diff --git a/.gitmodules b/.gitmodules index 888d42d..0ed56e4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/aave-helpers"] + path = lib/aave-helpers + url = https://github.com/marsfoundation/aave-helpers diff --git a/foundry.toml b/foundry.toml index e883058..cf8404d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,10 @@ src = "src" out = "out" libs = ["lib"] +remappings = [ + 'aave-helpers/=lib/aave-helpers/src/', + 'solidity-utils/=lib/aave-helpers/lib/solidity-utils/src/', + 'aave-address-book/=lib/aave-helpers/lib/aave-address-book/src' +] # See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/lib/aave-helpers b/lib/aave-helpers new file mode 160000 index 0000000..1f85825 --- /dev/null +++ b/lib/aave-helpers @@ -0,0 +1 @@ +Subproject commit 1f858252b541bbd0d6fe0e0573b7d462a4d8e3ca diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index 1a47b40..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console2} from "forge-std/Script.sol"; - -contract CounterScript is Script { - function setUp() public {} - - function run() public { - vm.broadcast(); - } -} diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/dependencies/arbitrum/AddressAliasHelper.sol b/src/dependencies/arbitrum/AddressAliasHelper.sol new file mode 100644 index 0000000..a9b7dcb --- /dev/null +++ b/src/dependencies/arbitrum/AddressAliasHelper.sol @@ -0,0 +1,29 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +library AddressAliasHelper { + uint160 internal constant OFFSET = uint160(0x1111000000000000000000000000000000001111); + + /// @notice Utility function that converts the address in the L1 that submitted a tx to + /// the inbox to the msg.sender viewed in the L2 + /// @param l1Address the address in the L1 that triggered the tx to L2 + /// @return l2Address L2 address as viewed in msg.sender + function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { + unchecked { + l2Address = address(uint160(l1Address) + OFFSET); + } + } + + /// @notice Utility function that converts the msg.sender viewed in the L2 to the + /// address in the L1 that submitted a tx to the inbox + /// @param l2Address L2 address as viewed in msg.sender + /// @return l1Address the address in the L1 that triggered the tx to L2 + function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) { + unchecked { + l1Address = address(uint160(l2Address) - OFFSET); + } + } +} diff --git a/src/dependencies/arbitrum/interfaces/IBridge.sol b/src/dependencies/arbitrum/interfaces/IBridge.sol new file mode 100644 index 0000000..088fc54 --- /dev/null +++ b/src/dependencies/arbitrum/interfaces/IBridge.sol @@ -0,0 +1,103 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE +// SPDX-License-Identifier: BUSL-1.1 + +// solhint-disable-next-line compiler-version +pragma solidity >=0.6.9 <0.9.0; + +import './IOwnable.sol'; + +interface IBridge { + event MessageDelivered( + uint256 indexed messageIndex, + bytes32 indexed beforeInboxAcc, + address inbox, + uint8 kind, + address sender, + bytes32 messageDataHash, + uint256 baseFeeL1, + uint64 timestamp + ); + + event BridgeCallTriggered(address indexed outbox, address indexed to, uint256 value, bytes data); + + event InboxToggle(address indexed inbox, bool enabled); + + event OutboxToggle(address indexed outbox, bool enabled); + + event SequencerInboxUpdated(address newSequencerInbox); + + function allowedDelayedInboxList(uint256) external returns (address); + + function allowedOutboxList(uint256) external returns (address); + + /// @dev Accumulator for delayed inbox messages; tail represents hash of the current state; each element represents the inclusion of a new message. + function delayedInboxAccs(uint256) external view returns (bytes32); + + /// @dev Accumulator for sequencer inbox messages; tail represents hash of the current state; each element represents the inclusion of a new message. + function sequencerInboxAccs(uint256) external view returns (bytes32); + + function rollup() external view returns (IOwnable); + + function sequencerInbox() external view returns (address); + + function activeOutbox() external view returns (address); + + function allowedDelayedInboxes(address inbox) external view returns (bool); + + function allowedOutboxes(address outbox) external view returns (bool); + + /** + * @dev Enqueue a message in the delayed inbox accumulator. + * These messages are later sequenced in the SequencerInbox, either + * by the sequencer as part of a normal batch, or by force inclusion. + */ + function enqueueDelayedMessage( + uint8 kind, + address sender, + bytes32 messageDataHash + ) external payable returns (uint256); + + function executeCall( + address to, + uint256 value, + bytes calldata data + ) external returns (bool success, bytes memory returnData); + + function delayedMessageCount() external view returns (uint256); + + function sequencerMessageCount() external view returns (uint256); + + // ---------- onlySequencerInbox functions ---------- + + function enqueueSequencerMessage(bytes32 dataHash, uint256 afterDelayedMessagesRead) + external + returns ( + uint256 seqMessageIndex, + bytes32 beforeAcc, + bytes32 delayedAcc, + bytes32 acc + ); + + /** + * @dev Allows the sequencer inbox to submit a delayed message of the batchPostingReport type + * This is done through a separate function entrypoint instead of allowing the sequencer inbox + * to call `enqueueDelayedMessage` to avoid the gas overhead of an extra SLOAD in either + * every delayed inbox or every sequencer inbox call. + */ + function submitBatchSpendingReport(address batchPoster, bytes32 dataHash) + external + returns (uint256 msgNum); + + // ---------- onlyRollupOrOwner functions ---------- + + function setSequencerInbox(address _sequencerInbox) external; + + function setDelayedInbox(address inbox, bool enabled) external; + + function setOutbox(address inbox, bool enabled) external; + + // ---------- initializer ---------- + + function initialize(IOwnable rollup_) external; +} diff --git a/src/dependencies/arbitrum/interfaces/IDelayedMessageProvider.sol b/src/dependencies/arbitrum/interfaces/IDelayedMessageProvider.sol new file mode 100644 index 0000000..e8d0e43 --- /dev/null +++ b/src/dependencies/arbitrum/interfaces/IDelayedMessageProvider.sol @@ -0,0 +1,15 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE +// SPDX-License-Identifier: BUSL-1.1 + +// solhint-disable-next-line compiler-version +pragma solidity >=0.6.9 <0.9.0; + +interface IDelayedMessageProvider { + /// @dev event emitted when a inbox message is added to the Bridge's delayed accumulator + event InboxMessageDelivered(uint256 indexed messageNum, bytes data); + + /// @dev event emitted when a inbox message is added to the Bridge's delayed accumulator + /// same as InboxMessageDelivered but the batch data is available in tx.input + event InboxMessageDeliveredFromOrigin(uint256 indexed messageNum); +} diff --git a/src/dependencies/arbitrum/interfaces/IGasRefunder.sol b/src/dependencies/arbitrum/interfaces/IGasRefunder.sol new file mode 100644 index 0000000..99037af --- /dev/null +++ b/src/dependencies/arbitrum/interfaces/IGasRefunder.sol @@ -0,0 +1,14 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE +// SPDX-License-Identifier: BUSL-1.1 + +// solhint-disable-next-line compiler-version +pragma solidity >=0.6.9 <0.9.0; + +interface IGasRefunder { + function onGasSpent( + address payable spender, + uint256 gasUsed, + uint256 calldataSize + ) external returns (bool success); +} diff --git a/src/dependencies/arbitrum/interfaces/IInbox.sol b/src/dependencies/arbitrum/interfaces/IInbox.sol new file mode 100644 index 0000000..c574650 --- /dev/null +++ b/src/dependencies/arbitrum/interfaces/IInbox.sol @@ -0,0 +1,153 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE +// SPDX-License-Identifier: BUSL-1.1 + +// solhint-disable-next-line compiler-version +pragma solidity >=0.6.9 <0.9.0; + +import './IBridge.sol'; +import './IDelayedMessageProvider.sol'; +import './ISequencerInbox.sol'; + +interface IInbox is IDelayedMessageProvider { + function bridge() external view returns (IBridge); + + function sequencerInbox() external view returns (ISequencerInbox); + + /** + * @notice Send a generic L2 message to the chain + * @dev This method is an optimization to avoid having to emit the entirety of the messageData in a log. Instead validators are expected to be able to parse the data from the transaction's input + * @param messageData Data of the message being sent + */ + function sendL2MessageFromOrigin(bytes calldata messageData) external returns (uint256); + + /** + * @notice Send a generic L2 message to the chain + * @dev This method can be used to send any type of message that doesn't require L1 validation + * @param messageData Data of the message being sent + */ + function sendL2Message(bytes calldata messageData) external returns (uint256); + + function sendL1FundedUnsignedTransaction( + uint256 gasLimit, + uint256 maxFeePerGas, + uint256 nonce, + address to, + bytes calldata data + ) external payable returns (uint256); + + function sendL1FundedContractTransaction( + uint256 gasLimit, + uint256 maxFeePerGas, + address to, + bytes calldata data + ) external payable returns (uint256); + + function sendUnsignedTransaction( + uint256 gasLimit, + uint256 maxFeePerGas, + uint256 nonce, + address to, + uint256 value, + bytes calldata data + ) external returns (uint256); + + function sendContractTransaction( + uint256 gasLimit, + uint256 maxFeePerGas, + address to, + uint256 value, + bytes calldata data + ) external returns (uint256); + + /** + * @notice Get the L1 fee for submitting a retryable + * @dev This fee can be paid by funds already in the L2 aliased address or by the current message value + * @dev This formula may change in the future, to future proof your code query this method instead of inlining!! + * @param dataLength The length of the retryable's calldata, in bytes + * @param baseFee The block basefee when the retryable is included in the chain, if 0 current block.basefee will be used + */ + function calculateRetryableSubmissionFee(uint256 dataLength, uint256 baseFee) + external + view + returns (uint256); + + /** + * @notice Deposit eth from L1 to L2 + * @dev This does not trigger the fallback function when receiving in the L2 side. + * Look into retryable tickets if you are interested in this functionality. + * @dev This function should not be called inside contract constructors + */ + function depositEth() external payable returns (uint256); + + /** + * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts + * @dev all msg.value will deposited to callValueRefundAddress on L2 + * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error + * @param to destination L2 contract address + * @param l2CallValue call value for retryable L2 message + * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee + * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance + * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled + * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param data ABI encoded data of L2 message + * @return unique message number of the retryable transaction + */ + function createRetryableTicket( + address to, + uint256 l2CallValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + bytes calldata data + ) external payable returns (uint256); + + /** + * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts + * @dev Same as createRetryableTicket, but does not guarantee that submission will succeed by requiring the needed funds + * come from the deposit alone, rather than falling back on the user's L2 balance + * @dev Advanced usage only (does not rewrite aliases for excessFeeRefundAddress and callValueRefundAddress). + * createRetryableTicket method is the recommended standard. + * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error + * @param to destination L2 contract address + * @param l2CallValue call value for retryable L2 message + * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee + * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance + * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled + * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) + * @param data ABI encoded data of L2 message + * @return unique message number of the retryable transaction + */ + function unsafeCreateRetryableTicket( + address to, + uint256 l2CallValue, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 gasLimit, + uint256 maxFeePerGas, + bytes calldata data + ) external payable returns (uint256); + + // ---------- onlyRollupOrOwner functions ---------- + + /// @notice pauses all inbox functionality + function pause() external; + + /// @notice unpauses all inbox functionality + function unpause() external; + + // ---------- initializer ---------- + + /** + * @dev function to be called one time during the inbox upgrade process + * this is used to fix the storage slots + */ + function postUpgradeInit(IBridge _bridge) external; + + function initialize(IBridge _bridge, ISequencerInbox _sequencerInbox) external; +} diff --git a/src/dependencies/arbitrum/interfaces/IOwnable.sol b/src/dependencies/arbitrum/interfaces/IOwnable.sol new file mode 100644 index 0000000..4c52ff0 --- /dev/null +++ b/src/dependencies/arbitrum/interfaces/IOwnable.sol @@ -0,0 +1,10 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE +// SPDX-License-Identifier: BUSL-1.1 + +// solhint-disable-next-line compiler-version +pragma solidity >=0.4.21 <0.9.0; + +interface IOwnable { + function owner() external view returns (address); +} diff --git a/src/dependencies/arbitrum/interfaces/ISequencerInbox.sol b/src/dependencies/arbitrum/interfaces/ISequencerInbox.sol new file mode 100644 index 0000000..83f349c --- /dev/null +++ b/src/dependencies/arbitrum/interfaces/ISequencerInbox.sol @@ -0,0 +1,155 @@ +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE +// SPDX-License-Identifier: BUSL-1.1 + +// solhint-disable-next-line compiler-version +pragma solidity >=0.6.9 <0.9.0; +pragma experimental ABIEncoderV2; + +import './IGasRefunder.sol'; +import './IDelayedMessageProvider.sol'; +import './IBridge.sol'; + +interface ISequencerInbox is IDelayedMessageProvider { + struct MaxTimeVariation { + uint256 delayBlocks; + uint256 futureBlocks; + uint256 delaySeconds; + uint256 futureSeconds; + } + + struct TimeBounds { + uint64 minTimestamp; + uint64 maxTimestamp; + uint64 minBlockNumber; + uint64 maxBlockNumber; + } + + enum BatchDataLocation { + TxInput, + SeparateBatchEvent, + NoData + } + + event SequencerBatchDelivered( + uint256 indexed batchSequenceNumber, + bytes32 indexed beforeAcc, + bytes32 indexed afterAcc, + bytes32 delayedAcc, + uint256 afterDelayedMessagesRead, + TimeBounds timeBounds, + BatchDataLocation dataLocation + ); + + event OwnerFunctionCalled(uint256 indexed id); + + /// @dev a separate event that emits batch data when this isn't easily accessible in the tx.input + event SequencerBatchData(uint256 indexed batchSequenceNumber, bytes data); + + /// @dev a valid keyset was added + event SetValidKeyset(bytes32 indexed keysetHash, bytes keysetBytes); + + /// @dev a keyset was invalidated + event InvalidateKeyset(bytes32 indexed keysetHash); + + function totalDelayedMessagesRead() external view returns (uint256); + + function bridge() external view returns (IBridge); + + /// @dev The size of the batch header + // solhint-disable-next-line func-name-mixedcase + function HEADER_LENGTH() external view returns (uint256); + + /// @dev If the first batch data byte after the header has this bit set, + /// the sequencer inbox has authenticated the data. Currently not used. + // solhint-disable-next-line func-name-mixedcase + function DATA_AUTHENTICATED_FLAG() external view returns (bytes1); + + function rollup() external view returns (IOwnable); + + function isBatchPoster(address) external view returns (bool); + + struct DasKeySetInfo { + bool isValidKeyset; + uint64 creationBlock; + } + + // https://github.com/ethereum/solidity/issues/11826 + // function maxTimeVariation() external view returns (MaxTimeVariation calldata); + // function dasKeySetInfo(bytes32) external view returns (DasKeySetInfo calldata); + + /// @notice Force messages from the delayed inbox to be included in the chain + /// Callable by any address, but message can only be force-included after maxTimeVariation.delayBlocks and + /// maxTimeVariation.delaySeconds has elapsed. As part of normal behaviour the sequencer will include these + /// messages so it's only necessary to call this if the sequencer is down, or not including any delayed messages. + /// @param _totalDelayedMessagesRead The total number of messages to read up to + /// @param kind The kind of the last message to be included + /// @param l1BlockAndTime The l1 block and the l1 timestamp of the last message to be included + /// @param baseFeeL1 The l1 gas price of the last message to be included + /// @param sender The sender of the last message to be included + /// @param messageDataHash The messageDataHash of the last message to be included + function forceInclusion( + uint256 _totalDelayedMessagesRead, + uint8 kind, + uint64[2] calldata l1BlockAndTime, + uint256 baseFeeL1, + address sender, + bytes32 messageDataHash + ) external; + + function inboxAccs(uint256 index) external view returns (bytes32); + + function batchCount() external view returns (uint256); + + function isValidKeysetHash(bytes32 ksHash) external view returns (bool); + + /// @notice the creation block is intended to still be available after a keyset is deleted + function getKeysetCreationBlock(bytes32 ksHash) external view returns (uint256); + + // ---------- BatchPoster functions ---------- + + function addSequencerL2BatchFromOrigin( + uint256 sequenceNumber, + bytes calldata data, + uint256 afterDelayedMessagesRead, + IGasRefunder gasRefunder + ) external; + + function addSequencerL2Batch( + uint256 sequenceNumber, + bytes calldata data, + uint256 afterDelayedMessagesRead, + IGasRefunder gasRefunder + ) external; + + // ---------- onlyRollupOrOwner functions ---------- + + /** + * @notice Set max delay for sequencer inbox + * @param maxTimeVariation_ the maximum time variation parameters + */ + function setMaxTimeVariation(MaxTimeVariation memory maxTimeVariation_) external; + + /** + * @notice Updates whether an address is authorized to be a batch poster at the sequencer inbox + * @param addr the address + * @param isBatchPoster_ if the specified address should be authorized as a batch poster + */ + function setIsBatchPoster(address addr, bool isBatchPoster_) external; + + /** + * @notice Makes Data Availability Service keyset valid + * @param keysetBytes bytes of the serialized keyset + */ + function setValidKeyset(bytes calldata keysetBytes) external; + + /** + * @notice Invalidates a Data Availability Service keyset + * @param ksHash hash of the keyset + */ + function invalidateKeysetHash(bytes32 ksHash) external; + + // ---------- initializer ---------- + + function initialize(IBridge bridge_, MaxTimeVariation calldata maxTimeVariation_) external; +} diff --git a/src/dependencies/optimism/interfaces/ICrossDomainMessenger.sol b/src/dependencies/optimism/interfaces/ICrossDomainMessenger.sol new file mode 100644 index 0000000..c7e4362 --- /dev/null +++ b/src/dependencies/optimism/interfaces/ICrossDomainMessenger.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.9.0; + +/** + * @title ICrossDomainMessenger + */ +interface ICrossDomainMessenger { + /********** + * Events * + **********/ + + event SentMessage( + address indexed target, + address sender, + bytes message, + uint256 messageNonce, + uint256 gasLimit + ); + event RelayedMessage(bytes32 indexed msgHash); + event FailedRelayedMessage(bytes32 indexed msgHash); + + /************* + * Variables * + *************/ + + function xDomainMessageSender() external view returns (address); + + /******************** + * Public Functions * + ********************/ + + /** + * Sends a cross domain message to the target messenger. + * @param _target Target contract address. + * @param _message Message to send to the target. + * @param _gasLimit Gas limit for the provided message. + */ + function sendMessage( + address _target, + bytes calldata _message, + uint32 _gasLimit + ) external; +} diff --git a/src/dependencies/optimism/interfaces/IL2CrossDomainMessenger.sol b/src/dependencies/optimism/interfaces/IL2CrossDomainMessenger.sol new file mode 100644 index 0000000..fa0e6f6 --- /dev/null +++ b/src/dependencies/optimism/interfaces/IL2CrossDomainMessenger.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +/* Interface Imports */ +import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol"; + +/** + * @title IL2CrossDomainMessenger + */ +interface IL2CrossDomainMessenger is ICrossDomainMessenger { + /******************** + * Public Functions * + ********************/ + + /** + * Relays a cross domain message to a contract. + * @param _target Target contract address. + * @param _sender Message sender address. + * @param _message Message to send to the target. + * @param _messageNonce Nonce for the provided message. + */ + function relayMessage( + address _target, + address _sender, + bytes memory _message, + uint256 _messageNonce + ) external; +} diff --git a/src/executors/ArbitrumBridgeExecutor.sol b/src/executors/ArbitrumBridgeExecutor.sol new file mode 100644 index 0000000..3272473 --- /dev/null +++ b/src/executors/ArbitrumBridgeExecutor.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.10; + +import {AddressAliasHelper} from '../dependencies/arbitrum/AddressAliasHelper.sol'; +import {L2BridgeExecutor} from './L2BridgeExecutor.sol'; + +/** + * @title ArbitrumBridgeExecutor + * @author Aave + * @notice Implementation of the Arbitrum Bridge Executor, able to receive cross-chain transactions from Ethereum + * @dev Queuing an ActionsSet into this Executor can only be done by the L2 Address Alias of the L1 EthereumGovernanceExecutor + */ +contract ArbitrumBridgeExecutor is L2BridgeExecutor { + /// @inheritdoc L2BridgeExecutor + modifier onlyEthereumGovernanceExecutor() override { + if (AddressAliasHelper.undoL1ToL2Alias(msg.sender) != _ethereumGovernanceExecutor) + revert UnauthorizedEthereumExecutor(); + _; + } + + /** + * @dev Constructor + * + * @param ethereumGovernanceExecutor The address of the EthereumGovernanceExecutor + * @param delay The delay before which an actions set can be executed + * @param gracePeriod The time period after a delay during which an actions set can be executed + * @param minimumDelay The minimum bound a delay can be set to + * @param maximumDelay The maximum bound a delay can be set to + * @param guardian The address of the guardian, which can cancel queued proposals (can be zero) + */ + constructor( + address ethereumGovernanceExecutor, + uint256 delay, + uint256 gracePeriod, + uint256 minimumDelay, + uint256 maximumDelay, + address guardian + ) + L2BridgeExecutor( + ethereumGovernanceExecutor, + delay, + gracePeriod, + minimumDelay, + maximumDelay, + guardian + ) + { + // Intentionally left blank + } +} diff --git a/src/executors/BridgeExecutorBase.sol b/src/executors/BridgeExecutorBase.sol new file mode 100644 index 0000000..e6095a1 --- /dev/null +++ b/src/executors/BridgeExecutorBase.sol @@ -0,0 +1,410 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.10; + +import {IExecutorBase} from '../interfaces/IExecutorBase.sol'; + +/** + * @title BridgeExecutorBase + * @author Aave + * @notice Abstract contract that implements basic executor functionality + * @dev It does not implement an external `queue` function. This should instead be done in the inheriting + * contract with proper access control + */ +abstract contract BridgeExecutorBase is IExecutorBase { + // Minimum allowed grace period, which reduces the risk of having an actions set expire due to network congestion + uint256 constant MINIMUM_GRACE_PERIOD = 10 minutes; + + // Time between queuing and execution + uint256 private _delay; + // Time after the execution time during which the actions set can be executed + uint256 private _gracePeriod; + // Minimum allowed delay + uint256 private _minimumDelay; + // Maximum allowed delay + uint256 private _maximumDelay; + // Address with the ability of canceling actions sets + address private _guardian; + + // Number of actions sets + uint256 private _actionsSetCounter; + // Map of registered actions sets (id => ActionsSet) + mapping(uint256 => ActionsSet) private _actionsSets; + // Map of queued actions (actionHash => isQueued) + mapping(bytes32 => bool) private _queuedActions; + + /** + * @dev Only guardian can call functions marked by this modifier. + **/ + modifier onlyGuardian() { + if (msg.sender != _guardian) revert NotGuardian(); + _; + } + + /** + * @dev Only this contract can call functions marked by this modifier. + **/ + modifier onlyThis() { + if (msg.sender != address(this)) revert OnlyCallableByThis(); + _; + } + + /** + * @dev Constructor + * + * @param delay The delay before which an actions set can be executed + * @param gracePeriod The time period after a delay during which an actions set can be executed + * @param minimumDelay The minimum bound a delay can be set to + * @param maximumDelay The maximum bound a delay can be set to + * @param guardian The address of the guardian, which can cancel queued proposals (can be zero) + */ + constructor( + uint256 delay, + uint256 gracePeriod, + uint256 minimumDelay, + uint256 maximumDelay, + address guardian + ) { + if ( + gracePeriod < MINIMUM_GRACE_PERIOD || + minimumDelay >= maximumDelay || + delay < minimumDelay || + delay > maximumDelay + ) revert InvalidInitParams(); + + _updateDelay(delay); + _updateGracePeriod(gracePeriod); + _updateMinimumDelay(minimumDelay); + _updateMaximumDelay(maximumDelay); + _updateGuardian(guardian); + } + + /// @inheritdoc IExecutorBase + function execute(uint256 actionsSetId) external payable override { + if (getCurrentState(actionsSetId) != ActionsSetState.Queued) revert OnlyQueuedActions(); + + ActionsSet storage actionsSet = _actionsSets[actionsSetId]; + if (block.timestamp < actionsSet.executionTime) revert TimelockNotFinished(); + + actionsSet.executed = true; + uint256 actionCount = actionsSet.targets.length; + + bytes[] memory returnedData = new bytes[](actionCount); + for (uint256 i = 0; i < actionCount; ) { + returnedData[i] = _executeTransaction( + actionsSet.targets[i], + actionsSet.values[i], + actionsSet.signatures[i], + actionsSet.calldatas[i], + actionsSet.executionTime, + actionsSet.withDelegatecalls[i] + ); + unchecked { + ++i; + } + } + + emit ActionsSetExecuted(actionsSetId, msg.sender, returnedData); + } + + /// @inheritdoc IExecutorBase + function cancel(uint256 actionsSetId) external override onlyGuardian { + if (getCurrentState(actionsSetId) != ActionsSetState.Queued) revert OnlyQueuedActions(); + + ActionsSet storage actionsSet = _actionsSets[actionsSetId]; + actionsSet.canceled = true; + + uint256 targetsLength = actionsSet.targets.length; + for (uint256 i = 0; i < targetsLength; ) { + _cancelTransaction( + actionsSet.targets[i], + actionsSet.values[i], + actionsSet.signatures[i], + actionsSet.calldatas[i], + actionsSet.executionTime, + actionsSet.withDelegatecalls[i] + ); + unchecked { + ++i; + } + } + + emit ActionsSetCanceled(actionsSetId); + } + + /// @inheritdoc IExecutorBase + function updateGuardian(address guardian) external override onlyThis { + _updateGuardian(guardian); + } + + /// @inheritdoc IExecutorBase + function updateDelay(uint256 delay) external override onlyThis { + _validateDelay(delay); + _updateDelay(delay); + } + + /// @inheritdoc IExecutorBase + function updateGracePeriod(uint256 gracePeriod) external override onlyThis { + if (gracePeriod < MINIMUM_GRACE_PERIOD) revert GracePeriodTooShort(); + _updateGracePeriod(gracePeriod); + } + + /// @inheritdoc IExecutorBase + function updateMinimumDelay(uint256 minimumDelay) external override onlyThis { + if (minimumDelay >= _maximumDelay) revert MinimumDelayTooLong(); + _updateMinimumDelay(minimumDelay); + _validateDelay(_delay); + } + + /// @inheritdoc IExecutorBase + function updateMaximumDelay(uint256 maximumDelay) external override onlyThis { + if (maximumDelay <= _minimumDelay) revert MaximumDelayTooShort(); + _updateMaximumDelay(maximumDelay); + _validateDelay(_delay); + } + + /// @inheritdoc IExecutorBase + function executeDelegateCall(address target, bytes calldata data) + external + payable + override + onlyThis + returns (bool, bytes memory) + { + bool success; + bytes memory resultData; + // solium-disable-next-line security/no-call-value + (success, resultData) = target.delegatecall(data); + return (success, resultData); + } + + /// @inheritdoc IExecutorBase + function receiveFunds() external payable override {} + + /// @inheritdoc IExecutorBase + function getDelay() external view override returns (uint256) { + return _delay; + } + + /// @inheritdoc IExecutorBase + function getGracePeriod() external view override returns (uint256) { + return _gracePeriod; + } + + /// @inheritdoc IExecutorBase + function getMinimumDelay() external view override returns (uint256) { + return _minimumDelay; + } + + /// @inheritdoc IExecutorBase + function getMaximumDelay() external view override returns (uint256) { + return _maximumDelay; + } + + /// @inheritdoc IExecutorBase + function getGuardian() external view override returns (address) { + return _guardian; + } + + /// @inheritdoc IExecutorBase + function getActionsSetCount() external view override returns (uint256) { + return _actionsSetCounter; + } + + /// @inheritdoc IExecutorBase + function getActionsSetById(uint256 actionsSetId) + external + view + override + returns (ActionsSet memory) + { + return _actionsSets[actionsSetId]; + } + + /// @inheritdoc IExecutorBase + function getCurrentState(uint256 actionsSetId) public view override returns (ActionsSetState) { + if (_actionsSetCounter <= actionsSetId) revert InvalidActionsSetId(); + ActionsSet storage actionsSet = _actionsSets[actionsSetId]; + if (actionsSet.canceled) { + return ActionsSetState.Canceled; + } else if (actionsSet.executed) { + return ActionsSetState.Executed; + } else if (block.timestamp > actionsSet.executionTime + _gracePeriod) { + return ActionsSetState.Expired; + } else { + return ActionsSetState.Queued; + } + } + + /// @inheritdoc IExecutorBase + function isActionQueued(bytes32 actionHash) public view override returns (bool) { + return _queuedActions[actionHash]; + } + + function _updateGuardian(address guardian) internal { + emit GuardianUpdate(_guardian, guardian); + _guardian = guardian; + } + + function _updateDelay(uint256 delay) internal { + emit DelayUpdate(_delay, delay); + _delay = delay; + } + + function _updateGracePeriod(uint256 gracePeriod) internal { + emit GracePeriodUpdate(_gracePeriod, gracePeriod); + _gracePeriod = gracePeriod; + } + + function _updateMinimumDelay(uint256 minimumDelay) internal { + emit MinimumDelayUpdate(_minimumDelay, minimumDelay); + _minimumDelay = minimumDelay; + } + + function _updateMaximumDelay(uint256 maximumDelay) internal { + emit MaximumDelayUpdate(_maximumDelay, maximumDelay); + _maximumDelay = maximumDelay; + } + + /** + * @notice Queue an ActionsSet + * @dev If a signature is empty, calldata is used for the execution, calldata is appended to signature otherwise + * @param targets Array of targets to be called by the actions set + * @param values Array of values to pass in each call by the actions set + * @param signatures Array of function signatures to encode in each call (can be empty) + * @param calldatas Array of calldata to pass in each call (can be empty) + * @param withDelegatecalls Array of whether to delegatecall for each call + **/ + function _queue( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + bool[] memory withDelegatecalls + ) internal { + if (targets.length == 0) revert EmptyTargets(); + uint256 targetsLength = targets.length; + if ( + targetsLength != values.length || + targetsLength != signatures.length || + targetsLength != calldatas.length || + targetsLength != withDelegatecalls.length + ) revert InconsistentParamsLength(); + + uint256 actionsSetId = _actionsSetCounter; + uint256 executionTime = block.timestamp + _delay; + unchecked { + ++_actionsSetCounter; + } + + for (uint256 i = 0; i < targetsLength; ) { + bytes32 actionHash = keccak256( + abi.encode( + targets[i], + values[i], + signatures[i], + calldatas[i], + executionTime, + withDelegatecalls[i] + ) + ); + if (isActionQueued(actionHash)) revert DuplicateAction(); + _queuedActions[actionHash] = true; + unchecked { + ++i; + } + } + + ActionsSet storage actionsSet = _actionsSets[actionsSetId]; + actionsSet.targets = targets; + actionsSet.values = values; + actionsSet.signatures = signatures; + actionsSet.calldatas = calldatas; + actionsSet.withDelegatecalls = withDelegatecalls; + actionsSet.executionTime = executionTime; + + emit ActionsSetQueued( + actionsSetId, + targets, + values, + signatures, + calldatas, + withDelegatecalls, + executionTime + ); + } + + function _executeTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 executionTime, + bool withDelegatecall + ) internal returns (bytes memory) { + if (address(this).balance < value) revert InsufficientBalance(); + + bytes32 actionHash = keccak256( + abi.encode(target, value, signature, data, executionTime, withDelegatecall) + ); + _queuedActions[actionHash] = false; + + bytes memory callData; + if (bytes(signature).length == 0) { + callData = data; + } else { + callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); + } + + bool success; + bytes memory resultData; + if (withDelegatecall) { + (success, resultData) = this.executeDelegateCall{value: value}(target, callData); + } else { + // solium-disable-next-line security/no-call-value + (success, resultData) = target.call{value: value}(callData); + } + return _verifyCallResult(success, resultData); + } + + function _cancelTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 executionTime, + bool withDelegatecall + ) internal { + bytes32 actionHash = keccak256( + abi.encode(target, value, signature, data, executionTime, withDelegatecall) + ); + _queuedActions[actionHash] = false; + } + + function _validateDelay(uint256 delay) internal view { + if (delay < _minimumDelay) revert DelayShorterThanMin(); + if (delay > _maximumDelay) revert DelayLongerThanMax(); + } + + function _verifyCallResult(bool success, bytes memory returnData) + private + pure + returns (bytes memory) + { + if (success) { + return returnData; + } else { + // Look for revert reason and bubble it up if present + if (returnData.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returnData) + revert(add(32, returnData), returndata_size) + } + } else { + revert FailedActionExecution(); + } + } + } +} diff --git a/src/executors/L2BridgeExecutor.sol b/src/executors/L2BridgeExecutor.sol new file mode 100644 index 0000000..06c9fe2 --- /dev/null +++ b/src/executors/L2BridgeExecutor.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.10; + +import {IL2BridgeExecutor} from '../interfaces/IL2BridgeExecutor.sol'; + +import {BridgeExecutorBase} from './BridgeExecutorBase.sol'; + +/** + * @title L2BridgeExecutor + * @author Aave + * @notice Abstract contract that implements bridge executor functionality for L2 + * @dev It does not implement the `onlyEthereumGovernanceExecutor` modifier. This should instead be done in the inheriting + * contract with proper configuration and adjustments depending on the L2 + */ +abstract contract L2BridgeExecutor is BridgeExecutorBase, IL2BridgeExecutor { + // Address of the Ethereum Governance Executor, which should be able to queue actions sets + address internal _ethereumGovernanceExecutor; + + /** + * @dev Only the Ethereum Governance Executor should be able to call functions marked by this modifier. + **/ + modifier onlyEthereumGovernanceExecutor() virtual; + + /** + * @dev Constructor + * + * @param ethereumGovernanceExecutor The address of the EthereumGovernanceExecutor + * @param delay The delay before which an actions set can be executed + * @param gracePeriod The time period after a delay during which an actions set can be executed + * @param minimumDelay The minimum bound a delay can be set to + * @param maximumDelay The maximum bound a delay can be set to + * @param guardian The address of the guardian, which can cancel queued proposals (can be zero) + */ + constructor( + address ethereumGovernanceExecutor, + uint256 delay, + uint256 gracePeriod, + uint256 minimumDelay, + uint256 maximumDelay, + address guardian + ) BridgeExecutorBase(delay, gracePeriod, minimumDelay, maximumDelay, guardian) { + _ethereumGovernanceExecutor = ethereumGovernanceExecutor; + } + + /// @inheritdoc IL2BridgeExecutor + function queue( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + bool[] memory withDelegatecalls + ) external onlyEthereumGovernanceExecutor { + _queue(targets, values, signatures, calldatas, withDelegatecalls); + } + + /// @inheritdoc IL2BridgeExecutor + function updateEthereumGovernanceExecutor(address ethereumGovernanceExecutor) external onlyThis { + emit EthereumGovernanceExecutorUpdate(_ethereumGovernanceExecutor, ethereumGovernanceExecutor); + _ethereumGovernanceExecutor = ethereumGovernanceExecutor; + } + + /// @inheritdoc IL2BridgeExecutor + function getEthereumGovernanceExecutor() external view returns (address) { + return _ethereumGovernanceExecutor; + } +} diff --git a/src/executors/OptimismBridgeExecutor.sol b/src/executors/OptimismBridgeExecutor.sol new file mode 100644 index 0000000..d1d7807 --- /dev/null +++ b/src/executors/OptimismBridgeExecutor.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.10; + +import {ICrossDomainMessenger} from '../dependencies/optimism/interfaces/ICrossDomainMessenger.sol'; + +import {L2BridgeExecutor} from './L2BridgeExecutor.sol'; + +/** + * @title OptimismBridgeExecutor + * @author Aave + * @notice Implementation of the Optimism Bridge Executor, able to receive cross-chain transactions from Ethereum + * @dev Queuing an ActionsSet into this Executor can only be done by the Optimism L2 Cross Domain Messenger and having + * the EthereumGovernanceExecutor as xDomainMessageSender + */ +contract OptimismBridgeExecutor is L2BridgeExecutor { + // Address of the Optimism L2 Cross Domain Messenger, in charge of redirecting cross-chain transactions in L2 + address public immutable OVM_L2_CROSS_DOMAIN_MESSENGER; + + /// @inheritdoc L2BridgeExecutor + modifier onlyEthereumGovernanceExecutor() override { + if ( + msg.sender != OVM_L2_CROSS_DOMAIN_MESSENGER || + ICrossDomainMessenger(OVM_L2_CROSS_DOMAIN_MESSENGER).xDomainMessageSender() != + _ethereumGovernanceExecutor + ) revert UnauthorizedEthereumExecutor(); + _; + } + + /** + * @dev Constructor + * + * @param ovmL2CrossDomainMessenger The address of the Optimism L2CrossDomainMessenger + * @param ethereumGovernanceExecutor The address of the EthereumGovernanceExecutor + * @param delay The delay before which an actions set can be executed + * @param gracePeriod The time period after a delay during which an actions set can be executed + * @param minimumDelay The minimum bound a delay can be set to + * @param maximumDelay The maximum bound a delay can be set to + * @param guardian The address of the guardian, which can cancel queued proposals (can be zero) + */ + constructor( + address ovmL2CrossDomainMessenger, + address ethereumGovernanceExecutor, + uint256 delay, + uint256 gracePeriod, + uint256 minimumDelay, + uint256 maximumDelay, + address guardian + ) + L2BridgeExecutor( + ethereumGovernanceExecutor, + delay, + gracePeriod, + minimumDelay, + maximumDelay, + guardian + ) + { + OVM_L2_CROSS_DOMAIN_MESSENGER = ovmL2CrossDomainMessenger; + } +} diff --git a/src/forwarders/CrosschainForwarderArbitrum.sol b/src/forwarders/CrosschainForwarderArbitrum.sol new file mode 100644 index 0000000..c0c36c0 --- /dev/null +++ b/src/forwarders/CrosschainForwarderArbitrum.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IInbox} from '../dependencies/arbitrum/interfaces/IInbox.sol'; +import {IL2BridgeExecutor} from '../interfaces/IL2BridgeExecutor.sol'; + +/** + * @title A generic executor for proposals targeting the arbitrum v3 pool + * @author BGD Labs + * @notice You can **only** use this executor when the arbitrum payload has a `execute()` signature without parameters + * @notice You can **only** use this executor when the arbitrum payload is expected to be executed via `DELEGATECALL` + * @notice This contract will assume the caller will be topped up with enough funds to fund the message relay + * @dev This executor is a generic wrapper to be used with Arbitrum Inbox (https://developer.offchainlabs.com/arbos/l1-to-l2-messaging) + * It encodes a parameterless `execute()` with delegate calls and a specified target. + * This encoded abi is then send to the Inbox to be synced executed on the arbitrum network. + * Once synced the ARBITRUM_BRIDGE_EXECUTOR will queue the execution of the payload. + */ +contract CrosschainForwarderArbitrum { + IInbox public constant INBOX = IInbox(0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f); + address public immutable ARBITRUM_BRIDGE_EXECUTOR; + address public constant ARBITRUM_GUARDIAN = 0xbbd9f90699c1FA0D7A65870D241DD1f1217c96Eb; + + // amount of gwei to overpay on basefee for fast submission + uint256 public constant BASE_FEE_MARGIN = 10 gwei; + + /** + * @dev calculateRetryableSubmissionFee on `0x00000000000000000000000000000000000000C8` for a queue call with 1 slot will yield a constant gasLimit of `429478` + * To account for some margin we rounded up to 450000 + */ + uint256 public constant L2_GAS_LIMIT = 450000; + + /** + * @dev There is currently no oracle on L1 exposing gasPrice of arbitrum. Therefore we overpay by assuming 1 gwei (10x of current price). + */ + uint256 public constant L2_MAX_FEE_PER_GAS = 1 gwei; + + /** + * @param bridgeExecutor The L2 executor + */ + constructor(address bridgeExecutor) { + ARBITRUM_BRIDGE_EXECUTOR = bridgeExecutor; + } + + /** + * @dev returns the amount of gas needed for submitting the ticket + * @param bytesLength the payload bytes length (usually 580) + * @return uint256 maxSubmissionFee needed on L2 with BASE_FEE_MARGIN + * @return uint256 estimated L2 redepmption fee + */ + function getRequiredGas(uint256 bytesLength) public view returns (uint256, uint256) { + return ( + INBOX.calculateRetryableSubmissionFee(bytesLength, block.basefee + BASE_FEE_MARGIN), + L2_GAS_LIMIT * L2_MAX_FEE_PER_GAS + ); + } + + /** + * @dev checks if the executor is topped up with enough eth for proposal execution + * with current basefee + * @param executor the address to check for sufficient gas + * @param bytesLength the payload bytes length (usually 580) + * @return bool indicating if the caller has sufficient funds + * @return uint256 the gas required for ticket creation and redemption + */ + function hasSufficientGasForExecution(address executor, uint256 bytesLength) public view returns (bool, uint256) { + (uint256 maxSubmission, uint256 maxRedemption) = getRequiredGas(bytesLength); + uint256 requiredGas = maxSubmission + maxRedemption; + return (executor.balance >= requiredGas, requiredGas); + } + + /** + * @dev encodes the queue call which is forwarded to arbitrum + * @param l2PayloadContract the address of the arbitrum payload + */ + function getEncodedPayload(address l2PayloadContract) public pure returns (bytes memory) { + address[] memory targets = new address[](1); + targets[0] = l2PayloadContract; + uint256[] memory values = new uint256[](1); + values[0] = 0; + string[] memory signatures = new string[](1); + signatures[0] = 'execute()'; + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = ''; + bool[] memory withDelegatecalls = new bool[](1); + withDelegatecalls[0] = true; + return + abi.encodeWithSelector( + IL2BridgeExecutor.queue.selector, + targets, + values, + signatures, + calldatas, + withDelegatecalls + ); + } + + /** + * @dev this function will be executed once the proposal passes the mainnet vote. + * @param l2PayloadContract the arbitrum contract containing the `execute()` signature. + */ + function execute(address l2PayloadContract) public { + bytes memory queue = getEncodedPayload(l2PayloadContract); + (uint256 maxSubmission, uint256 maxRedemption) = getRequiredGas(queue.length); + INBOX.unsafeCreateRetryableTicket{value: maxSubmission + maxRedemption}( + ARBITRUM_BRIDGE_EXECUTOR, + 0, // l2CallValue + maxSubmission, // maxSubmissionCost + address(ARBITRUM_BRIDGE_EXECUTOR), // excessFeeRefundAddress + address(ARBITRUM_GUARDIAN), // callValueRefundAddress + L2_GAS_LIMIT, // gasLimit + L2_MAX_FEE_PER_GAS, // maxFeePerGas + queue + ); + } +} diff --git a/src/forwarders/CrosschainForwarderOptimism.sol b/src/forwarders/CrosschainForwarderOptimism.sol new file mode 100644 index 0000000..0a46a2e --- /dev/null +++ b/src/forwarders/CrosschainForwarderOptimism.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ICrossDomainMessenger} from '../dependencies/optimism/interfaces/ICrossDomainMessenger.sol'; +import {IL2BridgeExecutor} from '../interfaces/IL2BridgeExecutor.sol'; + +interface ICanonicalTransactionChain { + function enqueueL2GasPrepaid() external view returns (uint256); +} + +/** + * @title A generic executor for proposals targeting the optimism v3 pool + * @author BGD Labs + * @notice You can **only** use this executor when the optimism payload has a `execute()` signature without parameters + * @notice You can **only** use this executor when the optimism payload is expected to be executed via `DELEGATECALL` + * @notice You can **only** execute payloads on optimism with up to prepayed gas which is specified in `enqueueL2GasPrepaid` gas. + * Prepaid gas is the maximum gas covered by the bridge without additional payment. + * @dev This executor is a generic wrapper to be used with Optimism CrossDomainMessenger (https://etherscan.io/address/0x25ace71c97b33cc4729cf772ae268934f7ab5fa1) + * It encodes and sends via the L2CrossDomainMessenger a message to queue for execution an action on L2, in the Aave OPTIMISM_BRIDGE_EXECUTOR. + */ +contract CrosschainForwarderOptimism { + /** + * @dev The L1 Cross Domain Messenger contract sends messages from L1 to L2, and relays messages + * from L2 onto L1. In this contract it's used to send the encoded L2 queuing over the bridge. + */ + address public constant L1_CROSS_DOMAIN_MESSENGER_ADDRESS = + 0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1; + + /** + * @dev The optimism bridge executor is a L2 governance execution contract. + * This contract allows queuing of proposals by allow listed addresses. + */ + address public immutable OPTIMISM_BRIDGE_EXECUTOR; + + /** + * @dev The CTC contract is an append only log of transactions which must be applied to the rollup state. + * It also holds configurations like the currently prepayed amount of gas which is what this contract is utilizing. + * https://etherscan.io/address/0x5e4e65926ba27467555eb562121fac00d24e9dd2#code + */ + ICanonicalTransactionChain public constant CANONICAL_TRANSACTION_CHAIN = + ICanonicalTransactionChain(0x5E4e65926BA27467555EB562121fac00D24E9dD2); + + /** + * @param bridgeExecutor The L2 executor + */ + constructor(address bridgeExecutor) { + OPTIMISM_BRIDGE_EXECUTOR = bridgeExecutor; + } + + /** + * @dev this function will be executed once the proposal passes the mainnet vote. + * @param l2PayloadContract the optimism contract containing the `execute()` signature. + */ + function execute(address l2PayloadContract) public { + address[] memory targets = new address[](1); + targets[0] = l2PayloadContract; + uint256[] memory values = new uint256[](1); + values[0] = 0; + string[] memory signatures = new string[](1); + signatures[0] = 'execute()'; + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = ''; + bool[] memory withDelegatecalls = new bool[](1); + withDelegatecalls[0] = true; + + bytes memory queue = abi.encodeWithSelector( + IL2BridgeExecutor.queue.selector, + targets, + values, + signatures, + calldatas, + withDelegatecalls + ); + ICrossDomainMessenger(L1_CROSS_DOMAIN_MESSENGER_ADDRESS).sendMessage( + OPTIMISM_BRIDGE_EXECUTOR, + queue, + uint32(CANONICAL_TRANSACTION_CHAIN.enqueueL2GasPrepaid()) + ); + } +} diff --git a/src/interfaces/IExecutorBase.sol b/src/interfaces/IExecutorBase.sol new file mode 100644 index 0000000..440c073 --- /dev/null +++ b/src/interfaces/IExecutorBase.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.10; + +/** + * @title IExecutorBase + * @author Aave + * @notice Defines the basic interface for the ExecutorBase abstract contract + */ +interface IExecutorBase { + error InvalidInitParams(); + error NotGuardian(); + error OnlyCallableByThis(); + error MinimumDelayTooLong(); + error MaximumDelayTooShort(); + error GracePeriodTooShort(); + error DelayShorterThanMin(); + error DelayLongerThanMax(); + error OnlyQueuedActions(); + error TimelockNotFinished(); + error InvalidActionsSetId(); + error EmptyTargets(); + error InconsistentParamsLength(); + error DuplicateAction(); + error InsufficientBalance(); + error FailedActionExecution(); + + /** + * @notice This enum contains all possible actions set states + */ + enum ActionsSetState { + Queued, + Executed, + Canceled, + Expired + } + + /** + * @notice This struct contains the data needed to execute a specified set of actions + * @param targets Array of targets to call + * @param values Array of values to pass in each call + * @param signatures Array of function signatures to encode in each call (can be empty) + * @param calldatas Array of calldatas to pass in each call, appended to the signature at the same array index if not empty + * @param withDelegateCalls Array of whether to delegatecall for each call + * @param executionTime Timestamp starting from which the actions set can be executed + * @param executed True if the actions set has been executed, false otherwise + * @param canceled True if the actions set has been canceled, false otherwise + */ + struct ActionsSet { + address[] targets; + uint256[] values; + string[] signatures; + bytes[] calldatas; + bool[] withDelegatecalls; + uint256 executionTime; + bool executed; + bool canceled; + } + + /** + * @dev Emitted when an ActionsSet is queued + * @param id Id of the ActionsSet + * @param targets Array of targets to be called by the actions set + * @param values Array of values to pass in each call by the actions set + * @param signatures Array of function signatures to encode in each call by the actions set + * @param calldatas Array of calldata to pass in each call by the actions set + * @param withDelegatecalls Array of whether to delegatecall for each call of the actions set + * @param executionTime The timestamp at which this actions set can be executed + **/ + event ActionsSetQueued( + uint256 indexed id, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + bool[] withDelegatecalls, + uint256 executionTime + ); + + /** + * @dev Emitted when an ActionsSet is successfully executed + * @param id Id of the ActionsSet + * @param initiatorExecution The address that triggered the ActionsSet execution + * @param returnedData The returned data from the ActionsSet execution + **/ + event ActionsSetExecuted( + uint256 indexed id, + address indexed initiatorExecution, + bytes[] returnedData + ); + + /** + * @dev Emitted when an ActionsSet is cancelled by the guardian + * @param id Id of the ActionsSet + **/ + event ActionsSetCanceled(uint256 indexed id); + + /** + * @dev Emitted when a new guardian is set + * @param oldGuardian The address of the old guardian + * @param newGuardian The address of the new guardian + **/ + event GuardianUpdate(address oldGuardian, address newGuardian); + + /** + * @dev Emitted when the delay (between queueing and execution) is updated + * @param oldDelay The value of the old delay + * @param newDelay The value of the new delay + **/ + event DelayUpdate(uint256 oldDelay, uint256 newDelay); + + /** + * @dev Emitted when the grace period (between executionTime and expiration) is updated + * @param oldGracePeriod The value of the old grace period + * @param newGracePeriod The value of the new grace period + **/ + event GracePeriodUpdate(uint256 oldGracePeriod, uint256 newGracePeriod); + + /** + * @dev Emitted when the minimum delay (lower bound of delay) is updated + * @param oldMinimumDelay The value of the old minimum delay + * @param newMinimumDelay The value of the new minimum delay + **/ + event MinimumDelayUpdate(uint256 oldMinimumDelay, uint256 newMinimumDelay); + + /** + * @dev Emitted when the maximum delay (upper bound of delay)is updated + * @param oldMaximumDelay The value of the old maximum delay + * @param newMaximumDelay The value of the new maximum delay + **/ + event MaximumDelayUpdate(uint256 oldMaximumDelay, uint256 newMaximumDelay); + + /** + * @notice Execute the ActionsSet + * @param actionsSetId The id of the ActionsSet to execute + **/ + function execute(uint256 actionsSetId) external payable; + + /** + * @notice Cancel the ActionsSet + * @param actionsSetId The id of the ActionsSet to cancel + **/ + function cancel(uint256 actionsSetId) external; + + /** + * @notice Update guardian + * @param guardian The address of the new guardian + **/ + function updateGuardian(address guardian) external; + + /** + * @notice Update the delay, time between queueing and execution of ActionsSet + * @dev It does not affect to actions set that are already queued + * @param delay The value of the delay (in seconds) + **/ + function updateDelay(uint256 delay) external; + + /** + * @notice Update the grace period, the period after the execution time during which an actions set can be executed + * @param gracePeriod The value of the grace period (in seconds) + **/ + function updateGracePeriod(uint256 gracePeriod) external; + + /** + * @notice Update the minimum allowed delay + * @param minimumDelay The value of the minimum delay (in seconds) + **/ + function updateMinimumDelay(uint256 minimumDelay) external; + + /** + * @notice Update the maximum allowed delay + * @param maximumDelay The maximum delay (in seconds) + **/ + function updateMaximumDelay(uint256 maximumDelay) external; + + /** + * @notice Allows to delegatecall a given target with an specific amount of value + * @dev This function is external so it allows to specify a defined msg.value for the delegate call, reducing + * the risk that a delegatecall gets executed with more value than intended + * @return True if the delegate call was successful, false otherwise + * @return The bytes returned by the delegate call + **/ + function executeDelegateCall(address target, bytes calldata data) + external + payable + returns (bool, bytes memory); + + /** + * @notice Allows to receive funds into the executor + * @dev Useful for actionsSet that needs funds to gets executed + */ + function receiveFunds() external payable; + + /** + * @notice Returns the delay (between queuing and execution) + * @return The value of the delay (in seconds) + **/ + function getDelay() external view returns (uint256); + + /** + * @notice Returns the grace period + * @return The value of the grace period (in seconds) + **/ + function getGracePeriod() external view returns (uint256); + + /** + * @notice Returns the minimum delay + * @return The value of the minimum delay (in seconds) + **/ + function getMinimumDelay() external view returns (uint256); + + /** + * @notice Returns the maximum delay + * @return The value of the maximum delay (in seconds) + **/ + function getMaximumDelay() external view returns (uint256); + + /** + * @notice Returns the address of the guardian + * @return The address of the guardian + **/ + function getGuardian() external view returns (address); + + /** + * @notice Returns the total number of actions sets of the executor + * @return The number of actions sets + **/ + function getActionsSetCount() external view returns (uint256); + + /** + * @notice Returns the data of an actions set + * @param actionsSetId The id of the ActionsSet + * @return The data of the ActionsSet + **/ + function getActionsSetById(uint256 actionsSetId) external view returns (ActionsSet memory); + + /** + * @notice Returns the current state of an actions set + * @param actionsSetId The id of the ActionsSet + * @return The current state of theI ActionsSet + **/ + function getCurrentState(uint256 actionsSetId) external view returns (ActionsSetState); + + /** + * @notice Returns whether an actions set (by actionHash) is queued + * @dev actionHash = keccak256(abi.encode(target, value, signature, data, executionTime, withDelegatecall)) + * @param actionHash hash of the action to be checked + * @return True if the underlying action of actionHash is queued, false otherwise + **/ + function isActionQueued(bytes32 actionHash) external view returns (bool); +} diff --git a/src/interfaces/IL2BridgeExecutor.sol b/src/interfaces/IL2BridgeExecutor.sol new file mode 100644 index 0000000..270c507 --- /dev/null +++ b/src/interfaces/IL2BridgeExecutor.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.10; + +import {IExecutorBase} from './IExecutorBase.sol'; + +/** + * @title IL2BridgeExecutorBase + * @author Aave + * @notice Defines the basic interface for the L2BridgeExecutor abstract contract + */ +interface IL2BridgeExecutor is IExecutorBase { + error UnauthorizedEthereumExecutor(); + + /** + * @dev Emitted when the Ethereum Governance Executor is updated + * @param oldEthereumGovernanceExecutor The address of the old EthereumGovernanceExecutor + * @param newEthereumGovernanceExecutor The address of the new EthereumGovernanceExecutor + **/ + event EthereumGovernanceExecutorUpdate( + address oldEthereumGovernanceExecutor, + address newEthereumGovernanceExecutor + ); + + /** + * @notice Queue an ActionsSet + * @dev If a signature is empty, calldata is used for the execution, calldata is appended to signature otherwise + * @param targets Array of targets to be called by the actions set + * @param values Array of values to pass in each call by the actions set + * @param signatures Array of function signatures to encode in each call by the actions (can be empty) + * @param calldatas Array of calldata to pass in each call by the actions set + * @param withDelegatecalls Array of whether to delegatecall for each call of the actions set + **/ + function queue( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + bool[] memory withDelegatecalls + ) external; + + /** + * @notice Update the address of the Ethereum Governance Executor + * @param ethereumGovernanceExecutor The address of the new EthereumGovernanceExecutor + **/ + function updateEthereumGovernanceExecutor(address ethereumGovernanceExecutor) external; + + /** + * @notice Returns the address of the Ethereum Governance Executor + * @return The address of the EthereumGovernanceExecutor + **/ + function getEthereumGovernanceExecutor() external view returns (address); +} diff --git a/test/ArbitrumCrossChainForwarderTest.t.sol b/test/ArbitrumCrossChainForwarderTest.t.sol new file mode 100644 index 0000000..031923f --- /dev/null +++ b/test/ArbitrumCrossChainForwarderTest.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; + +import {AaveV3Arbitrum, AaveMisc} from 'aave-address-book/AaveAddressBook.sol'; +import {AaveGovernanceV2, IExecutorWithTimelock} from 'aave-address-book/AaveGovernanceV2.sol'; + +import {GovHelpers} from 'aave-helpers/GovHelpers.sol'; +import {ProtocolV3TestBase, ReserveConfig, ReserveTokens, IERC20} from 'aave-helpers/ProtocolV3TestBase.sol'; + +import {IInbox} from '../src/dependencies/arbitrum/interfaces/IInbox.sol'; +import {IL2BridgeExecutor} from '../src/interfaces/IL2BridgeExecutor.sol'; + +import {AddressAliasHelper} from '../src/dependencies/arbitrum/AddressAliasHelper.sol'; +import {CrosschainForwarderArbitrum} from '../src/forwarders/CrosschainForwarderArbitrum.sol'; + +import {PayloadWithEmit} from './mocks/PayloadWithEmit.sol'; + +/** + * This test covers syncing between mainnet and arbitrum. + */ +contract ArbitrumCrossChainForwarderTest is ProtocolV3TestBase { + event TestEvent(); + + // the identifiers of the forks + uint256 mainnetFork; + uint256 arbitrumFork; + + PayloadWithEmit public payloadWithEmit; + + IInbox public constant INBOX = IInbox(0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f); + + address public constant ARBITRUM_BRIDGE_EXECUTOR = AaveGovernanceV2.ARBITRUM_BRIDGE_EXECUTOR; + + uint256 public constant MESSAGE_LENGTH = 580; + + CrosschainForwarderArbitrum public forwarder; + + function setUp() public { + mainnetFork = vm.createSelectFork(getChain('mainnet').rpcUrl, 16128510); + forwarder = new CrosschainForwarderArbitrum(ARBITRUM_BRIDGE_EXECUTOR); + arbitrumFork = vm.createSelectFork(getChain('arbitrum_one').rpcUrl, 124495315); + payloadWithEmit = new PayloadWithEmit(); + } + + // utility to transform memory to calldata so array range access is available + function _cutBytes(bytes calldata input) public pure returns (bytes calldata) { + return input[64:]; + } + + function testHasSufficientGas() public { + vm.selectFork(mainnetFork); + assertEq(AaveGovernanceV2.SHORT_EXECUTOR.balance, 0); + (bool hasEnoughGasBefore, ) = forwarder.hasSufficientGasForExecution(AaveGovernanceV2.SHORT_EXECUTOR, 580); + assertEq(hasEnoughGasBefore, false); + deal(address(AaveGovernanceV2.SHORT_EXECUTOR), 0.001 ether); + (bool hasEnoughGasAfter, ) = forwarder.hasSufficientGasForExecution(AaveGovernanceV2.SHORT_EXECUTOR, 580); + assertEq(hasEnoughGasAfter, true); + } + + function testgetGetMaxSubmissionCost() public { + vm.selectFork(mainnetFork); + (uint256 maxSubmission, ) = forwarder.getRequiredGas(580); + assertGt(maxSubmission, 0); + } + + function testProposalE2E() public { + // assumes the short exec will be topped up with some eth to pay for l2 fee + vm.selectFork(mainnetFork); + deal(address(AaveGovernanceV2.SHORT_EXECUTOR), 0.001 ether); + + // 1. create l1 proposal + vm.startPrank(AaveMisc.ECOSYSTEM_RESERVE); + GovHelpers.Payload[] memory payloads = new GovHelpers.Payload[](1); + payloads[0] = GovHelpers.Payload({ + target: address(forwarder), + value: 0, + signature: 'execute(address)', + callData: abi.encode(address(payloadWithEmit)), + withDelegatecall: true + }); + + uint256 proposalId = GovHelpers.createProposal( + payloads, + 0xec9d2289ab7db9bfbf2b0f2dd41ccdc0a4003e9e0d09e40dee09095145c63fb5 + ); + vm.stopPrank(); + + // 2. execute proposal and record logs so we can extract the emitted StateSynced event + vm.recordLogs(); + bytes memory payload = forwarder.getEncodedPayload(address(payloadWithEmit)); + + (uint256 maxSubmission, ) = forwarder.getRequiredGas(580); + // check ticket is created correctly + vm.expectCall( + address(INBOX), + abi.encodeCall( + IInbox.unsafeCreateRetryableTicket, + ( + ARBITRUM_BRIDGE_EXECUTOR, + 0, + maxSubmission, + forwarder.ARBITRUM_BRIDGE_EXECUTOR(), + forwarder.ARBITRUM_GUARDIAN(), + forwarder.L2_GAS_LIMIT(), + forwarder.L2_MAX_FEE_PER_GAS(), + payload + ) + ) + ); + GovHelpers.passVoteAndExecute(vm, proposalId); + + // check events are emitted correctly + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(keccak256('InboxMessageDelivered(uint256,bytes)'), entries[3].topics[0]); + // uint256 messageId = uint256(entries[3].topics[1]); + ( + address to, + uint256 callvalue, + uint256 value, + uint256 maxSubmissionCost, + address excessFeeRefundAddress, + address callValueRefundAddress, + uint256 maxGas, + uint256 gasPriceBid, + uint256 length + ) = abi.decode( + this._cutBytes(entries[3].data), + (address, uint256, uint256, uint256, address, address, uint256, uint256, uint256) + ); + assertEq(callvalue, 0); + assertEq(value > 0, true); + assertEq(maxSubmissionCost > 0, true); + assertEq(to, ARBITRUM_BRIDGE_EXECUTOR); + assertEq(excessFeeRefundAddress, ARBITRUM_BRIDGE_EXECUTOR); + assertEq(callValueRefundAddress, forwarder.ARBITRUM_GUARDIAN()); + assertEq(maxGas, forwarder.L2_GAS_LIMIT()); + assertEq(gasPriceBid, forwarder.L2_MAX_FEE_PER_GAS()); + assertEq(length, 580); + + // 3. mock the queuing on l2 with the data emitted on InboxMessageDelivered + vm.selectFork(arbitrumFork); + vm.startPrank(AddressAliasHelper.applyL1ToL2Alias(AaveGovernanceV2.SHORT_EXECUTOR)); + ARBITRUM_BRIDGE_EXECUTOR.call(payload); + vm.stopPrank(); + // 4. execute the proposal + vm.expectEmit(true, true, true, true); + emit TestEvent(); + GovHelpers.executeLatestActionSet(vm, ARBITRUM_BRIDGE_EXECUTOR); + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index c0dfa7d..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console2} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function testIncrement() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testSetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/OptimismCrossChainForwarderTest.t.sol b/test/OptimismCrossChainForwarderTest.t.sol new file mode 100644 index 0000000..d29c698 --- /dev/null +++ b/test/OptimismCrossChainForwarderTest.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; + +import {AaveV3Optimism, AaveMisc} from 'aave-address-book/AaveAddressBook.sol'; +import {AaveGovernanceV2, IExecutorWithTimelock} from 'aave-address-book/AaveGovernanceV2.sol'; + +import {GovHelpers} from 'aave-helpers/GovHelpers.sol'; +import {ProtocolV3TestBase, ReserveConfig, ReserveTokens, IERC20} from 'aave-helpers/ProtocolV3TestBase.sol'; + +import {IL2CrossDomainMessenger} from '../src/dependencies/optimism/interfaces/IL2CrossDomainMessenger.sol'; + +import {AddressAliasHelper} from '../src/dependencies/arbitrum/AddressAliasHelper.sol'; +import {CrosschainForwarderOptimism} from '../src/forwarders/CrosschainForwarderOptimism.sol'; + +import {PayloadWithEmit} from './mocks/PayloadWithEmit.sol'; + +/** + * This test covers syncing between mainnet and optimism. + */ +contract OptimismCrossChainForwarderTest is ProtocolV3TestBase { + event TestEvent(); + // the identifiers of the forks + uint256 mainnetFork; + uint256 optimismFork; + + address public constant OPTIMISM_BRIDGE_EXECUTOR = AaveGovernanceV2.OPTIMISM_BRIDGE_EXECUTOR; + + IL2CrossDomainMessenger public OVM_L2_CROSS_DOMAIN_MESSENGER = + IL2CrossDomainMessenger(0x4200000000000000000000000000000000000007); + + PayloadWithEmit public payloadWithEmit; + + CrosschainForwarderOptimism public forwarder; + + function setUp() public { + mainnetFork = vm.createSelectFork(getChain('mainnet').rpcUrl, 15783218); + forwarder = new CrosschainForwarderOptimism(OPTIMISM_BRIDGE_EXECUTOR); + optimismFork = vm.createSelectFork(getChain('optimism').rpcUrl, 30264427); + payloadWithEmit = new PayloadWithEmit(); + } + + function testProposalE2E() public { + // 1. create l1 proposal + vm.selectFork(mainnetFork); + vm.startPrank(AaveMisc.ECOSYSTEM_RESERVE); + GovHelpers.Payload[] memory payloads = new GovHelpers.Payload[](1); + payloads[0] = GovHelpers.Payload({ + value: 0, + withDelegatecall: true, + target: address(forwarder), + signature: 'execute(address)', + callData: abi.encode(address(payloadWithEmit)) + }); + + uint256 proposalId = GovHelpers.createProposal( + payloads, + 0x7ecafb3b0b7e418336cccb0c82b3e25944011bf11e41f8dc541841da073fe4f1 + ); + vm.stopPrank(); + + // 2. execute proposal and record logs so we can extract the emitted StateSynced event + vm.recordLogs(); + GovHelpers.passVoteAndExecute(vm, proposalId); + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(keccak256('SentMessage(address,address,bytes,uint256,uint256)'), entries[3].topics[0]); + assertEq(address(uint160(uint256(entries[3].topics[1]))), OPTIMISM_BRIDGE_EXECUTOR); + (address sender, bytes memory message, uint256 nonce) = abi.decode( + entries[3].data, + (address, bytes, uint256) + ); + + // 3. mock the receive on l2 with the data emitted on StateSynced + vm.selectFork(optimismFork); + vm.startPrank(0x36BDE71C97B33Cc4729cf772aE268934f7AB70B2); // AddressAliasHelper.applyL1ToL2Alias on L1_CROSS_DOMAIN_MESSENGER_ADDRESS + OVM_L2_CROSS_DOMAIN_MESSENGER.relayMessage(OPTIMISM_BRIDGE_EXECUTOR, sender, message, nonce); + vm.stopPrank(); + + // 4. execute proposal on l2 + vm.expectEmit(true, true, true, true); + emit TestEvent(); + GovHelpers.executeLatestActionSet(vm, OPTIMISM_BRIDGE_EXECUTOR); + } +} diff --git a/test/mocks/PayloadWithEmit.sol b/test/mocks/PayloadWithEmit.sol new file mode 100644 index 0000000..911d6b5 --- /dev/null +++ b/test/mocks/PayloadWithEmit.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IPoolConfigurator, ConfiguratorInputTypes} from 'aave-address-book/AaveV3.sol'; + +interface IProposalGenericExecutor { + function execute() external; +} + +/** + * @dev This payload simply emits an event on execution + */ +contract PayloadWithEmit is IProposalGenericExecutor { + event TestEvent(); + + function execute() external { + emit TestEvent(); + } +}