├── .env.sample ├── .gas-snapshot ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── CONTRIBUTING.md ├── DEVELOP.md ├── LICENSE ├── README.md ├── audits └── 2023-11-13-ToB.pdf ├── foundry.toml ├── remappings.txt ├── slither.config.json ├── src ├── MultiBridgeMessageReceiver.sol ├── MultiBridgeMessageSender.sol ├── adapters │ ├── BaseReceiverAdapter.sol │ ├── BaseSenderAdapter.sol │ ├── axelar │ │ ├── AxelarReceiverAdapter.sol │ │ ├── AxelarSenderAdapter.sol │ │ ├── interfaces │ │ │ ├── IAxelarExecutable.sol │ │ │ ├── IAxelarGasService.sol │ │ │ └── IAxelarGateway.sol │ │ └── libraries │ │ │ └── StringAddressConversion.sol │ └── wormhole │ │ ├── WormholeReceiverAdapter.sol │ │ └── WormholeSenderAdapter.sol ├── controllers │ ├── GAC.sol │ ├── GovernanceTimelock.sol │ ├── MessageReceiverGAC.sol │ └── MessageSenderGAC.sol ├── interfaces │ ├── EIP5164 │ │ ├── MessageDispatcher.sol │ │ ├── MessageExecutor.sol │ │ └── SingleMessageDispatcher.sol │ ├── IMultiBridgeMessageReceiver.sol │ ├── adapters │ │ ├── IMessageReceiverAdapter.sol │ │ └── IMessageSenderAdapter.sol │ └── controllers │ │ ├── IGAC.sol │ │ └── IGovernanceTimelock.sol └── libraries │ ├── EIP5164 │ └── ExecutorAware.sol │ ├── Error.sol │ ├── Message.sol │ ├── TypeCasts.sol │ └── Types.sol └── test ├── Setup.t.sol ├── contracts-mock ├── FailingSenderAdapter.sol ├── MockUniswapReceiver.sol ├── ZeroAddressReceiverGAC.sol └── adapters │ └── axelar │ └── MockAxelarGateway.sol ├── integration-tests ├── GracePeriodExpiry.t.sol ├── MultiMessageAggregation.t.sol ├── RemoteAdapterAdd.t.sol ├── RemoteAdapterRemove.t.sol ├── RemoteSetQuorum.t.sol ├── RemoteTimelockUpdate.t.sol └── TimelockCheck.t.sol └── unit-tests ├── GovernanceTimelock.t.sol ├── MessageReceiverGAC.t.sol ├── MessageSenderGAC.t.sol ├── MultiBridgeMessageReceiver.t.sol ├── MultiBridgeMessageSender.t.sol ├── adapters ├── BaseSenderAdapter.t.sol ├── axelar │ ├── AxelarReceiverAdapter.t.sol │ ├── AxelarSenderAdapter.t.sol │ └── libraries │ │ └── StringAddressConversion.t.sol └── wormhole │ ├── WormholeReceiverAdapter.t.sol │ └── WormholeSenderAdapter.t.sol └── libraries ├── EIP5164 └── ExecutorAware.t.sol ├── Message.t.sol └── TypeCasts.t.sol /.env.sample: -------------------------------------------------------------------------------- 1 | FOUNDRY_PROFILE=local 2 | ETH_FORK_URL= 3 | BSC_FORK_URL= 4 | POLYGON_FORK_URL= -------------------------------------------------------------------------------- /.gas-snapshot: -------------------------------------------------------------------------------- 1 | GracePeriodExpiry:test_timelockCheck() (gas: 1003057) 2 | MMA:test_mma_send_receive() (gas: 1052240) 3 | TimelockCheck:test_timelockCheck() (gas: 1058634) -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push] 4 | 5 | jobs: 6 | foundry-lint-check: 7 | name: Foundry Lint Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v2 # Updated to v2 12 | 13 | - name: Setup Foundry Toolchain 14 | uses: foundry-rs/foundry-toolchain@v1 15 | with: 16 | version: nightly 17 | 18 | - name: Install Dependencies 19 | run: forge install 20 | 21 | - name: Run Linter 22 | run: forge fmt --check 23 | env: 24 | ETH_FORK_URL: ${{ secrets.ETH_FORK_URL }} 25 | BSC_FORK_URL: ${{ secrets.BSC_FORK_URL }} 26 | POLYGON_FORK_URL: ${{ secrets.POLYGON_FORK_URL }} 27 | ARB_FORK_URL: ${{ secrets.ARB_FORK_URL }} 28 | 29 | foundry-testing: 30 | name: Foundry Testing 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v2 # Updated to v2 35 | 36 | - name: Setup Foundry Toolchain 37 | uses: foundry-rs/foundry-toolchain@v1 38 | with: 39 | version: nightly 40 | 41 | - name: Install Dependencies 42 | run: forge install 43 | 44 | - name: Run Size Check 45 | run: forge build --sizes 46 | env: 47 | ETH_FORK_URL: ${{ secrets.ETH_FORK_URL }} 48 | BSC_FORK_URL: ${{ secrets.BSC_FORK_URL }} 49 | POLYGON_FORK_URL: ${{ secrets.POLYGON_FORK_URL }} 50 | ARB_FORK_URL: ${{ secrets.ARB_FORK_URL }} 51 | 52 | - name: Run Tests 53 | run: forge test -vvv 54 | env: 55 | ETH_FORK_URL: ${{ secrets.ETH_FORK_URL }} 56 | BSC_FORK_URL: ${{ secrets.BSC_FORK_URL }} 57 | POLYGON_FORK_URL: ${{ secrets.POLYGON_FORK_URL }} 58 | ARB_FORK_URL: ${{ secrets.ARB_FORK_URL }} 59 | 60 | - name: Run Coverage 61 | run: forge coverage 62 | env: 63 | ETH_FORK_URL: ${{ secrets.ETH_FORK_URL }} 64 | BSC_FORK_URL: ${{ secrets.BSC_FORK_URL }} 65 | POLYGON_FORK_URL: ${{ secrets.POLYGON_FORK_URL }} 66 | ARB_FORK_URL: ${{ secrets.ARB_FORK_URL }} 67 | 68 | slither: 69 | name: Slither Run 70 | runs-on: ubuntu-latest 71 | steps: 72 | - name: Checkout repository 73 | uses: actions/checkout@v2 # Updated to v2 74 | 75 | - name: Install Foundry 76 | uses: foundry-rs/foundry-toolchain@v1 77 | 78 | - name: Build the contracts 79 | run: forge build --build-info 80 | 81 | - name: Run Slither 82 | uses: crytic/slither-action@v0.3.0 83 | id: slither 84 | with: 85 | fail-on: high 86 | ignore-compile: true 87 | solc-version: 0.8.19 88 | slither-args: --checklist --markdown-root ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/ 89 | slither-config: "slither.config.json" 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | bin/ 3 | build/ 4 | cache/ 5 | out/ 6 | 7 | *.pid 8 | *.log 9 | *.new 10 | *.bak 11 | 12 | .DS_Store 13 | 14 | .vscode/ 15 | .idea/ 16 | .env 17 | solc-input-*.json 18 | flattened-*.sol 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/openzeppelin/openzeppelin-contracts 7 | [submodule "lib/wormhole-solidity-sdk"] 8 | path = lib/wormhole-solidity-sdk 9 | url = https://github.com/wormhole-foundation/wormhole-solidity-sdk 10 | [submodule "lib/pigeon"] 11 | path = lib/pigeon 12 | url = https://github.com/exp-table/pigeon 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | Thank you for your interest in contributing to MMA! We welcome all contributions that improve our project. Before getting started, please ensure you've read our project's [Gitbook documentation](https://multi-message-aggregation.gitbook.io/multi-message-aggregation/). Should you have any questions, feel free to submit an issue in the repository or reach out to one of the maintainers directly. 3 | 4 | ## Reporting issues 5 | To report a minor bug or issue, please [open an issue] in the repository, providing as much detail as possible to help reproduce the issue. Feel free to create and submit a pull request to rectify minor issues you come across such as documentation errors, typos, or minor bugs. 6 | 7 | ## Code contribution 8 | For feature requests or significant changes or enhancements to the protocol, please first [submit an issue](#reporting-issues) to the repository for discussion. This will allow us to discuss your proposed changes and ensure they align with the project's goals before you begin working on a pull request. 9 | 10 | If you want to contribute code or documentation to the project, please follow these guidelines: 11 | 12 | 1. Fork the project repository and clone it to your local machine. 13 | 1. Create a new branch for your changes. 14 | 1. Make your changes and test them thoroughly. 15 | 1. Ensure that your changes are well-documented. 16 | 1. Create a pull request and explain your changes in detail. 17 | 1. Code review 18 | 1. All code changes will be reviewed by the project maintainers. The maintainers may ask for additional changes, and once the changes have been approved, they will be merged into the main branch. 19 | 20 | **Testing**: All code changes must be thoroughly tested. Please ensure that your tests cover all new functionality and edge cases. Use the [forge coverage tool](https://book.getfoundry.sh/reference/forge/forge-coverage) to ensure that changes have adequate code coverage. 21 | 22 | **License**: By contributing to the project, you agree that your contributions will be licensed under the project's [LICENSE](https://github.com/MultiMessageAggregation/multibridge/blob/main/LICENSE). 23 | -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | # Local Development 2 | **Pre-requisites:** 3 | - Install the [Foundry](https://github.com/foundry-rs/foundry) toolkit. 4 | - Clone repository: 5 | ```sh 6 | $ git clone https://github.com/MultiMessageAggregation/multibridge 7 | ``` 8 | **Step 1:** Install required forge submodules 9 | 10 | ```sh 11 | $ forge install 12 | ``` 13 | 14 | **Step 2:** Build the project 15 | 16 | ```sh 17 | $ forge build 18 | ``` 19 | 20 | **Step 3:** Run Tests 21 | 22 | To run the tests, you will need a local fork of Ethereum, Polygon, and BSC mainnet states. To accomplish this, you must specify RPC endpoints for each of these networks. You can obtain RPC endpoints to use for Ethereum and Polygon, from Alchemy, Infura, or other infrastructure providers. For BSC, you can choose from a list of public RPC endpoints available [here](https://docs.bscscan.com/misc-tools-and-utilities/public-rpc-nodes). 23 | 24 | To set the RPC endpoints, make a copy of the `.env.sample` file and name it `.env`. The file contains a list of parameter names (e.g. `ETH_FORK_URL`) that correspond to each network. Set the respective values of each of these parameters to the RPC endpoints you wish to use. 25 | 26 | Once you have set these values, you can run both the unit and integration tests using the following command: 27 | 28 | ```sh 29 | $ forge test 30 | ``` 31 | **note:** We use [pigeon](https://github.com/exp-table/pigeon/tree/docs) to simulate the cross-chain behavior on forked mainnets. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi-bridge Message Aggregation (MMA) 2 | 3 | > Additional Technical documentation can be found on [Gitbook here.](https://multi-message-aggregation.gitbook.io/multi-message-aggregation/) 4 | 5 | ## Introduction 6 | Multi-bridge Message Aggregation (MMA) is an additive security module for cross-chain communication across different EVM chains. It uses multiple [Arbitrary Messaging Bridges](https://blog.li.fi/navigating-arbitrary-messaging-bridges-a-comparison-framework-8720f302e2aa) to send messages, rather than relying on a single AMB. 7 | The protocol can be configured to withstand the failure of a subset of AMBs. This significantly improves the security and resilience of cross-chain communication. 8 | 9 | Specifically, if a subset of AMBs, below the configured failure threshold, is compromised or fails, invalid messages will not be executed on the target chain, and the protocol will continue to operate without disruptions. This improves the [safety and liveness properties](https://crosschainriskframework.github.io/framework/20categories/20architecture/architecture/#messaging-protocol) of the protocol. The protocol offers the following benefits: 10 | 11 | 1. **Increased Safety Guarantees** by verifying cross-chain messages across multiple bridges. 12 | 2. **Improve Liveness and Censorship Resistance** guarantees by providing redundancy through multiple bridges. 13 | 3. **Increase Flexibility** by allowing dApps a more seamless integration with new cross-chain protocols and a less disruptive phasing-out of defunct protocols over time. 14 | 15 | ## Design 16 | The design of MMA is guided by the following goals: 17 | - **Simplicity**: MMA provides a thin layer of abstraction over existing cross-chain protocols, adding minimal complexity and significantly reducing implementation risk. 18 | - **Extensibility**: The design of MMA allows for the seamless integration of new AMBs, and accommodates changes to the design and implementation of AMBs without requiring modifications to the core protocol. 19 | - **Flexibility**: The protocol offers flexibility, enabling dApps to choose the set of bridges and validation parameters to use for different chains and for specific messages. 20 | 21 | The protocol comprises core contracts and adapters. The core contracts implement the protocol's central logic, which includes sending and receiving messages across chains using multiple bridges for enhanced security and resilience. Adapters facilitate interaction between the core components and specific AMBs. They follow a [standard interface, EIP-5164](https://eips.ethereum.org/EIPS/eip-5164), for easy integration with the core protocol. The core contracts are designed for simplicity, while the adapters are designed for flexibility. This design approach allows for easy integration of new AMBs. It also accommodates changes in the design and implementation details of AMBs without necessitating modifications to the core protocol. 22 | 23 | ## Features 24 | **The current version of MMA is specifically tailored to address the cross-chain governance use case of the Uniswap protocol.** 25 | As a result, the capabilities are intentionally in the following two ways: 26 | 1. Uni-directional communication: Only a single sender chain is supported, allowing communication solely from the designated sender chain to other recipient chains. 27 | 2. Only EVM Chains are supported. 28 | 3. Timelock on destination chain. Messages are executed on the destination chain only after a specified timelock period has elapsed. 29 | 30 | The core features of the protocol are: 31 | 1. Sending and receive messages across EVM chains using multiple bridges. 32 | 1. Adding and removing bridges. 33 | 1. Configuring message validation parameters. 34 | 1. Administering adapter-specific parameters. 35 | 36 | An [independent fork](https://github.com/lifinance/MMAxERC20) of this repository, created by [Li.Fi](https://li.fi/), is expanding the current capabilities of MMA to support a broader range of cross-chain interaction patterns, additional use-cases, and non-EVM chains. 37 | 38 | ## Life-cycle of a Message 39 | The diagram below illustrates a typical scenario for the MMA protocol in the context of Uniswap's cross-chain governance workflow. 40 | 41 | The Uniswap DAO wants to send a governance action message to a remote chain for execution. An example of such a message could be changing fee parameters on the Uniswap deployment on the destination chain. The life cycle of a cross-chain governance transaction proceeds as follows, once it has passed the standard process of on-chain voting and time-lock queue on the governance chain (Ethereum): 42 | 1. The governance message is sent from the Uniswap V2 Timelock contract to the [`MultiBridgeMessageSender`](src/MultiBridgeMessageSender.sol) contract 43 | 1. The [`MultiBridgeMessageSender`](src/MultiBridgeMessageSender.sol) sends the message to all available AMB sender adapters (a caller could choose to exclude one or more AMBs in this process) 44 | 1. The AMB sender adapters send the message to AMB-specific components to relay the message to the intended destination. The adapters implement a common interface, [`IMessageSenderAdapter`](src/interfaces/adapters/IMessageSenderAdapter.sol), which allows the [`MultiBridgeMessageSender`](src/MultiBridgeMessageSender.sol) to interact with them in a uniform manner. 45 | 1. AMB receiver adapters receive the message from off-chain components (e.g. bridge validators or relayers) and forward them to the [`MultiBridgeMessageReceiver`](src/MultiBridgeMessageReceiver.sol) contract. 46 | 1. Once enough AMBs have relayed a specific message (i.e. a quorum has been achieved), anyone can call `scheduleMessageExecution()` on the [`MultiBridgeMessageReceiver`](src/MultiBridgeMessageReceiver.sol) contract which then queues the message for execution on the governance timelock. 47 | 1. Once a configured delay period has elapsed on the governance timelock, anyone can execute a time-locked message, which performs the intended execution on the target contract on the destination chain. 48 | 49 | ![Illustration of ](https://314948482-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyWOfgotvwuIBhzylK0ud%2Fuploads%2FYrd16Z8BdyejNqvCF5eO%2FScreenshot%202023-09-25%20at%207.57.32%20pm.png?alt=media&token=eb3ef911-1f44-4657-b234-8acbd55ddf1c) 50 | 51 | ## Contracts 52 | ``` 53 | src 54 | ├── MultiBridgeMessageReceiver.sol 55 | ├── MultiBridgeMessageSender.sol 56 | ├── adapters 57 | │   ├── BaseReceiverAdapter.sol 58 | │   ├── BaseSenderAdapter.sol 59 | │   ├── axelar 60 | │   │   ├── AxelarReceiverAdapter.sol 61 | │   │   ├── AxelarSenderAdapter.sol 62 | │   │   ├── interfaces 63 | │   │   │   ├── IAxelarExecutable.sol 64 | │   │   │   ├── IAxelarGasService.sol 65 | │   │   │   └── IAxelarGateway.sol 66 | │   │   └── libraries 67 | │   │   └── StringAddressConversion.sol 68 | │   └── wormhole 69 | │   ├── WormholeReceiverAdapter.sol 70 | │   └── WormholeSenderAdapter.sol 71 | ├── controllers 72 | │   ├── GAC.sol 73 | │   ├── GovernanceTimelock.sol 74 | │   ├── MessageReceiverGAC.sol 75 | │   └── MessageSenderGAC.sol 76 | ├── interfaces 77 | │   ├── EIP5164 78 | │   │   ├── MessageDispatcher.sol 79 | │   │   ├── MessageExecutor.sol 80 | │   │   └── SingleMessageDispatcher.sol 81 | │   ├── IMultiBridgeMessageReceiver.sol 82 | │   ├── adapters 83 | │   │   ├── IMessageReceiverAdapter.sol 84 | │   │   └── IMessageSenderAdapter.sol 85 | │   └── controllers 86 | │   ├── IGAC.sol 87 | │   └── IGovernanceTimelock.sol 88 | └── libraries 89 | ├── EIP5164 90 | │   └── ExecutorAware.sol 91 | ├── Error.sol 92 | ├── Message.sol 93 | ├── TypeCasts.sol 94 | └── Types.sol 95 | ``` 96 | 97 | 98 | ## Development 99 | Refer to the [Development Guide](./DEVELOP.md) for instructions on how to set up a development environment and run tests. 100 | 101 | ## Contributing 102 | Thank you for your interest in contributing to MMA! Please refer to our [Contributing Guidelines](./CONTRIBUTING.md) for more information. By contributing to the project, you agree that your contributions will be licensed under the project's [LICENSE](./LICENSE). -------------------------------------------------------------------------------- /audits/2023-11-13-ToB.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiMessageAggregation/multibridge/d1209977ff0592ae7048a7d1809413846a426a8d/audits/2023-11-13-ToB.pdf -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | solc = "0.8.19" 6 | optimizer = false 7 | runs = 100 8 | viaIR = false 9 | 10 | [profile.prod] 11 | src = "src" 12 | out = "out" 13 | libs = ["lib"] 14 | solc = "0.8.19" 15 | optimizer = true 16 | runs = 100 17 | viaIR = true 18 | 19 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin/=lib/openzeppelin-contracts/ 2 | pigeon/=lib/pigeon/src 3 | wormhole-solidity-sdk/=lib/wormhole-solidity-sdk/src/ -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "detectors_to_exclude": "timestamp,naming-convention,pragma,solc-version,uninitialized-local,constable-states,unused-return, arbitrary-send-eth", 3 | "filter_paths": "(mocks/|test/|lib/)" 4 | } -------------------------------------------------------------------------------- /src/MultiBridgeMessageReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// interfaces 5 | import "./interfaces/controllers/IGAC.sol"; 6 | import "./interfaces/adapters/IMessageReceiverAdapter.sol"; 7 | import "./interfaces/IMultiBridgeMessageReceiver.sol"; 8 | import "./libraries/EIP5164/ExecutorAware.sol"; 9 | import "./interfaces/controllers/IGovernanceTimelock.sol"; 10 | 11 | /// libraries 12 | import "./libraries/Error.sol"; 13 | import "./libraries/Message.sol"; 14 | 15 | /// @title Multi-bridge message receiver. 16 | /// @notice This contract is deployed on each destination chain, and receives messages sent by the MultiBridgeMessageSender 17 | /// contract on the source chain, through multiple bridge adapters. A message is considered valid and can be executed 18 | /// if it has been delivered by enough trusted bridge receiver adapters (i.e. has achieved a configured quorum threshold), 19 | /// before the message's expiration. If a message is successfully validated in time, it is queued for execution on the 20 | /// governance timelock contract. 21 | /// @dev The contract only accepts messages from trusted bridge receiver adapters, each of which implements the 22 | /// IMessageReceiverAdapter interface. 23 | contract MultiBridgeMessageReceiver is IMultiBridgeMessageReceiver, ExecutorAware { 24 | using MessageLibrary for MessageLibrary.Message; 25 | using MessageLibrary for MessageLibrary.MessageExecutionParams; 26 | 27 | /// @notice the id of the source chain that this contract can receive messages from 28 | uint256 public immutable srcChainId; 29 | /// @notice the global access control contract 30 | IGAC public immutable gac; 31 | 32 | /*///////////////////////////////////////////////////////////////// 33 | STATE VARIABLES 34 | ////////////////////////////////////////////////////////////////*/ 35 | 36 | /// @notice minimum number of bridges that must deliver a message for it to be considered valid 37 | uint64 public quorum; 38 | 39 | /// @notice the address of governance timelock contract on the same chain, that a message will be forwarded to for execution 40 | address public governanceTimelock; 41 | 42 | /// @notice maintains which bridge adapters have delivered each message 43 | mapping(bytes32 msgId => mapping(address receiverAdapter => bool delivered)) public msgDeliveries; 44 | 45 | /// @notice count of bridge adapters that have delivered each message 46 | mapping(bytes32 msgId => uint256 deliveryCount) public msgDeliveryCount; 47 | 48 | /// @notice the hash of the params required for executing a message 49 | mapping(bytes32 msgId => bytes32 execParamsHash) public msgExecParamsHash; 50 | 51 | /// @notice whether a message has been sent to the governance timelock for execution 52 | mapping(bytes32 msgId => bool scheduled) public isExecutionScheduled; 53 | 54 | /*///////////////////////////////////////////////////////////////// 55 | MODIFIERS 56 | ////////////////////////////////////////////////////////////////*/ 57 | 58 | /// @notice Checks whether the caller is a trusted bridge receiver adapter 59 | modifier onlyReceiverAdapter() { 60 | if (!isTrustedExecutor(msg.sender)) { 61 | revert Error.INVALID_RECEIVER_ADAPTER(); 62 | } 63 | _; 64 | } 65 | 66 | /// @notice Restricts the caller to the owner configured in GAC. 67 | modifier onlyGlobalOwner() { 68 | if (!gac.isGlobalOwner(msg.sender)) { 69 | revert Error.CALLER_NOT_OWNER(); 70 | } 71 | _; 72 | } 73 | 74 | /*///////////////////////////////////////////////////////////////// 75 | CONSTRUCTOR 76 | ////////////////////////////////////////////////////////////////*/ 77 | 78 | /// @notice sets the initial parameters 79 | constructor(uint256 _srcChainId, address _gac, address[] memory _receiverAdapters, uint64 _quorum) { 80 | if (_srcChainId == 0) { 81 | revert Error.INVALID_SENDER_CHAIN_ID(); 82 | } 83 | if (_gac == address(0)) { 84 | revert Error.ZERO_ADDRESS_INPUT(); 85 | } 86 | 87 | srcChainId = _srcChainId; 88 | gac = IGAC(_gac); 89 | 90 | for (uint256 i; i < _receiverAdapters.length;) { 91 | _updateReceiverAdapter(_receiverAdapters[i], true); 92 | unchecked { 93 | ++i; 94 | } 95 | } 96 | _updateQuorum(_quorum); 97 | } 98 | 99 | /*///////////////////////////////////////////////////////////////// 100 | EXTERNAL FUNCTIONS 101 | ////////////////////////////////////////////////////////////////*/ 102 | 103 | /// @notice receive messages from allowed bridge receiver adapters 104 | /// @param _message is the crosschain message sent by the mma sender 105 | function receiveMessage(MessageLibrary.Message calldata _message) external override onlyReceiverAdapter { 106 | if (_message.dstChainId != block.chainid) { 107 | revert Error.INVALID_DST_CHAIN(); 108 | } 109 | 110 | if (_message.target == address(0)) { 111 | revert Error.INVALID_TARGET(); 112 | } 113 | 114 | if (_message.srcChainId != srcChainId) { 115 | revert Error.INVALID_SENDER_CHAIN_ID(); 116 | } 117 | 118 | /// this msgId is totally different with each adapters' internal msgId(which is their internal nonce essentially) 119 | /// although each adapters' internal msgId is attached at the end of calldata, it's not useful to MultiBridgeMessageReceiver.sol. 120 | bytes32 msgId = _message.computeMsgId(); 121 | 122 | if (msgDeliveries[msgId][msg.sender]) { 123 | revert Error.DUPLICATE_MESSAGE_DELIVERY_BY_ADAPTER(); 124 | } 125 | 126 | /// @dev checks if msgId was already sent to the timelock for eventual execution 127 | if (isExecutionScheduled[msgId]) { 128 | revert Error.MSG_ID_ALREADY_SCHEDULED(); 129 | } 130 | 131 | msgDeliveries[msgId][msg.sender] = true; 132 | 133 | /// increment vote count for a message 134 | ++msgDeliveryCount[msgId]; 135 | 136 | /// stores the hash of the execution params required 137 | bytes32 prevStoredHash = msgExecParamsHash[msgId]; 138 | 139 | /// stores the message if the amb is the first one delivering the message 140 | if (prevStoredHash == bytes32(0)) { 141 | msgExecParamsHash[msgId] = _message.computeExecutionParamsHash(); 142 | } 143 | 144 | string memory bridgeName = IMessageReceiverAdapter(msg.sender).name(); 145 | emit BridgeMessageReceived(msgId, bridgeName, _message.nonce, msg.sender); 146 | } 147 | 148 | /// @inheritdoc IMultiBridgeMessageReceiver 149 | function scheduleMessageExecution(bytes32 _msgId, MessageLibrary.MessageExecutionParams calldata _execParams) 150 | external 151 | override 152 | { 153 | bytes32 execParamsHash = msgExecParamsHash[_msgId]; 154 | if (_execParams.computeExecutionParamsHash() != execParamsHash) { 155 | revert Error.EXEC_PARAMS_HASH_MISMATCH(); 156 | } 157 | 158 | /// @dev validates if msg execution is not beyond expiration 159 | if (block.timestamp > _execParams.expiration) { 160 | revert Error.MSG_EXECUTION_PASSED_DEADLINE(); 161 | } 162 | 163 | /// @dev checks if msgId was already sent to the timelock for eventual execution 164 | if (isExecutionScheduled[_msgId]) { 165 | revert Error.MSG_ID_ALREADY_SCHEDULED(); 166 | } 167 | 168 | isExecutionScheduled[_msgId] = true; 169 | 170 | /// @dev validates message quorum 171 | if (msgDeliveryCount[_msgId] < quorum) { 172 | revert Error.QUORUM_NOT_ACHIEVED(); 173 | } 174 | 175 | /// @dev queues the action on timelock for execution 176 | IGovernanceTimelock(governanceTimelock).scheduleTransaction( 177 | _execParams.target, _execParams.value, _execParams.callData 178 | ); 179 | 180 | emit MessageExecutionScheduled( 181 | _msgId, _execParams.target, _execParams.value, _execParams.nonce, _execParams.callData 182 | ); 183 | } 184 | 185 | /// @notice update the governance timelock contract. 186 | /// @dev called by admin to update the timelock contract 187 | function updateGovernanceTimelock(address _governanceTimelock) external onlyGlobalOwner { 188 | if (_governanceTimelock == address(0)) { 189 | revert Error.ZERO_GOVERNANCE_TIMELOCK(); 190 | } 191 | address oldGovernanceTimelock = governanceTimelock; 192 | governanceTimelock = _governanceTimelock; 193 | emit GovernanceTimelockUpdated(oldGovernanceTimelock, _governanceTimelock); 194 | } 195 | 196 | /// @notice Update bridge receiver adapters. 197 | /// @dev called by admin to update receiver bridge adapters on all other chains 198 | function updateReceiverAdapters(address[] calldata _receiverAdapters, bool[] calldata _operations) 199 | external 200 | override 201 | onlyGlobalOwner 202 | { 203 | _updateReceiverAdapters(_receiverAdapters, _operations); 204 | _validateQuorum(quorum); 205 | } 206 | 207 | /// @notice Update bridge receiver adapters and quorum 208 | /// @dev called by admin to update receiver bridge adapters on all other chains along with quorum 209 | function updateReceiverAdaptersAndQuorum( 210 | address[] calldata _receiverAdapters, 211 | bool[] calldata _operations, 212 | uint64 _newQuorum 213 | ) external override onlyGlobalOwner { 214 | _updateReceiverAdapters(_receiverAdapters, _operations); 215 | _updateQuorum(_newQuorum); 216 | } 217 | 218 | /// @notice Update power quorum threshold of message execution. 219 | function updateQuorum(uint64 _quorum) external override onlyGlobalOwner { 220 | _updateQuorum(_quorum); 221 | } 222 | 223 | /*///////////////////////////////////////////////////////////////// 224 | VIEW/READ-ONLY FUNCTIONS 225 | ////////////////////////////////////////////////////////////////*/ 226 | 227 | /// @notice View message info 228 | /// @return isExecutionScheduled is true if the message has been sent to the timelock for execution 229 | /// @return msgCurrentVotes is the number of bridges that have delivered the message 230 | /// @return successfulBridge is the list of bridges that have delivered the message 231 | function getMessageInfo(bytes32 _msgId) public view returns (bool, uint256, string[] memory) { 232 | uint256 msgCurrentVotes = msgDeliveryCount[_msgId]; 233 | string[] memory successfulBridge = new string[](msgCurrentVotes); 234 | 235 | if (msgCurrentVotes != 0) { 236 | uint256 currIndex; 237 | address[] memory executors = getTrustedExecutors(); 238 | for (uint256 i; i < executors.length;) { 239 | if (msgDeliveries[_msgId][executors[i]]) { 240 | successfulBridge[currIndex] = IMessageReceiverAdapter(executors[i]).name(); 241 | ++currIndex; 242 | } 243 | 244 | unchecked { 245 | ++i; 246 | } 247 | } 248 | } 249 | 250 | return (isExecutionScheduled[_msgId], msgCurrentVotes, successfulBridge); 251 | } 252 | 253 | /*///////////////////////////////////////////////////////////////// 254 | PRIVATE/INTERNAL FUNCTIONS 255 | ////////////////////////////////////////////////////////////////*/ 256 | 257 | function _updateQuorum(uint64 _quorum) private { 258 | _validateQuorum(_quorum); 259 | 260 | uint64 oldValue = quorum; 261 | 262 | quorum = _quorum; 263 | emit QuorumUpdated(oldValue, _quorum); 264 | } 265 | 266 | function _updateReceiverAdapters(address[] memory _receiverAdapters, bool[] memory _operations) private { 267 | uint256 len = _receiverAdapters.length; 268 | 269 | if (len == 0) { 270 | revert Error.ZERO_RECEIVER_ADAPTER(); 271 | } 272 | 273 | if (len != _operations.length) { 274 | revert Error.ARRAY_LENGTH_MISMATCHED(); 275 | } 276 | 277 | for (uint256 i; i < len;) { 278 | _updateReceiverAdapter(_receiverAdapters[i], _operations[i]); 279 | 280 | unchecked { 281 | ++i; 282 | } 283 | } 284 | } 285 | 286 | function _updateReceiverAdapter(address _receiverAdapter, bool _add) private { 287 | if (_receiverAdapter == address(0)) { 288 | revert Error.ZERO_ADDRESS_INPUT(); 289 | } 290 | bool success = _add ? _addTrustedExecutor(_receiverAdapter) : _removeTrustedExecutor(_receiverAdapter); 291 | 292 | if (!success) { 293 | // only fails because we are either attempting to add an existing adapter, or remove a non-existing adapter 294 | revert Error.UPDATE_RECEIVER_ADAPTER_FAILED(_add ? "adapter already added" : "adapter not found"); 295 | } 296 | 297 | emit BridgeReceiverAdapterUpdated(_receiverAdapter, _add); 298 | } 299 | 300 | function _validateQuorum(uint256 _quorum) private view { 301 | if (_quorum > trustedExecutorsCount() || _quorum == 0) { 302 | revert Error.INVALID_QUORUM_THRESHOLD(); 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/adapters/BaseReceiverAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | import "../interfaces/adapters/IMessageReceiverAdapter.sol"; 5 | import "../controllers/MessageReceiverGAC.sol"; 6 | 7 | abstract contract BaseReceiverAdapter is IMessageReceiverAdapter { 8 | MessageReceiverGAC public immutable receiverGAC; 9 | address public senderAdapter; 10 | 11 | modifier onlyGlobalOwner() { 12 | if (!receiverGAC.isGlobalOwner(msg.sender)) { 13 | revert Error.CALLER_NOT_OWNER(); 14 | } 15 | _; 16 | } 17 | 18 | constructor(address _receiverGAC) { 19 | if (_receiverGAC == address(0)) { 20 | revert Error.ZERO_ADDRESS_INPUT(); 21 | } 22 | receiverGAC = MessageReceiverGAC(_receiverGAC); 23 | } 24 | 25 | /// @inheritdoc IMessageReceiverAdapter 26 | function updateSenderAdapter(address _newSender) external override onlyGlobalOwner { 27 | if (_newSender == address(0)) { 28 | revert Error.ZERO_ADDRESS_INPUT(); 29 | } 30 | 31 | address oldSender = senderAdapter; 32 | senderAdapter = _newSender; 33 | 34 | emit SenderAdapterUpdated(oldSender, _newSender); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/adapters/BaseSenderAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | import "../libraries/Error.sol"; 5 | import "../interfaces/adapters/IMessageSenderAdapter.sol"; 6 | import "../controllers/MessageSenderGAC.sol"; 7 | 8 | abstract contract BaseSenderAdapter is IMessageSenderAdapter { 9 | MessageSenderGAC public immutable senderGAC; 10 | 11 | /*///////////////////////////////////////////////////////////////// 12 | STATE VARIABLES 13 | ////////////////////////////////////////////////////////////////*/ 14 | 15 | uint256 public nonce; 16 | mapping(uint256 => address) public receiverAdapters; 17 | 18 | /*///////////////////////////////////////////////////////////////// 19 | MODIFIER 20 | ////////////////////////////////////////////////////////////////*/ 21 | modifier onlyMultiBridgeMessageSender() { 22 | if (msg.sender != senderGAC.multiBridgeMessageSender()) { 23 | revert Error.CALLER_NOT_MULTI_MESSAGE_SENDER(); 24 | } 25 | _; 26 | } 27 | 28 | modifier onlyGlobalOwner() { 29 | if (!senderGAC.isGlobalOwner(msg.sender)) { 30 | revert Error.CALLER_NOT_OWNER(); 31 | } 32 | _; 33 | } 34 | 35 | /*///////////////////////////////////////////////////////////////// 36 | CONSTRUCTOR 37 | ////////////////////////////////////////////////////////////////*/ 38 | 39 | /// @param _senderGAC is the global access control contract 40 | constructor(address _senderGAC) { 41 | if (_senderGAC == address(0)) { 42 | revert Error.ZERO_ADDRESS_INPUT(); 43 | } 44 | 45 | senderGAC = MessageSenderGAC(_senderGAC); 46 | } 47 | 48 | /*///////////////////////////////////////////////////////////////// 49 | EXTERNAL FUNCTIONS 50 | ////////////////////////////////////////////////////////////////*/ 51 | 52 | /// @inheritdoc IMessageSenderAdapter 53 | function updateReceiverAdapter(uint256[] calldata _dstChainIds, address[] calldata _receiverAdapters) 54 | external 55 | override 56 | onlyGlobalOwner 57 | { 58 | uint256 arrLength = _dstChainIds.length; 59 | 60 | if (arrLength != _receiverAdapters.length) { 61 | revert Error.ARRAY_LENGTH_MISMATCHED(); 62 | } 63 | 64 | for (uint256 i; i < arrLength;) { 65 | address oldReceiver = receiverAdapters[_dstChainIds[i]]; 66 | receiverAdapters[_dstChainIds[i]] = _receiverAdapters[i]; 67 | emit ReceiverAdapterUpdated(_dstChainIds[i], oldReceiver, _receiverAdapters[i]); 68 | 69 | unchecked { 70 | ++i; 71 | } 72 | } 73 | } 74 | 75 | /*///////////////////////////////////////////////////////////////// 76 | HELPER FUNCTIONS 77 | ////////////////////////////////////////////////////////////////*/ 78 | 79 | /// @notice generates a new message id by incrementing nonce 80 | /// @param _receiverChainId is the destination chainId. 81 | /// @param _to is the contract address on the destination chain. 82 | function _getNewMessageId(uint256 _receiverChainId, address _to) internal returns (bytes32 messageId) { 83 | messageId = keccak256(abi.encodePacked(block.chainid, _receiverChainId, nonce, address(this), _to)); 84 | ++nonce; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/adapters/axelar/AxelarReceiverAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// local imports 5 | import "../../interfaces/adapters/IMessageReceiverAdapter.sol"; 6 | import "../../interfaces/IMultiBridgeMessageReceiver.sol"; 7 | import "../../libraries/Error.sol"; 8 | import "../../libraries/Types.sol"; 9 | import "../../libraries/Message.sol"; 10 | 11 | import "./interfaces/IAxelarGateway.sol"; 12 | import "./interfaces/IAxelarExecutable.sol"; 13 | import "./libraries/StringAddressConversion.sol"; 14 | 15 | import "../../controllers/MessageReceiverGAC.sol"; 16 | import "../BaseSenderAdapter.sol"; 17 | import "../BaseReceiverAdapter.sol"; 18 | 19 | /// @notice receiver adapter for axelar bridge 20 | contract AxelarReceiverAdapter is BaseReceiverAdapter, IAxelarExecutable { 21 | using StringAddressConversion for string; 22 | 23 | string public constant name = "AXELAR"; 24 | 25 | IAxelarGateway public immutable gateway; 26 | 27 | /*///////////////////////////////////////////////////////////////// 28 | STATE VARIABLES 29 | ////////////////////////////////////////////////////////////////*/ 30 | string public senderChainId; 31 | 32 | mapping(bytes32 => bool) public isMessageExecuted; 33 | mapping(bytes32 => bool) public commandIdStatus; 34 | 35 | /*///////////////////////////////////////////////////////////////// 36 | CONSTRUCTOR 37 | ////////////////////////////////////////////////////////////////*/ 38 | /// @param _gateway is axelar gateway contract address. 39 | /// @param _senderChainId is the chain id of the sender chain. 40 | /// @param _receiverGAC is global access controller. 41 | constructor(address _gateway, string memory _senderChainId, address _receiverGAC) 42 | BaseReceiverAdapter(_receiverGAC) 43 | { 44 | if (_gateway == address(0)) { 45 | revert Error.ZERO_ADDRESS_INPUT(); 46 | } 47 | 48 | if (bytes(_senderChainId).length == 0) { 49 | revert Error.INVALID_SENDER_CHAIN_ID(); 50 | } 51 | 52 | gateway = IAxelarGateway(_gateway); 53 | senderChainId = _senderChainId; 54 | } 55 | 56 | /*///////////////////////////////////////////////////////////////// 57 | EXTERNAL FUNCTIONS 58 | ////////////////////////////////////////////////////////////////*/ 59 | 60 | /// @dev accepts new cross-chain messages from axelar gateway 61 | /// @inheritdoc IAxelarExecutable 62 | function execute( 63 | bytes32 _commandId, 64 | string calldata _sourceChainId, 65 | string calldata _sourceAddress, 66 | bytes calldata _payload 67 | ) external override { 68 | /// @dev step-1: validate incoming chain id 69 | if (keccak256(bytes(_sourceChainId)) != keccak256(bytes(senderChainId))) { 70 | revert Error.INVALID_SENDER_CHAIN_ID(); 71 | } 72 | 73 | /// @dev step-2: validate the source address 74 | if (_sourceAddress.toAddress() != senderAdapter) { 75 | revert Error.INVALID_SENDER_ADAPTER(); 76 | } 77 | 78 | /// @dev step-3: validate the contract call 79 | if (!gateway.validateContractCall(_commandId, _sourceChainId, _sourceAddress, keccak256(_payload))) { 80 | revert Error.NOT_APPROVED_BY_GATEWAY(); 81 | } 82 | 83 | /// decode the cross-chain payload 84 | AdapterPayload memory decodedPayload = abi.decode(_payload, (AdapterPayload)); 85 | bytes32 msgId = decodedPayload.msgId; 86 | 87 | /// @dev step-4: check for duplicate message 88 | if (commandIdStatus[_commandId] || isMessageExecuted[msgId]) { 89 | revert MessageIdAlreadyExecuted(msgId); 90 | } 91 | 92 | /// @dev step-5: validate the receive adapter 93 | if (decodedPayload.receiverAdapter != address(this)) { 94 | revert Error.INVALID_RECEIVER_ADAPTER(); 95 | } 96 | 97 | /// @dev step-6: validate the destination 98 | if (decodedPayload.finalDestination != receiverGAC.multiBridgeMsgReceiver()) { 99 | revert Error.INVALID_FINAL_DESTINATION(); 100 | } 101 | 102 | isMessageExecuted[msgId] = true; 103 | commandIdStatus[_commandId] = true; 104 | 105 | MessageLibrary.Message memory _data = abi.decode(decodedPayload.data, (MessageLibrary.Message)); 106 | 107 | try IMultiBridgeMessageReceiver(decodedPayload.finalDestination).receiveMessage(_data) { 108 | emit MessageIdExecuted(_data.srcChainId, msgId); 109 | } catch (bytes memory lowLevelData) { 110 | revert MessageFailure(msgId, lowLevelData); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/adapters/axelar/AxelarSenderAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// local imports 5 | import "../BaseSenderAdapter.sol"; 6 | import "../../interfaces/controllers/IGAC.sol"; 7 | import "../../libraries/Error.sol"; 8 | import "../../libraries/Types.sol"; 9 | 10 | import "./interfaces/IAxelarGateway.sol"; 11 | import "./interfaces/IAxelarGasService.sol"; 12 | import "./libraries/StringAddressConversion.sol"; 13 | 14 | contract AxelarSenderAdapter is BaseSenderAdapter { 15 | /// @notice event emitted when a chain id mapping is updated 16 | event ChainIDMappingUpdated(uint256 indexed origId, string oldAxlId, string newAxlId); 17 | 18 | string public constant name = "AXELAR"; 19 | 20 | IAxelarGateway public immutable gateway; 21 | 22 | /*///////////////////////////////////////////////////////////////// 23 | STATE VARIABLES 24 | ////////////////////////////////////////////////////////////////*/ 25 | IAxelarGasService public immutable gasService; 26 | mapping(uint256 => string) public chainIdMap; 27 | 28 | /*///////////////////////////////////////////////////////////////// 29 | CONSTRUCTOR 30 | ////////////////////////////////////////////////////////////////*/ 31 | constructor(address _gasService, address _gateway, address _gac) BaseSenderAdapter(_gac) { 32 | if (_gasService == address(0) || _gateway == address(0)) { 33 | revert Error.ZERO_ADDRESS_INPUT(); 34 | } 35 | 36 | gasService = IAxelarGasService(_gasService); 37 | gateway = IAxelarGateway(_gateway); 38 | } 39 | 40 | /*///////////////////////////////////////////////////////////////// 41 | EXTERNAL FUNCTIONS 42 | ////////////////////////////////////////////////////////////////*/ 43 | 44 | /// @dev sendMessage sends a message to Axelar. 45 | /// @param _receiverChainId The ID of the destination chain. 46 | /// @param _to The address of the contract on the destination chain that will receive the message. 47 | /// @param _data The data to be included in the message. 48 | function dispatchMessage(uint256 _receiverChainId, address _to, bytes calldata _data) 49 | external 50 | payable 51 | override 52 | onlyMultiBridgeMessageSender 53 | returns (bytes32 msgId) 54 | { 55 | address receiverAdapter = receiverAdapters[_receiverChainId]; 56 | 57 | if (receiverAdapter == address(0)) { 58 | revert Error.ZERO_RECEIVER_ADAPTER(); 59 | } 60 | 61 | string memory destinationChain = chainIdMap[_receiverChainId]; 62 | 63 | if (bytes(destinationChain).length == 0) { 64 | revert Error.INVALID_DST_CHAIN(); 65 | } 66 | 67 | msgId = _getNewMessageId(_receiverChainId, _to); 68 | _callContract(destinationChain, receiverAdapter, msgId, _to, _data); 69 | 70 | emit MessageDispatched(msgId, msg.sender, _receiverChainId, _to, _data); 71 | } 72 | 73 | /// @dev maps the MMA chain id to bridge specific chain id 74 | /// @dev _origIds is the chain's native chain id 75 | /// @dev _axlIds are the bridge allocated chain id 76 | function setChainIdMap(uint256[] calldata _origIds, string[] calldata _axlIds) external onlyGlobalOwner { 77 | uint256 arrLength = _origIds.length; 78 | 79 | if (arrLength != _axlIds.length) { 80 | revert Error.ARRAY_LENGTH_MISMATCHED(); 81 | } 82 | 83 | for (uint256 i; i < arrLength;) { 84 | if (_origIds[i] == 0) { 85 | revert Error.ZERO_CHAIN_ID(); 86 | } 87 | 88 | string memory oldAxlId = chainIdMap[_origIds[i]]; 89 | chainIdMap[_origIds[i]] = _axlIds[i]; 90 | 91 | emit ChainIDMappingUpdated(_origIds[i], oldAxlId, _axlIds[i]); 92 | 93 | unchecked { 94 | ++i; 95 | } 96 | } 97 | } 98 | 99 | /*///////////////////////////////////////////////////////////////// 100 | HELPER FUNCTIONS 101 | ////////////////////////////////////////////////////////////////*/ 102 | 103 | /// @dev Sends a message to the IAxelarRelayer contract for relaying to the Axelar Network. 104 | /// @param _destinationChain The name of the destination chain. 105 | /// @param _receiverAdapter The address of the adapter on the destination chain that will receive the message. 106 | /// @param _msgId The ID of the message to be relayed. 107 | /// @param _multibridgeReceiver The address of the MultibridgeReceiver contract on the destination chain that will receive the message. 108 | /// @param _data The bytes data to pass to the contract on the destination chain. 109 | function _callContract( 110 | string memory _destinationChain, 111 | address _receiverAdapter, 112 | bytes32 _msgId, 113 | address _multibridgeReceiver, 114 | bytes calldata _data 115 | ) internal { 116 | string memory receiverAdapterInString = StringAddressConversion.toString(_receiverAdapter); 117 | bytes memory payload = 118 | abi.encode(AdapterPayload(_msgId, address(msg.sender), _receiverAdapter, _multibridgeReceiver, _data)); 119 | 120 | gasService.payNativeGasForContractCall{value: msg.value}( 121 | msg.sender, _destinationChain, receiverAdapterInString, payload, msg.sender 122 | ); 123 | 124 | gateway.callContract(_destinationChain, receiverAdapterInString, payload); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/adapters/axelar/interfaces/IAxelarExecutable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | interface IAxelarExecutable { 5 | /// @param _commandId is axelar specific message identifier 6 | /// @param _sourceChainId is the identifier for source chain 7 | /// @param _sourceAddress is the message sender address on source chain 8 | /// @param _payload is the cross-chain message sent 9 | function execute( 10 | bytes32 _commandId, 11 | string calldata _sourceChainId, 12 | string calldata _sourceAddress, 13 | bytes calldata _payload 14 | ) external; 15 | } 16 | -------------------------------------------------------------------------------- /src/adapters/axelar/interfaces/IAxelarGasService.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | // This should be owned by the microservice that is paying for gas. 5 | interface IAxelarGasService { 6 | // This is called on the source chain before calling the gateway to execute a remote contract. 7 | function payNativeGasForContractCall( 8 | address _sender, 9 | string calldata _destinationChain, 10 | string calldata _destinationAddress, 11 | bytes calldata _payload, 12 | address _refundAddress 13 | ) external payable; 14 | } 15 | -------------------------------------------------------------------------------- /src/adapters/axelar/interfaces/IAxelarGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | interface IAxelarGateway { 5 | function callContract(string calldata _destinationChain, string calldata _contractAddress, bytes calldata _payload) 6 | external; 7 | 8 | function isContractCallApproved( 9 | bytes32 _commandId, 10 | string calldata _sourceChain, 11 | string calldata _sourceAddress, 12 | address _contractAddress, 13 | bytes32 _payloadHash 14 | ) external view returns (bool); 15 | 16 | function validateContractCall( 17 | bytes32 _commandId, 18 | string calldata _sourceChain, 19 | string calldata _sourceAddress, 20 | bytes32 _payloadHash 21 | ) external returns (bool); 22 | } 23 | -------------------------------------------------------------------------------- /src/adapters/axelar/libraries/StringAddressConversion.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | library StringAddressConversion { 5 | error InvalidAddressString(); 6 | 7 | function toString(address _addr) internal pure returns (string memory) { 8 | bytes memory addressBytes = abi.encodePacked(_addr); 9 | uint256 length = addressBytes.length; 10 | bytes memory characters = "0123456789abcdef"; 11 | bytes memory stringBytes = new bytes(2 + addressBytes.length * 2); 12 | 13 | stringBytes[0] = "0"; 14 | stringBytes[1] = "x"; 15 | 16 | for (uint256 i; i < length; ++i) { 17 | stringBytes[2 + i * 2] = characters[uint8(addressBytes[i] >> 4)]; 18 | stringBytes[3 + i * 2] = characters[uint8(addressBytes[i] & 0x0f)]; 19 | } 20 | return string(stringBytes); 21 | } 22 | 23 | function toAddress(string memory _addressString) internal pure returns (address) { 24 | bytes memory stringBytes = bytes(_addressString); 25 | uint160 addressNumber = 0; 26 | uint8 stringByte; 27 | 28 | if (stringBytes.length != 42 || stringBytes[0] != "0" || stringBytes[1] != "x") revert InvalidAddressString(); 29 | 30 | for (uint256 i = 2; i < 42; ++i) { 31 | stringByte = uint8(stringBytes[i]); 32 | 33 | if ((stringByte >= 97) && (stringByte <= 102)) stringByte -= 87; 34 | else if ((stringByte >= 65) && (stringByte <= 70)) stringByte -= 55; 35 | else if ((stringByte >= 48) && (stringByte <= 57)) stringByte -= 48; 36 | else revert InvalidAddressString(); 37 | 38 | addressNumber |= uint160(uint256(stringByte) << ((41 - i) << 2)); 39 | } 40 | return address(addressNumber); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/adapters/wormhole/WormholeReceiverAdapter.sol: -------------------------------------------------------------------------------- 1 | /// SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import "wormhole-solidity-sdk/interfaces/IWormholeReceiver.sol"; 6 | 7 | /// local imports 8 | import "../../interfaces/adapters/IMessageReceiverAdapter.sol"; 9 | import "../../interfaces/IMultiBridgeMessageReceiver.sol"; 10 | import "../../libraries/Error.sol"; 11 | import "../../libraries/Types.sol"; 12 | import "../../libraries/TypeCasts.sol"; 13 | import "../../libraries/Message.sol"; 14 | 15 | import "../../controllers/MessageReceiverGAC.sol"; 16 | import "../BaseReceiverAdapter.sol"; 17 | 18 | /// @notice receiver adapter for wormhole bridge 19 | /// @dev allows wormhole relayers to write to receiver adapter which then forwards the message to 20 | /// the MMA receiver. 21 | contract WormholeReceiverAdapter is BaseReceiverAdapter, IWormholeReceiver { 22 | string public constant name = "WORMHOLE"; 23 | address public immutable relayer; 24 | uint16 public immutable senderChainId; 25 | 26 | /*///////////////////////////////////////////////////////////////// 27 | STATE VARIABLES 28 | ////////////////////////////////////////////////////////////////*/ 29 | 30 | mapping(bytes32 => bool) public isMessageExecuted; 31 | mapping(bytes32 => bool) public deliveryHashStatus; 32 | 33 | /*///////////////////////////////////////////////////////////////// 34 | MODIFIER 35 | ////////////////////////////////////////////////////////////////*/ 36 | 37 | modifier onlyRelayerContract() { 38 | if (msg.sender != relayer) { 39 | revert Error.CALLER_NOT_WORMHOLE_RELAYER(); 40 | } 41 | _; 42 | } 43 | 44 | /*///////////////////////////////////////////////////////////////// 45 | CONSTRUCTOR 46 | ////////////////////////////////////////////////////////////////*/ 47 | 48 | /// @param _relayer is wormhole relayer. 49 | /// @param _senderChainId is the chain id of the sender chain. 50 | /// @param _receiverGAC is global access controller. 51 | /// note: https://docs.wormhole.com/wormhole/quick-start/cross-chain-dev/automatic-relayer 52 | constructor(address _relayer, uint16 _senderChainId, address _receiverGAC) BaseReceiverAdapter(_receiverGAC) { 53 | if (_relayer == address(0)) { 54 | revert Error.ZERO_ADDRESS_INPUT(); 55 | } 56 | 57 | if (_senderChainId == uint16(0)) { 58 | revert Error.INVALID_SENDER_CHAIN_ID(); 59 | } 60 | 61 | relayer = _relayer; 62 | senderChainId = _senderChainId; 63 | } 64 | 65 | /*///////////////////////////////////////////////////////////////// 66 | EXTERNAL FUNCTIONS 67 | ////////////////////////////////////////////////////////////////*/ 68 | 69 | /// @inheritdoc IWormholeReceiver 70 | function receiveWormholeMessages( 71 | bytes memory _payload, 72 | bytes[] memory, 73 | bytes32 _sourceAddress, 74 | uint16 _sourceChainId, 75 | bytes32 _deliveryHash 76 | ) public payable override onlyRelayerContract { 77 | /// @dev validate the caller (done in modifier) 78 | /// @dev step-1: validate incoming chain id 79 | if (_sourceChainId != senderChainId) { 80 | revert Error.INVALID_SENDER_CHAIN_ID(); 81 | } 82 | 83 | /// @dev step-2: validate the source address 84 | if (TypeCasts.bytes32ToAddress(_sourceAddress) != senderAdapter) { 85 | revert Error.INVALID_SENDER_ADAPTER(); 86 | } 87 | 88 | /// decode the cross-chain payload 89 | AdapterPayload memory decodedPayload = abi.decode(_payload, (AdapterPayload)); 90 | bytes32 msgId = decodedPayload.msgId; 91 | 92 | /// @dev step-3: check for duplicate message 93 | if (isMessageExecuted[msgId] || deliveryHashStatus[_deliveryHash]) { 94 | revert MessageIdAlreadyExecuted(msgId); 95 | } 96 | 97 | isMessageExecuted[decodedPayload.msgId] = true; 98 | deliveryHashStatus[_deliveryHash] = true; 99 | 100 | /// @dev step-4: validate the receive adapter 101 | if (decodedPayload.receiverAdapter != address(this)) { 102 | revert Error.INVALID_RECEIVER_ADAPTER(); 103 | } 104 | 105 | /// @dev step-5: validate the destination 106 | if (decodedPayload.finalDestination != receiverGAC.multiBridgeMsgReceiver()) { 107 | revert Error.INVALID_FINAL_DESTINATION(); 108 | } 109 | 110 | MessageLibrary.Message memory _data = abi.decode(decodedPayload.data, (MessageLibrary.Message)); 111 | 112 | try IMultiBridgeMessageReceiver(decodedPayload.finalDestination).receiveMessage(_data) { 113 | emit MessageIdExecuted(_data.srcChainId, msgId); 114 | } catch (bytes memory lowLevelData) { 115 | revert MessageFailure(msgId, lowLevelData); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/adapters/wormhole/WormholeSenderAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; 6 | 7 | /// local imports 8 | import "../BaseSenderAdapter.sol"; 9 | import "../../interfaces/controllers/IGAC.sol"; 10 | import "../../libraries/Error.sol"; 11 | import "../../libraries/Types.sol"; 12 | 13 | /// @notice sender adapter for wormhole bridge 14 | contract WormholeSenderAdapter is BaseSenderAdapter { 15 | /// @notice event emitted when a chain id mapping is updated 16 | event ChainIDMappingUpdated(uint256 indexed origId, uint16 oldWhId, uint16 newWhId); 17 | 18 | string public constant name = "WORMHOLE"; 19 | IWormholeRelayer public immutable relayer; 20 | 21 | /*///////////////////////////////////////////////////////////////// 22 | STATE VARIABLES 23 | ////////////////////////////////////////////////////////////////*/ 24 | mapping(uint256 => uint16) public chainIdMap; 25 | 26 | /*///////////////////////////////////////////////////////////////// 27 | CONSTRUCTOR 28 | ////////////////////////////////////////////////////////////////*/ 29 | constructor(address _wormholeRelayer, address _gac) BaseSenderAdapter(_gac) { 30 | if (_wormholeRelayer == address(0)) { 31 | revert Error.ZERO_ADDRESS_INPUT(); 32 | } 33 | relayer = IWormholeRelayer(_wormholeRelayer); 34 | } 35 | 36 | /*///////////////////////////////////////////////////////////////// 37 | EXTERNAL FUNCTIONS 38 | ////////////////////////////////////////////////////////////////*/ 39 | 40 | /// @notice sends a message via wormhole relayer 41 | function dispatchMessage(uint256 _receiverChainId, address _to, bytes calldata _data) 42 | external 43 | payable 44 | override 45 | onlyMultiBridgeMessageSender 46 | returns (bytes32 msgId) 47 | { 48 | address receiverAdapter = receiverAdapters[_receiverChainId]; 49 | 50 | if (receiverAdapter == address(0)) { 51 | revert Error.ZERO_RECEIVER_ADAPTER(); 52 | } 53 | 54 | uint16 wormChainId = chainIdMap[_receiverChainId]; 55 | 56 | if (wormChainId == 0) { 57 | revert Error.INVALID_DST_CHAIN(); 58 | } 59 | 60 | msgId = _getNewMessageId(_receiverChainId, _to); 61 | bytes memory payload = abi.encode(AdapterPayload(msgId, msg.sender, receiverAdapter, _to, _data)); 62 | 63 | relayer.sendPayloadToEvm{value: msg.value}( 64 | wormChainId, 65 | receiverAdapter, 66 | payload, 67 | /// @dev no receiver value since just passing message 68 | 0, 69 | senderGAC.msgDeliveryGasLimit() 70 | ); 71 | 72 | emit MessageDispatched(msgId, msg.sender, _receiverChainId, _to, _data); 73 | } 74 | 75 | /// @dev maps the MMA chain id to bridge specific chain id 76 | /// @dev _origIds is the chain's native chain id 77 | /// @dev _whIds are the bridge allocated chain id 78 | function setChainIdMap(uint256[] calldata _origIds, uint16[] calldata _whIds) external onlyGlobalOwner { 79 | uint256 arrLength = _origIds.length; 80 | 81 | if (arrLength != _whIds.length) { 82 | revert Error.ARRAY_LENGTH_MISMATCHED(); 83 | } 84 | 85 | for (uint256 i; i < arrLength;) { 86 | if (_origIds[i] == 0) { 87 | revert Error.ZERO_CHAIN_ID(); 88 | } 89 | 90 | uint16 oldWhId = chainIdMap[_origIds[i]]; 91 | chainIdMap[_origIds[i]] = _whIds[i]; 92 | 93 | emit ChainIDMappingUpdated(_origIds[i], oldWhId, _whIds[i]); 94 | 95 | unchecked { 96 | ++i; 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/controllers/GAC.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity >=0.8.9; 4 | 5 | import "openzeppelin-contracts/contracts/access/Ownable.sol"; 6 | 7 | /// local imports 8 | import {IGAC} from "../interfaces/controllers/IGAC.sol"; 9 | import {Error} from "../libraries/Error.sol"; 10 | 11 | /// @dev is the global access control contract for bridge adapters 12 | contract GAC is IGAC, Ownable { 13 | /*/////////////////////////////////////////////////////////////// 14 | CONSTRUCTOR 15 | //////////////////////////////////////////////////////////////*/ 16 | constructor() Ownable() {} 17 | 18 | /*/////////////////////////////////////////////////////////////// 19 | EXTERNAL VIEW FUNCTIONS 20 | //////////////////////////////////////////////////////////////*/ 21 | 22 | /// @inheritdoc IGAC 23 | function isGlobalOwner(address _caller) external view override returns (bool) { 24 | return _caller == owner(); 25 | } 26 | 27 | /// @inheritdoc IGAC 28 | function getGlobalOwner() external view override returns (address _owner) { 29 | _owner = owner(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/controllers/GovernanceTimelock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// interfaces 5 | import "../interfaces/controllers/IGovernanceTimelock.sol"; 6 | 7 | /// libraries 8 | import "../libraries/Error.sol"; 9 | 10 | contract GovernanceTimelock is IGovernanceTimelock { 11 | /*/////////////////////////////////////////////////////////////// 12 | CONSTANTS 13 | //////////////////////////////////////////////////////////////*/ 14 | uint256 public constant MINIMUM_DELAY = 2 days; 15 | uint256 public constant MAXIMUM_DELAY = 30 days; 16 | 17 | /// @notice The time window within which a transaction can be executed, following its ETA. Beyond this point a transaction is considered stale and cannot be executed. 18 | uint256 public constant GRACE_PERIOD = 14 days; 19 | 20 | /*///////////////////////////////////////////////////////////////// 21 | STATE VARIABLES 22 | ////////////////////////////////////////////////////////////////*/ 23 | uint256 public txCounter; 24 | uint256 public delay; 25 | 26 | /// @notice the admin is the one allowed to schedule events 27 | address public admin; 28 | 29 | mapping(uint256 txId => bytes32) public scheduledTransaction; 30 | mapping(uint256 txId => bool) public isExecuted; 31 | 32 | /*///////////////////////////////////////////////////////////////// 33 | MODIFIERS 34 | ////////////////////////////////////////////////////////////////*/ 35 | 36 | /// @notice A modifier used for restricting the caller of some functions to be this contract itself. 37 | modifier onlySelf() { 38 | if (msg.sender != address(this)) { 39 | revert Error.INVALID_SELF_CALLER(); 40 | } 41 | _; 42 | } 43 | 44 | /// @notice A modifier used for restricting caller to admin contract 45 | modifier onlyAdmin() { 46 | if (msg.sender != admin) { 47 | revert Error.CALLER_NOT_ADMIN(); 48 | } 49 | _; 50 | } 51 | 52 | /*///////////////////////////////////////////////////////////////// 53 | CONSTRUCTOR 54 | ////////////////////////////////////////////////////////////////*/ 55 | 56 | /// @param _admin is the address of admin contract that schedule txs 57 | /// @param _delay is the initial delay 58 | constructor(address _admin, uint256 _delay) { 59 | if (_admin == address(0)) { 60 | revert Error.ZERO_ADDRESS_INPUT(); 61 | } 62 | _checkDelay(_delay); 63 | 64 | admin = _admin; 65 | emit AdminUpdated(address(0), _admin); 66 | 67 | delay = _delay; 68 | emit DelayUpdated(0, _delay); 69 | } 70 | 71 | /*///////////////////////////////////////////////////////////////// 72 | EXTERNAL FUNCTIONS 73 | ////////////////////////////////////////////////////////////////*/ 74 | 75 | /// @inheritdoc IGovernanceTimelock 76 | function scheduleTransaction(address _target, uint256 _value, bytes memory _data) external override onlyAdmin { 77 | if (_target == address(0)) { 78 | revert Error.INVALID_TARGET(); 79 | } 80 | 81 | /// increment tx counter 82 | ++txCounter; 83 | uint256 eta = block.timestamp + delay; 84 | 85 | scheduledTransaction[txCounter] = keccak256(abi.encodePacked(_target, _value, eta, _data)); 86 | emit TransactionScheduled(txCounter, _target, _value, _data, eta); 87 | } 88 | 89 | /// @inheritdoc IGovernanceTimelock 90 | function executeTransaction(uint256 _txId, address _target, uint256 _value, bytes memory _data, uint256 _eta) 91 | external 92 | payable 93 | override 94 | { 95 | /// @dev validates the txId 96 | if (_txId == 0 || _txId > txCounter) { 97 | revert Error.INVALID_TX_ID(); 98 | } 99 | 100 | /// @dev checks if tx is already executed; 101 | if (isExecuted[_txId]) { 102 | revert Error.TX_ALREADY_EXECUTED(); 103 | } 104 | 105 | /// @dev check the input params against hash 106 | if (scheduledTransaction[_txId] != keccak256(abi.encodePacked(_target, _value, _eta, _data))) { 107 | revert Error.INVALID_TX_INPUT(); 108 | } 109 | 110 | /// @dev checks timelock 111 | if (_eta > block.timestamp) { 112 | revert Error.TX_TIMELOCKED(); 113 | } 114 | 115 | /// @dev checks if tx within execution period 116 | if (block.timestamp > _eta + GRACE_PERIOD) { 117 | revert Error.TX_EXPIRED(); 118 | } 119 | 120 | /// @dev checks native funding 121 | if (msg.value != _value) { 122 | revert Error.INVALID_MSG_VALUE(); 123 | } 124 | 125 | isExecuted[_txId] = true; 126 | 127 | (bool status,) = _target.call{value: _value}(_data); 128 | 129 | if (!status) { 130 | revert Error.EXECUTION_FAILS_ON_DST(); 131 | } 132 | 133 | emit TransactionExecuted(_txId, _target, _value, _data, _eta); 134 | } 135 | 136 | /// @inheritdoc IGovernanceTimelock 137 | function setDelay(uint256 _delay) external override onlySelf { 138 | _checkDelay(_delay); 139 | 140 | uint256 oldDelay = delay; 141 | delay = _delay; 142 | 143 | emit DelayUpdated(oldDelay, _delay); 144 | } 145 | 146 | /// @inheritdoc IGovernanceTimelock 147 | function setAdmin(address _newAdmin) external override onlySelf { 148 | if (_newAdmin == address(0)) { 149 | revert Error.ZERO_TIMELOCK_ADMIN(); 150 | } 151 | 152 | address oldAdmin = admin; 153 | admin = _newAdmin; 154 | 155 | emit AdminUpdated(oldAdmin, _newAdmin); 156 | } 157 | 158 | /*///////////////////////////////////////////////////////////////// 159 | PRIVATE/INTERNAL FUNCTIONS 160 | ////////////////////////////////////////////////////////////////*/ 161 | 162 | function _checkDelay(uint256 _delay) internal pure { 163 | if (_delay < MINIMUM_DELAY) { 164 | revert Error.INVALID_DELAY_MIN(); 165 | } 166 | 167 | if (_delay > MAXIMUM_DELAY) { 168 | revert Error.INVALID_DELAY_MAX(); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/controllers/MessageReceiverGAC.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity >=0.8.9; 4 | 5 | import "./GAC.sol"; 6 | import "../interfaces/IMultiBridgeMessageReceiver.sol"; 7 | 8 | contract MessageReceiverGAC is GAC { 9 | event MultiBridgeMessageReceiverUpdated(address indexed oldMMR, address indexed newMMR); 10 | 11 | address public multiBridgeMsgReceiver; 12 | 13 | function setMultiBridgeMessageReceiver(address _mmaReceiver) external onlyOwner { 14 | if (_mmaReceiver == address(0)) { 15 | revert Error.ZERO_ADDRESS_INPUT(); 16 | } 17 | address oldMMR = multiBridgeMsgReceiver; 18 | multiBridgeMsgReceiver = _mmaReceiver; 19 | 20 | emit MultiBridgeMessageReceiverUpdated(oldMMR, _mmaReceiver); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/controllers/MessageSenderGAC.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity >=0.8.9; 4 | 5 | /// library imports 6 | import "openzeppelin-contracts/contracts/access/Ownable.sol"; 7 | 8 | import {Error} from "../libraries/Error.sol"; 9 | import {GAC} from "./GAC.sol"; 10 | 11 | contract MessageSenderGAC is GAC { 12 | /*/////////////////////////////////////////////////////////////// 13 | EVENT 14 | //////////////////////////////////////////////////////////////*/ 15 | event DstGasLimitUpdated(uint256 oldLimit, uint256 newLimit); 16 | 17 | event MultiBridgeMessageCallerUpdated(address indexed oldAuthCaller, address indexed newAuthCaller); 18 | 19 | event MultiBridgeMessageSenderUpdated(address indexed oldMMS, address indexed newMMS); 20 | 21 | event MultiBridgeMessageReceiverUpdated(uint256 indexed chainId, address indexed oldMMR, address indexed newMMR); 22 | 23 | /*/////////////////////////////////////////////////////////////// 24 | CONSTANTS 25 | //////////////////////////////////////////////////////////////*/ 26 | uint256 public constant MINIMUM_DST_GAS_LIMIT = 50000; 27 | 28 | /*/////////////////////////////////////////////////////////////// 29 | STATE VARIABLES 30 | //////////////////////////////////////////////////////////////*/ 31 | uint256 public msgDeliveryGasLimit; 32 | 33 | /// @notice is the MMA Core Contracts on the chain 34 | /// @dev leveraged by bridge adapters for authentication 35 | address public multiBridgeMessageSender; 36 | 37 | /// @dev is the authorised caller for the multi-message sender 38 | address public authorisedCaller; 39 | 40 | mapping(uint256 chainId => address mmaReceiver) public remoteMultiBridgeMessageReceiver; 41 | 42 | /*/////////////////////////////////////////////////////////////// 43 | EXTERNAL FUNCTIONS 44 | //////////////////////////////////////////////////////////////*/ 45 | 46 | function setMultiBridgeMessageSender(address _newMMS) external onlyOwner { 47 | if (_newMMS == address(0)) { 48 | revert Error.ZERO_ADDRESS_INPUT(); 49 | } 50 | 51 | address oldMMS = multiBridgeMessageSender; 52 | multiBridgeMessageSender = _newMMS; 53 | 54 | emit MultiBridgeMessageSenderUpdated(oldMMS, _newMMS); 55 | } 56 | 57 | function setAuthorisedCaller(address _newAuthCaller) external onlyOwner { 58 | if (_newAuthCaller == address(0)) { 59 | revert Error.ZERO_ADDRESS_INPUT(); 60 | } 61 | 62 | address oldAuthCaller = authorisedCaller; 63 | authorisedCaller = _newAuthCaller; 64 | 65 | emit MultiBridgeMessageCallerUpdated(oldAuthCaller, _newAuthCaller); 66 | } 67 | 68 | function setRemoteMultiBridgeMessageReceiver(uint256 _chainId, address _remoteMMR) external onlyOwner { 69 | if (_remoteMMR == address(0)) { 70 | revert Error.ZERO_ADDRESS_INPUT(); 71 | } 72 | 73 | if (_chainId == 0) { 74 | revert Error.ZERO_CHAIN_ID(); 75 | } 76 | 77 | address oldRemoteMMR = remoteMultiBridgeMessageReceiver[_chainId]; 78 | remoteMultiBridgeMessageReceiver[_chainId] = _remoteMMR; 79 | 80 | emit MultiBridgeMessageReceiverUpdated(_chainId, oldRemoteMMR, _remoteMMR); 81 | } 82 | 83 | function setGlobalMsgDeliveryGasLimit(uint256 _gasLimit) external onlyOwner { 84 | if (_gasLimit < MINIMUM_DST_GAS_LIMIT) { 85 | revert Error.INVALID_DST_GAS_LIMIT_MIN(); 86 | } 87 | 88 | uint256 oldLimit = msgDeliveryGasLimit; 89 | msgDeliveryGasLimit = _gasLimit; 90 | 91 | emit DstGasLimitUpdated(oldLimit, _gasLimit); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/interfaces/EIP5164/MessageDispatcher.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity >=0.8.9; 4 | 5 | /** 6 | * @dev The MessageDispatcher lives on the origin chain and dispatches messages to the MessageExecutor for execution. 7 | * More about MessageDispatcher of EIP5164, see https://eips.ethereum.org/EIPS/eip-5164#messagedispatcher. 8 | */ 9 | interface MessageDispatcher { 10 | /** 11 | * @dev The MessageDispatched event MUST be emitted by the MessageDispatcher when an individual message is dispatched. 12 | */ 13 | event MessageDispatched( 14 | bytes32 indexed messageId, address indexed from, uint256 indexed receiverChainId, address to, bytes data 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/interfaces/EIP5164/MessageExecutor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity >=0.8.9; 4 | 5 | /** 6 | * @dev The MessageExecutor executes dispatched messages and message batches. 7 | * 8 | * MessageExecutor MUST append the ABI-packed (messageId, fromChainId, from) to the calldata for each message 9 | * being executed. This allows the receiver of the message to verify the cross-chain sender and the chain that 10 | * the message is coming from. 11 | * ``` 12 | * to.call(abi.encodePacked(data, messageId, fromChainId, from)); 13 | * ``` 14 | * 15 | * More about MessageExecutor of EIP5164, see https://eips.ethereum.org/EIPS/eip-5164#messageexecutor. 16 | */ 17 | interface MessageExecutor { 18 | /** 19 | * @dev MessageExecutor MUST revert if a messageId has already been executed and SHOULD emit a 20 | * MessageIdAlreadyExecuted custom error. 21 | */ 22 | error MessageIdAlreadyExecuted(bytes32 messageId); 23 | 24 | /** 25 | * @dev MessageExecutor MUST revert if an individual message fails and SHOULD emit a MessageFailure custom error. 26 | */ 27 | error MessageFailure(bytes32 messageId, bytes errorData); 28 | 29 | /** 30 | * @dev MessageExecutor MUST revert the entire batch if any message in a batch fails and SHOULD emit a 31 | * MessageBatchFailure custom error. 32 | */ 33 | error MessageBatchFailure(bytes32 messageId, uint256 messageIndex, bytes errorData); 34 | 35 | /** 36 | * @dev MessageIdExecuted MUST be emitted once a message or message batch has been executed. 37 | */ 38 | event MessageIdExecuted(uint256 indexed fromChainId, bytes32 indexed messageId); 39 | } 40 | -------------------------------------------------------------------------------- /src/interfaces/EIP5164/SingleMessageDispatcher.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity >=0.8.9; 4 | 5 | import "./MessageDispatcher.sol"; 6 | 7 | /** 8 | * @dev The SingleMessageDispatcher is an extension of MessageDispatcher that defines a method, dispatchMessage, 9 | * for dispatching an individual message to be executed on the receiverChainId. 10 | * More about SingleMessageDispatcher of EIP5164, see https://eips.ethereum.org/EIPS/eip-5164#singlemessagedispatcher. 11 | */ 12 | interface SingleMessageDispatcher is MessageDispatcher { 13 | /** 14 | * @dev A method for dispatching an individual message to be executed on the receiver chain. 15 | */ 16 | function dispatchMessage(uint256 _receiverChainId, address _to, bytes calldata _data) 17 | external 18 | payable 19 | returns (bytes32 messageId); 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/IMultiBridgeMessageReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | import "../libraries/Message.sol"; 5 | 6 | /// @notice interface for the multi-bridge message receiver 7 | interface IMultiBridgeMessageReceiver { 8 | /// @notice emitted when a message has been received from a single bridge. 9 | /// @param msgId is the unique identifier of the message 10 | /// @param bridgeName is the name of the bridge from which the message was received 11 | /// @param nonce is the nonce of the message 12 | /// @param receiverAdapter is the address of the receiver adapter that received the message 13 | event BridgeMessageReceived( 14 | bytes32 indexed msgId, string indexed bridgeName, uint256 nonce, address receiverAdapter 15 | ); 16 | 17 | /// @notice emitted when a message has been queued for execution in the destination timelock contract. 18 | /// @param msgId is the unique identifier of the message 19 | /// @param target is the address of the final target address that will be called once the timelock matures 20 | /// @param nativeValue is the value that will be passed to the target address through low-level call 21 | /// @param nonce is the nonce of the message 22 | /// @param callData is the data that will be passed to the target address through low-level call 23 | event MessageExecutionScheduled( 24 | bytes32 indexed msgId, address indexed target, uint256 nativeValue, uint256 nonce, bytes callData 25 | ); 26 | 27 | /// @notice emitted when receiver adapter of a specific bridge is updated. 28 | /// @param receiverAdapter is the new receiver adapter address 29 | /// @param add is true if the receiver adapter was added, false if removed 30 | event BridgeReceiverAdapterUpdated(address indexed receiverAdapter, bool add); 31 | 32 | /// @notice emitted when the quorum for message validity is updated. 33 | /// @param oldQuorum is the old quorum value 34 | /// @param newQuorum is the new quorum value 35 | event QuorumUpdated(uint64 oldQuorum, uint64 newQuorum); 36 | 37 | /// @notice emitted when the governance timelock address is updated. 38 | /// @param oldTimelock is the previous governance timelock contract address 39 | /// @param newTimelock is the new governance timelock contract address 40 | event GovernanceTimelockUpdated(address oldTimelock, address newTimelock); 41 | 42 | /// @notice Receive messages from allowed bridge receiver adapters. 43 | /// @dev Every receiver adapter should call this function with decoded MessageLibrary.Message 44 | /// @param _message is the message to be received 45 | function receiveMessage(MessageLibrary.Message calldata _message) external; 46 | 47 | /// @notice Sends a message, that has achieved quorum and has not yet expired, to the governance timelock for eventual execution. 48 | /// @param _msgId is the unique identifier of the message 49 | /// @param _execParams are the params for message execution 50 | function scheduleMessageExecution(bytes32 _msgId, MessageLibrary.MessageExecutionParams calldata _execParams) 51 | external; 52 | 53 | /// @notice adds or removes bridge receiver adapters. 54 | /// @param _receiverAdapters the list of receiver adapters to add or remove 55 | /// @param _operations the list of operations to perform for corresponding receiver adapters, true for add, false for remove 56 | function updateReceiverAdapters(address[] calldata _receiverAdapters, bool[] calldata _operations) external; 57 | 58 | /// @notice updates the quorum for message validity, which is the number of bridges that must deliver the message for it to be considered valid. 59 | /// @param _quorum is the new quorum value 60 | function updateQuorum(uint64 _quorum) external; 61 | 62 | /// @notice updates the the list of receiver adapters and the quorum for message validity. 63 | /// @param _receiverAdapters the list of receiver adapters to add or remove 64 | /// @param _operations the list of operations to perform for corresponding receiver adapters, true for add, false for remove 65 | /// @param _newQuorum is the new quorum value 66 | function updateReceiverAdaptersAndQuorum( 67 | address[] calldata _receiverAdapters, 68 | bool[] calldata _operations, 69 | uint64 _newQuorum 70 | ) external; 71 | 72 | /// @notice updates the governance timelock address, which is the contract that ultimately executes valid messages. 73 | /// @param _newGovernanceTimelock is the new governance timelock contract address 74 | function updateGovernanceTimelock(address _newGovernanceTimelock) external; 75 | } 76 | -------------------------------------------------------------------------------- /src/interfaces/adapters/IMessageReceiverAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | import "../EIP5164/MessageExecutor.sol"; 5 | 6 | /** 7 | * @notice A common interface for AMB message receiver adapters, that builds on EIP-5164 MessageExecutor. 8 | * A message receiver adapter receives messages through the AMB that are sent by a specific sender adapter on the source chain. 9 | * It validates the message and forwards it to the IMultiBridgeMessageReceiver contract on the destination chain. 10 | */ 11 | interface IMessageReceiverAdapter is MessageExecutor { 12 | /// @notice emitted when the sender adapter address, which resides on the source chain, is updated. 13 | /// @param oldSenderAdapter is the old sender adapter address 14 | /// @param newSenderAdapter is the new sender adapter address 15 | event SenderAdapterUpdated(address indexed oldSenderAdapter, address indexed newSenderAdapter); 16 | 17 | /// @notice returns name of the message bridge wrapped by the adapter 18 | function name() external view returns (string memory); 19 | 20 | /// @dev Changes the address of the sender adapter on the source chain, that is authorised to send this receiver messages. 21 | /// @param _senderAdapter is the bridge's sender adapter deployed on the source chain (i.e. Ethereum) 22 | /// note: access controlled to be called by the global admin contract 23 | function updateSenderAdapter(address _senderAdapter) external; 24 | } 25 | -------------------------------------------------------------------------------- /src/interfaces/adapters/IMessageSenderAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity >=0.8.9; 4 | 5 | import "../EIP5164/SingleMessageDispatcher.sol"; 6 | 7 | /** 8 | * @notice A common interface for AMB message sender adapters, that builds on EIP-5164 SingleMessageDispatcher. 9 | * A message sender adapter for a specific AMB is responsible for receiving messages from an MultiBridgeMessageSender 10 | * on the source chain and sending the message to corresponding receiver adapters on the intended destination chain. 11 | * The sender adapter keeps a list of the remote receiver adapters on each destination chain that it forwards messages to. 12 | */ 13 | interface IMessageSenderAdapter is SingleMessageDispatcher { 14 | /// @notice emitted when a the sender's corresponding receiver adapter on a destination chain is changed 15 | /// @param dstChainId is the destination chain for which the receiver adapter is updated 16 | /// @param oldReceiver is the old receiver adapter address 17 | /// @param newReceiver is the new receiver adapter address 18 | event ReceiverAdapterUpdated(uint256 indexed dstChainId, address indexed oldReceiver, address indexed newReceiver); 19 | 20 | /// @notice Updates the corresponding message receiver adapters for different destination chains 21 | /// @param _dstChainIds are the destination chain IDs for which the receiver adapters are to be updated 22 | /// @param _receiverAdapters new receiver adapter addresses for the corresponding destination chain ids in _dstChainIds 23 | function updateReceiverAdapter(uint256[] calldata _dstChainIds, address[] calldata _receiverAdapters) external; 24 | 25 | /// @notice returns name of the message bridge wrapped by the adapter 26 | function name() external view returns (string memory); 27 | 28 | /// @notice returns the bridge receiver adapter address for a given destination chain id 29 | /// @param _chainId is the destination chain whose receiver adapter address is to be returned 30 | function receiverAdapters(uint256 _chainId) external view returns (address); 31 | } 32 | -------------------------------------------------------------------------------- /src/interfaces/controllers/IGAC.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity >=0.8.9; 4 | 5 | /// @notice interface for GAC (Global Access Controller) 6 | interface IGAC { 7 | /// @notice Checks whether a given address is the global owner 8 | /// @param _addr is the address to compare with the global owner 9 | /// @return boolean true if _caller is the global owner, false otherwise 10 | function isGlobalOwner(address _addr) external view returns (bool); 11 | 12 | /// @notice returns the global owner address. 13 | /// @return _owner is the global owner address 14 | function getGlobalOwner() external view returns (address _owner); 15 | } 16 | -------------------------------------------------------------------------------- /src/interfaces/controllers/IGovernanceTimelock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /** 5 | * @notice interface for governance timelock on the destination chain. 6 | * @dev this contract is responsible for executing all cross-chain messages on the destination chain 7 | * after they have been queued for a configurable period of time. 8 | * This contract is also typically, the GAC (Global Access Controller) on the destination chain, ensuring that all 9 | * admin actions on the destination chain go through the same validation process and time delay as other governance actions. 10 | */ 11 | interface IGovernanceTimelock { 12 | /// @notice emitted when a transaction is scheduled for execution. 13 | /// @param txId is the generated unique identifier of the scheduled transaction 14 | /// @param target is the address to call by low-level call 15 | /// @param value is the value to pass to target by low-level call 16 | /// @param data is the abieencoded function selector and arguments data, to execute on target 17 | /// @param eta is the timestamp after which the transaction should be executed 18 | event TransactionScheduled(uint256 indexed txId, address indexed target, uint256 value, bytes data, uint256 eta); 19 | 20 | /// @notice emitted when a transaction is executed. 21 | /// @param txId is the unique identifier of the executed transaction, generated when the transaction was scheduled 22 | /// @param target is the address called as part of the execution 23 | /// @param value is the value passed to target by low-level call 24 | /// @param data is the abieencoded function selector and arguments data, executed on target 25 | /// @param eta is the timestamp after which the transaction should be executed 26 | event TransactionExecuted(uint256 indexed txId, address indexed target, uint256 value, bytes data, uint256 eta); 27 | 28 | /// @notice emitted when the time delay parameter is changed. 29 | /// @param oldDelay is the previous value of the time delay 30 | /// @param newDelay is the new value of the time delay 31 | event DelayUpdated(uint256 oldDelay, uint256 newDelay); 32 | 33 | /// @notice emitted when the admin of the time lock contract is changed 34 | /// @param oldAdmin is the previous admin of the time lock contract 35 | /// @param newAdmin is the new admin of the time lock contract 36 | event AdminUpdated(address oldAdmin, address newAdmin); 37 | 38 | /// @notice Schedules the provided transaction for execution after a pre-configured delay period. 39 | /// @dev this function can only be called by the admin of the timelock contract. 40 | /// @param _target is the address to call by low-level call 41 | /// @param _value is the value to pass to target by low-level call 42 | /// @param _data is the abieencoded function selector and arguments data, to execute on target 43 | function scheduleTransaction(address _target, uint256 _value, bytes memory _data) external; 44 | 45 | /// @notice Executes a previously scheduled transaction if it has reached its ETA, but has not exceeded a grace period beyond that. 46 | /// @param _txId is the unique identifier of the executed transaction, generated when the transaction was scheduled 47 | /// @param _target is the address called as part of the execution 48 | /// @param _value is the value passed to target by low-level call 49 | /// @param _data is the abieencoded function selector and arguments data, executed on target 50 | /// @param _eta is the timestamp after which the transaction should be executed 51 | function executeTransaction(uint256 _txId, address _target, uint256 _value, bytes memory _data, uint256 _eta) 52 | external 53 | payable; 54 | 55 | /// @notice Changes the time period that transactions must be queued for before they can be executed. 56 | /// The new delay must still be within an allowed range. 57 | /// This function can only be invoked by the timelock contract itself, thus requiring that this change go 58 | /// through the process and time delays as other governance actions. 59 | function setDelay(uint256 _delay) external; 60 | 61 | /// @notice Changes the admin. 62 | /// This function can only be invoked by the timelock contract itself, thus requiring that this change go 63 | /// through the process and time delays as other governance actions. 64 | function setAdmin(address _newAdmin) external; 65 | } 66 | -------------------------------------------------------------------------------- /src/libraries/EIP5164/ExecutorAware.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | // Copied from https://github.com/pooltogether/ERC5164/blob/main/src/abstract/ExecutorAware.sol 3 | // Modifications: 4 | // 1. support higher version of solidity 5 | // 2. support multiple trustedExecutors 6 | 7 | pragma solidity ^0.8.16; 8 | 9 | import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 10 | /** 11 | * @title ExecutorAware abstract contract 12 | * @notice The ExecutorAware contract allows contracts on a receiving chain to execute messages from an origin chain. 13 | * These messages are sent by the `MessageDispatcher` contract which live on the origin chain. 14 | * The `MessageExecutor` contract on the receiving chain executes these messages 15 | * and then forward them to an ExecutorAware contract on the receiving chain. 16 | * @dev This contract implements EIP-2771 (https://eips.ethereum.org/EIPS/eip-2771) 17 | * to ensure that messages are sent by a trusted `MessageExecutor` contract. 18 | */ 19 | 20 | abstract contract ExecutorAware { 21 | using EnumerableSet for EnumerableSet.AddressSet; 22 | 23 | /* ============ Variables ============ */ 24 | /// @notice Addresses of the trusted executor contracts. 25 | EnumerableSet.AddressSet private trustedExecutors; 26 | 27 | /* ============ External Functions ============ */ 28 | 29 | /** 30 | * @notice Check whether the provided executor is trusted 31 | * @param _executor Address to check 32 | * @return Returns true if the provided executor is trusted 33 | */ 34 | function isTrustedExecutor(address _executor) public view returns (bool) { 35 | return EnumerableSet.contains(trustedExecutors, _executor); 36 | } 37 | 38 | /** 39 | * @notice Get the list of trusted executors 40 | * @return Returns an array of trusted executors 41 | */ 42 | function getTrustedExecutors() public view returns (address[] memory) { 43 | return EnumerableSet.values(trustedExecutors); 44 | } 45 | 46 | /** 47 | * @notice Get the total number of trusted executors 48 | * @return Returns the total number of trusted executors 49 | */ 50 | function trustedExecutorsCount() public view returns (uint256) { 51 | return EnumerableSet.length(trustedExecutors); 52 | } 53 | 54 | /* ============ Internal Functions ============ */ 55 | 56 | /** 57 | * @notice Add a new trusted executor, if it is has not already been registered as trusted. 58 | * @param _executor Address of the `MessageExecutor` contract 59 | * @return _success Returns true if the executor was not already registered, and was added successfully 60 | */ 61 | function _addTrustedExecutor(address _executor) internal returns (bool) { 62 | return EnumerableSet.add(trustedExecutors, _executor); 63 | } 64 | 65 | /** 66 | * @notice Remove a trusted executor, if it is registered as trusted. 67 | * @param _executor Address of the `MessageExecutor` contract 68 | * @return _success Returns true if the executor was previously registered, and was removed successfully 69 | */ 70 | function _removeTrustedExecutor(address _executor) internal returns (bool) { 71 | return EnumerableSet.remove(trustedExecutors, _executor); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/libraries/Error.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity >=0.8.9; 4 | 5 | /// @dev is a library that contains all the error codes 6 | library Error { 7 | /*///////////////////////////////////////////////////////////////// 8 | COMMON VALIDATION ERRORS 9 | ////////////////////////////////////////////////////////////////*/ 10 | 11 | /// @dev is thrown when input is zero address 12 | error ZERO_ADDRESS_INPUT(); 13 | 14 | /// @dev is thrown if the length of two arrays are mismatched 15 | error ARRAY_LENGTH_MISMATCHED(); 16 | 17 | /// @dev is thrown if caller is not the privileged caller 18 | error INVALID_PRIVILEGED_CALLER(); 19 | 20 | /// @dev is thrown if caller is invalid receiver adapter 21 | error INVALID_RECEIVER_ADAPTER(); 22 | 23 | /// @dev is thrown if updating a receiver adapter fails 24 | /// @param reason is the reason for failure 25 | error UPDATE_RECEIVER_ADAPTER_FAILED(string reason); 26 | 27 | /// @dev is thrown if caller is not self 28 | error INVALID_SELF_CALLER(); 29 | 30 | /// @dev is thrown if no sender adapter is found on MMA Sender 31 | error NO_SENDER_ADAPTER_FOUND(); 32 | 33 | /// @dev is thrown if msg id is already executed 34 | error MSG_ID_ALREADY_SCHEDULED(); 35 | 36 | /// @dev is thrown if bridge adapter already delivered the message to multi message receiver 37 | error DUPLICATE_MESSAGE_DELIVERY_BY_ADAPTER(); 38 | 39 | /// @dev is thrown if quorum threshold is greater than receiver adapters 40 | error INVALID_QUORUM_THRESHOLD(); 41 | 42 | /// @dev is thrown if sender adapter array has duplicates 43 | error DUPLICATE_SENDER_ADAPTER(); 44 | 45 | /// @dev is thrown if the sender adapter does not exist 46 | error SENDER_ADAPTER_NOT_EXIST(); 47 | 48 | /// @dev is thrown if sender adapter array is not in ascending order or has duplicates 49 | error INVALID_SENDER_ADAPTER_ORDER(); 50 | 51 | /// @dev is thrown if deadline is lapsed 52 | error MSG_EXECUTION_PASSED_DEADLINE(); 53 | 54 | /// @dev is thrown if quorum is not reached 55 | error QUORUM_NOT_ACHIEVED(); 56 | 57 | /// @dev is thrown if message execution fails on the destination chain 58 | error EXECUTION_FAILS_ON_DST(); 59 | 60 | /// @dev is thrown if caller is not admin of timelock 61 | error CALLER_NOT_ADMIN(); 62 | 63 | /// @dev is thrown if the expiration is outside a defined range 64 | error INVALID_EXPIRATION_DURATION(); 65 | 66 | /// @dev is thrown if refund address is zero (or) invalid 67 | error INVALID_REFUND_ADDRESS(); 68 | 69 | /// @dev is thrown if execution params do not match the stored hash 70 | error EXEC_PARAMS_HASH_MISMATCH(); 71 | 72 | /*///////////////////////////////////////////////////////////////// 73 | ADAPTER ERRORS 74 | ////////////////////////////////////////////////////////////////*/ 75 | 76 | /// @dev is thrown if caller is not multi message sender 77 | error CALLER_NOT_MULTI_MESSAGE_SENDER(); 78 | 79 | /// @dev is thrown if sender chain is not allowed on receiver adapter 80 | error INVALID_SENDER_CHAIN_ID(); 81 | 82 | /// @dev is thrown if sender adapter is not allowed on receiver adapter 83 | error INVALID_SENDER_ADAPTER(); 84 | 85 | /// @dev is thrown if the sender adapter fee array is invalid 86 | error INVALID_SENDER_ADAPTER_FEES(); 87 | 88 | /// @dev is thrown if final destination is not mma receiver on receiver adapter 89 | error INVALID_FINAL_DESTINATION(); 90 | 91 | /// @dev is thrown if chain id is zero 92 | error ZERO_CHAIN_ID(); 93 | 94 | /// @dev is thrown if receiver adapter is zero address 95 | error ZERO_RECEIVER_ADAPTER(); 96 | 97 | /// @dev is thrown when caller is not wormhole relayer 98 | error CALLER_NOT_WORMHOLE_RELAYER(); 99 | 100 | /// @dev is thrown when the destination chain id is invalid 101 | error INVALID_DST_CHAIN(); 102 | 103 | /// @dev is thrown if the target is invalid in remote call 104 | error INVALID_TARGET(); 105 | 106 | /// @dev is thrown if caller is not the global owner 107 | error CALLER_NOT_OWNER(); 108 | 109 | /// @dev is thrown if contract call is invalid (for axelar) 110 | error NOT_APPROVED_BY_GATEWAY(); 111 | 112 | /// @dev is thrown if a message could not be sent through a sufficient number of bridges 113 | error MULTI_MESSAGE_SEND_FAILED(); 114 | 115 | /*///////////////////////////////////////////////////////////////// 116 | TIMELOCK ERRORS 117 | ////////////////////////////////////////////////////////////////*/ 118 | 119 | /// @dev is thrown if the delay is less than minimum delay 120 | error INVALID_DELAY_MIN(); 121 | 122 | /// @dev is thrown if the delay is more than maximum delay 123 | error INVALID_DELAY_MAX(); 124 | 125 | /// @dev is thrown if the new admin is zero 126 | error ZERO_TIMELOCK_ADMIN(); 127 | 128 | /// @dev is thrown if timelock governance address input is zero 129 | error ZERO_GOVERNANCE_TIMELOCK(); 130 | 131 | /// @dev is thrown if tx id is zero (or) invalid 132 | error INVALID_TX_ID(); 133 | 134 | /// @dev is thrown if the hash stored mismatches 135 | error INVALID_TX_INPUT(); 136 | 137 | /// @dev is thrown if tx id is already executed 138 | error TX_ALREADY_EXECUTED(); 139 | 140 | /// @dev is thrown if msg.value is not equal to value 141 | error INVALID_MSG_VALUE(); 142 | 143 | /// @dev is thrown if timelock period is not over 144 | error TX_TIMELOCKED(); 145 | 146 | /// @dev is thrown if transaction is expired 147 | error TX_EXPIRED(); 148 | 149 | /*///////////////////////////////////////////////////////////////// 150 | GAC ERRORS 151 | ////////////////////////////////////////////////////////////////*/ 152 | /// @dev is thrown if the gas limit is less than minimum 153 | error INVALID_DST_GAS_LIMIT_MIN(); 154 | } 155 | -------------------------------------------------------------------------------- /src/libraries/Message.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// @title MessageStruct 5 | /// @dev library for cross-chain message & related helper functions 6 | library MessageLibrary { 7 | /// @dev Message indicates a remote call to target contract on destination chain. 8 | /// @param srcChainId is the id of chain where this message is sent from 9 | /// @param dstChainId is the id of chain where this message is sent to 10 | /// @param nonce is an incrementing number held by MultiBridgeMessageSender.sol to ensure msgId uniqueness 11 | /// @param target is the contract to be called on dst chain 12 | /// @param callData is the data to be sent to target by low-level call(eg. address(target).call(callData)) 13 | /// @param nativeValue is the native token value to be sent to target in the low-level call(eg. address(target).call{value: nativeValue}(callData)) 14 | /// @param expiration is the unix time when the message expires. 15 | struct Message { 16 | uint256 srcChainId; 17 | uint256 dstChainId; 18 | address target; 19 | uint256 nonce; 20 | bytes callData; 21 | uint256 nativeValue; 22 | uint256 expiration; 23 | } 24 | 25 | /// @notice encapsulates data that is relevant to a message's intended transaction execution. 26 | struct MessageExecutionParams { 27 | // target contract address on the destination chain 28 | address target; 29 | // data to pass to target by low-level call 30 | bytes callData; 31 | // value to pass to target by low-level call 32 | uint256 value; 33 | // nonce of the message 34 | uint256 nonce; 35 | // expiration timestamp for the message beyond which it cannot be executed 36 | uint256 expiration; 37 | } 38 | 39 | /// @notice computes the message id (32 byte hash of the encoded message parameters) 40 | /// @param _message is the cross-chain message 41 | function computeMsgId(Message memory _message) internal pure returns (bytes32) { 42 | return keccak256( 43 | abi.encodePacked( 44 | _message.srcChainId, 45 | _message.dstChainId, 46 | _message.nonce, 47 | _message.target, 48 | _message.nativeValue, 49 | _message.expiration, 50 | _message.callData 51 | ) 52 | ); 53 | } 54 | 55 | function extractExecutionParams(Message memory _message) internal pure returns (MessageExecutionParams memory) { 56 | return MessageExecutionParams({ 57 | target: _message.target, 58 | callData: _message.callData, 59 | value: _message.nativeValue, 60 | nonce: _message.nonce, 61 | expiration: _message.expiration 62 | }); 63 | } 64 | 65 | function computeExecutionParamsHash(MessageExecutionParams memory _params) internal pure returns (bytes32) { 66 | return keccak256( 67 | abi.encodePacked(_params.target, _params.callData, _params.value, _params.nonce, _params.expiration) 68 | ); 69 | } 70 | 71 | function computeExecutionParamsHash(Message memory _message) internal pure returns (bytes32) { 72 | return computeExecutionParamsHash(extractExecutionParams(_message)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/libraries/TypeCasts.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | pragma solidity >=0.8.9; 3 | 4 | /// @dev imported from https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/solidity/contracts/libs/TypeCasts.sol 5 | library TypeCasts { 6 | // alignment preserving cast 7 | function addressToBytes32(address _addr) internal pure returns (bytes32) { 8 | return bytes32(uint256(uint160(_addr))); 9 | } 10 | 11 | // alignment preserving cast 12 | function bytes32ToAddress(bytes32 _buf) internal pure returns (address) { 13 | return address(uint160(uint256(_buf))); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/libraries/Types.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity >=0.8.9; 4 | 5 | struct AdapterPayload { 6 | bytes32 msgId; 7 | address senderAdapterCaller; 8 | address receiverAdapter; 9 | address finalDestination; 10 | bytes data; 11 | } 12 | -------------------------------------------------------------------------------- /test/contracts-mock/FailingSenderAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// @dev A mock sender adapter that always fails at dispatching messages 5 | contract FailingSenderAdapter { 6 | function dispatchMessage(uint256, address, bytes calldata) external payable returns (bytes32) { 7 | revert(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/contracts-mock/MockUniswapReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// @dev assumes mock interactor is the one that send / receives message using the MMA infra 5 | contract MockUniswapReceiver { 6 | uint256 public i; 7 | 8 | function setValue() external { 9 | i = type(uint256).max; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/contracts-mock/ZeroAddressReceiverGAC.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// @dev A mock GAC with zero address receiver 5 | contract ZeroAddressReceiverGAC { 6 | address public immutable authorisedCaller; 7 | 8 | constructor(address _caller) { 9 | authorisedCaller = _caller; 10 | } 11 | 12 | function remoteMultiBridgeMessageReceiver(uint256) external pure returns (address _mmaReceiver) { 13 | return address(0); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/contracts-mock/adapters/axelar/MockAxelarGateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// @dev A mock Axelar gateway that either validates or rejects contract calls 5 | contract MockAxelarGateway { 6 | bool validate; 7 | 8 | constructor(bool _validate) { 9 | validate = _validate; 10 | } 11 | 12 | function validateContractCall(bytes32, string calldata, string calldata, bytes32) external view returns (bool) { 13 | return validate; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/integration-tests/GracePeriodExpiry.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; 7 | 8 | /// local imports 9 | import "test/Setup.t.sol"; 10 | import "test/contracts-mock/MockUniswapReceiver.sol"; 11 | 12 | import {MultiBridgeMessageSender} from "src/MultiBridgeMessageSender.sol"; 13 | import {MultiBridgeMessageReceiver} from "src/MultiBridgeMessageReceiver.sol"; 14 | import "src/libraries/Message.sol"; 15 | import {Error} from "src/libraries/Error.sol"; 16 | import {GovernanceTimelock} from "src/controllers/GovernanceTimelock.sol"; 17 | 18 | /// @dev scenario: tries to execute the txId after grace period ends 19 | contract GracePeriodExpiryTest is Setup { 20 | MockUniswapReceiver target; 21 | 22 | /// @dev initializes the setup 23 | function setUp() public override { 24 | super.setUp(); 25 | 26 | vm.selectFork(fork[DST_CHAIN_ID]); 27 | target = new MockUniswapReceiver(); 28 | } 29 | 30 | function test_timelockCheck() public { 31 | vm.selectFork(fork[SRC_CHAIN_ID]); 32 | vm.startPrank(caller); 33 | 34 | /// send cross-chain message using MMA infra 35 | vm.recordLogs(); 36 | (uint256 wormholeFee,) = 37 | IWormholeRelayer(POLYGON_RELAYER).quoteEVMDeliveryPrice(_wormholeChainId(DST_CHAIN_ID), 0, 0); 38 | (, uint256[] memory fees) = _sortTwoAdaptersWithFees( 39 | contractAddress[SRC_CHAIN_ID][bytes("AXELAR_SENDER_ADAPTER")], 40 | contractAddress[SRC_CHAIN_ID][bytes("WORMHOLE_SENDER_ADAPTER")], 41 | 0.01 ether, 42 | wormholeFee 43 | ); 44 | 45 | bytes memory callData = abi.encode(MockUniswapReceiver.setValue.selector, ""); 46 | uint256 nativeValue = 0; 47 | uint256 expiration = block.timestamp + EXPIRATION_CONSTANT; 48 | MultiBridgeMessageSender sender = MultiBridgeMessageSender(contractAddress[SRC_CHAIN_ID][bytes("MMA_SENDER")]); 49 | uint256 nonce = sender.nonce() + 1; 50 | sender.remoteCall{value: 2 ether}( 51 | DST_CHAIN_ID, 52 | address(target), 53 | callData, 54 | nativeValue, 55 | EXPIRATION_CONSTANT, 56 | refundAddress, 57 | fees, 58 | DEFAULT_SUCCESS_THRESHOLD, 59 | new address[](0) 60 | ); 61 | 62 | Vm.Log[] memory logs = vm.getRecordedLogs(); 63 | vm.stopPrank(); 64 | 65 | vm.recordLogs(); 66 | /// simulate off-chain actors 67 | _simulatePayloadDelivery(SRC_CHAIN_ID, DST_CHAIN_ID, logs); 68 | bytes32 msgId = _getMsgId(vm.getRecordedLogs()); 69 | 70 | vm.selectFork(fork[DST_CHAIN_ID]); 71 | vm.recordLogs(); 72 | /// schedule the message for execution by moving it to governance timelock contract 73 | MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).scheduleMessageExecution( 74 | msgId, 75 | MessageLibrary.MessageExecutionParams({ 76 | target: address(target), 77 | callData: callData, 78 | value: nativeValue, 79 | nonce: nonce, 80 | expiration: expiration 81 | }) 82 | ); 83 | (uint256 txId, address finalTarget, uint256 value, bytes memory data, uint256 eta) = 84 | _getExecParams(vm.getRecordedLogs()); 85 | 86 | /// increment the time by 21 day (beyond expiry, delay) 87 | /// @notice should revert here with TX_EXPIRED error 88 | vm.warp(block.timestamp + 21 days); 89 | vm.expectRevert(Error.TX_EXPIRED.selector); 90 | GovernanceTimelock(contractAddress[DST_CHAIN_ID][bytes("TIMELOCK")]).executeTransaction( 91 | txId, finalTarget, value, data, eta 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/integration-tests/MultiMessageAggregation.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; 7 | 8 | /// local imports 9 | import "test/Setup.t.sol"; 10 | import "test/contracts-mock/MockUniswapReceiver.sol"; 11 | 12 | import {MultiBridgeMessageSender} from "src/MultiBridgeMessageSender.sol"; 13 | import {MultiBridgeMessageReceiver} from "src/MultiBridgeMessageReceiver.sol"; 14 | import "src/libraries/Message.sol"; 15 | import {Error} from "src/libraries/Error.sol"; 16 | import {GovernanceTimelock} from "src/controllers/GovernanceTimelock.sol"; 17 | 18 | contract MultiBridgeMessageAggregationTest is Setup { 19 | MockUniswapReceiver target; 20 | 21 | /// @dev initializes the setup 22 | function setUp() public override { 23 | super.setUp(); 24 | 25 | vm.selectFork(fork[DST_CHAIN_ID]); 26 | target = new MockUniswapReceiver(); 27 | } 28 | 29 | /// @dev just sends a message 30 | function test_mmaSendMessage() public { 31 | vm.selectFork(fork[SRC_CHAIN_ID]); 32 | vm.startPrank(caller); 33 | 34 | /// send cross-chain message using MMA infra 35 | vm.recordLogs(); 36 | (uint256 wormholeFee,) = 37 | IWormholeRelayer(POLYGON_RELAYER).quoteEVMDeliveryPrice(_wormholeChainId(DST_CHAIN_ID), 0, 0); 38 | (, uint256[] memory fees) = _sortTwoAdaptersWithFees( 39 | contractAddress[SRC_CHAIN_ID][bytes("AXELAR_SENDER_ADAPTER")], 40 | contractAddress[SRC_CHAIN_ID][bytes("WORMHOLE_SENDER_ADAPTER")], 41 | 0.01 ether, 42 | wormholeFee 43 | ); 44 | 45 | bytes memory callData = abi.encode(MockUniswapReceiver.setValue.selector, ""); 46 | uint256 nativeValue = 0; 47 | uint256 expiration = block.timestamp + EXPIRATION_CONSTANT; 48 | MultiBridgeMessageSender sender = MultiBridgeMessageSender(contractAddress[SRC_CHAIN_ID][bytes("MMA_SENDER")]); 49 | uint256 nonce = sender.nonce() + 1; 50 | sender.remoteCall{value: 2 ether}( 51 | DST_CHAIN_ID, 52 | address(target), 53 | callData, 54 | nativeValue, 55 | EXPIRATION_CONSTANT, 56 | refundAddress, 57 | fees, 58 | DEFAULT_SUCCESS_THRESHOLD, 59 | new address[](0) 60 | ); 61 | 62 | Vm.Log[] memory logs = vm.getRecordedLogs(); 63 | vm.stopPrank(); 64 | 65 | vm.recordLogs(); 66 | /// simulate off-chain actors 67 | _simulatePayloadDelivery(SRC_CHAIN_ID, DST_CHAIN_ID, logs); 68 | bytes32 msgId = _getMsgId(vm.getRecordedLogs()); 69 | 70 | vm.selectFork(fork[DST_CHAIN_ID]); 71 | vm.recordLogs(); 72 | /// schedule the message for execution by moving it to governance timelock contract 73 | MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).scheduleMessageExecution( 74 | msgId, 75 | MessageLibrary.MessageExecutionParams({ 76 | target: address(target), 77 | callData: callData, 78 | value: nativeValue, 79 | nonce: nonce, 80 | expiration: expiration 81 | }) 82 | ); 83 | (uint256 txId, address finalTarget, uint256 value, bytes memory data, uint256 eta) = 84 | _getExecParams(vm.getRecordedLogs()); 85 | 86 | /// increment the time by 7 days (delay time) 87 | vm.warp(block.timestamp + 7 days); 88 | GovernanceTimelock(contractAddress[DST_CHAIN_ID][bytes("TIMELOCK")]).executeTransaction( 89 | txId, finalTarget, value, data, eta 90 | ); 91 | assertEq(target.i(), type(uint256).max); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/integration-tests/RemoteAdapterAdd.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; 7 | 8 | /// local imports 9 | import "test/Setup.t.sol"; 10 | 11 | import {MultiBridgeMessageSender} from "src/MultiBridgeMessageSender.sol"; 12 | import {MultiBridgeMessageReceiver} from "src/MultiBridgeMessageReceiver.sol"; 13 | import "src/libraries/Message.sol"; 14 | import {Error} from "src/libraries/Error.sol"; 15 | import {GovernanceTimelock} from "src/controllers/GovernanceTimelock.sol"; 16 | 17 | /// @dev scenario: admin updates sender adapters on dst chain using message from source chain 18 | /// @notice handles both single add and multiple add 19 | contract RemoteAdapterAdd is Setup { 20 | /// @dev initializes the setup 21 | function setUp() public override { 22 | super.setUp(); 23 | } 24 | 25 | /// @dev just add one adapter and assert 26 | function test_remoteAddReceiverAdapterSingle() public { 27 | address[] memory adaptersToAdd = new address[](1); 28 | adaptersToAdd[0] = address(420421422); 29 | 30 | /// true = add 31 | /// false = remove 32 | bool[] memory operation = new bool[](1); 33 | operation[0] = true; 34 | 35 | _adapterAdd(adaptersToAdd, operation); 36 | } 37 | 38 | /// @dev add multiple adapters and assert 39 | function test_remoteAddReceiverAdapterMulti() public { 40 | address[] memory adaptersToAdd = new address[](3); 41 | adaptersToAdd[0] = address(42042142232313); 42 | adaptersToAdd[1] = address(22132131); 43 | adaptersToAdd[2] = address(22132132131); 44 | 45 | /// true = add 46 | /// false = remove 47 | bool[] memory operation = new bool[](3); 48 | operation[0] = true; 49 | operation[1] = true; 50 | operation[2] = true; 51 | 52 | _adapterAdd(adaptersToAdd, operation); 53 | } 54 | 55 | function _adapterAdd(address[] memory adaptersToAdd, bool[] memory operation) private { 56 | vm.selectFork(fork[ETHEREUM_CHAIN_ID]); 57 | vm.startPrank(caller); 58 | 59 | /// send cross-chain message using MMA infra 60 | vm.recordLogs(); 61 | (uint256 wormholeFee,) = 62 | IWormholeRelayer(POLYGON_RELAYER).quoteEVMDeliveryPrice(_wormholeChainId(DST_CHAIN_ID), 0, 0); 63 | (, uint256[] memory fees) = _sortTwoAdaptersWithFees( 64 | contractAddress[SRC_CHAIN_ID][bytes("AXELAR_SENDER_ADAPTER")], 65 | contractAddress[SRC_CHAIN_ID][bytes("WORMHOLE_SENDER_ADAPTER")], 66 | 0.01 ether, 67 | wormholeFee 68 | ); 69 | 70 | _sendAndExecuteMessage(adaptersToAdd, operation, fees); 71 | 72 | for (uint256 j; j < adaptersToAdd.length; ++j) { 73 | bool isTrusted = MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]) 74 | .isTrustedExecutor(adaptersToAdd[j]); 75 | assert(isTrusted); 76 | } 77 | } 78 | 79 | function _sendAndExecuteMessage(address[] memory adaptersToAdd, bool[] memory operation, uint256[] memory fees) 80 | private 81 | { 82 | MultiBridgeMessageSender sender = MultiBridgeMessageSender(contractAddress[SRC_CHAIN_ID][bytes("MMA_SENDER")]); 83 | uint256 nonce = sender.nonce() + 1; 84 | bytes memory callData = 85 | abi.encodeWithSelector(MultiBridgeMessageReceiver.updateReceiverAdapters.selector, adaptersToAdd, operation); 86 | uint256 expiration = block.timestamp + EXPIRATION_CONSTANT; 87 | sender.remoteCall{value: 2 ether}( 88 | DST_CHAIN_ID, 89 | contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")], 90 | callData, 91 | 0, 92 | EXPIRATION_CONSTANT, 93 | refundAddress, 94 | fees, 95 | DEFAULT_SUCCESS_THRESHOLD, 96 | new address[](0) 97 | ); 98 | 99 | Vm.Log[] memory logs = vm.getRecordedLogs(); 100 | vm.stopPrank(); 101 | 102 | vm.recordLogs(); 103 | /// simulate off-chain actors 104 | _simulatePayloadDelivery(SRC_CHAIN_ID, DST_CHAIN_ID, logs); 105 | bytes32 msgId = _getMsgId(vm.getRecordedLogs()); 106 | 107 | vm.selectFork(fork[DST_CHAIN_ID]); 108 | vm.recordLogs(); 109 | /// schedule the message for execution by moving it to governance timelock contract 110 | MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).scheduleMessageExecution( 111 | msgId, 112 | MessageLibrary.MessageExecutionParams({ 113 | target: contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")], 114 | callData: callData, 115 | value: 0, 116 | nonce: nonce, 117 | expiration: expiration 118 | }) 119 | ); 120 | (uint256 txId, address finalTarget, uint256 value, bytes memory data, uint256 eta) = 121 | _getExecParams(vm.getRecordedLogs()); 122 | 123 | /// increment the time by 3 days (delay time) 124 | vm.warp(block.timestamp + 3 days); 125 | GovernanceTimelock(contractAddress[DST_CHAIN_ID][bytes("TIMELOCK")]).executeTransaction( 126 | txId, finalTarget, value, data, eta 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /test/integration-tests/RemoteAdapterRemove.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; 7 | 8 | /// local imports 9 | import "test/Setup.t.sol"; 10 | 11 | import {MultiBridgeMessageSender} from "src/MultiBridgeMessageSender.sol"; 12 | import {MultiBridgeMessageReceiver} from "src/MultiBridgeMessageReceiver.sol"; 13 | import "src/libraries/Message.sol"; 14 | import {Error} from "src/libraries/Error.sol"; 15 | import {GovernanceTimelock} from "src/controllers/GovernanceTimelock.sol"; 16 | 17 | /// @dev scenario: admin updates sender adapters on dst chain using message from source chain 18 | /// @notice handles both single add and multiple remove 19 | contract RemoteAdapterRemove is Setup { 20 | /// @dev initializes the setup 21 | function setUp() public override { 22 | super.setUp(); 23 | } 24 | 25 | /// @dev just remove one adapter and assert 26 | function test_remoteRemoveReceiverAdapterSingle() public { 27 | address[] memory adaptersToRemove = new address[](1); 28 | adaptersToRemove[0] = contractAddress[DST_CHAIN_ID]["AXELAR_RECEIVER_ADAPTER"]; 29 | 30 | /// true = add 31 | /// false = remove 32 | bool[] memory operation = new bool[](1); 33 | operation[0] = false; 34 | 35 | uint256 newQuorum = 1; 36 | 37 | _adapterRemove(newQuorum, adaptersToRemove, operation); 38 | } 39 | 40 | /// @dev add multiple adapters and assert 41 | function test_remoteRemoveReceiverAdapterMulti() public { 42 | /// @dev adds a dummy adapter since quorum threshold can never be 0 43 | _updateDummy(); 44 | 45 | address[] memory adaptersToRemove = new address[](2); 46 | adaptersToRemove[0] = contractAddress[DST_CHAIN_ID]["AXELAR_RECEIVER_ADAPTER"]; 47 | adaptersToRemove[1] = contractAddress[DST_CHAIN_ID]["WORMHOLE_RECEIVER_ADAPTER"]; 48 | 49 | /// true = add 50 | /// false = remove 51 | bool[] memory operation = new bool[](2); 52 | operation[0] = false; 53 | operation[1] = false; 54 | 55 | uint256 newQuorum = 1; 56 | 57 | _adapterRemove(newQuorum, adaptersToRemove, operation); 58 | } 59 | 60 | function _adapterRemove(uint256 newQuorum, address[] memory adaptersToRemove, bool[] memory operation) internal { 61 | vm.selectFork(fork[ETHEREUM_CHAIN_ID]); 62 | vm.startPrank(caller); 63 | 64 | /// send cross-chain message using MMA infra 65 | vm.recordLogs(); 66 | (uint256 wormholeFee,) = 67 | IWormholeRelayer(POLYGON_RELAYER).quoteEVMDeliveryPrice(_wormholeChainId(DST_CHAIN_ID), 0, 0); 68 | (, uint256[] memory fees) = _sortTwoAdaptersWithFees( 69 | contractAddress[SRC_CHAIN_ID][bytes("AXELAR_SENDER_ADAPTER")], 70 | contractAddress[SRC_CHAIN_ID][bytes("WORMHOLE_SENDER_ADAPTER")], 71 | 0.01 ether, 72 | wormholeFee 73 | ); 74 | 75 | _sendAndExecuteMessage(newQuorum, adaptersToRemove, operation, fees); 76 | 77 | /// @dev validates quorum post update 78 | assertEq(MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).quorum(), newQuorum); 79 | 80 | /// @dev validates adapters post update 81 | for (uint256 j; j < adaptersToRemove.length; ++j) { 82 | bool isTrusted = MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]) 83 | .isTrustedExecutor(adaptersToRemove[j]); 84 | assert(!isTrusted); 85 | } 86 | } 87 | 88 | function _updateDummy() private { 89 | address[] memory newDummyAdapter = new address[](1); 90 | newDummyAdapter[0] = address(420); 91 | 92 | /// true = add 93 | /// false = remove 94 | bool[] memory operation = new bool[](1); 95 | operation[0] = true; 96 | 97 | vm.startPrank(contractAddress[DST_CHAIN_ID]["TIMELOCK"]); 98 | MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID]["MMA_RECEIVER"]).updateReceiverAdapters( 99 | newDummyAdapter, operation 100 | ); 101 | vm.stopPrank(); 102 | } 103 | 104 | function _sendAndExecuteMessage( 105 | uint256 newQuorum, 106 | address[] memory adaptersToRemove, 107 | bool[] memory operation, 108 | uint256[] memory fees 109 | ) private { 110 | bytes memory callData = abi.encodeWithSelector( 111 | MultiBridgeMessageReceiver.updateReceiverAdaptersAndQuorum.selector, adaptersToRemove, operation, newQuorum 112 | ); 113 | uint256 expiration = block.timestamp + EXPIRATION_CONSTANT; 114 | MultiBridgeMessageSender sender = MultiBridgeMessageSender(contractAddress[SRC_CHAIN_ID][bytes("MMA_SENDER")]); 115 | uint256 nonce = sender.nonce() + 1; 116 | sender.remoteCall{value: 2 ether}( 117 | DST_CHAIN_ID, 118 | contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")], 119 | callData, 120 | 0, 121 | EXPIRATION_CONSTANT, 122 | refundAddress, 123 | fees, 124 | DEFAULT_SUCCESS_THRESHOLD, 125 | new address[](0) 126 | ); 127 | 128 | Vm.Log[] memory logs = vm.getRecordedLogs(); 129 | vm.stopPrank(); 130 | 131 | vm.recordLogs(); 132 | /// simulate off-chain actors 133 | _simulatePayloadDelivery(SRC_CHAIN_ID, DST_CHAIN_ID, logs); 134 | bytes32 msgId = _getMsgId(vm.getRecordedLogs()); 135 | 136 | vm.selectFork(fork[DST_CHAIN_ID]); 137 | vm.recordLogs(); 138 | /// schedule the message for execution by moving it to governance timelock contract 139 | MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).scheduleMessageExecution( 140 | msgId, 141 | MessageLibrary.MessageExecutionParams({ 142 | target: contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")], 143 | callData: callData, 144 | value: 0, 145 | nonce: nonce, 146 | expiration: expiration 147 | }) 148 | ); 149 | (uint256 txId, address finalTarget, uint256 value, bytes memory data, uint256 eta) = 150 | _getExecParams(vm.getRecordedLogs()); 151 | 152 | /// increment the time by 7 days (delay time) 153 | vm.warp(block.timestamp + 7 days); 154 | GovernanceTimelock(contractAddress[DST_CHAIN_ID][bytes("TIMELOCK")]).executeTransaction( 155 | txId, finalTarget, value, data, eta 156 | ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /test/integration-tests/RemoteSetQuorum.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; 7 | 8 | /// local imports 9 | import "test/Setup.t.sol"; 10 | 11 | import {MultiBridgeMessageSender} from "src/MultiBridgeMessageSender.sol"; 12 | import {MultiBridgeMessageReceiver} from "src/MultiBridgeMessageReceiver.sol"; 13 | import "src/libraries/Message.sol"; 14 | import {Error} from "src/libraries/Error.sol"; 15 | import {GovernanceTimelock} from "src/controllers/GovernanceTimelock.sol"; 16 | 17 | /// @dev scenario: admin updates quorum on dst chain using message from source chain 18 | contract RemoteQuorumUpdate is Setup { 19 | /// @dev initializes the setup 20 | function setUp() public override { 21 | super.setUp(); 22 | } 23 | 24 | /// @dev just set remote chain quorum to 1 from 2 (done in setup) 25 | function test_remoteQuorumUpdate() public { 26 | uint256 newQuorum = 1; 27 | 28 | vm.selectFork(fork[SRC_CHAIN_ID]); 29 | vm.startPrank(caller); 30 | 31 | /// send cross-chain message using MMA infra 32 | vm.recordLogs(); 33 | (uint256 wormholeFee,) = 34 | IWormholeRelayer(POLYGON_RELAYER).quoteEVMDeliveryPrice(_wormholeChainId(DST_CHAIN_ID), 0, 0); 35 | (, uint256[] memory fees) = _sortTwoAdaptersWithFees( 36 | contractAddress[SRC_CHAIN_ID][bytes("AXELAR_SENDER_ADAPTER")], 37 | contractAddress[SRC_CHAIN_ID][bytes("WORMHOLE_SENDER_ADAPTER")], 38 | 0.01 ether, 39 | wormholeFee 40 | ); 41 | 42 | _sendAndExecuteMessage(newQuorum, fees); 43 | 44 | uint256 currQuorum = MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).quorum(); 45 | assertEq(currQuorum, newQuorum); 46 | } 47 | 48 | function _sendAndExecuteMessage(uint256 newQuorum, uint256[] memory fees) private { 49 | address receiverAddr = contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]; 50 | bytes memory callData = abi.encodeWithSelector(MultiBridgeMessageReceiver.updateQuorum.selector, newQuorum); 51 | uint256 expiration = block.timestamp + EXPIRATION_CONSTANT; 52 | MultiBridgeMessageSender sender = MultiBridgeMessageSender(contractAddress[SRC_CHAIN_ID][bytes("MMA_SENDER")]); 53 | uint256 nonce = sender.nonce() + 1; 54 | sender.remoteCall{value: 2 ether}( 55 | DST_CHAIN_ID, 56 | receiverAddr, 57 | callData, 58 | 0, 59 | EXPIRATION_CONSTANT, 60 | refundAddress, 61 | fees, 62 | DEFAULT_SUCCESS_THRESHOLD, 63 | new address[](0) 64 | ); 65 | 66 | Vm.Log[] memory logs = vm.getRecordedLogs(); 67 | vm.stopPrank(); 68 | 69 | vm.recordLogs(); 70 | 71 | /// simulate off-chain actors 72 | _simulatePayloadDelivery(SRC_CHAIN_ID, DST_CHAIN_ID, logs); 73 | bytes32 msgId = _getMsgId(vm.getRecordedLogs()); 74 | 75 | vm.selectFork(fork[DST_CHAIN_ID]); 76 | vm.recordLogs(); 77 | /// schedule the message for execution by moving it to governance timelock contract 78 | MultiBridgeMessageReceiver(receiverAddr).scheduleMessageExecution( 79 | msgId, 80 | MessageLibrary.MessageExecutionParams({ 81 | target: receiverAddr, 82 | callData: callData, 83 | value: 0, 84 | nonce: nonce, 85 | expiration: expiration 86 | }) 87 | ); 88 | (uint256 txId, address finalTarget, uint256 value, bytes memory data, uint256 eta) = 89 | _getExecParams(vm.getRecordedLogs()); 90 | 91 | uint256 oldQuorum = MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).quorum(); 92 | assertEq(oldQuorum, 2); 93 | 94 | /// increment the time by 3 days (delay time) 95 | vm.warp(block.timestamp + 3 days); 96 | GovernanceTimelock(contractAddress[DST_CHAIN_ID][bytes("TIMELOCK")]).executeTransaction( 97 | txId, finalTarget, value, data, eta 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/integration-tests/RemoteTimelockUpdate.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; 7 | 8 | /// local imports 9 | import "test/Setup.t.sol"; 10 | 11 | import {MultiBridgeMessageSender} from "src/MultiBridgeMessageSender.sol"; 12 | import {MultiBridgeMessageReceiver} from "src/MultiBridgeMessageReceiver.sol"; 13 | import "src/libraries/Message.sol"; 14 | import {Error} from "src/libraries/Error.sol"; 15 | import {GovernanceTimelock} from "src/controllers/GovernanceTimelock.sol"; 16 | 17 | /// @dev scenario: admin updates timelock delay on dst chain using message from source chain 18 | contract RemoteTimelockUpdate is Setup { 19 | /// @dev initializes the setup 20 | function setUp() public override { 21 | super.setUp(); 22 | } 23 | 24 | /// @dev just set timelock delay to 19 days and assert 25 | function test_remoteTimelockUpdate() public { 26 | uint256 newDelay = 19 days; 27 | 28 | vm.selectFork(fork[SRC_CHAIN_ID]); 29 | vm.startPrank(caller); 30 | 31 | /// send cross-chain message using MMA infra 32 | vm.recordLogs(); 33 | (uint256 wormholeFee,) = 34 | IWormholeRelayer(POLYGON_RELAYER).quoteEVMDeliveryPrice(_wormholeChainId(DST_CHAIN_ID), 0, 0); 35 | (, uint256[] memory fees) = _sortTwoAdaptersWithFees( 36 | contractAddress[SRC_CHAIN_ID][bytes("AXELAR_SENDER_ADAPTER")], 37 | contractAddress[SRC_CHAIN_ID][bytes("WORMHOLE_SENDER_ADAPTER")], 38 | 0.01 ether, 39 | wormholeFee 40 | ); 41 | 42 | _sendAndExecuteMessage(newDelay, fees); 43 | 44 | uint256 currDelay = GovernanceTimelock(contractAddress[POLYGON_CHAIN_ID][bytes("TIMELOCK")]).delay(); 45 | assertEq(currDelay, newDelay); 46 | } 47 | 48 | function _sendAndExecuteMessage(uint256 newDelay, uint256[] memory fees) private { 49 | address timelockAddr = contractAddress[POLYGON_CHAIN_ID][bytes("TIMELOCK")]; 50 | bytes memory callData = abi.encodeWithSelector(GovernanceTimelock.setDelay.selector, newDelay); 51 | uint256 nativeValue = 0; 52 | uint256 expiration = block.timestamp + EXPIRATION_CONSTANT; 53 | MultiBridgeMessageSender sender = MultiBridgeMessageSender(contractAddress[SRC_CHAIN_ID][bytes("MMA_SENDER")]); 54 | uint256 nonce = sender.nonce() + 1; 55 | sender.remoteCall{value: 2 ether}( 56 | POLYGON_CHAIN_ID, 57 | timelockAddr, 58 | callData, 59 | nativeValue, 60 | EXPIRATION_CONSTANT, 61 | refundAddress, 62 | fees, 63 | DEFAULT_SUCCESS_THRESHOLD, 64 | new address[](0) 65 | ); 66 | 67 | Vm.Log[] memory logs = vm.getRecordedLogs(); 68 | vm.stopPrank(); 69 | 70 | vm.recordLogs(); 71 | 72 | /// simulate off-chain actors 73 | _simulatePayloadDelivery(ETHEREUM_CHAIN_ID, POLYGON_CHAIN_ID, logs); 74 | bytes32 msgId = _getMsgId(vm.getRecordedLogs()); 75 | 76 | vm.selectFork(fork[POLYGON_CHAIN_ID]); 77 | vm.recordLogs(); 78 | /// schedule the message for execution by moving it to governance timelock contract 79 | MultiBridgeMessageReceiver(contractAddress[POLYGON_CHAIN_ID][bytes("MMA_RECEIVER")]).scheduleMessageExecution( 80 | msgId, 81 | MessageLibrary.MessageExecutionParams({ 82 | target: timelockAddr, 83 | callData: callData, 84 | value: nativeValue, 85 | nonce: nonce, 86 | expiration: expiration 87 | }) 88 | ); 89 | (uint256 txId, address finalTarget, uint256 value, bytes memory data, uint256 eta) = 90 | _getExecParams(vm.getRecordedLogs()); 91 | 92 | uint256 oldDelay = GovernanceTimelock(contractAddress[POLYGON_CHAIN_ID][bytes("TIMELOCK")]).delay(); 93 | assertEq(oldDelay, 3 days); 94 | 95 | /// increment the time by 3 days (delay time) 96 | vm.warp(block.timestamp + 3 days); 97 | GovernanceTimelock(contractAddress[POLYGON_CHAIN_ID][bytes("TIMELOCK")]).executeTransaction( 98 | txId, finalTarget, value, data, eta 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/integration-tests/TimelockCheck.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; 7 | 8 | /// local imports 9 | import "test/Setup.t.sol"; 10 | import "test/contracts-mock/MockUniswapReceiver.sol"; 11 | 12 | import {MultiBridgeMessageSender} from "src/MultiBridgeMessageSender.sol"; 13 | import {MultiBridgeMessageReceiver} from "src/MultiBridgeMessageReceiver.sol"; 14 | import "src/libraries/Message.sol"; 15 | import {Error} from "src/libraries/Error.sol"; 16 | import {GovernanceTimelock} from "src/controllers/GovernanceTimelock.sol"; 17 | 18 | /// @dev scenario 1: tries to execute the txId before timelock ends 19 | /// @dev scenario 2: tries to execute the txId post timelock ends and within expiry 20 | contract TimelockCheckTest is Setup { 21 | MockUniswapReceiver target; 22 | 23 | /// @dev initializes the setup 24 | function setUp() public override { 25 | super.setUp(); 26 | 27 | vm.selectFork(fork[DST_CHAIN_ID]); 28 | target = new MockUniswapReceiver(); 29 | } 30 | 31 | /// @dev just sends a message 32 | function test_timelockCheck() public { 33 | vm.selectFork(fork[SRC_CHAIN_ID]); 34 | vm.startPrank(caller); 35 | 36 | /// send cross-chain message using MMA infra 37 | vm.recordLogs(); 38 | (uint256 wormholeFee,) = 39 | IWormholeRelayer(POLYGON_RELAYER).quoteEVMDeliveryPrice(_wormholeChainId(DST_CHAIN_ID), 0, 0); 40 | (, uint256[] memory fees) = _sortTwoAdaptersWithFees( 41 | contractAddress[SRC_CHAIN_ID][bytes("AXELAR_SENDER_ADAPTER")], 42 | contractAddress[SRC_CHAIN_ID][bytes("WORMHOLE_SENDER_ADAPTER")], 43 | 0.01 ether, 44 | wormholeFee 45 | ); 46 | 47 | bytes memory callData = abi.encode(MockUniswapReceiver.setValue.selector, ""); 48 | uint256 nativeValue = 0; 49 | uint256 expiration = block.timestamp + EXPIRATION_CONSTANT; 50 | MultiBridgeMessageSender sender = MultiBridgeMessageSender(contractAddress[SRC_CHAIN_ID][bytes("MMA_SENDER")]); 51 | uint256 nonce = sender.nonce() + 1; 52 | sender.remoteCall{value: 2 ether}( 53 | DST_CHAIN_ID, 54 | address(target), 55 | callData, 56 | nativeValue, 57 | EXPIRATION_CONSTANT, 58 | refundAddress, 59 | fees, 60 | DEFAULT_SUCCESS_THRESHOLD, 61 | new address[](0) 62 | ); 63 | 64 | Vm.Log[] memory logs = vm.getRecordedLogs(); 65 | vm.stopPrank(); 66 | 67 | vm.recordLogs(); 68 | /// simulate off-chain actors 69 | _simulatePayloadDelivery(SRC_CHAIN_ID, DST_CHAIN_ID, logs); 70 | bytes32 msgId = _getMsgId(vm.getRecordedLogs()); 71 | 72 | vm.selectFork(fork[DST_CHAIN_ID]); 73 | vm.recordLogs(); 74 | /// schedule the message for execution by moving it to governance timelock contract 75 | MultiBridgeMessageReceiver(contractAddress[DST_CHAIN_ID][bytes("MMA_RECEIVER")]).scheduleMessageExecution( 76 | msgId, 77 | MessageLibrary.MessageExecutionParams({ 78 | target: address(target), 79 | callData: callData, 80 | value: nativeValue, 81 | nonce: nonce, 82 | expiration: expiration 83 | }) 84 | ); 85 | (uint256 txId, address finalTarget, uint256 value, bytes memory data, uint256 eta) = 86 | _getExecParams(vm.getRecordedLogs()); 87 | 88 | /// increment the time by 1 day (less than delay time) 89 | /// @notice should revert here with TX_TIMELOCKED error 90 | vm.warp(block.timestamp + 1 days); 91 | vm.expectRevert(Error.TX_TIMELOCKED.selector); 92 | GovernanceTimelock(contractAddress[DST_CHAIN_ID][bytes("TIMELOCK")]).executeTransaction( 93 | txId, finalTarget, value, data, eta 94 | ); 95 | 96 | /// increment the time by 2 day (delay time) 97 | vm.warp(block.timestamp + 2 days); 98 | GovernanceTimelock(contractAddress[DST_CHAIN_ID][bytes("TIMELOCK")]).executeTransaction( 99 | txId, finalTarget, value, data, eta 100 | ); 101 | assertEq(target.i(), type(uint256).max); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/unit-tests/GovernanceTimelock.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | 7 | /// local imports 8 | import "../Setup.t.sol"; 9 | import "src/libraries/Error.sol"; 10 | import {GovernanceTimelock} from "src/controllers/GovernanceTimelock.sol"; 11 | 12 | contract GovernanceTimelockTest is Setup { 13 | event TransactionScheduled(uint256 indexed txId, address indexed target, uint256 value, bytes data, uint256 eta); 14 | event TransactionExecuted(uint256 indexed txId, address indexed target, uint256 value, bytes data, uint256 eta); 15 | 16 | event DelayUpdated(uint256 oldDelay, uint256 newDelay); 17 | event AdminUpdated(address oldAdmin, address newAdmin); 18 | 19 | GovernanceTimelock timelock; 20 | address admin; 21 | 22 | /// @dev initializes the setup 23 | function setUp() public override { 24 | super.setUp(); 25 | 26 | vm.selectFork(fork[DST_CHAIN_ID]); 27 | // admin is set to the receiver in setup 28 | admin = contractAddress[DST_CHAIN_ID]["MMA_RECEIVER"]; 29 | timelock = GovernanceTimelock(contractAddress[DST_CHAIN_ID]["TIMELOCK"]); 30 | } 31 | 32 | /// @dev constructor 33 | function test_constructor() public { 34 | // checks existing setup 35 | assertEq(address(timelock.admin()), admin); 36 | assertEq(timelock.delay(), 3 days); 37 | assertEq(timelock.txCounter(), 0); 38 | } 39 | 40 | /// @dev constructor emits events for updating admin and delay period 41 | function test_constructor_emits_events() public { 42 | vm.expectEmit(true, true, true, true); 43 | emit AdminUpdated(address(0), address(42)); 44 | vm.expectEmit(true, true, true, true); 45 | emit DelayUpdated(0, 3 days); 46 | 47 | new GovernanceTimelock(address(42), 3 days); 48 | } 49 | 50 | /// @dev cannot be called with zero address admin 51 | function test_constructor_zero_address_input() public { 52 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 53 | new GovernanceTimelock(address(0), 3 days); 54 | } 55 | 56 | /// @dev schedule transaction 57 | function test_schedule_transaction() public { 58 | vm.startPrank(admin); 59 | 60 | uint256 eta = block.timestamp + timelock.delay(); 61 | 62 | vm.expectEmit(true, true, true, true, address(timelock)); 63 | emit TransactionScheduled(1, address(42), 1, bytes("42"), eta); 64 | 65 | timelock.scheduleTransaction(address(42), 1, bytes("42")); 66 | 67 | assertEq(timelock.txCounter(), 1); 68 | assertEq( 69 | timelock.scheduledTransaction(1), keccak256(abi.encodePacked(address(42), uint256(1), eta, bytes("42"))) 70 | ); 71 | } 72 | 73 | /// @dev only admin can schedule transaction 74 | function test_schedule_transaction_only_admin() public { 75 | vm.startPrank(caller); 76 | 77 | vm.expectRevert(Error.CALLER_NOT_ADMIN.selector); 78 | timelock.scheduleTransaction(address(42), 1, bytes("42")); 79 | } 80 | 81 | /// @dev cannot call with target address of 0 82 | function test_schedule_transaction_zero_target_address() public { 83 | vm.startPrank(admin); 84 | 85 | vm.expectRevert(Error.INVALID_TARGET.selector); 86 | timelock.scheduleTransaction(address(0), 1, bytes("42")); 87 | } 88 | 89 | /// @dev execute transaction 90 | function test_execute_transaction() public { 91 | vm.startPrank(admin); 92 | 93 | // schedule a transaction first 94 | uint256 eta = block.timestamp + timelock.delay(); 95 | timelock.scheduleTransaction(address(42), 1, bytes("42")); 96 | 97 | // let timelock pass 98 | skip(timelock.delay()); 99 | vm.startPrank(caller); 100 | 101 | vm.expectEmit(true, true, true, true, address(timelock)); 102 | emit TransactionExecuted(1, address(42), uint256(1), bytes("42"), eta); 103 | 104 | timelock.executeTransaction{value: 1}(1, address(42), 1, bytes("42"), eta); 105 | 106 | assertTrue(timelock.isExecuted(1)); 107 | } 108 | 109 | /// @dev cannot execute with zero tx ID 110 | function test_execute_transaction_zero_tx_id() public { 111 | vm.startPrank(caller); 112 | 113 | vm.expectRevert(Error.INVALID_TX_ID.selector); 114 | timelock.executeTransaction(0, address(42), 0, bytes("42"), block.timestamp); 115 | } 116 | 117 | /// @dev cannot execute with a tx ID too large 118 | function test_execute_transaction_tx_id_too_large() public { 119 | vm.startPrank(caller); 120 | 121 | vm.expectRevert(Error.INVALID_TX_ID.selector); 122 | timelock.executeTransaction(1, address(42), 0, bytes("42"), block.timestamp); 123 | } 124 | 125 | /// @dev cannot execute tx that is already executed 126 | function test_execute_transaction_already_executed() public { 127 | vm.startPrank(admin); 128 | 129 | uint256 eta = block.timestamp + timelock.delay(); 130 | timelock.scheduleTransaction(address(42), 0, bytes("42")); 131 | skip(timelock.delay()); 132 | 133 | vm.startPrank(caller); 134 | 135 | timelock.executeTransaction(1, address(42), 0, bytes("42"), eta); 136 | 137 | vm.expectRevert(Error.TX_ALREADY_EXECUTED.selector); 138 | timelock.executeTransaction(1, address(42), 0, bytes("42"), eta); 139 | } 140 | 141 | /// @dev cannot execute tx with wrong hash 142 | function test_execute_transaction_invalid_input() public { 143 | vm.startPrank(admin); 144 | 145 | uint256 eta = block.timestamp + timelock.delay(); 146 | timelock.scheduleTransaction(address(42), 0, bytes("42")); 147 | skip(timelock.delay()); 148 | 149 | vm.startPrank(caller); 150 | 151 | vm.expectRevert(Error.INVALID_TX_INPUT.selector); 152 | timelock.executeTransaction(1, address(42), 0, bytes("42"), eta + 1); 153 | } 154 | 155 | /// @dev cannot execute tx that is still timelocked 156 | function test_execute_transaction_timelocked() public { 157 | vm.startPrank(admin); 158 | 159 | uint256 eta = block.timestamp + timelock.delay(); 160 | timelock.scheduleTransaction(address(42), 0, bytes("42")); 161 | 162 | vm.startPrank(caller); 163 | 164 | vm.expectRevert(Error.TX_TIMELOCKED.selector); 165 | timelock.executeTransaction(1, address(42), 0, bytes("42"), eta); 166 | } 167 | 168 | /// @dev cannot execute tx that has expired 169 | function test_execute_transaction_expired() public { 170 | vm.startPrank(admin); 171 | 172 | uint256 eta = block.timestamp + timelock.delay(); 173 | timelock.scheduleTransaction(address(42), 0, bytes("42")); 174 | skip(timelock.delay() + timelock.GRACE_PERIOD() + 1); 175 | 176 | vm.startPrank(caller); 177 | 178 | vm.expectRevert(Error.TX_EXPIRED.selector); 179 | timelock.executeTransaction(1, address(42), 0, bytes("42"), eta); 180 | } 181 | 182 | /// @dev cannot execute tx with invalid value 183 | function test_execute_transaction_invalid_value() public { 184 | vm.startPrank(admin); 185 | 186 | uint256 eta = block.timestamp + timelock.delay(); 187 | timelock.scheduleTransaction(address(42), 1, bytes("42")); 188 | skip(timelock.delay()); 189 | 190 | vm.startPrank(caller); 191 | 192 | vm.expectRevert(Error.INVALID_MSG_VALUE.selector); 193 | timelock.executeTransaction(1, address(42), 1, bytes("42"), eta); 194 | } 195 | 196 | /// @dev failed to execute tx on dst chain 197 | function test_execute_transaction_fails_on_dst() public { 198 | vm.startPrank(admin); 199 | 200 | uint256 eta = block.timestamp + timelock.delay(); 201 | // Use admin as dummy target address 202 | timelock.scheduleTransaction(admin, 0, bytes("42")); 203 | skip(timelock.delay()); 204 | 205 | vm.startPrank(caller); 206 | 207 | vm.expectRevert(Error.EXECUTION_FAILS_ON_DST.selector); 208 | timelock.executeTransaction(1, admin, 0, bytes("42"), eta); 209 | } 210 | 211 | /// @dev sets delay 212 | function test_set_delay() public { 213 | vm.startPrank(address(timelock)); 214 | 215 | uint256 oldDelay = timelock.delay(); 216 | vm.expectEmit(true, true, true, true, address(timelock)); 217 | emit DelayUpdated(oldDelay, 7 days); 218 | 219 | timelock.setDelay(7 days); 220 | } 221 | 222 | /// @dev only timelock can set delay 223 | function test_set_delay_only_self() public { 224 | vm.startPrank(caller); 225 | 226 | vm.expectRevert(Error.INVALID_SELF_CALLER.selector); 227 | timelock.setDelay(7 days); 228 | } 229 | 230 | /// @dev cannot set delay below minimum 231 | function test_set_delay_below_minimum() public { 232 | vm.startPrank(address(timelock)); 233 | 234 | uint256 minDelay = timelock.MINIMUM_DELAY(); 235 | vm.expectRevert(Error.INVALID_DELAY_MIN.selector); 236 | timelock.setDelay(minDelay - 1); 237 | } 238 | 239 | /// @dev cannot set delay above maximum 240 | function test_set_delay_above_maximum() public { 241 | vm.startPrank(address(timelock)); 242 | 243 | uint256 maxDelay = timelock.MAXIMUM_DELAY(); 244 | vm.expectRevert(Error.INVALID_DELAY_MAX.selector); 245 | timelock.setDelay(maxDelay + 1); 246 | } 247 | 248 | /// @dev sets admin 249 | function test_set_admin() public { 250 | vm.startPrank(address(timelock)); 251 | 252 | address oldAdmin = timelock.admin(); 253 | vm.expectEmit(true, true, true, true, address(timelock)); 254 | emit AdminUpdated(oldAdmin, address(42)); 255 | 256 | timelock.setAdmin(address(42)); 257 | } 258 | 259 | /// @dev only timelock can set admin 260 | function test_set_admin_only_self() public { 261 | vm.startPrank(caller); 262 | 263 | vm.expectRevert(Error.INVALID_SELF_CALLER.selector); 264 | timelock.setAdmin(address(42)); 265 | } 266 | 267 | /// @dev cannot set admin to zero address 268 | function test_set_admin_zero_address() public { 269 | vm.startPrank(address(timelock)); 270 | 271 | vm.expectRevert(Error.ZERO_TIMELOCK_ADMIN.selector); 272 | timelock.setAdmin(address(0)); 273 | } 274 | 275 | /// @dev sets delay via scheduled transaction 276 | function test_set_delay_scheduled() public { 277 | vm.startPrank(address(admin)); 278 | 279 | bytes memory data = abi.encodeWithSelector(GovernanceTimelock.setDelay.selector, 10 days); 280 | timelock.scheduleTransaction(address(timelock), 0, data); 281 | uint256 eta = block.timestamp + timelock.delay(); 282 | 283 | skip(3 days); 284 | 285 | timelock.executeTransaction(1, address(timelock), 0, data, eta); 286 | 287 | assertEq(timelock.delay(), 10 days); 288 | } 289 | 290 | /// @dev sets admin via scheduled transaction 291 | function test_set_admin_scheduled() public { 292 | vm.startPrank(address(admin)); 293 | 294 | bytes memory data = abi.encodeWithSelector(GovernanceTimelock.setAdmin.selector, address(42)); 295 | timelock.scheduleTransaction(address(timelock), 0, data); 296 | uint256 eta = block.timestamp + timelock.delay(); 297 | 298 | skip(3 days); 299 | 300 | timelock.executeTransaction(1, address(timelock), 0, data, eta); 301 | 302 | assertEq(timelock.admin(), address(42)); 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /test/unit-tests/MessageReceiverGAC.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | 7 | import "openzeppelin-contracts/contracts/access/Ownable.sol"; 8 | 9 | /// local imports 10 | import "../Setup.t.sol"; 11 | import "../contracts-mock/ZeroAddressReceiverGAC.sol"; 12 | import "src/interfaces/IMultiBridgeMessageReceiver.sol"; 13 | import "../../src/interfaces/controllers/IGAC.sol"; 14 | import "src/libraries/Error.sol"; 15 | import "src/libraries/Message.sol"; 16 | 17 | contract MessageReceiverGACTest is Setup { 18 | event MultiBridgeMessageReceiverUpdated(address indexed oldReceiver, address indexed newReceiver); 19 | 20 | MessageReceiverGAC receiverGAC; 21 | address gacOwner; 22 | /// @dev initializes the setup 23 | 24 | function setUp() public override { 25 | super.setUp(); 26 | vm.selectFork(fork[DST_CHAIN_ID]); 27 | receiverGAC = MessageReceiverGAC(contractAddress[DST_CHAIN_ID]["GAC"]); 28 | gacOwner = contractAddress[DST_CHAIN_ID]["TIMELOCK"]; 29 | } 30 | 31 | /// @dev constructor 32 | function test_constructor() public { 33 | // checks existing setup 34 | assertEq(address(Ownable(address(receiverGAC)).owner()), gacOwner); 35 | } 36 | 37 | /// @dev sets multi message receiver 38 | function test_set_multi_message_receiver() public { 39 | vm.startPrank(gacOwner); 40 | 41 | vm.expectEmit(true, true, true, true, address(receiverGAC)); 42 | emit MultiBridgeMessageReceiverUpdated(address(receiverGAC.multiBridgeMsgReceiver()), address(42)); 43 | 44 | receiverGAC.setMultiBridgeMessageReceiver(address(42)); 45 | 46 | assertEq(receiverGAC.multiBridgeMsgReceiver(), address(42)); 47 | } 48 | 49 | /// @dev only owner can set multi message receiver 50 | function test_set_multi_message_receiver_only_owner() public { 51 | vm.startPrank(caller); 52 | 53 | vm.expectRevert("Ownable: caller is not the owner"); 54 | receiverGAC.setMultiBridgeMessageReceiver(address(42)); 55 | } 56 | 57 | /// @dev cannot set multi message receiver to zero address 58 | function test_set_multi_message_receiver_zero_address() public { 59 | vm.startPrank(gacOwner); 60 | 61 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 62 | receiverGAC.setMultiBridgeMessageReceiver(address(0)); 63 | } 64 | 65 | /// @dev checks if address is the global owner 66 | function test_is_global_owner() public { 67 | assertTrue(receiverGAC.isGlobalOwner(gacOwner)); 68 | assertFalse(receiverGAC.isGlobalOwner(caller)); 69 | } 70 | 71 | /// @dev gets the global owner 72 | function test_get_global_owner() public { 73 | assertEq(receiverGAC.getGlobalOwner(), gacOwner); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/unit-tests/MessageSenderGAC.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | 7 | import "openzeppelin-contracts/contracts/access/Ownable.sol"; 8 | 9 | /// local imports 10 | import "../Setup.t.sol"; 11 | import "../contracts-mock/FailingSenderAdapter.sol"; 12 | import "../contracts-mock/ZeroAddressReceiverGAC.sol"; 13 | import "../../src/interfaces/adapters/IMessageSenderAdapter.sol"; 14 | import "src/interfaces/IMultiBridgeMessageReceiver.sol"; 15 | import "../../src/interfaces/controllers/IGAC.sol"; 16 | import "src/libraries/Error.sol"; 17 | import "src/libraries/Message.sol"; 18 | import {MultiBridgeMessageSender} from "src/MultiBridgeMessageSender.sol"; 19 | 20 | contract MessageSenderGACTest is Setup { 21 | event DstGasLimitUpdated(uint256 oldLimit, uint256 newLimit); 22 | event MultiBridgeMessageCallerUpdated(address indexed oldAuthCaller, address indexed newAuthCaller); 23 | event MultiBridgeMessageSenderUpdated(address indexed oldMMS, address indexed newMMS); 24 | event MultiBridgeMessageReceiverUpdated(uint256 indexed chainId, address indexed oldMMR, address indexed newMMR); 25 | 26 | address senderAddr; 27 | address receiverAddr; 28 | MessageSenderGAC senderGAC; 29 | 30 | /// @dev initializes the setup 31 | function setUp() public override { 32 | super.setUp(); 33 | 34 | vm.selectFork(fork[SRC_CHAIN_ID]); 35 | senderAddr = contractAddress[SRC_CHAIN_ID]["MMA_SENDER"]; 36 | receiverAddr = contractAddress[DST_CHAIN_ID]["MMA_RECEIVER"]; 37 | senderGAC = MessageSenderGAC(contractAddress[SRC_CHAIN_ID]["GAC"]); 38 | } 39 | 40 | /// @dev constructor 41 | function test_constructor() public { 42 | // checks existing setup 43 | assertEq(address(Ownable(address(senderGAC)).owner()), owner); 44 | } 45 | 46 | /// @dev sets multi message sender 47 | function test_set_multi_message_sender() public { 48 | vm.startPrank(owner); 49 | 50 | vm.expectEmit(true, true, true, true, address(senderGAC)); 51 | emit MultiBridgeMessageSenderUpdated(senderGAC.multiBridgeMessageSender(), address(42)); 52 | 53 | senderGAC.setMultiBridgeMessageSender(address(42)); 54 | 55 | assertEq(senderGAC.multiBridgeMessageSender(), address(42)); 56 | } 57 | 58 | /// @dev only owner can set multi message sender 59 | function test_set_multi_message_sender_only_owner() public { 60 | vm.startPrank(caller); 61 | 62 | vm.expectRevert("Ownable: caller is not the owner"); 63 | senderGAC.setMultiBridgeMessageSender(address(42)); 64 | } 65 | 66 | /// @dev cannot set multi message sender to zero address 67 | function test_set_multi_message_sender_zero_address() public { 68 | vm.startPrank(owner); 69 | 70 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 71 | senderGAC.setMultiBridgeMessageSender(address(0)); 72 | } 73 | 74 | /// @dev sets multi message caller 75 | function test_set_multi_message_caller() public { 76 | vm.startPrank(owner); 77 | 78 | vm.expectEmit(true, true, true, true, address(senderGAC)); 79 | emit MultiBridgeMessageCallerUpdated(senderGAC.authorisedCaller(), address(42)); 80 | 81 | senderGAC.setAuthorisedCaller(address(42)); 82 | 83 | assertEq(senderGAC.authorisedCaller(), address(42)); 84 | } 85 | 86 | /// @dev only owner can set multi message caller 87 | function test_set_multi_message_caller_only_owner() public { 88 | vm.startPrank(caller); 89 | 90 | vm.expectRevert("Ownable: caller is not the owner"); 91 | senderGAC.setAuthorisedCaller(address(42)); 92 | } 93 | 94 | /// @dev cannot set multi message caller to zero address 95 | function test_set_multi_message_caller_zero_address() public { 96 | vm.startPrank(owner); 97 | 98 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 99 | senderGAC.setAuthorisedCaller(address(0)); 100 | } 101 | 102 | /// @dev sets multi message receiver 103 | function test_set_multi_message_receiver() public { 104 | vm.startPrank(owner); 105 | 106 | vm.expectEmit(true, true, true, true, address(senderGAC)); 107 | emit MultiBridgeMessageReceiverUpdated( 108 | DST_CHAIN_ID, senderGAC.remoteMultiBridgeMessageReceiver(DST_CHAIN_ID), address(42) 109 | ); 110 | 111 | senderGAC.setRemoteMultiBridgeMessageReceiver(DST_CHAIN_ID, address(42)); 112 | 113 | assertEq(senderGAC.remoteMultiBridgeMessageReceiver(DST_CHAIN_ID), address(42)); 114 | } 115 | 116 | /// @dev only owner can set multi message receiver 117 | function test_set_multi_message_receiver_only_owner() public { 118 | vm.startPrank(caller); 119 | 120 | vm.expectRevert("Ownable: caller is not the owner"); 121 | senderGAC.setRemoteMultiBridgeMessageReceiver(DST_CHAIN_ID, address(42)); 122 | } 123 | 124 | /// @dev cannot set multi message receiver to zero address 125 | function test_set_multi_message_receiver_zero_address() public { 126 | vm.startPrank(owner); 127 | 128 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 129 | senderGAC.setRemoteMultiBridgeMessageReceiver(DST_CHAIN_ID, address(0)); 130 | } 131 | 132 | /// @dev cannot set multi message receiver on zero chain ID 133 | function test_set_multi_message_receiver_zero_chain_id() public { 134 | vm.startPrank(owner); 135 | 136 | vm.expectRevert(Error.ZERO_CHAIN_ID.selector); 137 | senderGAC.setRemoteMultiBridgeMessageReceiver(0, address(42)); 138 | } 139 | 140 | /// @dev sets global message delivery gas limit 141 | function test_set_global_msg_delivery_gas_limit() public { 142 | vm.startPrank(owner); 143 | 144 | uint256 oldLimit = senderGAC.msgDeliveryGasLimit(); 145 | vm.expectEmit(true, true, true, true, address(senderGAC)); 146 | emit DstGasLimitUpdated(oldLimit, 420000); 147 | 148 | senderGAC.setGlobalMsgDeliveryGasLimit(420000); 149 | 150 | assertEq(senderGAC.msgDeliveryGasLimit(), 420000); 151 | } 152 | 153 | /// @dev only owner can set global message delivery gas limit 154 | function test_set_global_msg_delivery_gas_limit_only_owner() public { 155 | vm.startPrank(caller); 156 | 157 | vm.expectRevert("Ownable: caller is not the owner"); 158 | senderGAC.setGlobalMsgDeliveryGasLimit(420000); 159 | } 160 | 161 | /// @dev cannot set a gas limit lower than the minimum 162 | function test_set_global_msg_delivery_gas_limit_lower_than_min() public { 163 | vm.startPrank(owner); 164 | 165 | vm.expectRevert(Error.INVALID_DST_GAS_LIMIT_MIN.selector); 166 | senderGAC.setGlobalMsgDeliveryGasLimit(30000); 167 | } 168 | 169 | /// @dev checks if address is the global owner 170 | function test_is_global_owner() public { 171 | assertTrue(senderGAC.isGlobalOwner(owner)); 172 | assertFalse(senderGAC.isGlobalOwner(caller)); 173 | } 174 | 175 | /// @dev gets the global owner 176 | function test_get_global_owner() public { 177 | assertEq(senderGAC.getGlobalOwner(), owner); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /test/unit-tests/adapters/BaseSenderAdapter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | 7 | /// local imports 8 | import "../../Setup.t.sol"; 9 | import "src/libraries/Error.sol"; 10 | import {AxelarSenderAdapter} from "src/adapters/axelar/AxelarSenderAdapter.sol"; 11 | import {BaseSenderAdapter} from "src/adapters/BaseSenderAdapter.sol"; 12 | 13 | contract AxelarSenderAdapterTest is Setup { 14 | event ReceiverAdapterUpdated(uint256 indexed dstChainId, address indexed oldReceiver, address indexed newReceiver); 15 | 16 | // Test base contract with Axelar adapter 17 | BaseSenderAdapter adapter; 18 | 19 | /// @dev initializes the setup 20 | function setUp() public override { 21 | super.setUp(); 22 | 23 | vm.selectFork(fork[SRC_CHAIN_ID]); 24 | adapter = AxelarSenderAdapter(contractAddress[SRC_CHAIN_ID]["AXELAR_SENDER_ADAPTER"]); 25 | } 26 | 27 | /// @dev updates receiver adapter 28 | function test_update_receiver_adapter() public { 29 | vm.startPrank(owner); 30 | 31 | address[] memory receiverAdapters = new address[](2); 32 | receiverAdapters[0] = address(42); 33 | receiverAdapters[1] = address(43); 34 | 35 | vm.expectEmit(true, true, true, true, address(adapter)); 36 | emit ReceiverAdapterUpdated(BSC_CHAIN_ID, adapter.receiverAdapters(BSC_CHAIN_ID), address(42)); 37 | vm.expectEmit(true, true, true, true, address(adapter)); 38 | emit ReceiverAdapterUpdated(POLYGON_CHAIN_ID, adapter.receiverAdapters(POLYGON_CHAIN_ID), address(43)); 39 | 40 | adapter.updateReceiverAdapter(DST_CHAINS, receiverAdapters); 41 | 42 | assertEq(adapter.receiverAdapters(BSC_CHAIN_ID), address(42)); 43 | assertEq(adapter.receiverAdapters(POLYGON_CHAIN_ID), address(43)); 44 | } 45 | 46 | /// @dev only global owner can update receiver adapter 47 | function test_update_receiver_adapter_only_global_owner() public { 48 | vm.startPrank(caller); 49 | 50 | vm.expectRevert(Error.CALLER_NOT_OWNER.selector); 51 | adapter.updateReceiverAdapter(new uint256[](0), new address[](0)); 52 | } 53 | 54 | /// @dev cannot update receiver adapter with invalid arrays 55 | function test_update_receiver_adapter_array_length_mismatched() public { 56 | vm.startPrank(owner); 57 | 58 | vm.expectRevert(Error.ARRAY_LENGTH_MISMATCHED.selector); 59 | adapter.updateReceiverAdapter(new uint256[](0), new address[](1)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/unit-tests/adapters/axelar/AxelarReceiverAdapter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | 7 | /// local imports 8 | import "../../../Setup.t.sol"; 9 | import "../../../contracts-mock/adapters/axelar/MockAxelarGateway.sol"; 10 | import "src/adapters/axelar/libraries/StringAddressConversion.sol"; 11 | import "src/MultiBridgeMessageReceiver.sol"; 12 | import "src/interfaces/EIP5164/MessageExecutor.sol"; 13 | import "src/libraries/Error.sol"; 14 | import "src/libraries/Message.sol"; 15 | import "src/libraries/Types.sol"; 16 | import {AxelarReceiverAdapter} from "src/adapters/axelar/AxelarReceiverAdapter.sol"; 17 | 18 | contract AxelarReceiverAdapterTest is Setup { 19 | using MessageLibrary for MessageLibrary.Message; 20 | using StringAddressConversion for address; 21 | 22 | event MessageIdExecuted(uint256 indexed fromChainId, bytes32 indexed messageId); 23 | event SenderAdapterUpdated(address indexed oldSenderAdapter, address indexed newSenderAdapter); 24 | 25 | AxelarReceiverAdapter adapter; 26 | address currOwner; 27 | 28 | /// @dev initializes the setup 29 | function setUp() public override { 30 | super.setUp(); 31 | 32 | vm.selectFork(fork[DST_CHAIN_ID]); 33 | adapter = AxelarReceiverAdapter(contractAddress[DST_CHAIN_ID]["AXELAR_RECEIVER_ADAPTER"]); 34 | currOwner = GAC(contractAddress[DST_CHAIN_ID]["GAC"]).owner(); 35 | } 36 | 37 | /// @dev constructor 38 | function test_constructor() public { 39 | // checks existing setup 40 | assertEq(address(adapter.gateway()), POLYGON_GATEWAY); 41 | assertEq(address(adapter.receiverGAC()), contractAddress[DST_CHAIN_ID]["GAC"]); 42 | assertEq(adapter.senderChainId(), "ethereum"); 43 | } 44 | 45 | /// @dev constructor with invalid parameters should fail 46 | function test_constructor_zero_gateway_address() public { 47 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 48 | new AxelarReceiverAdapter(address(0), "", address(42)); 49 | } 50 | 51 | /// @dev constructor with invalid parameters should fail 52 | function test_constructor_zero_gac_address() public { 53 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 54 | new AxelarReceiverAdapter(address(32), "", address(0)); 55 | } 56 | 57 | /// @dev constructor cannot be called with zero chain id 58 | function test_constructor_zero_chain_id() public { 59 | vm.expectRevert(Error.INVALID_SENDER_CHAIN_ID.selector); 60 | new AxelarReceiverAdapter(address(42), "", address(42)); 61 | } 62 | 63 | /// @dev gets the name 64 | function test_name() public { 65 | assertEq(adapter.name(), "AXELAR"); 66 | } 67 | 68 | /// @dev updates sender adapter 69 | function test_update_sender_adapter() public { 70 | vm.startPrank(currOwner); 71 | 72 | vm.expectEmit(true, true, true, true, address(adapter)); 73 | emit SenderAdapterUpdated(contractAddress[SRC_CHAIN_ID]["AXELAR_SENDER_ADAPTER"], address(42)); 74 | 75 | adapter.updateSenderAdapter(address(42)); 76 | 77 | assertEq(adapter.senderAdapter(), address(42)); 78 | } 79 | 80 | /// @dev only global owner can update sender adapter 81 | function test_update_sender_adapter_only_global_owner() public { 82 | vm.startPrank(caller); 83 | 84 | vm.expectRevert(Error.CALLER_NOT_OWNER.selector); 85 | adapter.updateSenderAdapter(address(42)); 86 | } 87 | 88 | /// @dev cannot update sender adapter with zero address 89 | function test_update_sender_adapter_zero_address_input() public { 90 | vm.startPrank(currOwner); 91 | 92 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 93 | adapter.updateSenderAdapter(address(0)); 94 | } 95 | 96 | /// @dev executes message 97 | function test_execute() public { 98 | (AxelarReceiverAdapter dummyAdapter, address senderAdapter, address receiverAddr) = _prepareDummyAdapter(true); 99 | 100 | MessageLibrary.Message memory message = MessageLibrary.Message({ 101 | srcChainId: SRC_CHAIN_ID, 102 | dstChainId: DST_CHAIN_ID, 103 | target: address(42), 104 | nonce: 0, 105 | callData: bytes("42"), 106 | nativeValue: 0, 107 | expiration: type(uint256).max 108 | }); 109 | bytes32 msgId = message.computeMsgId(); 110 | 111 | AdapterPayload memory payload = AdapterPayload({ 112 | msgId: msgId, 113 | senderAdapterCaller: address(42), 114 | receiverAdapter: address(dummyAdapter), 115 | finalDestination: receiverAddr, 116 | data: abi.encode(message) 117 | }); 118 | 119 | vm.expectEmit(true, true, true, true, address(dummyAdapter)); 120 | emit MessageIdExecuted(SRC_CHAIN_ID, msgId); 121 | 122 | dummyAdapter.execute(bytes32("commandId"), "ethereum", senderAdapter.toString(), abi.encode(payload)); 123 | 124 | assertTrue(dummyAdapter.isMessageExecuted(msgId)); 125 | assertTrue(dummyAdapter.commandIdStatus(bytes32("commandId"))); 126 | } 127 | 128 | /// @dev cannot execute message with invalid sender chain ID 129 | function test_execute_invalid_sender_chain_id() public { 130 | (AxelarReceiverAdapter dummyAdapter, address senderAdapter, address receiverAddr) = _prepareDummyAdapter(true); 131 | 132 | MessageLibrary.Message memory message = MessageLibrary.Message({ 133 | srcChainId: SRC_CHAIN_ID, 134 | dstChainId: DST_CHAIN_ID, 135 | target: address(42), 136 | nonce: 0, 137 | callData: bytes("42"), 138 | nativeValue: 0, 139 | expiration: type(uint256).max 140 | }); 141 | bytes32 msgId = message.computeMsgId(); 142 | 143 | AdapterPayload memory payload = AdapterPayload({ 144 | msgId: msgId, 145 | senderAdapterCaller: address(42), 146 | receiverAdapter: address(dummyAdapter), 147 | finalDestination: receiverAddr, 148 | data: abi.encode(message) 149 | }); 150 | 151 | vm.expectRevert(Error.INVALID_SENDER_CHAIN_ID.selector); 152 | dummyAdapter.execute(bytes32("commandId"), "bsc", senderAdapter.toString(), abi.encode(payload)); 153 | } 154 | 155 | /// @dev cannot execute message that is not approved by the Axelar gateway 156 | function test_execute_not_approved_by_gateway() public { 157 | (AxelarReceiverAdapter dummyAdapter, address senderAdapter, address receiverAddr) = _prepareDummyAdapter(false); 158 | 159 | MessageLibrary.Message memory message = MessageLibrary.Message({ 160 | srcChainId: SRC_CHAIN_ID, 161 | dstChainId: DST_CHAIN_ID, 162 | target: address(42), 163 | nonce: 0, 164 | callData: bytes("42"), 165 | nativeValue: 0, 166 | expiration: type(uint256).max 167 | }); 168 | bytes32 msgId = message.computeMsgId(); 169 | 170 | AdapterPayload memory payload = AdapterPayload({ 171 | msgId: msgId, 172 | senderAdapterCaller: address(42), 173 | receiverAdapter: address(dummyAdapter), 174 | finalDestination: receiverAddr, 175 | data: abi.encode(message) 176 | }); 177 | 178 | vm.expectRevert(Error.NOT_APPROVED_BY_GATEWAY.selector); 179 | dummyAdapter.execute(bytes32("commandId"), "ethereum", senderAdapter.toString(), abi.encode(payload)); 180 | } 181 | 182 | /// @dev cannot execute message with invalid sender adapter 183 | function test_execute_invalid_sender_adapter() public { 184 | (AxelarReceiverAdapter dummyAdapter,, address receiverAddr) = _prepareDummyAdapter(true); 185 | 186 | MessageLibrary.Message memory message = MessageLibrary.Message({ 187 | srcChainId: SRC_CHAIN_ID, 188 | dstChainId: DST_CHAIN_ID, 189 | target: address(42), 190 | nonce: 0, 191 | callData: bytes("42"), 192 | nativeValue: 0, 193 | expiration: type(uint256).max 194 | }); 195 | bytes32 msgId = message.computeMsgId(); 196 | 197 | AdapterPayload memory payload = AdapterPayload({ 198 | msgId: msgId, 199 | senderAdapterCaller: address(42), 200 | receiverAdapter: address(dummyAdapter), 201 | finalDestination: receiverAddr, 202 | data: abi.encode(message) 203 | }); 204 | 205 | vm.expectRevert(Error.INVALID_SENDER_ADAPTER.selector); 206 | dummyAdapter.execute(bytes32("commandId"), "ethereum", address(43).toString(), abi.encode(payload)); 207 | } 208 | 209 | /// @dev cannot execute message that is already executed 210 | function test_execute_message_id_already_executed() public { 211 | (AxelarReceiverAdapter dummyAdapter, address senderAdapter, address receiverAddr) = _prepareDummyAdapter(true); 212 | 213 | MessageLibrary.Message memory message = MessageLibrary.Message({ 214 | srcChainId: SRC_CHAIN_ID, 215 | dstChainId: DST_CHAIN_ID, 216 | target: address(42), 217 | nonce: 0, 218 | callData: bytes("42"), 219 | nativeValue: 0, 220 | expiration: type(uint256).max 221 | }); 222 | bytes32 msgId = message.computeMsgId(); 223 | 224 | AdapterPayload memory payload = AdapterPayload({ 225 | msgId: msgId, 226 | senderAdapterCaller: address(42), 227 | receiverAdapter: address(dummyAdapter), 228 | finalDestination: receiverAddr, 229 | data: abi.encode(message) 230 | }); 231 | 232 | dummyAdapter.execute(bytes32("commandId"), "ethereum", senderAdapter.toString(), abi.encode(payload)); 233 | 234 | vm.expectRevert(abi.encodeWithSelector(MessageExecutor.MessageIdAlreadyExecuted.selector, msgId)); 235 | dummyAdapter.execute(bytes32("commandId"), "ethereum", senderAdapter.toString(), abi.encode(payload)); 236 | } 237 | 238 | /// @dev cannot execute message with invalid receiver adapter 239 | function test_execute_invalid_receiver_adapter() public { 240 | (AxelarReceiverAdapter dummyAdapter, address senderAdapter,) = _prepareDummyAdapter(true); 241 | 242 | MessageLibrary.Message memory message = MessageLibrary.Message({ 243 | srcChainId: SRC_CHAIN_ID, 244 | dstChainId: DST_CHAIN_ID, 245 | target: address(42), 246 | nonce: 0, 247 | callData: bytes("42"), 248 | nativeValue: 0, 249 | expiration: type(uint256).max 250 | }); 251 | bytes32 msgId = message.computeMsgId(); 252 | 253 | AdapterPayload memory payload = AdapterPayload({ 254 | msgId: msgId, 255 | senderAdapterCaller: address(42), 256 | receiverAdapter: address(43), 257 | finalDestination: address(44), 258 | data: abi.encode(message) 259 | }); 260 | 261 | vm.expectRevert(Error.INVALID_RECEIVER_ADAPTER.selector); 262 | dummyAdapter.execute(bytes32("commandId"), "ethereum", senderAdapter.toString(), abi.encode(payload)); 263 | } 264 | 265 | /// @dev cannot execute message with invalid final destination 266 | function test_execute_invalid_final_destination() public { 267 | (AxelarReceiverAdapter dummyAdapter, address senderAdapter,) = _prepareDummyAdapter(true); 268 | 269 | MessageLibrary.Message memory message = MessageLibrary.Message({ 270 | srcChainId: SRC_CHAIN_ID, 271 | dstChainId: DST_CHAIN_ID, 272 | target: address(42), 273 | nonce: 0, 274 | callData: bytes("42"), 275 | nativeValue: 0, 276 | expiration: type(uint256).max 277 | }); 278 | bytes32 msgId = message.computeMsgId(); 279 | 280 | AdapterPayload memory payload = AdapterPayload({ 281 | msgId: msgId, 282 | senderAdapterCaller: address(42), 283 | receiverAdapter: address(dummyAdapter), 284 | finalDestination: address(43), 285 | data: abi.encode(message) 286 | }); 287 | 288 | vm.expectRevert(Error.INVALID_FINAL_DESTINATION.selector); 289 | dummyAdapter.execute(bytes32("commandId"), "ethereum", senderAdapter.toString(), abi.encode(payload)); 290 | } 291 | 292 | /// @dev reverts if message fails to be received 293 | function test_execute_message_failure() public { 294 | (AxelarReceiverAdapter dummyAdapter, address senderAdapter, address receiverAddr) = _prepareDummyAdapter(true); 295 | 296 | MessageLibrary.Message memory message = MessageLibrary.Message({ 297 | srcChainId: SRC_CHAIN_ID, 298 | dstChainId: DST_CHAIN_ID, 299 | target: address(0), // invalid target 300 | nonce: 0, 301 | callData: bytes("42"), 302 | nativeValue: 0, 303 | expiration: type(uint256).max 304 | }); 305 | bytes32 msgId = message.computeMsgId(); 306 | 307 | AdapterPayload memory payload = AdapterPayload({ 308 | msgId: msgId, 309 | senderAdapterCaller: address(42), 310 | receiverAdapter: address(dummyAdapter), 311 | finalDestination: receiverAddr, 312 | data: abi.encode(message) 313 | }); 314 | 315 | vm.expectRevert( 316 | abi.encodeWithSelector( 317 | MessageExecutor.MessageFailure.selector, msgId, abi.encodePacked(Error.INVALID_TARGET.selector) 318 | ) 319 | ); 320 | dummyAdapter.execute(bytes32("commandId"), "ethereum", senderAdapter.toString(), abi.encode(payload)); 321 | } 322 | 323 | function _prepareDummyAdapter(bool _validateCall) 324 | internal 325 | returns (AxelarReceiverAdapter dummyAdapter, address senderAdapter, address receiverAddr) 326 | { 327 | vm.startPrank(currOwner); 328 | 329 | senderAdapter = contractAddress[SRC_CHAIN_ID]["AXELAR_SENDER_ADAPTER"]; 330 | dummyAdapter = new AxelarReceiverAdapter( 331 | address(new MockAxelarGateway(_validateCall)), "ethereum", contractAddress[DST_CHAIN_ID]["GAC"] 332 | ); 333 | dummyAdapter.updateSenderAdapter(senderAdapter); 334 | 335 | vm.startPrank(contractAddress[DST_CHAIN_ID]["TIMELOCK"]); 336 | receiverAddr = contractAddress[DST_CHAIN_ID]["MMA_RECEIVER"]; 337 | address[] memory receiverAdapters = new address[](1); 338 | receiverAdapters[0] = address(dummyAdapter); 339 | bool[] memory operations = new bool[](1); 340 | operations[0] = true; 341 | MultiBridgeMessageReceiver(receiverAddr).updateReceiverAdapters(receiverAdapters, operations); 342 | 343 | vm.startPrank(caller); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /test/unit-tests/adapters/axelar/AxelarSenderAdapter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | 7 | /// local imports 8 | import "../../../Setup.t.sol"; 9 | import "src/libraries/Error.sol"; 10 | import {AxelarSenderAdapter} from "src/adapters/axelar/AxelarSenderAdapter.sol"; 11 | 12 | contract AxelarSenderAdapterTest is Setup { 13 | event MessageDispatched( 14 | bytes32 indexed messageId, address indexed from, uint256 indexed receiverChainId, address to, bytes data 15 | ); 16 | event ChainIDMappingUpdated(uint256 indexed origId, string oldAxlId, string newAxlId); 17 | 18 | address senderAddr; 19 | AxelarSenderAdapter adapter; 20 | 21 | /// @dev initializes the setup 22 | function setUp() public override { 23 | super.setUp(); 24 | 25 | vm.selectFork(fork[SRC_CHAIN_ID]); 26 | senderAddr = contractAddress[SRC_CHAIN_ID]["MMA_SENDER"]; 27 | adapter = AxelarSenderAdapter(contractAddress[SRC_CHAIN_ID]["AXELAR_SENDER_ADAPTER"]); 28 | } 29 | 30 | /// @dev constructor 31 | function test_constructor() public { 32 | // checks existing setup 33 | assertEq(address(adapter.gasService()), ETH_GAS_SERVICE); 34 | assertEq(address(adapter.gateway()), ETH_GATEWAY); 35 | assertEq(address(adapter.senderGAC()), contractAddress[SRC_CHAIN_ID]["GAC"]); 36 | } 37 | 38 | /// @dev constructor cannot be called with zero address gas service 39 | function test_constructor_zero_address_relayer() public { 40 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 41 | new AxelarSenderAdapter(address(0), address(42), address(42)); 42 | } 43 | 44 | /// @dev constructor cannot be called with zero address gateway 45 | function test_constructor_zero_address_gateway() public { 46 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 47 | new AxelarSenderAdapter(address(42), address(0), address(42)); 48 | } 49 | 50 | /// @dev constructor cannot be called with zero address GAC 51 | function test_constructor_zero_address_gac() public { 52 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 53 | new AxelarSenderAdapter(address(42), address(42), address(0)); 54 | } 55 | 56 | /// @dev dispatches message 57 | function test_dispatch_message() public { 58 | vm.startPrank(senderAddr); 59 | vm.deal(senderAddr, 1 ether); 60 | 61 | bytes32 msgId = 62 | keccak256(abi.encodePacked(SRC_CHAIN_ID, DST_CHAIN_ID, uint256(0), address(adapter), address(42))); 63 | vm.expectEmit(true, true, true, true, address(adapter)); 64 | emit MessageDispatched(msgId, senderAddr, DST_CHAIN_ID, address(42), bytes("42")); 65 | 66 | adapter.dispatchMessage{value: 0.01 ether}(DST_CHAIN_ID, address(42), bytes("42")); 67 | 68 | assertEq(adapter.nonce(), 1); 69 | } 70 | 71 | /// @dev only sender can dispatch message 72 | function test_dispatch_message_only_sender() public { 73 | vm.startPrank(caller); 74 | 75 | vm.expectRevert(Error.CALLER_NOT_MULTI_MESSAGE_SENDER.selector); 76 | adapter.dispatchMessage{value: 1 ether}(DST_CHAIN_ID, address(42), bytes("42")); 77 | } 78 | 79 | /// @dev cannot dispatch message with zero receiver adapter 80 | function test_dispatch_message_zero_receiver_adapter() public { 81 | vm.startPrank(senderAddr); 82 | vm.deal(senderAddr, 1 ether); 83 | 84 | vm.expectRevert(Error.ZERO_RECEIVER_ADAPTER.selector); 85 | adapter.dispatchMessage{value: 1 ether}(9999, address(42), bytes("42")); 86 | } 87 | 88 | /// @dev cannot dispatch message to invalid dst chain 89 | function test_dispatch_message_invalid_dst_chain() public { 90 | // clear chain ID map entry first 91 | vm.startPrank(owner); 92 | uint256[] memory origIds = new uint256[](1); 93 | origIds[0] = DST_CHAIN_ID; 94 | string[] memory axlIds = new string[](1); 95 | axlIds[0] = ""; 96 | adapter.setChainIdMap(origIds, axlIds); 97 | 98 | vm.startPrank(senderAddr); 99 | vm.deal(senderAddr, 1 ether); 100 | 101 | vm.expectRevert(Error.INVALID_DST_CHAIN.selector); 102 | adapter.dispatchMessage{value: 1 ether}(DST_CHAIN_ID, address(42), bytes("42")); 103 | } 104 | 105 | /// @dev sets chain ID map 106 | function test_set_chain_id_map() public { 107 | vm.startPrank(owner); 108 | uint256[] memory origIds = new uint256[](1); 109 | origIds[0] = DST_CHAIN_ID; 110 | string[] memory axlIds = new string[](1); 111 | axlIds[0] = "42"; 112 | 113 | vm.expectEmit(true, true, true, true, address(adapter)); 114 | emit ChainIDMappingUpdated(origIds[0], adapter.chainIdMap(DST_CHAIN_ID), axlIds[0]); 115 | 116 | adapter.setChainIdMap(origIds, axlIds); 117 | 118 | assertEq(adapter.chainIdMap(DST_CHAIN_ID), "42"); 119 | } 120 | 121 | /// @dev only global owner can set chain ID map 122 | function test_set_chain_id_map_only_global_owner() public { 123 | vm.startPrank(caller); 124 | 125 | vm.expectRevert(Error.CALLER_NOT_OWNER.selector); 126 | adapter.setChainIdMap(new uint256[](0), new string[](0)); 127 | } 128 | 129 | /// @dev cannot set chain ID map with invalid ID arrays 130 | function test_set_chain_id_map_array_length_mismatched() public { 131 | vm.startPrank(owner); 132 | 133 | vm.expectRevert(Error.ARRAY_LENGTH_MISMATCHED.selector); 134 | adapter.setChainIdMap(new uint256[](0), new string[](1)); 135 | } 136 | 137 | /// @dev cannot set chain ID map with invalid chain ID 138 | function test_set_chain_id_map_zero_chain_id() public { 139 | vm.startPrank(owner); 140 | 141 | vm.expectRevert(Error.ZERO_CHAIN_ID.selector); 142 | adapter.setChainIdMap(new uint256[](1), new string[](1)); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /test/unit-tests/adapters/axelar/libraries/StringAddressConversion.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Test, Vm} from "forge-std/Test.sol"; 6 | 7 | /// local imports 8 | import "src/adapters/axelar/libraries/StringAddressConversion.sol"; 9 | 10 | /// @dev helper for testing StringAddressConversion library 11 | /// @dev library testing using foundry can only be done through helper contracts 12 | /// @dev see https://github.com/foundry-rs/foundry/issues/2567 13 | contract StringAddressConversionTestClient { 14 | function toString(address _addr) external pure returns (string memory) { 15 | return StringAddressConversion.toString(_addr); 16 | } 17 | 18 | function toAddress(string calldata _addressString) external pure returns (address) { 19 | return StringAddressConversion.toAddress(_addressString); 20 | } 21 | } 22 | 23 | contract StringAddressConversionTest is Test { 24 | StringAddressConversionTestClient public conversionHelper; 25 | 26 | /*/////////////////////////////////////////////////////////////// 27 | SETUP 28 | //////////////////////////////////////////////////////////////*/ 29 | function setUp() public { 30 | conversionHelper = new StringAddressConversionTestClient(); 31 | } 32 | 33 | /*/////////////////////////////////////////////////////////////// 34 | TEST CASES 35 | //////////////////////////////////////////////////////////////*/ 36 | 37 | /// @dev tests conversion of address to string 38 | function test_to_string() public { 39 | address testAddr = address(0x1234567890123456789012345678901234567890); 40 | string memory result = conversionHelper.toString(testAddr); 41 | string memory expected = "0x1234567890123456789012345678901234567890"; 42 | 43 | assertEq(keccak256(bytes(result)), keccak256(bytes(expected))); 44 | } 45 | 46 | /// @dev tests conversion of string to address 47 | function test_to_address() public { 48 | string memory testString = "0x1234567890123456789012345678901234567890"; 49 | address result = conversionHelper.toAddress(testString); 50 | address expected = address(0x1234567890123456789012345678901234567890); 51 | 52 | assertEq(result, expected); 53 | } 54 | 55 | /// @dev tests invalid address conversion 56 | function test_invalid_address_string_conversion() public { 57 | string memory invalidAddressString = "1234567890123456789012345678901234567892"; 58 | 59 | bytes4 selector = StringAddressConversion.InvalidAddressString.selector; 60 | vm.expectRevert(selector); 61 | conversionHelper.toAddress(invalidAddressString); 62 | } 63 | 64 | /// @dev tests short address string 65 | function test_short_address_string_conversion() public { 66 | string memory shortAddressString = "0x12345678901234567890123456789012345678"; 67 | 68 | bytes4 selector = StringAddressConversion.InvalidAddressString.selector; 69 | vm.expectRevert(selector); 70 | conversionHelper.toAddress(shortAddressString); 71 | } 72 | 73 | /// @dev tests long address string 74 | function test_long_address_string_conversion() public { 75 | string memory longAddressString = "0x123456789012345678901234567890123456789012"; 76 | 77 | bytes4 selector = StringAddressConversion.InvalidAddressString.selector; 78 | vm.expectRevert(selector); 79 | conversionHelper.toAddress(longAddressString); 80 | } 81 | 82 | /// @dev tests invalid prefix in address string 83 | function test_invalid_prefix_address_string_conversion() public { 84 | string memory invalidPrefixAddressString = "1x1234567890123456789012345678901234567890"; 85 | 86 | bytes4 selector = StringAddressConversion.InvalidAddressString.selector; 87 | vm.expectRevert(selector); 88 | conversionHelper.toAddress(invalidPrefixAddressString); 89 | } 90 | 91 | /// @dev tests address string with invalid characters 92 | function test_invalid_character_address_string_conversion() public { 93 | string memory invalidCharacterAddressString = "0x12345678901234567890123456789012345678g0"; // 'g' is an invalid character 94 | 95 | bytes4 selector = StringAddressConversion.InvalidAddressString.selector; 96 | vm.expectRevert(selector); 97 | conversionHelper.toAddress(invalidCharacterAddressString); 98 | } 99 | 100 | /// @dev tests conversion of string with lowercase hex characters to address 101 | function test_lowercase_hex_character_to_address() public { 102 | string memory testString = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; 103 | address result = conversionHelper.toAddress(testString); 104 | address expected = address(0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD); 105 | 106 | assertEq(result, expected); 107 | } 108 | 109 | /// @dev tests conversion of string with uppercase hex characters to address 110 | function test_ppercase_hex_character_to_address() public { 111 | string memory testString = "0xABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCD"; 112 | address result = conversionHelper.toAddress(testString); 113 | address expected = address(0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD); 114 | 115 | assertEq(result, expected); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /test/unit-tests/adapters/wormhole/WormholeReceiverAdapter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | 7 | /// local imports 8 | import "../../../Setup.t.sol"; 9 | import "src/MultiBridgeMessageReceiver.sol"; 10 | import "src/interfaces/EIP5164/MessageExecutor.sol"; 11 | import "src/libraries/Error.sol"; 12 | import "src/libraries/Message.sol"; 13 | import "src/libraries/Types.sol"; 14 | import "src/libraries/TypeCasts.sol"; 15 | import {WormholeReceiverAdapter} from "src/adapters/wormhole/WormholeReceiverAdapter.sol"; 16 | 17 | contract WormholeReceiverAdapterTest is Setup { 18 | using MessageLibrary for MessageLibrary.Message; 19 | 20 | event MessageIdExecuted(uint256 indexed fromChainId, bytes32 indexed messageId); 21 | event SenderAdapterUpdated(address indexed oldSenderAdapter, address indexed newSenderAdapter); 22 | 23 | WormholeReceiverAdapter adapter; 24 | address currOwner; 25 | 26 | /// @dev initializes the setup 27 | function setUp() public override { 28 | super.setUp(); 29 | 30 | vm.selectFork(fork[DST_CHAIN_ID]); 31 | adapter = WormholeReceiverAdapter(contractAddress[DST_CHAIN_ID]["WORMHOLE_RECEIVER_ADAPTER"]); 32 | currOwner = GAC(contractAddress[DST_CHAIN_ID]["GAC"]).owner(); 33 | } 34 | 35 | /// @dev constructor 36 | function test_constructor() public { 37 | // checks existing setup 38 | assertEq(address(adapter.relayer()), POLYGON_RELAYER); 39 | assertEq(address(adapter.receiverGAC()), contractAddress[DST_CHAIN_ID]["GAC"]); 40 | } 41 | 42 | /// @dev constructor cannot be called with zero address relayer 43 | function test_constructor_zero_address_relayer() public { 44 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 45 | new WormholeReceiverAdapter(address(0), _wormholeChainId(ETHEREUM_CHAIN_ID), address(42)); 46 | } 47 | 48 | /// @dev constructor cannot be called with zero address GAC 49 | function test_constructor_zero_address_gac() public { 50 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 51 | new WormholeReceiverAdapter(address(42), _wormholeChainId(ETHEREUM_CHAIN_ID), address(0)); 52 | } 53 | 54 | /// @dev constructor cannot be called with zero sender chain id 55 | function test_constructor_zero_chain_id() public { 56 | vm.expectRevert(Error.INVALID_SENDER_CHAIN_ID.selector); 57 | new WormholeReceiverAdapter(address(42), uint16(0), address(42)); 58 | } 59 | 60 | /// @dev gets the name 61 | function test_name() public { 62 | assertEq(adapter.name(), "WORMHOLE"); 63 | } 64 | 65 | /// @dev updates sender adapter 66 | function test_update_sender_adapter() public { 67 | vm.startPrank(currOwner); 68 | 69 | vm.expectEmit(true, true, true, true, address(adapter)); 70 | emit SenderAdapterUpdated(contractAddress[SRC_CHAIN_ID]["WORMHOLE_SENDER_ADAPTER"], address(42)); 71 | 72 | adapter.updateSenderAdapter(address(42)); 73 | 74 | assertEq(adapter.senderAdapter(), address(42)); 75 | assertEq(adapter.senderChainId(), uint16(2)); 76 | } 77 | 78 | /// @dev only global owner can update sender adapter 79 | function test_update_sender_adapter_only_global_owner() public { 80 | vm.startPrank(caller); 81 | 82 | vm.expectRevert(Error.CALLER_NOT_OWNER.selector); 83 | adapter.updateSenderAdapter(address(42)); 84 | } 85 | /// @dev cannot update sender adapter with zero address 86 | 87 | function test_update_sender_adapter_zero_address_input() public { 88 | vm.startPrank(currOwner); 89 | 90 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 91 | adapter.updateSenderAdapter(address(0)); 92 | } 93 | 94 | /// @dev receives Wormhole message 95 | function test_receive_wormhole_messages() public { 96 | vm.startPrank(POLYGON_RELAYER); 97 | 98 | address senderAdapter = contractAddress[SRC_CHAIN_ID]["WORMHOLE_SENDER_ADAPTER"]; 99 | address receiverAddr = contractAddress[DST_CHAIN_ID]["MMA_RECEIVER"]; 100 | MessageLibrary.Message memory message = MessageLibrary.Message({ 101 | srcChainId: SRC_CHAIN_ID, 102 | dstChainId: DST_CHAIN_ID, 103 | target: address(42), 104 | nonce: 0, 105 | callData: bytes("42"), 106 | nativeValue: 0, 107 | expiration: type(uint256).max 108 | }); 109 | bytes32 msgId = message.computeMsgId(); 110 | 111 | AdapterPayload memory payload = AdapterPayload({ 112 | msgId: msgId, 113 | senderAdapterCaller: address(42), 114 | receiverAdapter: address(adapter), 115 | finalDestination: receiverAddr, 116 | data: abi.encode(message) 117 | }); 118 | 119 | vm.expectEmit(true, true, true, true, address(adapter)); 120 | emit MessageIdExecuted(SRC_CHAIN_ID, msgId); 121 | 122 | adapter.receiveWormholeMessages( 123 | abi.encode(payload), new bytes[](0), TypeCasts.addressToBytes32(senderAdapter), uint16(2), bytes32("1234") 124 | ); 125 | 126 | assertTrue(adapter.isMessageExecuted(msgId)); 127 | assertTrue(adapter.deliveryHashStatus(bytes32("1234"))); 128 | } 129 | 130 | /// @dev cannot receive message with invalid sender chain ID 131 | function test_receive_wormhole_messages_invalid_sender_chain_id() public { 132 | vm.startPrank(POLYGON_RELAYER); 133 | 134 | address senderAdapter = contractAddress[SRC_CHAIN_ID]["WORMHOLE_SENDER_ADAPTER"]; 135 | address receiverAddr = contractAddress[DST_CHAIN_ID]["MMA_RECEIVER"]; 136 | MessageLibrary.Message memory message = MessageLibrary.Message({ 137 | srcChainId: SRC_CHAIN_ID, 138 | dstChainId: DST_CHAIN_ID, 139 | target: address(42), 140 | nonce: 0, 141 | callData: bytes("42"), 142 | nativeValue: 0, 143 | expiration: type(uint256).max 144 | }); 145 | bytes32 msgId = message.computeMsgId(); 146 | 147 | AdapterPayload memory payload = AdapterPayload({ 148 | msgId: msgId, 149 | senderAdapterCaller: address(42), 150 | receiverAdapter: address(adapter), 151 | finalDestination: receiverAddr, 152 | data: abi.encode(message) 153 | }); 154 | 155 | vm.expectRevert(Error.INVALID_SENDER_CHAIN_ID.selector); 156 | adapter.receiveWormholeMessages( 157 | abi.encode(payload), new bytes[](0), TypeCasts.addressToBytes32(senderAdapter), uint16(42), bytes32("1234") 158 | ); 159 | } 160 | 161 | /// @dev cannot receive message with invalid sender adapter 162 | function test_receive_wormhole_messages_invalid_sender_adapter() public { 163 | vm.startPrank(POLYGON_RELAYER); 164 | 165 | address receiverAddr = contractAddress[DST_CHAIN_ID]["MMA_RECEIVER"]; 166 | MessageLibrary.Message memory message = MessageLibrary.Message({ 167 | srcChainId: SRC_CHAIN_ID, 168 | dstChainId: DST_CHAIN_ID, 169 | target: address(42), 170 | nonce: 0, 171 | callData: bytes("42"), 172 | nativeValue: 0, 173 | expiration: type(uint256).max 174 | }); 175 | bytes32 msgId = message.computeMsgId(); 176 | 177 | AdapterPayload memory payload = AdapterPayload({ 178 | msgId: msgId, 179 | senderAdapterCaller: address(42), 180 | receiverAdapter: address(adapter), 181 | finalDestination: receiverAddr, 182 | data: abi.encode(message) 183 | }); 184 | 185 | vm.expectRevert(Error.INVALID_SENDER_ADAPTER.selector); 186 | adapter.receiveWormholeMessages( 187 | abi.encode(payload), new bytes[](0), TypeCasts.addressToBytes32(address(43)), uint16(2), bytes32("1234") 188 | ); 189 | } 190 | 191 | /// @dev cannot receive message that is already executed 192 | function test_receive_wormhole_messages_message_id_already_executed() public { 193 | vm.startPrank(POLYGON_RELAYER); 194 | 195 | address senderAdapter = contractAddress[SRC_CHAIN_ID]["WORMHOLE_SENDER_ADAPTER"]; 196 | address receiverAddr = contractAddress[DST_CHAIN_ID]["MMA_RECEIVER"]; 197 | MessageLibrary.Message memory message = MessageLibrary.Message({ 198 | srcChainId: SRC_CHAIN_ID, 199 | dstChainId: DST_CHAIN_ID, 200 | target: address(42), 201 | nonce: 0, 202 | callData: bytes("42"), 203 | nativeValue: 0, 204 | expiration: type(uint256).max 205 | }); 206 | bytes32 msgId = message.computeMsgId(); 207 | 208 | AdapterPayload memory payload = AdapterPayload({ 209 | msgId: msgId, 210 | senderAdapterCaller: address(42), 211 | receiverAdapter: address(adapter), 212 | finalDestination: receiverAddr, 213 | data: abi.encode(message) 214 | }); 215 | 216 | adapter.receiveWormholeMessages( 217 | abi.encode(payload), new bytes[](0), TypeCasts.addressToBytes32(senderAdapter), uint16(2), bytes32("1234") 218 | ); 219 | vm.expectRevert(abi.encodeWithSelector(MessageExecutor.MessageIdAlreadyExecuted.selector, msgId)); 220 | adapter.receiveWormholeMessages( 221 | abi.encode(payload), new bytes[](0), TypeCasts.addressToBytes32(senderAdapter), uint16(2), bytes32("1234") 222 | ); 223 | } 224 | 225 | /// @dev cannot receive message with invalid receiver adapter 226 | function test_receive_wormhole_messages_invalid_receiver_adapter() public { 227 | vm.startPrank(POLYGON_RELAYER); 228 | 229 | address senderAdapter = contractAddress[SRC_CHAIN_ID]["WORMHOLE_SENDER_ADAPTER"]; 230 | MessageLibrary.Message memory message = MessageLibrary.Message({ 231 | srcChainId: SRC_CHAIN_ID, 232 | dstChainId: DST_CHAIN_ID, 233 | target: address(42), 234 | nonce: 0, 235 | callData: bytes("42"), 236 | nativeValue: 0, 237 | expiration: type(uint256).max 238 | }); 239 | bytes32 msgId = message.computeMsgId(); 240 | 241 | AdapterPayload memory payload = AdapterPayload({ 242 | msgId: msgId, 243 | senderAdapterCaller: address(42), 244 | receiverAdapter: address(43), 245 | finalDestination: address(44), 246 | data: abi.encode(message) 247 | }); 248 | 249 | vm.expectRevert(Error.INVALID_RECEIVER_ADAPTER.selector); 250 | adapter.receiveWormholeMessages( 251 | abi.encode(payload), new bytes[](0), TypeCasts.addressToBytes32(senderAdapter), uint16(2), bytes32("1234") 252 | ); 253 | } 254 | 255 | /// @dev cannot receive message with invalid final destination 256 | function test_receive_wormhole_messages_invalid_final_destination() public { 257 | vm.startPrank(POLYGON_RELAYER); 258 | 259 | address senderAdapter = contractAddress[SRC_CHAIN_ID]["WORMHOLE_SENDER_ADAPTER"]; 260 | MessageLibrary.Message memory message = MessageLibrary.Message({ 261 | srcChainId: SRC_CHAIN_ID, 262 | dstChainId: DST_CHAIN_ID, 263 | target: address(42), 264 | nonce: 0, 265 | callData: bytes("42"), 266 | nativeValue: 0, 267 | expiration: type(uint256).max 268 | }); 269 | bytes32 msgId = message.computeMsgId(); 270 | 271 | AdapterPayload memory payload = AdapterPayload({ 272 | msgId: msgId, 273 | senderAdapterCaller: address(42), 274 | receiverAdapter: address(adapter), 275 | finalDestination: address(43), 276 | data: abi.encode(message) 277 | }); 278 | 279 | vm.expectRevert(Error.INVALID_FINAL_DESTINATION.selector); 280 | adapter.receiveWormholeMessages( 281 | abi.encode(payload), new bytes[](0), TypeCasts.addressToBytes32(senderAdapter), uint16(2), bytes32("1234") 282 | ); 283 | } 284 | 285 | /// @dev reverts if message fails to be received 286 | function test_receive_wormhole_messages_message_failure() public { 287 | vm.startPrank(POLYGON_RELAYER); 288 | 289 | address senderAdapter = contractAddress[SRC_CHAIN_ID]["WORMHOLE_SENDER_ADAPTER"]; 290 | address receiverAddr = contractAddress[DST_CHAIN_ID]["MMA_RECEIVER"]; 291 | MessageLibrary.Message memory message = MessageLibrary.Message({ 292 | srcChainId: SRC_CHAIN_ID, 293 | dstChainId: DST_CHAIN_ID, 294 | target: address(0), // invalid target 295 | nonce: 0, 296 | callData: bytes("42"), 297 | nativeValue: 0, 298 | expiration: type(uint256).max 299 | }); 300 | bytes32 msgId = message.computeMsgId(); 301 | 302 | AdapterPayload memory payload = AdapterPayload({ 303 | msgId: msgId, 304 | senderAdapterCaller: address(42), 305 | receiverAdapter: address(adapter), 306 | finalDestination: receiverAddr, 307 | data: abi.encode(message) 308 | }); 309 | 310 | vm.expectRevert( 311 | abi.encodeWithSelector( 312 | MessageExecutor.MessageFailure.selector, msgId, abi.encodePacked(Error.INVALID_TARGET.selector) 313 | ) 314 | ); 315 | adapter.receiveWormholeMessages( 316 | abi.encode(payload), new bytes[](0), TypeCasts.addressToBytes32(senderAdapter), uint16(2), bytes32("1234") 317 | ); 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /test/unit-tests/adapters/wormhole/WormholeSenderAdapter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Vm} from "forge-std/Test.sol"; 6 | 7 | /// local imports 8 | import "../../../Setup.t.sol"; 9 | import "src/libraries/Error.sol"; 10 | import {WormholeSenderAdapter} from "src/adapters/wormhole/WormholeSenderAdapter.sol"; 11 | import {IWormholeRelayer} from "lib/wormhole-solidity-sdk/src/interfaces/IWormholeRelayer.sol"; 12 | 13 | contract WormholeSenderAdapterTest is Setup { 14 | event MessageDispatched( 15 | bytes32 indexed messageId, address indexed from, uint256 indexed receiverChainId, address to, bytes data 16 | ); 17 | event ChainIDMappingUpdated(uint256 indexed origId, uint16 oldWhId, uint16 newWhId); 18 | 19 | address senderAddr; 20 | WormholeSenderAdapter adapter; 21 | 22 | /// @dev initializes the setup 23 | function setUp() public override { 24 | super.setUp(); 25 | 26 | vm.selectFork(fork[SRC_CHAIN_ID]); 27 | senderAddr = contractAddress[SRC_CHAIN_ID]["MMA_SENDER"]; 28 | adapter = WormholeSenderAdapter(contractAddress[SRC_CHAIN_ID]["WORMHOLE_SENDER_ADAPTER"]); 29 | } 30 | 31 | /// @dev constructor 32 | function test_constructor() public { 33 | // checks existing setup 34 | assertEq(address(adapter.senderGAC()), contractAddress[SRC_CHAIN_ID]["GAC"]); 35 | } 36 | 37 | /// @dev constructor cannot be called with zero address relayer 38 | function test_constructor_zero_address_relayer() public { 39 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 40 | new WormholeSenderAdapter(address(0), address(42)); 41 | } 42 | 43 | /// @dev constructor cannot be called with zero address GAC 44 | function test_constructor_zero_address_gac() public { 45 | vm.expectRevert(Error.ZERO_ADDRESS_INPUT.selector); 46 | new WormholeSenderAdapter(address(42), address(0)); 47 | } 48 | 49 | /// @dev dispatches message 50 | function test_dispatch_message() public { 51 | vm.startPrank(senderAddr); 52 | vm.deal(senderAddr, 1 ether); 53 | 54 | bytes32 msgId = 55 | keccak256(abi.encodePacked(SRC_CHAIN_ID, DST_CHAIN_ID, uint256(0), address(adapter), address(42))); 56 | (uint256 fee,) = IWormholeRelayer(POLYGON_RELAYER).quoteEVMDeliveryPrice( 57 | _wormholeChainId(DST_CHAIN_ID), 0, adapter.senderGAC().msgDeliveryGasLimit() 58 | ); 59 | 60 | vm.expectEmit(true, true, true, true, address(adapter)); 61 | emit MessageDispatched(msgId, senderAddr, DST_CHAIN_ID, address(42), bytes("42")); 62 | 63 | adapter.dispatchMessage{value: fee}(DST_CHAIN_ID, address(42), bytes("42")); 64 | 65 | assertEq(adapter.nonce(), 1); 66 | } 67 | 68 | /// @dev only sender can dispatch message 69 | function test_dispatch_message_only_sender() public { 70 | vm.startPrank(caller); 71 | 72 | vm.expectRevert(Error.CALLER_NOT_MULTI_MESSAGE_SENDER.selector); 73 | adapter.dispatchMessage{value: 1 ether}(DST_CHAIN_ID, address(42), bytes("42")); 74 | } 75 | 76 | /// @dev cannot dispatch message with zero receiver adapter 77 | function test_dispatch_message_zero_receiver_adapter() public { 78 | vm.startPrank(senderAddr); 79 | vm.deal(senderAddr, 1 ether); 80 | 81 | vm.expectRevert(Error.ZERO_RECEIVER_ADAPTER.selector); 82 | adapter.dispatchMessage{value: 1 ether}(9999, address(42), bytes("42")); 83 | } 84 | 85 | /// @dev cannot dispatch message to invalid dst chain 86 | function test_dispatch_message_unknown_chain_id() public { 87 | // clear chain ID map entry first 88 | vm.startPrank(owner); 89 | uint256[] memory origIds = new uint256[](1); 90 | origIds[0] = DST_CHAIN_ID; 91 | uint16[] memory whIds = new uint16[](1); 92 | whIds[0] = 0; 93 | adapter.setChainIdMap(origIds, whIds); 94 | 95 | vm.startPrank(senderAddr); 96 | vm.deal(senderAddr, 1 ether); 97 | 98 | vm.expectRevert(Error.INVALID_DST_CHAIN.selector); 99 | adapter.dispatchMessage{value: 1 ether}(DST_CHAIN_ID, address(42), bytes("42")); 100 | } 101 | 102 | /// @dev sets chain ID map 103 | function test_set_chain_id_map() public { 104 | vm.startPrank(owner); 105 | uint256[] memory origIds = new uint256[](1); 106 | origIds[0] = DST_CHAIN_ID; 107 | uint16[] memory whIds = new uint16[](1); 108 | whIds[0] = 42; 109 | 110 | vm.expectEmit(true, true, true, true, address(adapter)); 111 | emit ChainIDMappingUpdated(origIds[0], adapter.chainIdMap(DST_CHAIN_ID), whIds[0]); 112 | 113 | adapter.setChainIdMap(origIds, whIds); 114 | 115 | assertEq(adapter.chainIdMap(DST_CHAIN_ID), 42); 116 | } 117 | 118 | /// @dev only global owner can set chain ID map 119 | function test_set_chain_id_map_only_global_owner() public { 120 | vm.startPrank(caller); 121 | 122 | vm.expectRevert(Error.CALLER_NOT_OWNER.selector); 123 | adapter.setChainIdMap(new uint256[](0), new uint16[](0)); 124 | } 125 | 126 | /// @dev cannot set chain ID map with invalid ID arrays 127 | function test_set_chain_id_map_array_length_mismatched() public { 128 | vm.startPrank(owner); 129 | 130 | vm.expectRevert(Error.ARRAY_LENGTH_MISMATCHED.selector); 131 | adapter.setChainIdMap(new uint256[](0), new uint16[](1)); 132 | } 133 | 134 | /// @dev cannot set chain ID map with invalid chain ID 135 | function test_set_chain_id_map_zero_chain_id() public { 136 | vm.startPrank(owner); 137 | 138 | vm.expectRevert(Error.ZERO_CHAIN_ID.selector); 139 | adapter.setChainIdMap(new uint256[](1), new uint16[](1)); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /test/unit-tests/libraries/EIP5164/ExecutorAware.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Test, Vm} from "forge-std/Test.sol"; 6 | 7 | /// local imports 8 | import "src/libraries/EIP5164/ExecutorAware.sol"; 9 | 10 | /// @dev helper to test abstract contract 11 | contract ExecutorAwareTestClient is ExecutorAware { 12 | function addTrustedExecutor(address _executor) external returns (bool) { 13 | return _addTrustedExecutor(_executor); 14 | } 15 | 16 | function removeTrustedExecutor(address _executor) external returns (bool) { 17 | return _removeTrustedExecutor(_executor); 18 | } 19 | } 20 | 21 | contract ExecutorAwareTest is Test { 22 | ExecutorAwareTestClient public executorAware; 23 | 24 | /*/////////////////////////////////////////////////////////////// 25 | SETUP 26 | //////////////////////////////////////////////////////////////*/ 27 | function setUp() public { 28 | executorAware = new ExecutorAwareTestClient(); 29 | } 30 | 31 | /*/////////////////////////////////////////////////////////////// 32 | TEST CASES 33 | //////////////////////////////////////////////////////////////*/ 34 | 35 | /// @dev tests adding a trusted executor 36 | function test_add_trusted_executor() public { 37 | address executor = address(0x1234567890123456789012345678901234567890); 38 | bool added = executorAware.addTrustedExecutor(executor); 39 | 40 | assertEq(added, true); 41 | assertEq(executorAware.isTrustedExecutor(executor), true); 42 | } 43 | 44 | /// @dev tests removing a trusted executor 45 | function test_remove_trusted_executor() public { 46 | address executor = address(0x1234567890123456789012345678901234567890); 47 | executorAware.addTrustedExecutor(executor); 48 | 49 | bool removed = executorAware.removeTrustedExecutor(executor); 50 | 51 | assertEq(removed, true); 52 | assertEq(executorAware.isTrustedExecutor(executor), false); 53 | } 54 | 55 | /// @dev tests retrieval of trusted executors 56 | function test_get_trusted_executors() public { 57 | address executor1 = address(420); 58 | address executor2 = address(421); 59 | executorAware.addTrustedExecutor(executor1); 60 | executorAware.addTrustedExecutor(executor2); 61 | 62 | address[] memory executors = executorAware.getTrustedExecutors(); 63 | 64 | assertEq(executors.length == 2, true); 65 | assertEq(executors[0], executor1); 66 | assertEq(executors[1], executor2); 67 | } 68 | 69 | /// @dev tests counting the number of trusted executors 70 | function test_trusted_executors_count() public { 71 | address executor1 = address(420); 72 | address executor2 = address(421); 73 | executorAware.addTrustedExecutor(executor1); 74 | executorAware.addTrustedExecutor(executor2); 75 | 76 | uint256 count = executorAware.trustedExecutorsCount(); 77 | 78 | assertEq(count, 2); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/unit-tests/libraries/Message.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Test, Vm} from "forge-std/Test.sol"; 6 | 7 | /// local imports 8 | import "src/libraries/Message.sol"; 9 | 10 | /// @dev is a helper function to test library contracts 11 | /// @dev library testing using foundry can only be done through helper contracts 12 | /// @dev see https://github.com/foundry-rs/foundry/issues/2567 13 | contract MessageLibraryTestClient { 14 | using MessageLibrary for MessageLibrary.Message; 15 | 16 | function computeMsgId(MessageLibrary.Message memory _message) external pure returns (bytes32) { 17 | return _message.computeMsgId(); 18 | } 19 | 20 | function extractExecutionParams(MessageLibrary.Message memory _message) 21 | external 22 | pure 23 | returns (MessageLibrary.MessageExecutionParams memory) 24 | { 25 | return _message.extractExecutionParams(); 26 | } 27 | 28 | function computeExecutionParamsHash(MessageLibrary.MessageExecutionParams memory _params) 29 | external 30 | pure 31 | returns (bytes32) 32 | { 33 | return MessageLibrary.computeExecutionParamsHash(_params); 34 | } 35 | 36 | function computeExecutionParamsHashFromMessage(MessageLibrary.Message memory _message) 37 | external 38 | pure 39 | returns (bytes32) 40 | { 41 | return _message.computeExecutionParamsHash(); 42 | } 43 | } 44 | 45 | contract MessageLibraryTest is Test { 46 | MessageLibraryTestClient messageLibraryTestClient; 47 | 48 | function setUp() public { 49 | messageLibraryTestClient = new MessageLibraryTestClient(); 50 | } 51 | 52 | /// @dev tests computation of message id 53 | function test_compute_msg_id() public { 54 | // convert the string literal to bytes constant 55 | bytes memory callDataBytes = hex"abcdef"; 56 | 57 | MessageLibrary.Message memory message = MessageLibrary.Message({ 58 | srcChainId: 1, 59 | dstChainId: 2, 60 | target: address(0x1234567890123456789012345678901234567890), 61 | nonce: 123, 62 | callData: callDataBytes, 63 | nativeValue: 456, 64 | expiration: 10000 65 | }); 66 | 67 | bytes32 computedId = messageLibraryTestClient.computeMsgId(message); 68 | 69 | // Update the expectedId calculation to use the bytes constant 70 | bytes32 expectedId = keccak256( 71 | abi.encodePacked( 72 | uint256(1), 73 | uint256(2), 74 | uint256(123), 75 | address(0x1234567890123456789012345678901234567890), 76 | uint256(456), 77 | uint256(10000), 78 | callDataBytes 79 | ) 80 | ); 81 | 82 | assertEq(computedId, expectedId); 83 | } 84 | 85 | /// @dev tests extraction of execution parameters from a message 86 | function test_extract_execution_params() public { 87 | MessageLibrary.Message memory message = MessageLibrary.Message({ 88 | srcChainId: 1, 89 | dstChainId: 2, 90 | target: address(0x1234567890123456789012345678901234567890), 91 | nonce: 123, 92 | callData: hex"abcdef", 93 | nativeValue: 456, 94 | expiration: 10000 95 | }); 96 | 97 | MessageLibrary.MessageExecutionParams memory params = messageLibraryTestClient.extractExecutionParams(message); 98 | 99 | assertEq(params.target, address(0x1234567890123456789012345678901234567890)); 100 | assertEq(keccak256(params.callData), keccak256(hex"abcdef")); 101 | assertEq(params.value, 456); 102 | assertEq(params.nonce, 123); 103 | assertEq(params.expiration, 10000); 104 | } 105 | 106 | /// @dev tests computation of execution parameters hash directly from parameters 107 | function test_compute_execution_params_hash() public { 108 | MessageLibrary.MessageExecutionParams memory params = MessageLibrary.MessageExecutionParams({ 109 | target: address(0x1234567890123456789012345678901234567890), 110 | callData: hex"abcdef", 111 | value: 456, 112 | nonce: 123, 113 | expiration: 10000 114 | }); 115 | 116 | bytes32 computedHash = messageLibraryTestClient.computeExecutionParamsHash(params); 117 | bytes32 expectedHash = keccak256( 118 | abi.encodePacked( 119 | address(0x1234567890123456789012345678901234567890), 120 | hex"abcdef", 121 | uint256(456), 122 | uint256(123), 123 | uint256(10000) 124 | ) 125 | ); 126 | 127 | assertEq(computedHash, expectedHash); 128 | } 129 | 130 | /// @dev tests computation of execution parameters hash from a message 131 | function test_compute_execution_params_hash_from_message() public { 132 | MessageLibrary.Message memory message = MessageLibrary.Message({ 133 | srcChainId: 1, 134 | dstChainId: 2, 135 | target: address(0x1234567890123456789012345678901234567890), 136 | nonce: 123, 137 | callData: hex"abcdef", 138 | nativeValue: 456, 139 | expiration: 10000 140 | }); 141 | 142 | bytes32 computedHash = messageLibraryTestClient.computeExecutionParamsHashFromMessage(message); 143 | bytes32 expectedHash = keccak256( 144 | abi.encodePacked( 145 | address(0x1234567890123456789012345678901234567890), 146 | hex"abcdef", 147 | uint256(456), 148 | uint256(123), 149 | uint256(10000) 150 | ) 151 | ); 152 | 153 | assertEq(computedHash, expectedHash); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /test/unit-tests/libraries/TypeCasts.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity >=0.8.9; 3 | 4 | /// library imports 5 | import {Test, Vm} from "forge-std/Test.sol"; 6 | 7 | /// local imports 8 | import "src/libraries/TypeCasts.sol"; 9 | 10 | /// @dev helper to test TypeCasts library 11 | /// @dev library testing using foundry can only be done through helper contracts 12 | /// @dev see https://github.com/foundry-rs/foundry/issues/2567 13 | contract TypeCastsLibraryTestClient { 14 | function addressToBytes32(address _addr) external pure returns (bytes32) { 15 | return TypeCasts.addressToBytes32(_addr); 16 | } 17 | 18 | function bytes32ToAddress(bytes32 _buf) external pure returns (address) { 19 | return TypeCasts.bytes32ToAddress(_buf); 20 | } 21 | } 22 | 23 | contract TypeCastsTest is Test { 24 | TypeCastsLibraryTestClient public typeCastsLibraryTestClient; 25 | 26 | /*/////////////////////////////////////////////////////////////// 27 | SETUP 28 | //////////////////////////////////////////////////////////////*/ 29 | function setUp() public { 30 | typeCastsLibraryTestClient = new TypeCastsLibraryTestClient(); 31 | } 32 | 33 | /*/////////////////////////////////////////////////////////////// 34 | TEST CASES 35 | //////////////////////////////////////////////////////////////*/ 36 | 37 | /// @dev tests conversion of address to bytes32 38 | function test_address_to_bytes32() public { 39 | address testAddr = address(0x1234567890123456789012345678901234567890); 40 | bytes32 expected = bytes32(uint256(uint160(testAddr))); // Correct casting here 41 | bytes32 result = typeCastsLibraryTestClient.addressToBytes32(testAddr); 42 | 43 | assertEq(result, expected); 44 | } 45 | 46 | /// @dev tests conversion of bytes32 to address 47 | function test_bytes32_to_address() public { 48 | bytes32 testBytes = bytes32(uint256(uint160(0x1234567890123456789012345678901234567890))); 49 | 50 | address expected = address(uint160(uint256(testBytes))); // Correct casting here 51 | address result = typeCastsLibraryTestClient.bytes32ToAddress(testBytes); 52 | 53 | assertEq(result, expected); 54 | } 55 | } 56 | --------------------------------------------------------------------------------