diff --git a/.github/workflows/ci-olympix.yml b/.github/workflows/ci-olympix.yml new file mode 100644 index 0000000..7354677 --- /dev/null +++ b/.github/workflows/ci-olympix.yml @@ -0,0 +1,13 @@ +name: "Olympix Scan" +on: + pull_request: + branches: [ "master" ] + workflow_dispatch: + schedule: + - cron: '31 14 * * 1' # Every Monday 2:31PM UTC + +jobs: + run_olympix: + if: ${{ github.repository_owner == 'circlefin' }} + uses: circlefin/security-seceng-templates/.github/workflows/olympix_scan.yml@v1 + \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9dfec98..da2e374 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,11 @@ jobs: with: submodules: 'true' + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Install Node uses: actions/setup-node@v4 @@ -30,10 +35,56 @@ jobs: - name: Run Integration Tests run: make anvil-test - - name: Run Slither - uses: crytic/slither-action@v0.3.0 + - name: Run v2 Integration Tests + run: make anvil-test-v2 + + analyze-message-transmitter: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + submodules: 'true' + + - name: Set up Python + uses: actions/setup-python@v5 with: - fail-on: none + python-version: '3.10' + + - name: Run Static Analysis on Message Transmitter + run: make analyze-message-transmitter + + analyze-message-transmitter-v2: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + submodules: 'true' + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Run Static Analysis on Message Transmitter V2 + run: make analyze-message-transmitter-v2 + + analyze-token-messenger-minter: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + submodules: 'true' + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Run Static Analysis on Token Messenger Minter + run: make analyze-token-messenger-minter scan: needs: lint-and-test diff --git a/.vscode/settings.json b/.vscode/settings.json index 941e1a7..867ded9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,12 +1,14 @@ { - "editor.formatOnSave": true, - "solidity.compilerOptimization": 200, - "solidity.enabledAsYouTypeCompilationErrorCheck": true, - "solidity.compileUsingRemoteVersion": "v0.7.6+commit.7338295f", - "solidity.formatter": "prettier", - "solidity.linter": "solhint", - "solidity.validationDelay": 1500, - "[solidity]": { - "editor.tabSize": 4, - } + "editor.formatOnSave": true, + "solidity.compilerOptimization": 200, + "solidity.enabledAsYouTypeCompilationErrorCheck": true, + "solidity.compileUsingRemoteVersion": "v0.7.6+commit.7338295f", + "solidity.formatter": "prettier", + "solidity.linter": "solhint", + "solidity.validationDelay": 1500, + "[solidity]": { + "editor.tabSize": 4, + }, + "security.olympix.project.includePath": "/src", + "security.olympix.project.testsPath": "/test" } diff --git a/Dockerfile b/Dockerfile index e240985..1f9bcc4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ +ARG FOUNDRY_VERSION=nightly-3fa02706ca732c994715ba42d923605692062375 # Use fixed foundry image - -FROM ghcr.io/foundry-rs/foundry:nightly-4a8c7d0e26a1befa526222e22737740f80a7f1c5 +FROM ghcr.io/foundry-rs/foundry:${FOUNDRY_VERSION} # Copy our source code into the container WORKDIR /app diff --git a/Makefile b/Makefile index 3fa14cf..81d4d62 100644 --- a/Makefile +++ b/Makefile @@ -9,19 +9,59 @@ build: test: @${FOUNDRY} "forge test -vv" -simulate: - forge script scripts/deploy.s.sol:DeployScript --rpc-url ${RPC_URL} --sender ${SENDER} +simulate-deploy: + forge script scripts/v1/deploy.s.sol:DeployScript --rpc-url ${RPC_URL} --sender ${SENDER} deploy: - forge script scripts/deploy.s.sol:DeployScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast + forge script scripts/v1/deploy.s.sol:DeployScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast + +simulate-deploy-implementations-v2: + forge script scripts/v2/DeployImplementationsV2.s.sol:DeployImplementationsV2Script --rpc-url ${RPC_URL} --sender ${SENDER} + +deploy-implementations-v2: + forge script scripts/v2/DeployImplementationsV2.s.sol:DeployImplementationsV2Script --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast + +simulate-deploy-create2-factory: + forge script scripts/DeployCreate2Factory.s.sol:DeployCreate2FactoryScript --rpc-url ${RPC_URL} --sender ${SENDER} + +deploy-create2-factory: + forge script scripts/DeployCreate2Factory.s.sol:DeployCreate2FactoryScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast + +simulate-deploy-proxies-v2: + forge script scripts/v2/DeployProxiesV2.s.sol:DeployProxiesV2Script --rpc-url ${RPC_URL} --sender ${SENDER} + +deploy-proxies-v2: + forge script scripts/v2/DeployProxiesV2.s.sol:DeployProxiesV2Script --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast + +simulate-setup-remote-resources-v2: + forge script scripts/v2/SetupRemoteResourcesV2.s.sol:SetupRemoteResourcesV2Script --rpc-url ${RPC_URL} --sender ${SENDER} + +setup-remote-resources-v2: + forge script scripts/v2/SetupRemoteResourcesV2.s.sol:SetupRemoteResourcesV2Script --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast + +simulate-rotate-keys-v2: + forge script scripts/v2/RotateKeysV2.s.sol:RotateKeysV2Script --rpc-url ${RPC_URL} --sender ${SENDER} + +rotate-keys-v2: + forge script scripts/v2/RotateKeysV2.s.sol:RotateKeysV2Script --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast + +simulate-deploy-address-utils-external: + forge script scripts/v2/DeployAddressUtilsExternal.s.sol:DeployAddressUtilsExternalScript --rpc-url ${RPC_URL} --sender ${SENDER} + +deploy-address-utils-external: + forge script scripts/v2/DeployAddressUtilsExternal.s.sol:DeployAddressUtilsExternalScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast anvil: docker rm -f anvil || true - @${ANVIL} "anvil --host 0.0.0.0 -a 13 --code-size-limit 250000" + @${ANVIL} "anvil --host 0.0.0.0 -a 13 --code-size-limit 250000" anvil-test: anvil pip3 install -r requirements.txt - python3 anvil/crosschainTransferIT.py + python anvil/crosschainTransferIT.py + +anvil-test-v2: anvil + pip3 install -r requirements.txt + python anvil/crosschainTransferITV2.py deploy-local: @docker exec anvil forge script anvil/scripts/${contract}.s.sol:${contract}Script --rpc-url http://localhost:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcast @@ -31,10 +71,21 @@ cast-call: cast-send: @docker exec anvil cast send ${contract_address} "${function}" --rpc-url http://localhost:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 - + clean: @${FOUNDRY} "forge clean" -analyze: - pip3 install -r requirements.txt - slither . +analyze-message-transmitter: + pip3 install mythril==0.24.8 + myth -v4 analyze src/MessageTransmitter.sol --solc-json mythril.config.json --solv 0.7.6 + +analyze-message-transmitter-v2: + pip3 install mythril==0.24.8 + myth -v4 analyze src/v2/MessageTransmitterV2.sol --solc-json mythril.config.json --solv 0.7.6 + +analyze-token-messenger-minter: + pip3 install mythril==0.24.8 + myth -v4 analyze src/TokenMessenger.sol --solc-json mythril.config.json --solv 0.7.6 + myth -v4 analyze src/TokenMinter.sol --solc-json mythril.config.json --solv 0.7.6 + myth -v4 analyze src/v2/TokenMessengerV2.sol --solc-json mythril.config.json --solv 0.7.6 + myth -v4 analyze src/v2/TokenMinterV2.sol --solc-json mythril.config.json --solv 0.7.6 diff --git a/README.md b/README.md index 2e0cdd9..3b07cb1 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,14 @@ ## Prerequisites ### Install dependencies + - Run `git submodule update --init --recursive` to update/download all libraries. - Run `yarn install` to install any additional dependencies. ### VSCode IDE Setup + - Install solidity extension https://marketplace.visualstudio.com/items?itemName=juanblanco.solidity -- Navigate to a .sol file +- Navigate to a .sol file - Right-click, select `Solidity: Change global compiler version (Remote)` ![](./pictures/Solidity-Change-Compiler.png) @@ -20,39 +22,59 @@ - Install solhint extension https://marketplace.visualstudio.com/items?itemName=idrabenia.solidity-solhint ### Install Foundry -Install Foundry CLI (forge 0.2.0) from official [website](https://book.getfoundry.sh/getting-started/installation.html#on-linux-and-macos. ). -- To install a specific verison, see [here](https://github.com/foundry-rs/foundry/blob/3f13a986e69c18ea19ce634fea00f4df6b3666b0/foundryup/README.md#usage). +Install Foundry CLI (forge 0.2.0) from official [website](https://book.getfoundry.sh/getting-started/installation.html#on-linux-and-macos.). + +- To install a specific version, see [here](https://github.com/foundry-rs/foundry/blob/3f13a986e69c18ea19ce634fea00f4df6b3666b0/foundryup/README.md#usage). ## Testing + ### Unit tests + Run `forge test` to run test using installed forge cli or `make test` to run tests in docker container. ### Run unit tests with debug logs + Log level is controlled by the -v flag. For example, `forge test -vv` displays console.log() statements from within contracts. Highest verbosity is -vvvvv. More info: https://book.getfoundry.sh/forge/tests.html#logs-and-traces. Contracts that use console.log() must import lib/forge-std/src/console.sol. ### Integration tests + Run `make anvil-test` to setup `anvil` test node in docker container and run integration tests. There is an example in `anvil/` folder ### Linting + Run `yarn lint` to lint all `.sol` files in the `src` and `test` directories. ### Static analysis -Run `make analyze` to set up Python dependencies from `requirements.txt` and run Slither on all source files, requiring the foundry cli to be installed locally. If all dependencies have been installed, alternatively run `slither .` to run static analysis on all `.sol` files in the `src` directory. + +Run `make analyze-{message-transmitter | message-transmitter-v2 | token-messenger-minter}` to set up Mythril dependency and run Mythril on all source files. If Mythril dependency has been installed, alternatively run `myth -v4 analyze $FILE_PATH --solc-json mythril.config.json --solv 0.7.6` to run static analysis on a `.sol` file at the given `$FILE_PATH`. Please note that this can take several minutes. ### Continuous Integration using Github Actions + We use Github actions to run linter and all the tests. The workflow configuration can be found in [.github/workflows/ci.yml](.github/workflows/ci.yml) +### Manual Triggering of the Olympix CI Workflow for Security Alerts +You can manually trigger the Olympix.ai Code Scanning workflow using the `workflow_dispatch` feature of GitHub Actions. +1. Click on the `Actions` tab. +2. In the left sidebar, select `Olympix Scan`. +3. Select the branch & click on the `Run workflow` button. + ### Alternative Installations #### Docker + Foundry + Use Docker to run Foundry commands. Run `make build` to build Foundry docker image. Then run `docker run --rm foundry ""` to run any [forge](https://book.getfoundry.sh/reference/forge/), [anvil](https://book.getfoundry.sh/reference/anvil/) or [cast](https://book.getfoundry.sh/reference/cast/) commands. There are some pre defined commands available in `Makefile` for testing and deploying contract on `anvil`. More info on Docker and Foundry [here](https://book.getfoundry.sh/tutorials/foundry-docker). ℹ️ Note + - Some machines (including those with M1 chips) may be unable to build the docker image locally. This is a known issue. ## Deployment -The contracts are deployed using [Forge Scripts](https://book.getfoundry.sh/tutorials/solidity-scripting). The script is located in [scripts/deploy.s.sol](/scripts/deploy.s.sol). Follow the below steps to deploy the contracts: + +### V1 + +The contracts are deployed using [Forge Scripts](https://book.getfoundry.sh/tutorials/solidity-scripting). The script is located in [scripts/v1/deploy.s.sol](/scripts/v1/deploy.s.sol). Follow the below steps to deploy the contracts: + 1. Add the below environment variables to your [env](.env) file - `MESSAGE_TRANSMITTER_DEPLOYER_KEY` - `TOKEN_MESSENGER_DEPLOYER_KEY` @@ -75,8 +97,136 @@ The contracts are deployed using [Forge Scripts](https://book.getfoundry.sh/tuto - Add the `REMOTE_TOKEN_MESSENGER_DEPLOYER` address to your [env](.env) file and run [scripts/precomputeRemoteMessengerAddress.py](/scripts/precomputeRemoteMessengerAddress.py) with argument `--REMOTE_RPC_URL` for the remote chain, which will automatically add the `REMOTE_TOKEN_MESSENGER_ADDRESS` to the .env file - Manually add the `REMOTE_TOKEN_MESSENGER_ADDRESS` to your .env file. -2. Run `make simulate RPC_URL= SENDER=` to perform a dry run. *Note: Use address from one of the private keys (used for deploying) above as `sender`. It is used to deploy the shared libraries that contracts use* +2. Run `make simulate-deploy RPC_URL= SENDER=` to perform a dry run. *Note: Use address from one of the private keys (used for deploying) above as `sender`. It is used to deploy the shared libraries that contracts use* 3. Run `make deploy RPC_URL= SENDER=` to deploy the contracts +### V2 + +#### Create2Factory + +Deploy Create2Factory first if not yet deployed. + +1. Add the environment variable `CREATE2_FACTORY_DEPLOYER_KEY` to your [env](.env) file. +2. Run `make simulate-deploy-create2-factory RPC_URL= SENDER=` to perform a dry run. +3. Run + ```make deploy-create2-factory RPC_URL= SENDER=``` + to deploy the Create2Factory. + +#### V2 Implementation Contracts + +Deploy the implementation contracts. + +1. Add the following [env](.env) variables + + - `CREATE2_FACTORY_CONTRACT_ADDRESS` + - `CREATE2_FACTORY_OWNER_KEY` + - `TOKEN_MINTER_V2_OWNER_ADDRESS` + - `TOKEN_MINTER_V2_OWNER_KEY` + - `TOKEN_CONTROLLER_ADDRESS` + - `DOMAIN` + - `MESSAGE_BODY_VERSION` + - `VERSION` + +2. Run `make simulate-deploy-implementations-v2 RPC_URL= SENDER=` to perform a dry run. + +3. Run + ```make deploy-implementations-v2 RPC_URL= SENDER=``` + to deploy MessageTransmitterV2, TokenMinterV2, and TokenMessengerV2. + +#### V2 Proxies + +The proxies are deployed via `CREATE2` through Create2Factory. The scripts assumes the remote chains are EVM compatible and predicts that remote contracts will be deployed at the same addresses. Follow the below steps to deploy the contracts: + +1. Replace the environment variables in your [env](.env) file with the following: + + Note: `REMOTE_DOMAINS`, `REMOTE_USDC_CONTRACT_ADDRESSES`, and `REMOTE_TOKEN_MESSENGER_V2_ADDRESSES` must all correspond 1:1:1 in order. + + - `USDC_CONTRACT_ADDRESS` + - `TOKEN_CONTROLLER_ADDRESS` + - `REMOTE_DOMAINS` + - `REMOTE_USDC_CONTRACT_ADDRESSES` + - `REMOTE_TOKEN_MESSENGER_V2_ADDRESSES` + - `CREATE2_FACTORY_CONTRACT_ADDRESS` + + - `MESSAGE_TRANSMITTER_V2_IMPLEMENTATION_ADDRESS` + - `MESSAGE_TRANSMITTER_V2_OWNER_ADDRESS` + - `MESSAGE_TRANSMITTER_V2_PAUSER_ADDRESS` + - `MESSAGE_TRANSMITTER_V2_RESCUER_ADDRESS` + - `MESSAGE_TRANSMITTER_V2_ATTESTER_MANAGER_ADDRESS` + - `MESSAGE_TRANSMITTER_V2_ATTESTER_1_ADDRESS` + - `MESSAGE_TRANSMITTER_V2_ATTESTER_2_ADDRESS` + - `MESSAGE_TRANSMITTER_V2_PROXY_ADMIN_ADDRESS` + + - `TOKEN_MINTER_V2_CONTRACT_ADDRESS` + - `TOKEN_MINTER_V2_PAUSER_ADDRESS` + - `TOKEN_MINTER_V2_RESCUER_ADDRESS` + + - `TOKEN_MESSENGER_V2_IMPLEMENTATION_ADDRESS` + - `TOKEN_MESSENGER_V2_OWNER_ADDRESS` + - `TOKEN_MESSENGER_V2_RESCUER_ADDRESS` + - `TOKEN_MESSENGER_V2_FEE_RECIPIENT_ADDRESS` + - `TOKEN_MESSENGER_V2_DENYLISTER_ADDRESS` + - `TOKEN_MESSENGER_V2_PROXY_ADMIN_ADDRESS` + + - `DOMAIN` + - `BURN_LIMIT_PER_MESSAGE` + + - `CREATE2_FACTORY_OWNER_KEY` + - `TOKEN_CONTROLLER_KEY` + - `TOKEN_MINTER_V2_OWNER_KEY` + +2. Run `make simulate-deploy-proxies-v2 RPC_URL= SENDER=` to perform a dry run. + +3. Run `make deploy-proxies-v2 RPC_URL= SENDER=` to deploy the contracts + +4. ONLY perform steps 5-7 for additional remote resources NOT already configured above. + +5. Replace the environment variables in your [env](.env) file with the following. We'll just add one remote resource (e.g. adding remote token messenger and remote usdc contract addresses) at a time, so just pick any and then repeat these steps. This will need to be repeated for each remote chain: + + - `TOKEN_MESSENGER_V2_OWNER_KEY` + - `TOKEN_CONTROLLER_KEY` + - `TOKEN_MESSENGER_V2_CONTRACT_ADDRESS` + - `TOKEN_MINTER_V2_CONTRACT_ADDRESS` + - `USDC_CONTRACT_ADDRESS` + - `REMOTE_USDC_CONTRACT_ADDRESS` + - `REMOTE_DOMAIN` + +6. Run `make simulate-setup-remote-resources-v2 RPC_URL= SENDER=` to perform a dry run of adding remote resources. + +7. Run `make setup-remote-resources-v2 RPC_URL= SENDER=` to setup the remote resources. + +**[Remaining steps are only for mainnet]** + +8. Replace the environment variables in your [env](.env) file with: + + - `MESSAGE_TRANSMITTER_V2_CONTRACT_ADDRESS` + - `TOKEN_MESSENGER_V2_CONTRACT_ADDRESS` + - `TOKEN_MINTER_V2_CONTRACT_ADDRESS` + - `MESSAGE_TRANSMITTER_V2_OWNER_KEY` + - `TOKEN_MESSENGER_V2_OWNER_KEY` + - `TOKEN_MINTER_V2_OWNER_KEY` + - `MESSAGE_TRANSMITTER_V2_NEW_OWNER_ADDRESS` + - `TOKEN_MESSENGER_V2_NEW_OWNER_ADDRESS` + - `TOKEN_MINTER_V2_NEW_OWNER_ADDRESS` + - `NEW_TOKEN_CONTROLLER_ADDRESS` + +9. Run `make simulate-rotate-keys-v2 RPC_URL= SENDER=` to perform a dry run of rotating the keys. + +10. Run `make rotate-keys-v2 RPC_URL= SENDER=` to rotate keys. + +#### AddressUtilsExternal + +Use Create2Factory to deploy the helper library to a deterministic address for easy integration. + +1. Set the following [env](.env) variables: + + - `CREATE2_FACTORY_CONTRACT_ADDRESS` + - `CREATE2_FACTORY_OWNER_KEY` + +2. Run `make simulate-deploy-address-utils-external RPC_URL= SENDER=` to perform a dry run. + +3. Run `make deploy-address-utils-external RPC_URL= SENDER=` to deploy. + ## License -For license information, see LICENSE and additional notices stored in NOTICES. \ No newline at end of file + +For license information, see LICENSE and additional notices stored in NOTICES. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4097fd2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,4 @@ +# Security Policy + +## Reporting a Vulnerability +Please do not file public issues on Github for security vulnerabilities. All security vulnerabilities should be reported to Circle privately, through Circle's [Bug Bounty Program](https://hackerone.com/circle-bbp). Please read through the program policy before submitting a report. \ No newline at end of file diff --git a/anvil/Counter.sol b/anvil/Counter.sol deleted file mode 100644 index 288ebfc..0000000 --- a/anvil/Counter.sol +++ /dev/null @@ -1,20 +0,0 @@ -pragma solidity >=0.7.6; - -contract Counter { - int private count; - - constructor(int _count) { - count = _count; - } - - function incrementCounter() public { - count += 1; - } - function decrementCounter() public { - count -= 1; - } - - function getCount() public view returns (int) { - return count; - } -} diff --git a/anvil/crosschainTransferIT.py b/anvil/crosschainTransferIT.py index 44b6a6e..e400326 100644 --- a/anvil/crosschainTransferIT.py +++ b/anvil/crosschainTransferIT.py @@ -1,3 +1,21 @@ +# +# Copyright 2025 Circle Internet Group, Inc. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + from typing import List, Dict from web3 import Web3 import solcx diff --git a/anvil/crosschainTransferITV2.py b/anvil/crosschainTransferITV2.py new file mode 100644 index 0000000..ac79b57 --- /dev/null +++ b/anvil/crosschainTransferITV2.py @@ -0,0 +1,355 @@ +from typing import List, Dict +from web3 import Web3 +import solcx +import unittest +import time +import requests +from eth_abi import encode +from crosschainTransferIT import addresses, keys + +# Miscellaneous fixed values for contract deployment and configuration +eth_domain = 0 +avax_domain = 1 +max_message_body_size = 8192 +message_version = 1 +message_body_version = 1 +minter_allowance = 1000 +mint_amount = 100 +max_burn_message_amount = 1000000 +finality_threshold_executed = 1000 +fee_executed = 5 + +# Message constants +nonce_index_start = 12 +nonce_length = 32 +finality_threshold_executed_start = 144 +finality_threshold_executed_length = 4 +fee_executed_index_start = 148 + 164 # 148 is the start of the messageBody; 164 is the feeExecuted index in BurnMessageV2 +fee_executed_length = 32 + +def compile_source_file(file_path: str, contract_name: str, version: str = '0.7.6') -> Dict: + """ + Takes in file path to a Solidity contract, contract name, and optional version params + and returns a dictionary representing the compiled contract. + """ + solcx.install_solc(version) + solcx.set_solc_version(version) + return solcx.compile_files( + [file_path], + output_values = ["abi", "bin"], + import_remappings = { + "@memview-sol/": "lib/memview-sol/", + "@openzeppelin/": "lib/openzeppelin-contracts/", + "ds-test/": "lib/ds-test/src/", + "forge-std/": "lib/forge-std/src/" + }, + allow_paths = ["."] + )[f'{file_path}:{contract_name}'] + +class TestTokenMessengerWithUSDC(unittest.TestCase): + def deploy_contract_from_source( + self, + file_path: str, + contract_name: str, + version: str = '0.7.6', + libraries: Dict = {}, + constructor_args: List = [], + caller = "" + ): + """ + Takes in a Solidity contract file path, contract name and optional Solidity + compiler version, dictionary of libraries to link, arguments for contract + constructor, and caller address to compile, deploy, and construct a Solidity + contract. Returns a web3 contract object representing the deployed contract. + """ + # Compile + contract_interface = compile_source_file(file_path, contract_name, version) + + # Deploy + if caller: + unsigned_tx = self.w3.eth.contract( + abi=contract_interface['abi'], + bytecode=solcx.link_code(contract_interface['bin'], libraries) + ).constructor(*constructor_args).build_transaction({ + 'nonce': self.w3.eth.get_transaction_count(addresses[caller]), + 'from': addresses[caller] + }) + + signed_tx = self.w3.eth.account.sign_transaction(unsigned_tx, keys[caller]) + tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) + else: + tx_hash = self.w3.eth.contract( + abi=contract_interface['abi'], + bytecode=solcx.link_code(contract_interface['bin'], libraries) + ).constructor(*constructor_args).transact() + + self.confirm_transaction(tx_hash) + + # Retrieve address and deployed contract + address = self.w3.eth.get_transaction_receipt(tx_hash)['contractAddress'] + return self.w3.eth.contract( + address=address, + abi=contract_interface['abi'] + ) + + def send_transaction(self, function_call, caller: str): + """ + Takes in an initialized function call and a designated caller and builds, + signs, and sends the transaction. Verifies the transaction was received. + """ + unsigned_tx = function_call.build_transaction({ + 'nonce': self.w3.eth.get_transaction_count(addresses[caller]), + 'from': addresses[caller] + }) + signed_tx = self.w3.eth.account.sign_transaction(unsigned_tx, keys[caller]) + tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) + self.confirm_transaction(tx_hash) + + def verify_balances(self, expected_eth_usdc_balance, expected_avax_usdc_balance): + """ + Verifies that the USDC balances at the test eth_token_messenger_user and avax_token_messenger_user + accounts matche the expected values. + """ + assert(self.eth_usdc.functions.balanceOf(addresses["eth_token_messenger_user"]).call() == expected_eth_usdc_balance) + assert(self.avax_usdc.functions.balanceOf(addresses["avax_token_messenger_user"]).call() == expected_avax_usdc_balance) + + def verify_fees_collected(self, expected_eth_fees, expected_avax_fees): + """ + Verifies that the USDC balances at the eth_token_messenger_deployer and avax_token_messenger_deployer + accounts matche the expected values. + """ + assert(self.eth_usdc.functions.balanceOf(addresses["eth_token_messenger_deployer"]).call() == expected_eth_fees) + assert(self.avax_usdc.functions.balanceOf(addresses["avax_token_messenger_deployer"]).call() == expected_avax_fees) + + def update_and_sign_emitted_message(self, message_bytes): + """ + Inserts into emitted message the nonce, feeExecuted, and finalityThresholdExecuted fields, and then signs + the message via the attester. + """ + mutable_message_bytes = bytearray(message_bytes) + mutable_message_bytes[nonce_index_start: nonce_index_start + nonce_length] = Web3.keccak(text="nonce") + mutable_message_bytes[finality_threshold_executed_start: finality_threshold_executed_start + finality_threshold_executed_length] = finality_threshold_executed.to_bytes(4, 'big') + mutable_message_bytes[fee_executed_index_start: fee_executed_index_start + fee_executed_length] = fee_executed.to_bytes(32, 'big') + signable_bytes = bytes(mutable_message_bytes) + signed_bytes = self.w3.eth.account.signHash(Web3.keccak(signable_bytes), keys["attester"]).signature + return signable_bytes, signed_bytes + + def to_32byte_hex(self, address): + """ + Converts a hex address to its zero-padded 32-byte representation. + """ + return Web3.toHex(Web3.toBytes(hexstr=address).rjust(32, b'\0')) + + def confirm_transaction(self, tx_hash, timeout=30): + """ + Waits until transaction receipt associated with tx_hash confirms completion. + """ + counter = 0 + while counter < timeout: + try: + if self.w3.eth.get_transaction_receipt(tx_hash).status == 1: + return + except: + pass + counter += 1 + time.sleep(1) + + raise RuntimeError(f"Transaction with hash {tx_hash} did not complete within {timeout} seconds") + + def setUp(self): + # Connect to node + self.w3 = Web3(Web3.HTTPProvider('http://0.0.0.0:8545')) + assert self.w3.isConnected() + + # Deploy and initialize USDC on ETH + self.eth_usdc = self.deploy_contract_from_source('lib/centre-tokens.git/contracts/v2/FiatTokenV2_1.sol', 'FiatTokenV2_1', '0.6.12') + self.send_transaction(self.eth_usdc.functions.initialize( + "USDC", + "USDC", + "USDC", + 0, + addresses["eth_usdc_master_minter"], + self.w3.eth.account.create().address, + self.w3.eth.account.create().address, + addresses["eth_usdc_master_minter"] + ), "eth_usdc_master_minter") + self.send_transaction(self.eth_usdc.functions.initializeV2("USDC"), "eth_usdc_master_minter") + self.send_transaction(self.eth_usdc.functions.initializeV2_1(Web3.toChecksumAddress("0xb794f5ea0ba39494ce839613fffba74279579268")), "eth_usdc_master_minter") + + # Deploy and initialize USDC on AVAX + self.avax_usdc = self.deploy_contract_from_source('lib/centre-tokens.git/contracts/v2/FiatTokenV2_1.sol', 'FiatTokenV2_1', '0.6.12') + self.send_transaction(self.avax_usdc.functions.initialize( + "USDC", + "USDC", + "USDC", + 0, + addresses["avax_usdc_master_minter"], + self.w3.eth.account.create().address, + self.w3.eth.account.create().address, + addresses["avax_usdc_master_minter"] + ), "avax_usdc_master_minter") + self.send_transaction(self.avax_usdc.functions.initializeV2("USDC"), "avax_usdc_master_minter") + self.send_transaction(self.avax_usdc.functions.initializeV2_1(Web3.toChecksumAddress("0xb794f5ea0ba39494ce839613fffba74279579268")), "avax_usdc_master_minter") + + # First, deploy TokenMessengerV2, MessageTransmitterV2 on ETH + # Deploy each behind an AdminUpgradableProxy instance + # Then, deploy TokenMinterV2 + eth_message_transmitter_impl = self.deploy_contract_from_source('src/v2/MessageTransmitterV2.sol', 'MessageTransmitterV2', + constructor_args = [eth_domain, message_version], caller="eth_message_transmitter_deployer") + eth_message_transmitter_proxy = self.deploy_contract_from_source('src/proxy/AdminUpgradableProxy.sol', 'AdminUpgradableProxy', + constructor_args = [eth_message_transmitter_impl.address, addresses["eth_message_transmitter_deployer"], b''], caller="eth_message_transmitter_deployer") + self.eth_message_transmitter = self.w3.eth.contract( + address=eth_message_transmitter_proxy.address, + abi=eth_message_transmitter_impl.abi + ) + self.send_transaction(self.eth_message_transmitter.functions.initialize( + addresses["eth_message_transmitter_deployer"], + addresses["eth_message_transmitter_deployer"], + addresses["eth_message_transmitter_deployer"], + addresses["eth_message_transmitter_deployer"], + [addresses["attester"]], + 1, + max_message_body_size + ), "eth_message_transmitter_deployer") + + # TokenMinterV2 ETH + self.eth_token_minter = self.deploy_contract_from_source('src/v2/TokenMinterV2.sol', 'TokenMinterV2', + constructor_args = [addresses["eth_token_controller"]], caller="eth_token_minter_deployer") + + # TokenMessengerV2 ETH + eth_token_messenger_impl = self.deploy_contract_from_source('src/v2/TokenMessengerV2.sol', 'TokenMessengerV2', + constructor_args = [eth_message_transmitter_proxy.address, message_body_version], caller="eth_token_messenger_deployer") + eth_token_messenger_proxy = self.deploy_contract_from_source('src/proxy/AdminUpgradableProxy.sol', 'AdminUpgradableProxy', + constructor_args = [eth_token_messenger_impl.address, addresses["eth_token_messenger_deployer"], b''], caller="eth_token_messenger_deployer") + self.eth_token_messenger = self.w3.eth.contract( + address=eth_token_messenger_proxy.address, + abi=eth_token_messenger_impl.abi + ) + self.send_transaction(self.eth_token_messenger.functions.initialize( + addresses["eth_token_messenger_deployer"], + addresses["eth_token_messenger_deployer"], + addresses["eth_token_messenger_deployer"], + addresses["eth_token_messenger_deployer"], + self.eth_token_minter.address, + [], + [] + ), "eth_token_messenger_deployer") + + # Repeat the above on AVAX + avax_message_transmitter_impl = self.deploy_contract_from_source('src/v2/MessageTransmitterV2.sol', 'MessageTransmitterV2', + constructor_args = [avax_domain, message_version], caller="avax_message_transmitter_deployer") + avax_message_transmitter_proxy = self.deploy_contract_from_source('src/proxy/AdminUpgradableProxy.sol', 'AdminUpgradableProxy', + constructor_args = [avax_message_transmitter_impl.address, addresses["avax_message_transmitter_deployer"], b''], caller="avax_message_transmitter_deployer") + self.avax_message_transmitter = self.w3.eth.contract( + address=avax_message_transmitter_proxy.address, + abi=avax_message_transmitter_impl.abi + ) + self.send_transaction(self.avax_message_transmitter.functions.initialize( + addresses["avax_message_transmitter_deployer"], + addresses["avax_message_transmitter_deployer"], + addresses["avax_message_transmitter_deployer"], + addresses["avax_message_transmitter_deployer"], + [addresses["attester"]], + 1, + max_message_body_size + ), "avax_message_transmitter_deployer") + self.avax_token_minter = self.deploy_contract_from_source('src/v2/TokenMinterV2.sol', 'TokenMinterV2', + constructor_args = [addresses["avax_token_controller"]], caller="avax_token_minter_deployer") + avax_token_messenger_impl = self.deploy_contract_from_source('src/v2/TokenMessengerV2.sol', 'TokenMessengerV2', + constructor_args = [avax_message_transmitter_proxy.address, message_body_version], caller="avax_token_messenger_deployer") + avax_token_messenger_proxy = self.deploy_contract_from_source('src/proxy/AdminUpgradableProxy.sol', 'AdminUpgradableProxy', + constructor_args = [avax_token_messenger_impl.address, addresses["avax_token_messenger_deployer"], b''], caller="avax_token_messenger_deployer") + self.avax_token_messenger = self.w3.eth.contract( + address=avax_token_messenger_proxy.address, + abi=avax_token_messenger_impl.abi + ) + self.send_transaction(self.avax_token_messenger.functions.initialize( + addresses["avax_token_messenger_deployer"], + addresses["avax_token_messenger_deployer"], + addresses["avax_token_messenger_deployer"], + addresses["avax_token_messenger_deployer"], + self.avax_token_minter.address, + [eth_domain], + [self.to_32byte_hex(self.eth_token_messenger.address)] + ), "eth_token_messenger_deployer") + + # configureMinter to add minters + self.send_transaction(self.eth_usdc.functions.configureMinter(addresses["eth_usdc_master_minter"], minter_allowance), "eth_usdc_master_minter") + self.send_transaction(self.avax_usdc.functions.configureMinter(addresses["avax_usdc_master_minter"], minter_allowance), "avax_usdc_master_minter") + self.send_transaction(self.eth_usdc.functions.configureMinter(self.eth_token_minter.address, minter_allowance), "eth_usdc_master_minter") + self.send_transaction(self.avax_usdc.functions.configureMinter(self.avax_token_minter.address, minter_allowance), "avax_usdc_master_minter") + + # addLocalTokenMessenger to minter contracts + self.send_transaction(self.eth_token_minter.functions.addLocalTokenMessenger(self.eth_token_messenger.address), "eth_token_minter_deployer") + self.send_transaction(self.avax_token_minter.functions.addLocalTokenMessenger(self.avax_token_messenger.address), "avax_token_minter_deployer") + + # setMaxBurnAmountPerMessage to token messenger contracts + self.send_transaction(self.eth_token_minter.functions.setMaxBurnAmountPerMessage(self.eth_usdc.address, max_burn_message_amount), "eth_token_controller") + self.send_transaction(self.avax_token_minter.functions.setMaxBurnAmountPerMessage(self.avax_usdc.address, max_burn_message_amount), "avax_token_controller") + + # linkTokenPair + self.send_transaction(self.eth_token_minter.functions.linkTokenPair(self.eth_usdc.address, avax_domain, self.to_32byte_hex(self.avax_usdc.address)), "eth_token_controller") + self.send_transaction(self.avax_token_minter.functions.linkTokenPair(self.avax_usdc.address, eth_domain, self.to_32byte_hex(self.eth_usdc.address)), "avax_token_controller") + + # addRemoteTokenMessenger on ETH; AVAX was configured through initialize() + self.send_transaction(self.eth_token_messenger.functions.addRemoteTokenMessenger(avax_domain, self.to_32byte_hex(self.avax_token_messenger.address)), "eth_token_messenger_deployer") + + def test_crosschain_transfer(self): + # Allocate 100 USDC each to avax_token_messenger_user and eth_token_messenger_user + self.send_transaction(self.avax_usdc.functions.mint(addresses["avax_token_messenger_user"], mint_amount), "avax_usdc_master_minter") + self.send_transaction(self.eth_usdc.functions.mint(addresses["eth_token_messenger_user"], mint_amount), "eth_usdc_master_minter") + self.verify_balances(100, 100) + self.verify_fees_collected(0, 0) + + # Approve USDC transfer from avax_token_messenger_user to avax_token_messenger + self.send_transaction(self.avax_usdc.functions.approve(self.avax_token_messenger.address, mint_amount), "avax_token_messenger_user") + + # depositForBurn from avax_token_messenger_user to avax_token_messenger + self.send_transaction(self.avax_token_messenger.functions.depositForBurn( + mint_amount, + eth_domain, + self.to_32byte_hex(addresses["eth_token_messenger_user"]), + self.avax_usdc.address, + self.to_32byte_hex(addresses["eth_token_messenger_user"]), # destinationCaller + 10, # maxFee + 1000 # minFinalityThreshold + ), "avax_token_messenger_user") + self.verify_balances(100, 0) + self.verify_fees_collected(0, 0) + + # parse MessageSent event emitted by avax_message_transmitter + avax_message_sent_filter = self.avax_message_transmitter.events.MessageSent.createFilter(fromBlock="0x0") + avax_message, signed_avax_message = self.update_and_sign_emitted_message(avax_message_sent_filter.get_new_entries()[0]['args']['message']) + + # receiveMessage with eth_message_transmitter to eth_token_messenger_user + self.send_transaction(self.eth_message_transmitter.functions.receiveMessage(avax_message, signed_avax_message), "eth_token_messenger_user") + self.verify_balances(195, 0) + self.verify_fees_collected(5, 0) + + # Approve USDC transfer from eth_token_messenger_user to eth_token_messenger + self.send_transaction(self.eth_usdc.functions.approve(self.eth_token_messenger.address, mint_amount), "eth_token_messenger_user") + + # depositForBurn from eth_token_messenger_user to eth_token_messenger + self.send_transaction(self.eth_token_messenger.functions.depositForBurn( + mint_amount, + avax_domain, + self.to_32byte_hex(addresses["avax_token_messenger_user"]), + self.eth_usdc.address, + self.to_32byte_hex(addresses["avax_token_messenger_user"]), # destinationCaller + 10, # maxFee + 1000 # minFinalityThreshold + ), "eth_token_messenger_user") + self.verify_balances(95, 0) + self.verify_fees_collected(5, 0) + + # parse MessageSent event emitted by eth_message_transmitter + eth_message_sent_filter = self.eth_message_transmitter.events.MessageSent.createFilter(fromBlock="0x0") + eth_message, signed_eth_message = self.update_and_sign_emitted_message(eth_message_sent_filter.get_new_entries()[0]['args']['message']) + + # receiveMessage with avax_message_transmitter to avax_token_messenger_user + self.send_transaction(self.avax_message_transmitter.functions.receiveMessage(eth_message, signed_eth_message), "avax_token_messenger_user") + self.verify_balances(95, 95) + self.verify_fees_collected(5, 5) + +if __name__ == '__main__': + unittest.main() diff --git a/anvil/scripts/Counter.s.sol b/anvil/scripts/Counter.s.sol deleted file mode 100644 index bef456d..0000000 --- a/anvil/scripts/Counter.s.sol +++ /dev/null @@ -1,14 +0,0 @@ -pragma solidity ^0.7.6; - -import "forge-std/Script.sol"; -import "../Counter.sol"; - -contract CounterScript is Script { - function setUp() public {} - - function run() public { - vm.startBroadcast(); - new Counter(10); - vm.stopBroadcast(); - } -} diff --git a/lib/forge-std b/lib/forge-std index 2a2ce36..1714bee 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 2a2ce3692b8c1523b29de3ec9d961ee9fbbc43a6 +Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d diff --git a/lib/memview-sol b/lib/memview-sol index 3071bb1..79a08cb 160000 --- a/lib/memview-sol +++ b/lib/memview-sol @@ -1 +1 @@ -Subproject commit 3071bb11a8f87dfaa65846f3f12bba2ddf16add8 +Subproject commit 79a08cb25aac047d81c67c5422c9b55abfac8635 diff --git a/mythril.config.json b/mythril.config.json new file mode 100644 index 0000000..62a08ae --- /dev/null +++ b/mythril.config.json @@ -0,0 +1,8 @@ +{ + "remappings": [ + "@memview-sol/=lib/memview-sol/", + "@openzeppelin/=lib/openzeppelin-contracts/", + "ds-test/=lib/ds-test/src/", + "forge-std/=lib/forge-std/src/" + ] +} diff --git a/requirements.txt b/requirements.txt index 5d92b31..68b3142 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ hexbytes==0.2.3 idna==3.3 ipfshttpclient==0.8.0a2 jsonschema==4.9.1 -lru-dict==1.1.8 +lru-dict==1.2.0 multiaddr==0.0.9 multidict==6.0.2 netaddr==0.8.0 @@ -34,7 +34,6 @@ requests==2.28.1 rlp==2.0.1 semantic-version==2.10.0 six==1.16.0 -slither-analyzer==0.8.3 toolz==0.12.0 urllib3==1.26.11 varint==1.0.2 diff --git a/scripts/DeployCreate2Factory.s.sol b/scripts/DeployCreate2Factory.s.sol new file mode 100644 index 0000000..164b356 --- /dev/null +++ b/scripts/DeployCreate2Factory.s.sol @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import "forge-std/Script.sol"; +import {Create2Factory} from "../src/v2/Create2Factory.sol"; + +contract DeployCreate2FactoryScript is Script { + uint256 private create2FactoryDeployerKey; + + Create2Factory private create2Factory; + + function deployCreate2Factory( + uint256 deployerKey + ) internal returns (Create2Factory _create2Factory) { + vm.startBroadcast(deployerKey); + + _create2Factory = new Create2Factory(); + + vm.stopBroadcast(); + } + + function setUp() public { + create2FactoryDeployerKey = vm.envUint("CREATE2_FACTORY_DEPLOYER_KEY"); + } + + function run() public { + create2Factory = deployCreate2Factory(create2FactoryDeployerKey); + } +} diff --git a/scripts/deploy.s.sol b/scripts/v1/deploy.s.sol similarity index 90% rename from scripts/deploy.s.sol rename to scripts/v1/deploy.s.sol index 2574545..ce6bcaf 100644 --- a/scripts/deploy.s.sol +++ b/scripts/v1/deploy.s.sol @@ -1,3 +1,20 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ pragma solidity ^0.7.6; import "forge-std/Script.sol"; @@ -35,10 +52,9 @@ contract DeployScript is Script { * @param privateKey Private Key for signing the transactions * @return MessageTransmitter instance */ - function deployMessageTransmitter(uint256 privateKey) - private - returns (MessageTransmitter) - { + function deployMessageTransmitter( + uint256 privateKey + ) private returns (MessageTransmitter) { // Start recording transactions vm.startBroadcast(privateKey); @@ -140,9 +156,10 @@ contract DeployScript is Script { /** * @notice link current chain and remote chain tokens */ - function linkTokenPair(TokenMinter tokenMinter, uint256 privateKey) - private - { + function linkTokenPair( + TokenMinter tokenMinter, + uint256 privateKey + ) private { // Start recording transations vm.startBroadcast(privateKey); diff --git a/scripts/v2/DeployAddressUtilsExternal.s.sol b/scripts/v2/DeployAddressUtilsExternal.s.sol new file mode 100644 index 0000000..6258006 --- /dev/null +++ b/scripts/v2/DeployAddressUtilsExternal.s.sol @@ -0,0 +1,54 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {Script} from "forge-std/Script.sol"; +import {AddressUtilsExternal} from "../../src/messages/v2/AddressUtilsExternal.sol"; +import {Create2Factory} from "../../src/v2/Create2Factory.sol"; +import {SALT_ADDRESS_UTILS_EXTERNAL} from "./Salts.sol"; + +contract DeployAddressUtilsExternalScript is Script { + Create2Factory private create2Factory; + uint256 private create2FactoryOwnerKey; + + function deployAddressUtilsExternalScript() + private + returns (AddressUtilsExternal _addressUtilsExternal) + { + vm.startBroadcast(create2FactoryOwnerKey); + _addressUtilsExternal = AddressUtilsExternal( + create2Factory.deploy( + 0, + SALT_ADDRESS_UTILS_EXTERNAL, + type(AddressUtilsExternal).creationCode + ) + ); + vm.stopBroadcast(); + } + + function setUp() public { + create2Factory = Create2Factory( + vm.envAddress("CREATE2_FACTORY_CONTRACT_ADDRESS") + ); + create2FactoryOwnerKey = vm.envUint("CREATE2_FACTORY_OWNER_KEY"); + } + + function run() public { + deployAddressUtilsExternalScript(); + } +} diff --git a/scripts/v2/DeployImplementationsV2.s.sol b/scripts/v2/DeployImplementationsV2.s.sol new file mode 100644 index 0000000..a1f3cc1 --- /dev/null +++ b/scripts/v2/DeployImplementationsV2.s.sol @@ -0,0 +1,153 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {Script} from "forge-std/Script.sol"; +import {AdminUpgradableProxy} from "../../src/proxy/AdminUpgradableProxy.sol"; +import {TokenMessengerV2} from "../../src/v2/TokenMessengerV2.sol"; +import {TokenMinterV2} from "../../src/v2/TokenMinterV2.sol"; +import {MessageTransmitterV2} from "../../src/v2/MessageTransmitterV2.sol"; +import {Create2Factory} from "../../src/v2/Create2Factory.sol"; +import {Ownable2Step} from "../../src/roles/Ownable2Step.sol"; +import {SALT_MESSAGE_TRANSMITTER, SALT_TOKEN_MESSENGER, SALT_TOKEN_MINTER} from "./Salts.sol"; + +contract DeployImplementationsV2Script is Script { + // Expose for tests + MessageTransmitterV2 public messageTransmitterV2; + TokenMessengerV2 public tokenMessengerV2; + TokenMinterV2 public tokenMinterV2; + address public expectedMessageTransmitterV2ProxyAddress; + + address private factoryAddress; + address private tokenMinterOwnerAddress; + uint256 private tokenMinterOwnerKey; + address private tokenControllerAddress; + uint32 private messageBodyVersion; + uint32 private version; + uint32 private domain; + uint256 private create2FactoryOwnerPrivateKey; + + function deployImplementationsV2() + private + returns (MessageTransmitterV2, TokenMinterV2, TokenMessengerV2) + { + // Calculate MessageTransmitterV2 proxy address + expectedMessageTransmitterV2ProxyAddress = vm.computeCreate2Address( + SALT_MESSAGE_TRANSMITTER, + keccak256( + abi.encodePacked( + type(AdminUpgradableProxy).creationCode, + abi.encode(factoryAddress, factoryAddress, "") + ) + ), + factoryAddress + ); + + Create2Factory factory = Create2Factory(factoryAddress); + + // Start recording transactions + vm.startBroadcast(create2FactoryOwnerPrivateKey); + + // Deploy MessageTransmitterV2 implementation + messageTransmitterV2 = MessageTransmitterV2( + factory.deploy( + 0, + SALT_MESSAGE_TRANSMITTER, + abi.encodePacked( + type(MessageTransmitterV2).creationCode, + abi.encode(domain, version) + ) + ) + ); + + // Deploy TokenMessengerV2 implementation + tokenMessengerV2 = TokenMessengerV2( + factory.deploy( + 0, + SALT_TOKEN_MESSENGER, + abi.encodePacked( + type(TokenMessengerV2).creationCode, + abi.encode( + expectedMessageTransmitterV2ProxyAddress, + messageBodyVersion + ) + ) + ) + ); + + // Since the TokenMinter sets the msg.sender of the deployment to be + // the Owner, we'll need to rotate it from the Create2Factory atomically. + bytes memory tokenMinterOwnershipRotation = abi.encodeWithSelector( + Ownable2Step.transferOwnership.selector, + tokenMinterOwnerAddress + ); + bytes[] memory tokenMinterMultiCallData = new bytes[](1); + tokenMinterMultiCallData[0] = tokenMinterOwnershipRotation; + + // Deploy TokenMinter + tokenMinterV2 = TokenMinterV2( + factory.deployAndMultiCall( + 0, + SALT_TOKEN_MINTER, + abi.encodePacked( + type(TokenMinterV2).creationCode, + abi.encode(tokenControllerAddress) + ), + tokenMinterMultiCallData + ) + ); + + // Stop recording transactions + vm.stopBroadcast(); + + // Accept the TokenMinter 2-step ownership + vm.startBroadcast(tokenMinterOwnerKey); + tokenMinterV2.acceptOwnership(); + vm.stopBroadcast(); + + return (messageTransmitterV2, tokenMinterV2, tokenMessengerV2); + } + + /** + * @notice initialize variables from environment + */ + function setUp() public { + factoryAddress = vm.envAddress("CREATE2_FACTORY_CONTRACT_ADDRESS"); + create2FactoryOwnerPrivateKey = vm.envUint("CREATE2_FACTORY_OWNER_KEY"); + tokenMinterOwnerAddress = vm.envAddress( + "TOKEN_MINTER_V2_OWNER_ADDRESS" + ); + tokenMinterOwnerKey = vm.envUint("TOKEN_MINTER_V2_OWNER_KEY"); + tokenControllerAddress = vm.envAddress("TOKEN_CONTROLLER_ADDRESS"); + domain = uint32(vm.envUint("DOMAIN")); + messageBodyVersion = uint32(vm.envUint("MESSAGE_BODY_VERSION")); + version = uint32(vm.envUint("VERSION")); + } + + /** + * @notice main function that will be run by forge + */ + function run() public { + ( + messageTransmitterV2, + tokenMinterV2, + tokenMessengerV2 + ) = deployImplementationsV2(); + } +} diff --git a/scripts/v2/DeployProxiesV2.s.sol b/scripts/v2/DeployProxiesV2.s.sol new file mode 100644 index 0000000..d4245d8 --- /dev/null +++ b/scripts/v2/DeployProxiesV2.s.sol @@ -0,0 +1,383 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {Script} from "forge-std/Script.sol"; +import {Create2Factory} from "../../src/v2/Create2Factory.sol"; +import {AdminUpgradableProxy} from "../../src/proxy/AdminUpgradableProxy.sol"; +import {TokenMessengerV2} from "../../src/v2/TokenMessengerV2.sol"; +import {TokenMinterV2} from "../../src/v2/TokenMinterV2.sol"; +import {MessageTransmitterV2} from "../../src/v2/MessageTransmitterV2.sol"; +import {AddressUtils} from "../../src/messages/v2/AddressUtils.sol"; +import {SALT_TOKEN_MESSENGER, SALT_MESSAGE_TRANSMITTER} from "./Salts.sol"; + +contract DeployProxiesV2Script is Script { + // Expose for tests + MessageTransmitterV2 public messageTransmitterV2; + TokenMessengerV2 public tokenMessengerV2; + + address private usdcContractAddress; + address private create2Factory; + uint32[] private remoteDomains; + bytes32[] private usdcRemoteContractAddresses; + bytes32[] private remoteTokenMessengerV2Addresses; + + address private messageTransmitterV2Implementation; + address private messageTransmitterV2OwnerAddress; + address private messageTransmitterV2PauserAddress; + address private messageTransmitterV2RescuerAddress; + address private messageTransmitterV2AttesterManagerAddress; + address private messageTransmitterV2Attester1Address; + address private messageTransmitterV2Attester2Address; + uint256 private messageTransmitterV2SignatureThreshold = 2; + address private messageTransmitterV2AdminAddress; + + TokenMinterV2 private tokenMinterV2; + address private tokenMinterV2PauserAddress; + address private tokenMinterV2RescuerAddress; + + address private tokenMessengerV2Implementation; + address private tokenMessengerV2OwnerAddress; + address private tokenMessengerV2PauserAddress; + address private tokenMessengerV2RescuerAddress; + address private tokenMessengerV2FeeRecipientAddress; + address private tokenMessengerV2DenylisterAddress; + address private tokenMessengerV2AdminAddress; + + uint32 private domain; + uint32 private maxMessageBodySize = 8192; + uint256 private burnLimitPerMessage; + + uint256 private create2FactoryOwnerPrivateKey; + uint256 private tokenMinterV2OwnerPrivateKey; + uint256 private tokenControllerPrivateKey; + + function getProxyCreationCode( + address _implementation, + address _admin, + bytes memory _data + ) public pure returns (bytes memory) { + return + abi.encodePacked( + type(AdminUpgradableProxy).creationCode, + abi.encode(_implementation, _admin, _data) + ); + } + + function deployMessageTransmitterV2( + address factory, + uint256 privateKey + ) private returns (MessageTransmitterV2) { + // Get proxy creation code + bytes memory proxyCreateCode = getProxyCreationCode( + factory, + factory, + "" + ); + + // Construct initializer + address[] memory attesters = new address[](2); + attesters[0] = messageTransmitterV2Attester1Address; + attesters[1] = messageTransmitterV2Attester2Address; + bytes memory initializer = abi.encodeWithSelector( + MessageTransmitterV2.initialize.selector, + messageTransmitterV2OwnerAddress, + messageTransmitterV2PauserAddress, + messageTransmitterV2RescuerAddress, + messageTransmitterV2AttesterManagerAddress, + attesters, + messageTransmitterV2SignatureThreshold, + maxMessageBodySize + ); + + // Construct upgrade and initialize data + bytes memory upgradeAndInitializeData = abi.encodeWithSelector( + AdminUpgradableProxy.upgradeToAndCall.selector, + messageTransmitterV2Implementation, + initializer + ); + + // Construct admin rotation data + bytes memory adminRotationData = abi.encodeWithSelector( + AdminUpgradableProxy.changeAdmin.selector, + messageTransmitterV2AdminAddress + ); + + bytes[] memory multiCallData = new bytes[](2); + multiCallData[0] = upgradeAndInitializeData; + multiCallData[1] = adminRotationData; + + // Start recording transactions + vm.startBroadcast(privateKey); + + // Deploy and multicall proxy + address messageTransmitterV2ProxyAddress = Create2Factory(factory) + .deployAndMultiCall( + 0, + SALT_MESSAGE_TRANSMITTER, + proxyCreateCode, + multiCallData + ); + + // Stop recording transactions + vm.stopBroadcast(); + return MessageTransmitterV2(messageTransmitterV2ProxyAddress); + } + + function deployTokenMessengerV2( + address factory, + uint256 privateKey, + address tokenMinterV2Address + ) private returns (TokenMessengerV2) { + // Get proxy creation code + bytes memory proxyCreateCode = getProxyCreationCode( + factory, + factory, + "" + ); + + // Calculate TokenMessengerV2 proxy address + address expectedTokenMessengerV2ProxyAddress = vm.computeCreate2Address( + SALT_TOKEN_MESSENGER, + keccak256(proxyCreateCode), + factory + ); + + bool remoteTokenMessengerV2FromEnv = remoteTokenMessengerV2Addresses + .length > 0; + + // Construct initializer + bytes32[] memory remoteTokenMessengerAddresses = new bytes32[]( + remoteDomains.length + ); + uint256 remoteDomainsLength = remoteDomains.length; + for (uint256 i = 0; i < remoteDomainsLength; ++i) { + if (remoteTokenMessengerV2FromEnv) { + remoteTokenMessengerAddresses[ + i + ] = remoteTokenMessengerV2Addresses[i]; + } else { + remoteTokenMessengerAddresses[i] = AddressUtils.toBytes32( + expectedTokenMessengerV2ProxyAddress + ); + } + } + bytes memory initializer = abi.encodeWithSelector( + TokenMessengerV2.initialize.selector, + tokenMessengerV2OwnerAddress, + tokenMessengerV2RescuerAddress, + tokenMessengerV2FeeRecipientAddress, + tokenMessengerV2DenylisterAddress, + tokenMinterV2Address, + remoteDomains, + remoteTokenMessengerAddresses + ); + + // Construct upgrade and initialize data + bytes memory upgradeAndInitializeData = abi.encodeWithSelector( + AdminUpgradableProxy.upgradeToAndCall.selector, + tokenMessengerV2Implementation, + initializer + ); + + // Construct admin rotation data + bytes memory adminRotationData = abi.encodeWithSelector( + AdminUpgradableProxy.changeAdmin.selector, + tokenMessengerV2AdminAddress + ); + + bytes[] memory multiCallData = new bytes[](2); + multiCallData[0] = upgradeAndInitializeData; + multiCallData[1] = adminRotationData; + + // Start recording transations + vm.startBroadcast(privateKey); + + // Deploy proxy + address tokenMessengerV2ProxyAddress = Create2Factory(factory) + .deployAndMultiCall( + 0, + SALT_TOKEN_MESSENGER, + proxyCreateCode, + multiCallData + ); + // Stop recording transations + vm.stopBroadcast(); + + return TokenMessengerV2(tokenMessengerV2ProxyAddress); + } + + function addMessengerPauserRescuerToTokenMinterV2( + uint256 tokenMinterV2OwnerPrivateKey, + uint256 _tokenControllerPrivateKey, + TokenMinterV2 _tokenMinterV2, + address tokenMessengerV2Address + ) private { + // Start recording transations + vm.startBroadcast(tokenMinterV2OwnerPrivateKey); + + _tokenMinterV2.addLocalTokenMessenger(tokenMessengerV2Address); + _tokenMinterV2.updatePauser(tokenMinterV2PauserAddress); + _tokenMinterV2.updateRescuer(tokenMinterV2RescuerAddress); + + // Stop recording transations + vm.stopBroadcast(); + + // Start recording transations + vm.startBroadcast(_tokenControllerPrivateKey); + + _tokenMinterV2.setMaxBurnAmountPerMessage( + usdcContractAddress, + burnLimitPerMessage + ); + + uint256 remoteDomainsLength = remoteDomains.length; + for (uint256 i = 0; i < remoteDomainsLength; ++i) { + _tokenMinterV2.linkTokenPair( + usdcContractAddress, + remoteDomains[i], + usdcRemoteContractAddresses[i] + ); + } + + // Stop recording transations + vm.stopBroadcast(); + } + + /** + * @notice initialize variables from environment + */ + function setUp() public { + usdcContractAddress = vm.envAddress("USDC_CONTRACT_ADDRESS"); + bytes32[] memory usdcRemoteContractAddressesMemory = vm.envBytes32( + "REMOTE_USDC_CONTRACT_ADDRESSES", + "," + ); + uint256 usdcRemoteContractAddressesMemoryLength = usdcRemoteContractAddressesMemory + .length; + for (uint256 i = 0; i < usdcRemoteContractAddressesMemoryLength; ++i) { + usdcRemoteContractAddresses.push( + usdcRemoteContractAddressesMemory[i] + ); + } + create2Factory = vm.envAddress("CREATE2_FACTORY_CONTRACT_ADDRESS"); + + messageTransmitterV2Implementation = vm.envAddress( + "MESSAGE_TRANSMITTER_V2_IMPLEMENTATION_ADDRESS" + ); + messageTransmitterV2OwnerAddress = vm.envAddress( + "MESSAGE_TRANSMITTER_V2_OWNER_ADDRESS" + ); + messageTransmitterV2PauserAddress = vm.envAddress( + "MESSAGE_TRANSMITTER_V2_PAUSER_ADDRESS" + ); + messageTransmitterV2RescuerAddress = vm.envAddress( + "MESSAGE_TRANSMITTER_V2_RESCUER_ADDRESS" + ); + messageTransmitterV2AttesterManagerAddress = vm.envAddress( + "MESSAGE_TRANSMITTER_V2_ATTESTER_MANAGER_ADDRESS" + ); + messageTransmitterV2Attester1Address = vm.envAddress( + "MESSAGE_TRANSMITTER_V2_ATTESTER_1_ADDRESS" + ); + messageTransmitterV2Attester2Address = vm.envAddress( + "MESSAGE_TRANSMITTER_V2_ATTESTER_2_ADDRESS" + ); + messageTransmitterV2AdminAddress = vm.envAddress( + "MESSAGE_TRANSMITTER_V2_PROXY_ADMIN_ADDRESS" + ); + + tokenMinterV2 = TokenMinterV2( + vm.envAddress("TOKEN_MINTER_V2_CONTRACT_ADDRESS") + ); + tokenMinterV2PauserAddress = vm.envAddress( + "TOKEN_MINTER_V2_PAUSER_ADDRESS" + ); + tokenMinterV2RescuerAddress = vm.envAddress( + "TOKEN_MINTER_V2_RESCUER_ADDRESS" + ); + + tokenMessengerV2Implementation = vm.envAddress( + "TOKEN_MESSENGER_V2_IMPLEMENTATION_ADDRESS" + ); + tokenMessengerV2OwnerAddress = vm.envAddress( + "TOKEN_MESSENGER_V2_OWNER_ADDRESS" + ); + tokenMessengerV2RescuerAddress = vm.envAddress( + "TOKEN_MESSENGER_V2_RESCUER_ADDRESS" + ); + tokenMessengerV2FeeRecipientAddress = vm.envAddress( + "TOKEN_MESSENGER_V2_FEE_RECIPIENT_ADDRESS" + ); + tokenMessengerV2DenylisterAddress = vm.envAddress( + "TOKEN_MESSENGER_V2_DENYLISTER_ADDRESS" + ); + tokenMessengerV2AdminAddress = vm.envAddress( + "TOKEN_MESSENGER_V2_PROXY_ADMIN_ADDRESS" + ); + + domain = uint32(vm.envUint("DOMAIN")); + + uint256[] memory remoteDomainsUint256 = vm.envUint( + "REMOTE_DOMAINS", + "," + ); + uint256 remoteDomainsUint256Length = remoteDomainsUint256.length; + for (uint256 i = 0; i < remoteDomainsUint256Length; ++i) { + remoteDomains.push(uint32(remoteDomainsUint256[i])); + } + burnLimitPerMessage = vm.envUint("BURN_LIMIT_PER_MESSAGE"); + + create2FactoryOwnerPrivateKey = vm.envUint("CREATE2_FACTORY_OWNER_KEY"); + tokenMinterV2OwnerPrivateKey = vm.envUint("TOKEN_MINTER_V2_OWNER_KEY"); + tokenControllerPrivateKey = vm.envUint("TOKEN_CONTROLLER_KEY"); + + bytes32[] memory emptyRemoteTokenMessengerV2Addresses = new bytes32[]( + 0 + ); + remoteTokenMessengerV2Addresses = vm.envOr( + "REMOTE_TOKEN_MESSENGER_V2_ADDRESSES", + ",", + emptyRemoteTokenMessengerV2Addresses + ); + } + + /** + * @notice main function that will be run by forge + */ + function run() public { + messageTransmitterV2 = deployMessageTransmitterV2( + create2Factory, + create2FactoryOwnerPrivateKey + ); + + tokenMessengerV2 = deployTokenMessengerV2( + create2Factory, + create2FactoryOwnerPrivateKey, + address(tokenMinterV2) + ); + + addMessengerPauserRescuerToTokenMinterV2( + tokenMinterV2OwnerPrivateKey, + tokenControllerPrivateKey, + tokenMinterV2, + address(tokenMessengerV2) + ); + } +} diff --git a/scripts/v2/RotateKeysV2.s.sol b/scripts/v2/RotateKeysV2.s.sol new file mode 100644 index 0000000..c305765 --- /dev/null +++ b/scripts/v2/RotateKeysV2.s.sol @@ -0,0 +1,133 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import "forge-std/Script.sol"; +import {TokenMessengerV2} from "../../src/v2/TokenMessengerV2.sol"; +import {TokenMinterV2} from "../../src/v2/TokenMinterV2.sol"; +import {MessageTransmitterV2} from "../../src/v2/MessageTransmitterV2.sol"; + +contract RotateKeysV2Script is Script { + address private messageTransmitterV2ContractAddress; + address private tokenMessengerV2ContractAddress; + address private tokenMinterV2ContractAddress; + address private newTokenControllerAddress; + + uint256 private messageTransmitterV2OwnerPrivateKey; + uint256 private tokenMessengerV2OwnerPrivateKey; + uint256 private tokenMinterV2OwnerPrivateKey; + + address private messageTransmitterV2NewOwnerAddress; + address private tokenMessengerV2NewOwnerAddress; + address private tokenMinterV2NewOwnerAddress; + + function rotateMessageTransmitterV2Owner(uint256 privateKey) public { + // load messageTransmitter + MessageTransmitterV2 messageTransmitterV2 = MessageTransmitterV2( + messageTransmitterV2ContractAddress + ); + + vm.startBroadcast(privateKey); + + messageTransmitterV2.transferOwnership( + messageTransmitterV2NewOwnerAddress + ); + + vm.stopBroadcast(); + } + + function rotateTokenMessengerV2Owner(uint256 privateKey) public { + TokenMessengerV2 tokenMessengerV2 = TokenMessengerV2( + tokenMessengerV2ContractAddress + ); + + vm.startBroadcast(privateKey); + + tokenMessengerV2.transferOwnership(tokenMessengerV2NewOwnerAddress); + + vm.stopBroadcast(); + } + + function rotateTokenControllerThenTokenMinterV2Owner( + uint256 privateKey + ) public { + TokenMinterV2 tokenMinterV2 = TokenMinterV2( + tokenMinterV2ContractAddress + ); + + vm.startBroadcast(privateKey); + + tokenMinterV2.setTokenController(newTokenControllerAddress); + + tokenMinterV2.transferOwnership(tokenMinterV2NewOwnerAddress); + + vm.stopBroadcast(); + } + + /** + * @notice initialize variables from environment + */ + function setUp() public { + messageTransmitterV2ContractAddress = vm.envAddress( + "MESSAGE_TRANSMITTER_V2_CONTRACT_ADDRESS" + ); + + tokenMessengerV2ContractAddress = vm.envAddress( + "TOKEN_MESSENGER_V2_CONTRACT_ADDRESS" + ); + + tokenMinterV2ContractAddress = vm.envAddress( + "TOKEN_MINTER_V2_CONTRACT_ADDRESS" + ); + + messageTransmitterV2OwnerPrivateKey = vm.envUint( + "MESSAGE_TRANSMITTER_V2_OWNER_KEY" + ); + tokenMessengerV2OwnerPrivateKey = vm.envUint( + "TOKEN_MESSENGER_V2_OWNER_KEY" + ); + tokenMinterV2OwnerPrivateKey = vm.envUint("TOKEN_MINTER_V2_OWNER_KEY"); + + messageTransmitterV2NewOwnerAddress = vm.envAddress( + "MESSAGE_TRANSMITTER_V2_NEW_OWNER_ADDRESS" + ); + + tokenMessengerV2NewOwnerAddress = vm.envAddress( + "TOKEN_MESSENGER_V2_NEW_OWNER_ADDRESS" + ); + + tokenMinterV2NewOwnerAddress = vm.envAddress( + "TOKEN_MINTER_V2_NEW_OWNER_ADDRESS" + ); + + newTokenControllerAddress = vm.envAddress( + "NEW_TOKEN_CONTROLLER_ADDRESS" + ); + } + + /** + * @notice main function that will be run by forge + */ + function run() public { + rotateMessageTransmitterV2Owner(messageTransmitterV2OwnerPrivateKey); + rotateTokenMessengerV2Owner(tokenMessengerV2OwnerPrivateKey); + rotateTokenControllerThenTokenMinterV2Owner( + tokenMinterV2OwnerPrivateKey + ); + } +} diff --git a/scripts/v2/Salts.sol b/scripts/v2/Salts.sol new file mode 100644 index 0000000..536efee --- /dev/null +++ b/scripts/v2/Salts.sol @@ -0,0 +1,29 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +// Salts used for CREATE2 deployments + +bytes32 constant SALT_TOKEN_MINTER = keccak256("cctp.v2.tokenminter"); +bytes32 constant SALT_TOKEN_MESSENGER = keccak256("cctp.v2.tokenmessenger"); +bytes32 constant SALT_MESSAGE_TRANSMITTER = keccak256( + "cctp.v2.messagetransmitter" +); +bytes32 constant SALT_ADDRESS_UTILS_EXTERNAL = keccak256( + "cctp.v2.addressutilsexternal" +); diff --git a/scripts/v2/SetupRemoteResourcesV2.s.sol b/scripts/v2/SetupRemoteResourcesV2.s.sol new file mode 100644 index 0000000..058b4bc --- /dev/null +++ b/scripts/v2/SetupRemoteResourcesV2.s.sol @@ -0,0 +1,123 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {Script} from "forge-std/Script.sol"; +import {TokenMessengerV2} from "../../src/v2/TokenMessengerV2.sol"; +import {TokenMinterV2} from "../../src/v2/TokenMinterV2.sol"; +import {Message} from "../../src/messages/Message.sol"; + +contract SetupRemoteResourcesV2Script is Script { + address private usdcRemoteContractAddress; + address private usdcContractAddress; + address private tokenMessengerV2ContractAddress; + address private tokenMinterV2ContractAddress; + + uint32 private remoteDomain; + + uint256 private tokenMessengerV2OwnerPrivateKey; + uint256 private tokenControllerPrivateKey; + + /** + * @notice link current chain and remote chain tokens + */ + function linkTokenPairV2( + TokenMinterV2 tokenMinterV2, + uint256 privateKey + ) private { + // Start recording transactions + vm.startBroadcast(privateKey); + + bytes32 remoteUsdcContractAddressInBytes32 = Message.addressToBytes32( + usdcRemoteContractAddress + ); + + tokenMinterV2.linkTokenPair( + usdcContractAddress, + remoteDomain, + remoteUsdcContractAddressInBytes32 + ); + + // Stop recording transactions + vm.stopBroadcast(); + } + + /** + * @notice add address of TokenMessenger deployed on another chain + */ + function addRemoteTokenMessengerV2( + TokenMessengerV2 tokenMessengerV2, + uint256 privateKey + ) private { + // Start recording transactions + vm.startBroadcast(privateKey); + bytes32 remoteTokenMessengerAddressInBytes32 = Message.addressToBytes32( + address(tokenMessengerV2) + ); + tokenMessengerV2.addRemoteTokenMessenger( + remoteDomain, + remoteTokenMessengerAddressInBytes32 + ); + + // Stop recording transactions + vm.stopBroadcast(); + } + + /** + * @notice initialize variables from environment + */ + function setUp() public { + tokenMessengerV2OwnerPrivateKey = vm.envUint( + "TOKEN_MESSENGER_V2_OWNER_KEY" + ); + tokenControllerPrivateKey = vm.envUint("TOKEN_CONTROLLER_KEY"); + + tokenMessengerV2ContractAddress = vm.envAddress( + "TOKEN_MESSENGER_V2_CONTRACT_ADDRESS" + ); + tokenMinterV2ContractAddress = vm.envAddress( + "TOKEN_MINTER_V2_CONTRACT_ADDRESS" + ); + usdcContractAddress = vm.envAddress("USDC_CONTRACT_ADDRESS"); + usdcRemoteContractAddress = vm.envAddress( + "REMOTE_USDC_CONTRACT_ADDRESS" + ); + + remoteDomain = uint32(vm.envUint("REMOTE_DOMAIN")); + } + + /** + * @notice main function that will be run by forge + * this links the remote usdc token and the remote token messenger + */ + function run() public { + TokenMessengerV2 tokenMessengerV2 = TokenMessengerV2( + tokenMessengerV2ContractAddress + ); + TokenMinterV2 tokenMinterV2 = TokenMinterV2( + tokenMinterV2ContractAddress + ); + + // Link token pair and add remote token messenger + linkTokenPairV2(tokenMinterV2, tokenControllerPrivateKey); + addRemoteTokenMessengerV2( + tokenMessengerV2, + tokenMessengerV2OwnerPrivateKey + ); + } +} diff --git a/slither.config.json b/slither.config.json deleted file mode 100644 index 14d940b..0000000 --- a/slither.config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "filter_paths": "lib|test", - "solc_remaps": [ - "@memview-sol/=lib/memview-sol", - "@openzeppelin/=lib/openzeppelin-contracts", - "ds-test/=lib/ds-test/src/", - "forge-std/=lib/forge-std/src/" - ] - } \ No newline at end of file diff --git a/src/MessageTransmitter.sol b/src/MessageTransmitter.sol index 59f8250..75c11e9 100644 --- a/src/MessageTransmitter.sol +++ b/src/MessageTransmitter.sol @@ -247,12 +247,10 @@ contract MessageTransmitter is * of the attester address recovered from signatures. * @return success bool, true if successful */ - function receiveMessage(bytes calldata message, bytes calldata attestation) - external - override - whenNotPaused - returns (bool success) - { + function receiveMessage( + bytes calldata message, + bytes calldata attestation + ) external override whenNotPaused returns (bool success) { // Validate each signature in the attestation _verifyAttestationSignatures(message, attestation); @@ -313,10 +311,9 @@ contract MessageTransmitter is * to avoid impacting users who rely on large messages. * @param newMaxMessageBodySize new max message body size, in bytes */ - function setMaxMessageBodySize(uint256 newMaxMessageBodySize) - external - onlyOwner - { + function setMaxMessageBodySize( + uint256 newMaxMessageBodySize + ) external onlyOwner { maxMessageBodySize = newMaxMessageBodySize; emit MaxMessageBodySizeUpdated(maxMessageBodySize); } @@ -372,11 +369,10 @@ contract MessageTransmitter is destination * @return hash of source and nonce */ - function _hashSourceAndNonce(uint32 _source, uint64 _nonce) - internal - pure - returns (bytes32) - { + function _hashSourceAndNonce( + uint32 _source, + uint64 _nonce + ) internal pure returns (bytes32) { return keccak256(abi.encodePacked(_source, _nonce)); } diff --git a/src/examples/CCTPHookWrapper.sol b/src/examples/CCTPHookWrapper.sol new file mode 100644 index 0000000..82e926a --- /dev/null +++ b/src/examples/CCTPHookWrapper.sol @@ -0,0 +1,160 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {IReceiverV2} from "../interfaces/v2/IReceiverV2.sol"; +import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol"; +import {MessageV2} from "../messages/v2/MessageV2.sol"; +import {BurnMessageV2} from "../messages/v2/BurnMessageV2.sol"; +import {Ownable2Step} from "../roles/Ownable2Step.sol"; + +/** + * @title CCTPHookWrapper + * @notice A sample wrapper around CCTP v2 that relays a message and + * optionally executes the hook contained in the Burn Message. + * @dev Intended to only work with CCTP v2 message formats and interfaces. + */ +contract CCTPHookWrapper is Ownable2Step { + // ============ Constants ============ + // Address of the local message transmitter + IReceiverV2 public immutable messageTransmitter; + + // The supported Message Format version + uint32 public constant supportedMessageVersion = 1; + + // The supported Message Body version + uint32 public constant supportedMessageBodyVersion = 1; + + // Byte-length of an address + uint256 internal constant ADDRESS_BYTE_LENGTH = 20; + + // ============ Libraries ============ + using TypedMemView for bytes; + using TypedMemView for bytes29; + + // ============ Constructor ============ + /** + * @param _messageTransmitter The address of the local message transmitter + */ + constructor(address _messageTransmitter) Ownable2Step() { + require( + _messageTransmitter != address(0), + "Message transmitter is the zero address" + ); + + messageTransmitter = IReceiverV2(_messageTransmitter); + } + + // ============ External Functions ============ + /** + * @notice Relays a burn message to a local message transmitter + * and executes the hook, if present. + * + * @dev The hook data contained in the Burn Message is expected to follow this format: + * Field Bytes Type Index + * target 20 address 0 + * hookCallData dynamic bytes 20 + * + * The hook handler will call the target address with the hookCallData, even if hookCallData + * is zero-length. Additional data about the burn message is not passed in this call. + * + * @dev Reverts if not called by the Owner. Due to the lack of atomicity with the hook call, permissionless relay of messages containing hooks via + * an implementation like this contract should be carefully considered, as a malicious caller could use a low gas attack to consume + * the message's nonce without executing the hook. + * + * WARNING: this implementation does NOT enforce atomicity in the hook call. This is to prevent a failed hook call + * from preventing relay of a message if this contract is set as the destinationCaller. + * + * @dev Reverts if the receiveMessage() call to the local message transmitter reverts, or returns false. + * @param message The message to relay, as bytes + * @param attestation The attestation corresponding to the message, as bytes + * @return relaySuccess True if the call to the local message transmitter succeeded. + * @return hookSuccess True if the call to the hook target succeeded. False if the hook call failed, + * or if no hook was present. + * @return hookReturnData The data returned from the call to the hook target. This will be empty + * if there was no hook in the message. + */ + function relay( + bytes calldata message, + bytes calldata attestation + ) + external + virtual + returns ( + bool relaySuccess, + bool hookSuccess, + bytes memory hookReturnData + ) + { + _checkOwner(); + + // Validate message + bytes29 _msg = message.ref(0); + MessageV2._validateMessageFormat(_msg); + require( + MessageV2._getVersion(_msg) == supportedMessageVersion, + "Invalid message version" + ); + + // Validate burn message + bytes29 _msgBody = MessageV2._getMessageBody(_msg); + BurnMessageV2._validateBurnMessageFormat(_msgBody); + require( + BurnMessageV2._getVersion(_msgBody) == supportedMessageBodyVersion, + "Invalid message body version" + ); + + // Relay message + relaySuccess = messageTransmitter.receiveMessage(message, attestation); + require(relaySuccess, "Receive message failed"); + + // Handle hook if present + bytes29 _hookData = BurnMessageV2._getHookData(_msgBody); + if (_hookData.isValid()) { + uint256 _hookDataLength = _hookData.len(); + if (_hookDataLength >= ADDRESS_BYTE_LENGTH) { + address _target = _hookData.indexAddress(0); + bytes memory _hookCalldata = _hookData + .postfix(_hookDataLength - ADDRESS_BYTE_LENGTH, 0) + .clone(); + + (hookSuccess, hookReturnData) = _executeHook( + _target, + _hookCalldata + ); + } + } + } + + // ============ Internal Functions ============ + /** + * @notice Handles hook data by executing a call to a target address + * @dev Can be overridden to customize execution behavior + * @dev Does not revert if the CALL to the hook target fails + * @param _hookTarget The target address of the hook + * @param _hookCalldata The hook calldata + * @return _success True if the call to the encoded hook target succeeds + * @return _returnData The data returned from the call to the hook target + */ + function _executeHook( + address _hookTarget, + bytes memory _hookCalldata + ) internal virtual returns (bool _success, bytes memory _returnData) { + (_success, _returnData) = address(_hookTarget).call(_hookCalldata); + } +} diff --git a/src/interfaces/v2/IMessageHandlerV2.sol b/src/interfaces/v2/IMessageHandlerV2.sol new file mode 100644 index 0000000..64f9c54 --- /dev/null +++ b/src/interfaces/v2/IMessageHandlerV2.sol @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +/** + * @title IMessageHandlerV2 + * @notice Handles messages on the destination domain, forwarded from + * an IReceiverV2. + */ +interface IMessageHandlerV2 { + /** + * @notice Handles an incoming finalized message from an IReceiverV2 + * @dev Finalized messages have finality threshold values greater than or equal to 2000 + * @param sourceDomain The source domain of the message + * @param sender The sender of the message + * @param finalityThresholdExecuted the finality threshold at which the message was attested to + * @param messageBody The raw bytes of the message body + * @return success True, if successful; false, if not. + */ + function handleReceiveFinalizedMessage( + uint32 sourceDomain, + bytes32 sender, + uint32 finalityThresholdExecuted, + bytes calldata messageBody + ) external returns (bool); + + /** + * @notice Handles an incoming unfinalized message from an IReceiverV2 + * @dev Unfinalized messages have finality threshold values less than 2000 + * @param sourceDomain The source domain of the message + * @param sender The sender of the message + * @param finalityThresholdExecuted The finality threshold at which the message was attested to + * @param messageBody The raw bytes of the message body + * @return success True, if successful; false, if not. + */ + function handleReceiveUnfinalizedMessage( + uint32 sourceDomain, + bytes32 sender, + uint32 finalityThresholdExecuted, + bytes calldata messageBody + ) external returns (bool); +} diff --git a/src/interfaces/v2/IMessageTransmitterV2.sol b/src/interfaces/v2/IMessageTransmitterV2.sol new file mode 100644 index 0000000..36b1bbc --- /dev/null +++ b/src/interfaces/v2/IMessageTransmitterV2.sol @@ -0,0 +1,29 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {IReceiverV2} from "./IReceiverV2.sol"; +import {IRelayerV2} from "./IRelayerV2.sol"; + +/** + * @title IMessageTransmitterV2 + * @notice Interface for V2 message transmitters, which both relay and receive messages. + */ +interface IMessageTransmitterV2 is IRelayerV2, IReceiverV2 { + +} diff --git a/src/interfaces/v2/IReceiverV2.sol b/src/interfaces/v2/IReceiverV2.sol new file mode 100644 index 0000000..d9c8295 --- /dev/null +++ b/src/interfaces/v2/IReceiverV2.sol @@ -0,0 +1,29 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {IReceiver} from "../IReceiver.sol"; + +/** + * @title IReceiverV2 + * @notice Receives messages on the destination chain and forwards them to contracts implementing + * IMessageHandlerV2. + */ +interface IReceiverV2 is IReceiver { + +} diff --git a/src/interfaces/v2/IRelayerV2.sol b/src/interfaces/v2/IRelayerV2.sol new file mode 100644 index 0000000..a498786 --- /dev/null +++ b/src/interfaces/v2/IRelayerV2.sol @@ -0,0 +1,44 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +/** + * @title IRelayerV2 + * @notice Sends messages from the source domain to the destination domain + */ +interface IRelayerV2 { + /** + * @notice Sends an outgoing message from the source domain. + * @dev Emits a `MessageSent` event with message information. + * WARNING: if the `destinationCaller` does not represent a valid address as bytes32, then it will not be possible + * to broadcast the message on the destination domain. If set to bytes32(0), anyone will be able to broadcast it. + * This is an advanced feature, and using bytes32(0) should be preferred for use cases where a specific destination caller is not required. + * @param destinationDomain Domain of destination chain + * @param recipient Address of message recipient on destination domain as bytes32 + * @param destinationCaller Allowed caller on destination domain (see above WARNING). + * @param minFinalityThreshold Minimum finality threshold at which the message must be attested to. + * @param messageBody Content of the message, as raw bytes + */ + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes32 destinationCaller, + uint32 minFinalityThreshold, + bytes calldata messageBody + ) external; +} diff --git a/src/interfaces/v2/ITokenMinterV2.sol b/src/interfaces/v2/ITokenMinterV2.sol new file mode 100644 index 0000000..0e3164a --- /dev/null +++ b/src/interfaces/v2/ITokenMinterV2.sol @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {ITokenMinter} from "../ITokenMinter.sol"; + +/** + * @title ITokenMinterV2 + * @notice Interface for a minter of tokens that are mintable, burnable, and interchangeable + * across domains. + */ +interface ITokenMinterV2 is ITokenMinter { + /** + * @notice Mints to multiple recipients amounts of tokens corresponding to the + * given (`sourceDomain`, `burnToken`) pair. + * @param sourceDomain Source domain where `burnToken` was burned. + * @param burnToken Burned token address as bytes32. + * @param recipientOne Address to receive `amountOne` of minted tokens + * @param recipientTwo Address to receive `amountTwo` of minted tokens + * @param amountOne Amount of tokens to mint to `recipientOne` + * @param amountTwo Amount of tokens to mint to `recipientTwo` + * @return mintToken Address of the token that was minted, corresponding to the (`sourceDomain`, `burnToken`) pair + */ + function mint( + uint32 sourceDomain, + bytes32 burnToken, + address recipientOne, + address recipientTwo, + uint256 amountOne, + uint256 amountTwo + ) external returns (address); +} diff --git a/src/messages/v2/AddressUtils.sol b/src/messages/v2/AddressUtils.sol new file mode 100644 index 0000000..32784d3 --- /dev/null +++ b/src/messages/v2/AddressUtils.sol @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +/** + * @title AddressUtils Library + * @notice Helper functions for converting addresses to and from bytes + **/ +library AddressUtils { + /** + * @notice Converts an address to bytes32 by left-padding with zeros (alignment preserving cast.) + * @param addr The address to convert to bytes32 + */ + function toBytes32(address addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } + + /** + * @notice Converts bytes32 to address (alignment preserving cast.) + * @dev Warning: it is possible to have different input values _buf map to the same address. + * For use cases where this is not acceptable, validate that the first 12 bytes of _buf are zero-padding. + * @param _buf the bytes32 to convert to address + */ + function toAddress(bytes32 _buf) internal pure returns (address) { + return address(uint160(uint256(_buf))); + } +} diff --git a/src/messages/v2/AddressUtilsExternal.sol b/src/messages/v2/AddressUtilsExternal.sol new file mode 100644 index 0000000..f8c8069 --- /dev/null +++ b/src/messages/v2/AddressUtilsExternal.sol @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +/** + * @title AddressUtilsExternal Library + * @notice Helper functions for converting addresses to and from bytes + **/ +library AddressUtilsExternal { + /** + * @notice converts address to bytes32 (alignment preserving cast.) + * @param addr the address to convert to bytes32 + */ + function addressToBytes32(address addr) external pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } + + /** + * @notice converts bytes32 to address (alignment preserving cast.) + * @dev Warning: it is possible to have different input values _buf map to the same address. + * For use cases where this is not acceptable, validate that the first 12 bytes of _buf are zero-padding. + * @param _buf the bytes32 to convert to address + */ + function bytes32ToAddress(bytes32 _buf) external pure returns (address) { + return address(uint160(uint256(_buf))); + } +} diff --git a/src/messages/v2/BurnMessageV2.sol b/src/messages/v2/BurnMessageV2.sol new file mode 100644 index 0000000..703dc06 --- /dev/null +++ b/src/messages/v2/BurnMessageV2.sol @@ -0,0 +1,158 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol"; +import {BurnMessage} from "../BurnMessage.sol"; + +/** + * @title BurnMessageV2 Library + * @notice Library for formatted V2 BurnMessages used by TokenMessengerV2. + * @dev BurnMessageV2 format: + * Field Bytes Type Index + * version 4 uint32 0 + * burnToken 32 bytes32 4 + * mintRecipient 32 bytes32 36 + * amount 32 uint256 68 + * messageSender 32 bytes32 100 + * maxFee 32 uint256 132 + * feeExecuted 32 uint256 164 + * expirationBlock 32 uint256 196 + * hookData dynamic bytes 228 + * @dev Additions from v1: + * - maxFee + * - feeExecuted + * - expirationBlock + * - hookData + **/ +library BurnMessageV2 { + using TypedMemView for bytes; + using TypedMemView for bytes29; + using BurnMessage for bytes29; + + // Field indices + uint8 private constant MAX_FEE_INDEX = 132; + uint8 private constant FEE_EXECUTED_INDEX = 164; + uint8 private constant EXPIRATION_BLOCK_INDEX = 196; + uint8 private constant HOOK_DATA_INDEX = 228; + + uint256 private constant EMPTY_FEE_EXECUTED = 0; + uint256 private constant EMPTY_EXPIRATION_BLOCK = 0; + + /** + * @notice Formats a V2 burn message + * @param _version The message body version + * @param _burnToken The burn token address on the source domain, as bytes32 + * @param _mintRecipient The mint recipient address as bytes32 + * @param _amount The burn amount + * @param _messageSender The message sender + * @param _maxFee The maximum fee to be paid on destination domain + * @param _hookData Optional hook data for processing on the destination domain + * @return Formatted message bytes. + */ + function _formatMessageForRelay( + uint32 _version, + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _messageSender, + uint256 _maxFee, + bytes calldata _hookData + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + _version, + _burnToken, + _mintRecipient, + _amount, + _messageSender, + _maxFee, + EMPTY_FEE_EXECUTED, + EMPTY_EXPIRATION_BLOCK, + _hookData + ); + } + + // @notice Returns _message's version field + function _getVersion(bytes29 _message) internal pure returns (uint32) { + return _message._getVersion(); + } + + // @notice Returns _message's burnToken field + function _getBurnToken(bytes29 _message) internal pure returns (bytes32) { + return _message._getBurnToken(); + } + + // @notice Returns _message's mintRecipient field + function _getMintRecipient( + bytes29 _message + ) internal pure returns (bytes32) { + return _message._getMintRecipient(); + } + + // @notice Returns _message's amount field + function _getAmount(bytes29 _message) internal pure returns (uint256) { + return _message._getAmount(); + } + + // @notice Returns _message's messageSender field + function _getMessageSender( + bytes29 _message + ) internal pure returns (bytes32) { + return _message._getMessageSender(); + } + + // @notice Returns _message's maxFee field + function _getMaxFee(bytes29 _message) internal pure returns (uint256) { + return _message.indexUint(MAX_FEE_INDEX, 32); + } + + // @notice Returns _message's feeExecuted field + function _getFeeExecuted(bytes29 _message) internal pure returns (uint256) { + return _message.indexUint(FEE_EXECUTED_INDEX, 32); + } + + // @notice Returns _message's expirationBlock field + function _getExpirationBlock( + bytes29 _message + ) internal pure returns (uint256) { + return _message.indexUint(EXPIRATION_BLOCK_INDEX, 32); + } + + // @notice Returns _message's hookData field + function _getHookData(bytes29 _message) internal pure returns (bytes29) { + return + _message.slice( + HOOK_DATA_INDEX, + _message.len() - HOOK_DATA_INDEX, + 0 + ); + } + + /** + * @notice Reverts if burn message is malformed or invalid length + * @param _message The burn message as bytes29 + */ + function _validateBurnMessageFormat(bytes29 _message) internal pure { + require(_message.isValid(), "Malformed message"); + require( + _message.len() >= HOOK_DATA_INDEX, + "Invalid burn message: too short" + ); + } +} diff --git a/src/messages/v2/MessageV2.sol b/src/messages/v2/MessageV2.sol new file mode 100644 index 0000000..c5dbd91 --- /dev/null +++ b/src/messages/v2/MessageV2.sol @@ -0,0 +1,177 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol"; + +/** + * @title MessageV2 Library + * @notice Library for formatted v2 messages used by Relayer and Receiver. + * + * @dev The message body is dynamically-sized to support custom message body + * formats. Other fields must be fixed-size to avoid hash collisions. + * Each other input value has an explicit type to guarantee fixed-size. + * Padding: uintNN fields are left-padded, and bytesNN fields are right-padded. + * + * Field Bytes Type Index + * version 4 uint32 0 + * sourceDomain 4 uint32 4 + * destinationDomain 4 uint32 8 + * nonce 32 bytes32 12 + * sender 32 bytes32 44 + * recipient 32 bytes32 76 + * destinationCaller 32 bytes32 108 + * minFinalityThreshold 4 uint32 140 + * finalityThresholdExecuted 4 uint32 144 + * messageBody dynamic bytes 148 + * @dev Differences from v1: + * - Nonce is now bytes32 (vs. uint64) + * - minFinalityThreshold added + * - finalityThresholdExecuted added + **/ +library MessageV2 { + using TypedMemView for bytes; + using TypedMemView for bytes29; + + // Indices of each field in message + uint8 private constant VERSION_INDEX = 0; + uint8 private constant SOURCE_DOMAIN_INDEX = 4; + uint8 private constant DESTINATION_DOMAIN_INDEX = 8; + uint8 private constant NONCE_INDEX = 12; + uint8 private constant SENDER_INDEX = 44; + uint8 private constant RECIPIENT_INDEX = 76; + uint8 private constant DESTINATION_CALLER_INDEX = 108; + uint8 private constant MIN_FINALITY_THRESHOLD_INDEX = 140; + uint8 private constant FINALITY_THRESHOLD_EXECUTED_INDEX = 144; + uint8 private constant MESSAGE_BODY_INDEX = 148; + + bytes32 private constant EMPTY_NONCE = bytes32(0); + uint32 private constant EMPTY_FINALITY_THRESHOLD_EXECUTED = 0; + + /** + * @notice Returns formatted (packed) message with provided fields + * @param _version the version of the message format + * @param _sourceDomain Domain of home chain + * @param _destinationDomain Domain of destination chain + * @param _sender Address of sender on source chain as bytes32 + * @param _recipient Address of recipient on destination chain as bytes32 + * @param _destinationCaller Address of caller on destination chain as bytes32 + * @param _minFinalityThreshold the minimum finality at which the message should be attested to + * @param _messageBody Raw bytes of message body + * @return Formatted message + **/ + function _formatMessageForRelay( + uint32 _version, + uint32 _sourceDomain, + uint32 _destinationDomain, + bytes32 _sender, + bytes32 _recipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _messageBody + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + _version, + _sourceDomain, + _destinationDomain, + EMPTY_NONCE, + _sender, + _recipient, + _destinationCaller, + _minFinalityThreshold, + EMPTY_FINALITY_THRESHOLD_EXECUTED, + _messageBody + ); + } + + // @notice Returns _message's version field + function _getVersion(bytes29 _message) internal pure returns (uint32) { + return uint32(_message.indexUint(VERSION_INDEX, 4)); + } + + // @notice Returns _message's sourceDomain field + function _getSourceDomain(bytes29 _message) internal pure returns (uint32) { + return uint32(_message.indexUint(SOURCE_DOMAIN_INDEX, 4)); + } + + // @notice Returns _message's destinationDomain field + function _getDestinationDomain( + bytes29 _message + ) internal pure returns (uint32) { + return uint32(_message.indexUint(DESTINATION_DOMAIN_INDEX, 4)); + } + + // @notice Returns _message's nonce field + function _getNonce(bytes29 _message) internal pure returns (bytes32) { + return _message.index(NONCE_INDEX, 32); + } + + // @notice Returns _message's sender field + function _getSender(bytes29 _message) internal pure returns (bytes32) { + return _message.index(SENDER_INDEX, 32); + } + + // @notice Returns _message's recipient field + function _getRecipient(bytes29 _message) internal pure returns (bytes32) { + return _message.index(RECIPIENT_INDEX, 32); + } + + // @notice Returns _message's destinationCaller field + function _getDestinationCaller( + bytes29 _message + ) internal pure returns (bytes32) { + return _message.index(DESTINATION_CALLER_INDEX, 32); + } + + // @notice Returns _message's minFinalityThreshold field + function _getMinFinalityThreshold( + bytes29 _message + ) internal pure returns (uint32) { + return uint32(_message.indexUint(MIN_FINALITY_THRESHOLD_INDEX, 4)); + } + + // @notice Returns _message's finalityThresholdExecuted field + function _getFinalityThresholdExecuted( + bytes29 _message + ) internal pure returns (uint32) { + return uint32(_message.indexUint(FINALITY_THRESHOLD_EXECUTED_INDEX, 4)); + } + + // @notice Returns _message's messageBody field + function _getMessageBody(bytes29 _message) internal pure returns (bytes29) { + return + _message.slice( + MESSAGE_BODY_INDEX, + _message.len() - MESSAGE_BODY_INDEX, + 0 + ); + } + + /** + * @notice Reverts if message is malformed or too short + * @param _message The message as bytes29 + */ + function _validateMessageFormat(bytes29 _message) internal pure { + require(_message.isValid(), "Malformed message"); + require( + _message.len() >= MESSAGE_BODY_INDEX, + "Invalid message: too short" + ); + } +} diff --git a/src/proxy/AdminUpgradableProxy.sol b/src/proxy/AdminUpgradableProxy.sol new file mode 100644 index 0000000..df892f8 --- /dev/null +++ b/src/proxy/AdminUpgradableProxy.sol @@ -0,0 +1,148 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {UpgradeableProxy, Address} from "@openzeppelin/contracts/proxy/UpgradeableProxy.sol"; + +/** + * @title AdminUpgradeableProxy + * @notice This contract combines an upgradeable proxy with an authorization + * mechanism for administrative tasks. + * + * @dev Forked from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/8e0296096449d9b1cd7c5631e917330635244c37/contracts/proxy/TransparentUpgradeableProxy.sol#L1 + * Modifications (10/1/2024): + * - Remove ifAdmin modifier from admin() and implementation() and updated natspec. + * - Update admin() and implementation() functions to be view functions. + * - Pin Solidity to 0.7.6. + * - Remove constructor visibility specifier. + * - Remove overriden _beforeFallback() implementation. + * - Bump constants, modifiers, and event declarations above constructor for consistency. + * - Use "AdminUpgradableProxy" in revert string + */ +contract AdminUpgradableProxy is UpgradeableProxy { + /** + * @dev Emitted when the admin account has changed. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev Storage slot with the admin of the contract. + * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is + * validated in the constructor. + */ + bytes32 private constant _ADMIN_SLOT = + 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /** + * @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin. + */ + modifier ifAdmin() { + if (msg.sender == _admin()) { + _; + } else { + _fallback(); + } + } + + /** + * @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and + * optionally initialized with `_data`. + */ + constructor( + address _logic, + address admin_, + bytes memory _data + ) payable UpgradeableProxy(_logic, _data) { + assert( + _ADMIN_SLOT == + bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1) + ); + _setAdmin(admin_); + } + + /** + * @dev Returns the current admin. + */ + function admin() external view returns (address admin_) { + admin_ = _admin(); + } + + /** + * @dev Returns the current implementation. + */ + function implementation() external view returns (address implementation_) { + implementation_ = _implementation(); + } + + /** + * @dev Changes the admin of the proxy. + * + * Emits an {AdminChanged} event. + * @dev Only the admin can call this function; other callers are delegated + */ + function changeAdmin(address newAdmin) external virtual ifAdmin { + require( + newAdmin != address(0), + "AdminUpgradableProxy: new admin is the zero address" + ); + emit AdminChanged(_admin(), newAdmin); + _setAdmin(newAdmin); + } + + /** + * @dev Upgrade the implementation of the proxy. + * @dev Only the admin can call this function; other callers are delegated + */ + function upgradeTo(address newImplementation) external virtual ifAdmin { + _upgradeTo(newImplementation); + } + + /** + * @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified + * by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the + * proxied contract. + * @dev Only the admin can call this function; other callers are delegated + */ + function upgradeToAndCall( + address newImplementation, + bytes calldata data + ) external payable virtual ifAdmin { + _upgradeTo(newImplementation); + Address.functionDelegateCall(newImplementation, data); + } + + /** + * @dev Returns the current admin. + */ + function _admin() internal view virtual returns (address adm) { + bytes32 slot = _ADMIN_SLOT; + assembly { + adm := sload(slot) + } + } + + /** + * @dev Stores a new address in the EIP1967 admin slot. + */ + function _setAdmin(address newAdmin) private { + bytes32 slot = _ADMIN_SLOT; + assembly { + sstore(slot, newAdmin) + } + } +} diff --git a/src/proxy/Initializable.sol b/src/proxy/Initializable.sol new file mode 100644 index 0000000..81b440d --- /dev/null +++ b/src/proxy/Initializable.sol @@ -0,0 +1,200 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title Initializable + * @notice Base class to support implementation contracts behind a proxy + * @dev Forked from: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/3e6c86392c97fbc30d3d20a378a6f58beba08eba/contracts/proxy/utils/Initializable.sol + * Modifications (10/5/2024): + * - Pinned to Solidity 0.7.6 + * - Replaced errors with revert strings + * - Replaced address.code call with Address.isContract for Solidity 0.7.6 + */ +abstract contract Initializable { + /** + * @dev Storage of the initializable contract. + * + * It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions + * when using with upgradeable contracts. + * + * @custom:storage-location erc7201:openzeppelin.storage.Initializable + */ + struct InitializableStorage { + // Indicates that the contract has been initialized. + uint64 _initialized; + // Indicates that the contract is in the process of being initialized. + bool _initializing; + } + + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant INITIALIZABLE_STORAGE = + 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint64 version); + + /** + * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, + * `onlyInitializing` functions can be used to initialize parent contracts. + * + * Similar to `reinitializer(1)`, except that in the context of a constructor an `initializer` may be invoked any + * number of times. This behavior in the constructor can be useful during testing and is not expected to be used in + * production. + * + * Emits an {Initialized} event. + */ + modifier initializer() { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + // Cache values to avoid duplicated sloads + bool isTopLevelCall = !$._initializing; + uint64 initialized = $._initialized; + + // Allowed calls: + // - initialSetup: the contract is not in the initializing state and no previous version was + // initialized + // - construction: the contract is initialized at version 1 (no reininitialization) and the + // current contract is just being deployed + bool initialSetup = initialized == 0 && isTopLevelCall; + // 10/5/2024 fork: use Address.isContract instead of address(this).code.length for Solidity 0.7.6. + bool construction = initialized == 1 && + !Address.isContract(address(this)); + + // 10/5/2024 fork: convert custom error to require statement + require( + initialSetup || construction, + "Initializable: invalid initialization" + ); + + $._initialized = 1; + if (isTopLevelCall) { + $._initializing = true; + } + _; + if (isTopLevelCall) { + $._initializing = false; + emit Initialized(1); + } + } + + /** + * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the + * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be + * used to initialize parent contracts. + * + * A reinitializer may be used after the original initialization step. This is essential to configure modules that + * are added through upgrades and that require initialization. + * + * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer` + * cannot be nested. If one is invoked in the context of another, execution will revert. + * + * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in + * a contract, executing them in the right order is up to the developer or operator. + * + * WARNING: Setting the version to 2**64 - 1 will prevent any future reinitialization. + * + * Emits an {Initialized} event. + */ + modifier reinitializer(uint64 version) { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + // 10/5/2024 fork: convert custom error to require statement + require( + !$._initializing && $._initialized < version, + "Initializable: invalid initialization" + ); + + $._initialized = version; + $._initializing = true; + _; + $._initializing = false; + emit Initialized(version); + } + + /** + * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the + * {initializer} and {reinitializer} modifiers, directly or indirectly. + */ + modifier onlyInitializing() { + _checkInitializing(); + _; + } + + /** + * @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}. + */ + function _checkInitializing() internal view virtual { + // 10/5/2024 fork: convert custom error to require statement + require(_isInitializing(), "Initializable: not initializing"); + } + + /** + * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. + * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized + * to any version. It is recommended to use this to lock implementation contracts that are designed to be called + * through proxies. + * + * Emits an {Initialized} event the first time it is successfully executed. + */ + function _disableInitializers() internal virtual { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + // 10/5/2024 fork: convert custom error to require statement + require(!$._initializing, "Initializable: invalid initialization"); + if ($._initialized != type(uint64).max) { + $._initialized = type(uint64).max; + emit Initialized(type(uint64).max); + } + } + + /** + * @dev Returns the highest version that has been initialized. See {reinitializer}. + */ + function _getInitializedVersion() internal view returns (uint64) { + return _getInitializableStorage()._initialized; + } + + /** + * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}. + */ + function _isInitializing() internal view returns (bool) { + return _getInitializableStorage()._initializing; + } + + /** + * @dev Returns a pointer to the storage namespace. + */ + // solhint-disable-next-line var-name-mixedcase + function _getInitializableStorage() + private + pure + returns (InitializableStorage storage $) + { + assembly { + $.slot := INITIALIZABLE_STORAGE + } + } +} diff --git a/src/roles/Attestable.sol b/src/roles/Attestable.sol index a88cf7e..2889d91 100644 --- a/src/roles/Attestable.sol +++ b/src/roles/Attestable.sol @@ -98,9 +98,7 @@ contract Attestable is Ownable2Step { * @param newAttester attester to enable */ function enableAttester(address newAttester) public onlyAttesterManager { - require(newAttester != address(0), "New attester must be nonzero"); - require(enabledAttesters.add(newAttester), "Attester already enabled"); - emit AttesterEnabled(newAttester); + _enableAttester(newAttester); } /** @@ -124,17 +122,10 @@ contract Attestable is Ownable2Step { * @dev Allows the current attester manager to transfer control of the contract to a newAttesterManager. * @param newAttesterManager The address to update attester manager to. */ - function updateAttesterManager(address newAttesterManager) - external - onlyOwner - { - require( - newAttesterManager != address(0), - "Invalid attester manager address" - ); - address _oldAttesterManager = _attesterManager; + function updateAttesterManager( + address newAttesterManager + ) external onlyOwner { _setAttesterManager(newAttesterManager); - emit AttesterManagerUpdated(_oldAttesterManager, newAttesterManager); } /** @@ -167,29 +158,10 @@ contract Attestable is Ownable2Step { * of enabled attesters. * @param newSignatureThreshold new signature threshold */ - function setSignatureThreshold(uint256 newSignatureThreshold) - external - onlyAttesterManager - { - require(newSignatureThreshold != 0, "Invalid signature threshold"); - - // New signature threshold cannot exceed the number of enabled attesters - require( - newSignatureThreshold <= enabledAttesters.length(), - "New signature threshold too high" - ); - - require( - newSignatureThreshold != signatureThreshold, - "Signature threshold already set" - ); - - uint256 _oldSignatureThreshold = signatureThreshold; - signatureThreshold = newSignatureThreshold; - emit SignatureThresholdUpdated( - _oldSignatureThreshold, - signatureThreshold - ); + function setSignatureThreshold( + uint256 newSignatureThreshold + ) external onlyAttesterManager { + _setSignatureThreshold(newSignatureThreshold); } /** @@ -212,10 +184,29 @@ contract Attestable is Ownable2Step { // ============ Internal Utils ============ /** * @dev Sets a new attester manager address + * @dev Emits an {AttesterManagerUpdated} event + * @dev Reverts if _newAttesterManager is the zero address * @param _newAttesterManager attester manager address to set */ function _setAttesterManager(address _newAttesterManager) internal { + require( + _newAttesterManager != address(0), + "Invalid attester manager address" + ); + address _oldAttesterManager = _attesterManager; _attesterManager = _newAttesterManager; + emit AttesterManagerUpdated(_oldAttesterManager, _newAttesterManager); + } + + /** + * @notice Enables an attester + * @dev New attester must be nonzero, and currently disabled. + * @param _newAttester attester to enable + */ + function _enableAttester(address _newAttester) internal { + require(_newAttester != address(0), "New attester must be nonzero"); + require(enabledAttesters.add(_newAttester), "Attester already enabled"); + emit AttesterEnabled(_newAttester); } /** @@ -277,11 +268,39 @@ contract Attestable is Ownable2Step { * @param _signature message signature * @return address of recovered signer **/ - function _recoverAttesterSignature(bytes32 _digest, bytes memory _signature) - internal - pure - returns (address) - { + function _recoverAttesterSignature( + bytes32 _digest, + bytes memory _signature + ) internal pure returns (address) { return (ECDSA.recover(_digest, _signature)); } + + /** + * @notice Sets the threshold of signatures required to attest to a message. + * (This is the m in m/n multisig.) + * @dev New signature threshold must be nonzero, and must not exceed number + * of enabled attesters. + * @param _newSignatureThreshold new signature threshold + */ + function _setSignatureThreshold(uint256 _newSignatureThreshold) internal { + require(_newSignatureThreshold != 0, "Invalid signature threshold"); + + // New signature threshold cannot exceed the number of enabled attesters + require( + _newSignatureThreshold <= enabledAttesters.length(), + "New signature threshold too high" + ); + + require( + _newSignatureThreshold != signatureThreshold, + "Signature threshold already set" + ); + + uint256 _oldSignatureThreshold = signatureThreshold; + signatureThreshold = _newSignatureThreshold; + emit SignatureThresholdUpdated( + _oldSignatureThreshold, + _newSignatureThreshold + ); + } } diff --git a/src/roles/Pausable.sol b/src/roles/Pausable.sol index 445db8e..5491941 100644 --- a/src/roles/Pausable.sol +++ b/src/roles/Pausable.sol @@ -24,6 +24,7 @@ import "./Ownable2Step.sol"; * Modifications: * 1. Update Solidity version from 0.6.12 to 0.7.6 (8/23/2022) * 2. Change pauser visibility to private, declare external getter (11/19/22) + * 3. Add internal _updatePauser (10/8/2024) */ contract Pausable is Ownable2Step { event Pause(); @@ -77,6 +78,13 @@ contract Pausable is Ownable2Step { * @dev update the pauser role */ function updatePauser(address _newPauser) external onlyOwner { + _updatePauser(_newPauser); + } + + /** + * @dev update the pauser role + */ + function _updatePauser(address _newPauser) internal { require( _newPauser != address(0), "Pausable: new pauser is the zero address" diff --git a/src/roles/Rescuable.sol b/src/roles/Rescuable.sol index e0982fd..f8ee034 100644 --- a/src/roles/Rescuable.sol +++ b/src/roles/Rescuable.sol @@ -24,6 +24,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; * @dev Forked from https://github.com/centrehq/centre-tokens/blob/0d3cab14ebd133a83fc834dbd48d0468bdf0b391/contracts/v1.1/Rescuable.sol * Modifications: * 1. Update Solidity version from 0.6.12 to 0.7.6 (8/23/2022) + * 2. Add internal _updateRescuer (10/8/2024) */ contract Rescuable is Ownable2Step { using SafeERC20 for IERC20; @@ -67,6 +68,14 @@ contract Rescuable is Ownable2Step { * @param newRescuer New rescuer's address */ function updateRescuer(address newRescuer) external onlyOwner { + _updateRescuer(newRescuer); + } + + /** + * @notice Assign the rescuer role to a given address. + * @param newRescuer New rescuer's address + */ + function _updateRescuer(address newRescuer) internal { require( newRescuer != address(0), "Rescuable: new rescuer is the zero address" diff --git a/src/roles/TokenController.sol b/src/roles/TokenController.sol index 556e3f3..1c7e357 100644 --- a/src/roles/TokenController.sol +++ b/src/roles/TokenController.sol @@ -121,7 +121,7 @@ abstract contract TokenController { * Note: * - A remote token (on a certain remote domain) can only map to one local token, but many remote tokens * can map to the same local token. - * - Setting a token pair does not enable the `localToken` (that requires calling setLocalTokenEnabledStatus.) + * - Setting a token pair does not enable the `localToken` for deposits (that requires calling setMaxBurnAmountPerMessage.) */ function linkTokenPair( address localToken, @@ -214,11 +214,10 @@ abstract contract TokenController { * @param remoteToken Remote token * @return Local token address */ - function _getLocalToken(uint32 remoteDomain, bytes32 remoteToken) - internal - view - returns (address) - { + function _getLocalToken( + uint32 remoteDomain, + bytes32 remoteToken + ) internal view returns (address) { bytes32 _remoteTokensKey = _hashRemoteDomainAndToken( remoteDomain, remoteToken @@ -233,11 +232,10 @@ abstract contract TokenController { * @param remoteToken Address of remote token as bytes32 * @return keccak hash of packed remote domain and token */ - function _hashRemoteDomainAndToken(uint32 remoteDomain, bytes32 remoteToken) - internal - pure - returns (bytes32) - { + function _hashRemoteDomainAndToken( + uint32 remoteDomain, + bytes32 remoteToken + ) internal pure returns (bytes32) { return keccak256(abi.encodePacked(remoteDomain, remoteToken)); } } diff --git a/src/roles/v2/AttestableV2.sol b/src/roles/v2/AttestableV2.sol new file mode 100644 index 0000000..e46101d --- /dev/null +++ b/src/roles/v2/AttestableV2.sol @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {Attestable} from "../Attestable.sol"; + +/** + * @title AttestableV2 + * @notice Builds on Attestable by adding a storage gap to enable more flexible future additions to + * any AttestableV2 child contracts. + */ +contract AttestableV2 is Attestable { + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[20] private __gap; + + // ============ Constructor ============ + /** + * @dev The constructor sets the original attester manager and the first enabled attester to the + * msg.sender address. + */ + constructor() Attestable(msg.sender) {} +} diff --git a/src/roles/v2/Denylistable.sol b/src/roles/v2/Denylistable.sol new file mode 100644 index 0000000..3f69d46 --- /dev/null +++ b/src/roles/v2/Denylistable.sol @@ -0,0 +1,166 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {Ownable2Step} from "../Ownable2Step.sol"; + +/** + * @title Denylistable + * @notice Contract that allows the management and application of a denylist + */ +abstract contract Denylistable is Ownable2Step { + // ============ Events ============ + /** + * @notice Emitted when the denylister is updated + * @param oldDenylister Address of the previous Denylister + * @param newDenylister Address of the new Denylister + */ + event DenylisterChanged( + address indexed oldDenylister, + address indexed newDenylister + ); + + /** + * @notice Emitted when `account` is added to the denylist + * @param account Address added to the denylist + */ + event Denylisted(address indexed account); + + /** + * @notice Emitted when `account` is removed from the denylist + * @param account Address removed from the denylist + */ + event UnDenylisted(address indexed account); + + // ============ Constants ============ + // A true boolean representation in uint256 + uint256 private constant _TRUE = 1; + + // A false boolean representation in uint256 + uint256 private constant _FALSE = 0; + + // ============ State Variables ============ + // The currently set denylister + address internal _denylister; + + // A mapping indicating whether an account is on the denylist. 1 indicates that an + // address is on the denylist; 0 otherwise. + mapping(address => uint256) internal _denylist; + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[20] private __gap; + + // ============ Modifiers ============ + /** + * @dev Throws if called by any account other than the denylister. + */ + modifier onlyDenylister() { + require( + msg.sender == _denylister, + "Denylistable: caller is not denylister" + ); + _; + } + + /** + * @dev Performs denylist checks on the msg.sender and tx.origin addresses + */ + modifier notDenylistedCallers() { + _requireNotDenylisted(msg.sender); + if (msg.sender != tx.origin) { + _requireNotDenylisted(tx.origin); + } + _; + } + + // ============ External Functions ============ + /** + * @notice Updates the currently set Denylister + * @dev Reverts if not called by the Owner + * @dev Reverts if the new denylister address is the zero address + * @param newDenylister The new denylister address + */ + function updateDenylister(address newDenylister) external onlyOwner { + _updateDenylister(newDenylister); + } + + /** + * @notice Adds an address to the denylist + * @param account Address to add to the denylist + */ + function denylist(address account) external onlyDenylister { + _denylist[account] = _TRUE; + emit Denylisted(account); + } + + /** + * @notice Removes an address from the denylist + * @param account Address to remove from the denylist + */ + function unDenylist(address account) external onlyDenylister { + _denylist[account] = _FALSE; + emit UnDenylisted(account); + } + + /** + * @notice Returns the currently set Denylister + * @return Denylister address + */ + function denylister() external view returns (address) { + return _denylister; + } + + /** + * @notice Returns whether an address is currently on the denylist + * @param account Address to check + * @return True if the account is on the deny list and false if the account is not. + */ + function isDenylisted(address account) external view returns (bool) { + return _denylist[account] == _TRUE; + } + + // ============ Internal Utils ============ + /** + * @notice Updates the currently set denylister + * @param _newDenylister The new denylister address + */ + function _updateDenylister(address _newDenylister) internal { + require( + _newDenylister != address(0), + "Denylistable: new denylister is the zero address" + ); + address _oldDenylister = _denylister; + _denylister = _newDenylister; + emit DenylisterChanged(_oldDenylister, _newDenylister); + } + + /** + * @notice Checks an address against the denylist + * @dev Reverts if address is on the denylist + */ + function _requireNotDenylisted(address _address) internal view { + require( + _denylist[_address] == _FALSE, + "Denylistable: account is on denylist" + ); + } +} diff --git a/src/v2/BaseMessageTransmitter.sol b/src/v2/BaseMessageTransmitter.sol new file mode 100644 index 0000000..7b0e380 --- /dev/null +++ b/src/v2/BaseMessageTransmitter.sol @@ -0,0 +1,99 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {AttestableV2} from "../roles/v2/AttestableV2.sol"; +import {Pausable} from "../roles/Pausable.sol"; +import {Rescuable} from "../roles/Rescuable.sol"; +import {Initializable} from "../proxy/Initializable.sol"; + +/** + * @title BaseMessageTransmitter + * @notice A base type containing administrative and configuration functionality for message transmitters. + */ +contract BaseMessageTransmitter is + Initializable, + Pausable, + Rescuable, + AttestableV2 +{ + // ============ Events ============ + /** + * @notice Emitted when max message body size is updated + * @param newMaxMessageBodySize new maximum message body size, in bytes + */ + event MaxMessageBodySizeUpdated(uint256 newMaxMessageBodySize); + + // ============ Constants ============ + // A constant value indicating that a nonce has been used + uint256 public constant NONCE_USED = 1; + + // ============ State Variables ============ + // Domain of chain on which the contract is deployed + uint32 public immutable localDomain; + + // Message Format version + uint32 public immutable version; + + // Maximum size of message body, in bytes. + // This value is set by owner. + uint256 public maxMessageBodySize; + + // Maps a bytes32 nonce -> uint256 (0 if unused, 1 if used) + mapping(bytes32 => uint256) public usedNonces; + + // ============ Constructor ============ + /** + * @param _localDomain Domain of chain on which the contract is deployed + * @param _version Message Format version + */ + constructor(uint32 _localDomain, uint32 _version) AttestableV2() { + localDomain = _localDomain; + version = _version; + } + + // ============ External Functions ============ + /** + * @notice Sets the max message body size + * @dev This value should not be reduced without good reason, + * to avoid impacting users who rely on large messages. + * @param newMaxMessageBodySize new max message body size, in bytes + */ + function setMaxMessageBodySize( + uint256 newMaxMessageBodySize + ) external onlyOwner { + _setMaxMessageBodySize(newMaxMessageBodySize); + } + + /** + * @notice Returns the current initialized version + */ + function initializedVersion() external view returns (uint64) { + return _getInitializedVersion(); + } + + // ============ Internal Utils ============ + /** + * @notice Sets the max message body size + * @param _newMaxMessageBodySize new max message body size, in bytes + */ + function _setMaxMessageBodySize(uint256 _newMaxMessageBodySize) internal { + maxMessageBodySize = _newMaxMessageBodySize; + emit MaxMessageBodySizeUpdated(maxMessageBodySize); + } +} diff --git a/src/v2/BaseTokenMessenger.sol b/src/v2/BaseTokenMessenger.sol new file mode 100644 index 0000000..6d5e2a7 --- /dev/null +++ b/src/v2/BaseTokenMessenger.sol @@ -0,0 +1,358 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {ITokenMinterV2} from "../interfaces/v2/ITokenMinterV2.sol"; +import {Rescuable} from "../roles/Rescuable.sol"; +import {Denylistable} from "../roles/v2/Denylistable.sol"; +import {IMintBurnToken} from "../interfaces/IMintBurnToken.sol"; +import {Initializable} from "../proxy/Initializable.sol"; + +/** + * @title BaseTokenMessenger + * @notice Base administrative functionality for TokenMessenger implementations, + * including managing remote token messengers and the local token minter. + */ +abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable { + // ============ Events ============ + /** + * @notice Emitted when a remote TokenMessenger is added + * @param domain remote domain + * @param tokenMessenger TokenMessenger on remote domain + */ + event RemoteTokenMessengerAdded(uint32 domain, bytes32 tokenMessenger); + + /** + * @notice Emitted when a remote TokenMessenger is removed + * @param domain remote domain + * @param tokenMessenger TokenMessenger on remote domain + */ + event RemoteTokenMessengerRemoved(uint32 domain, bytes32 tokenMessenger); + + /** + * @notice Emitted when the local minter is added + * @param localMinter address of local minter + */ + event LocalMinterAdded(address localMinter); + + /** + * @notice Emitted when the local minter is removed + * @param localMinter address of local minter + */ + event LocalMinterRemoved(address localMinter); + + /** + * @notice Emitted when the fee recipient is set + * @param feeRecipient address of fee recipient set + */ + event FeeRecipientSet(address feeRecipient); + + /** + * @notice Emitted when tokens are minted + * @param mintRecipient recipient address of minted tokens + * @param amount amount of minted tokens received by `mintRecipient` + * @param mintToken contract address of minted token + * @param feeCollected fee collected for mint + */ + event MintAndWithdraw( + address indexed mintRecipient, + uint256 amount, + address indexed mintToken, + uint256 feeCollected + ); + + // ============ State Variables ============ + // Local Message Transmitter responsible for sending and receiving messages to/from remote domains + address public immutable localMessageTransmitter; + + // Version of message body format + uint32 public immutable messageBodyVersion; + + // Minter responsible for minting and burning tokens on the local domain + ITokenMinterV2 public localMinter; + + // Valid TokenMessengers on remote domains + mapping(uint32 => bytes32) public remoteTokenMessengers; + + // Address to receive collected fees + address public feeRecipient; + + // ============ Modifiers ============ + /** + * @notice Only accept messages from a registered TokenMessenger contract on given remote domain + * @param domain The remote domain + * @param tokenMessenger The address of the TokenMessenger contract for the given remote domain + */ + modifier onlyRemoteTokenMessenger(uint32 domain, bytes32 tokenMessenger) { + require( + _isRemoteTokenMessenger(domain, tokenMessenger), + "Remote TokenMessenger unsupported" + ); + _; + } + + /** + * @notice Only accept messages from the registered message transmitter on local domain + */ + modifier onlyLocalMessageTransmitter() { + // Caller must be the registered message transmitter for this domain + require(_isLocalMessageTransmitter(), "Invalid message transmitter"); + _; + } + + // ============ Constructor ============ + /** + * @param _messageTransmitter Message transmitter address + * @param _messageBodyVersion Message body version + */ + constructor(address _messageTransmitter, uint32 _messageBodyVersion) { + require( + _messageTransmitter != address(0), + "MessageTransmitter not set" + ); + localMessageTransmitter = _messageTransmitter; + messageBodyVersion = _messageBodyVersion; + } + + // ============ External Functions ============ + /** + * @notice Add the TokenMessenger for a remote domain. + * @dev Reverts if there is already a TokenMessenger set for domain. + * @param domain Domain of remote TokenMessenger. + * @param tokenMessenger Address of remote TokenMessenger as bytes32. + */ + function addRemoteTokenMessenger( + uint32 domain, + bytes32 tokenMessenger + ) external onlyOwner { + _addRemoteTokenMessenger(domain, tokenMessenger); + } + + /** + * @notice Remove the TokenMessenger for a remote domain. + * @dev Reverts if there is no TokenMessenger set for `domain`. + * @param domain Domain of remote TokenMessenger + */ + function removeRemoteTokenMessenger(uint32 domain) external onlyOwner { + // No TokenMessenger set for given remote domain. + require( + remoteTokenMessengers[domain] != bytes32(0), + "No TokenMessenger set" + ); + + bytes32 _removedTokenMessenger = remoteTokenMessengers[domain]; + delete remoteTokenMessengers[domain]; + emit RemoteTokenMessengerRemoved(domain, _removedTokenMessenger); + } + + /** + * @notice Add minter for the local domain. + * @dev Reverts if a minter is already set for the local domain. + * @param newLocalMinter The address of the minter on the local domain. + */ + function addLocalMinter(address newLocalMinter) external onlyOwner { + _setLocalMinter(newLocalMinter); + } + + /** + * @notice Remove the minter for the local domain. + * @dev Reverts if the minter of the local domain is not set. + */ + function removeLocalMinter() external onlyOwner { + address _localMinterAddress = address(localMinter); + require(_localMinterAddress != address(0), "No local minter is set."); + + delete localMinter; + emit LocalMinterRemoved(_localMinterAddress); + } + + /** + * @notice Sets the fee recipient address + * @dev Reverts if not called by the owner + * @dev Reverts if `_feeRecipient` is the zero address + * @param _feeRecipient Address of fee recipient + */ + function setFeeRecipient(address _feeRecipient) external onlyOwner { + _setFeeRecipient(_feeRecipient); + } + + /** + * @notice Returns the current initialized version + */ + function initializedVersion() external view returns (uint64) { + return _getInitializedVersion(); + } + + // ============ Internal Utils ============ + /** + * @notice return the remote TokenMessenger for the given `_domain` if one exists, else revert. + * @param _domain The domain for which to get the remote TokenMessenger + * @return _tokenMessenger The address of the TokenMessenger on `_domain` as bytes32 + */ + function _getRemoteTokenMessenger( + uint32 _domain + ) internal view returns (bytes32) { + bytes32 _tokenMessenger = remoteTokenMessengers[_domain]; + require(_tokenMessenger != bytes32(0), "No TokenMessenger for domain"); + return _tokenMessenger; + } + + /** + * @notice return the local minter address if it is set, else revert. + * @return local minter as ITokenMinter. + */ + function _getLocalMinter() internal view returns (ITokenMinterV2) { + require(address(localMinter) != address(0), "Local minter is not set"); + return localMinter; + } + + /** + * @notice Return true if the given remote domain and TokenMessenger is registered + * on this TokenMessenger. + * @param _domain The remote domain of the message. + * @param _tokenMessenger The address of the TokenMessenger on remote domain. + * @return true if a remote TokenMessenger is registered for `_domain` and `_tokenMessenger`, + * on this TokenMessenger. + */ + function _isRemoteTokenMessenger( + uint32 _domain, + bytes32 _tokenMessenger + ) internal view returns (bool) { + return + _tokenMessenger != bytes32(0) && + remoteTokenMessengers[_domain] == _tokenMessenger; + } + + /** + * @notice Returns true if the message sender is the local registered MessageTransmitter + * @return true if message sender is the registered local message transmitter + */ + function _isLocalMessageTransmitter() internal view returns (bool) { + return msg.sender == localMessageTransmitter; + } + + /** + * @notice Deposits tokens from `_from` address and burns them + * @param _burnToken address of contract to burn deposited tokens, on local domain + * @param _from address depositing the funds + * @param _amount deposit amount + */ + function _depositAndBurn( + address _burnToken, + address _from, + uint256 _amount + ) internal { + ITokenMinterV2 _localMinter = _getLocalMinter(); + IMintBurnToken _mintBurnToken = IMintBurnToken(_burnToken); + require( + _mintBurnToken.transferFrom(_from, address(_localMinter), _amount), + "Transfer operation failed" + ); + _localMinter.burn(_burnToken, _amount); + } + + /** + * @notice Mints tokens to a recipient and optionally a fee to the + * currently set fee recipient. + * @param _remoteDomain domain where burned tokens originate from + * @param _burnToken address of token burned + * @param _mintRecipient recipient address of minted tokens + * @param _amount amount of tokens to mint to `_mintRecipient` + * @param _fee fee collected for mint + */ + function _mintAndWithdraw( + uint32 _remoteDomain, + bytes32 _burnToken, + address _mintRecipient, + uint256 _amount, + uint256 _fee + ) internal { + ITokenMinterV2 _minter = _getLocalMinter(); + + address _mintToken; + if (_fee > 0) { + _mintToken = _minter.mint( + _remoteDomain, + _burnToken, + _mintRecipient, + feeRecipient, + _amount, + _fee + ); + } else { + _mintToken = _minter.mint( + _remoteDomain, + _burnToken, + _mintRecipient, + _amount + ); + } + + emit MintAndWithdraw(_mintRecipient, _amount, _mintToken, _fee); + } + + /** + * @notice Sets the fee recipient address + * @dev Reverts if `_feeRecipient` is the zero address + * @param _feeRecipient Address of fee recipient + */ + function _setFeeRecipient(address _feeRecipient) internal { + require(_feeRecipient != address(0), "Zero address not allowed"); + feeRecipient = _feeRecipient; + emit FeeRecipientSet(_feeRecipient); + } + + /** + * @notice Sets the local minter for the local domain. + * @dev Reverts if a minter is already set for the local domain. + * @param _newLocalMinter The address of the minter on the local domain. + */ + function _setLocalMinter(address _newLocalMinter) internal { + require(_newLocalMinter != address(0), "Zero address not allowed"); + + require( + address(localMinter) == address(0), + "Local minter is already set." + ); + + localMinter = ITokenMinterV2(_newLocalMinter); + + emit LocalMinterAdded(_newLocalMinter); + } + + /** + * @notice Add the TokenMessenger for a remote domain. + * @dev Reverts if there is already a TokenMessenger set for domain. + * @param _domain Domain of remote TokenMessenger. + * @param _tokenMessenger Address of remote TokenMessenger as bytes32. + */ + function _addRemoteTokenMessenger( + uint32 _domain, + bytes32 _tokenMessenger + ) internal { + require(_tokenMessenger != bytes32(0), "bytes32(0) not allowed"); + + require( + remoteTokenMessengers[_domain] == bytes32(0), + "TokenMessenger already set" + ); + + remoteTokenMessengers[_domain] = _tokenMessenger; + emit RemoteTokenMessengerAdded(_domain, _tokenMessenger); + } +} diff --git a/src/v2/Create2Factory.sol b/src/v2/Create2Factory.sol new file mode 100644 index 0000000..0e5941a --- /dev/null +++ b/src/v2/Create2Factory.sol @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {Ownable} from "../roles/Ownable.sol"; + +/** + * @title Create2Factory + * @notice Contract used for deterministic contract deployments across chains. + */ +contract Create2Factory is Ownable { + /** + * @notice Deploys the contract. + * @param amount Amount of native token to seed the deployment + * @param salt A unique identifier + * @param bytecode The contract bytecode to deploy + * @return addr The deployed address + */ + function deploy( + uint256 amount, + bytes32 salt, + bytes calldata bytecode + ) external payable onlyOwner returns (address addr) { + // Deploy deterministically + addr = Create2.deploy(amount, salt, bytecode); + } + + /** + * @notice Deploys the contract and calls into it. + * @param amount Amount of native token to seed the deployment + * @param salt A unique identifier + * @param bytecode The contract bytecode to deploy + * @param data The data to call the implementation with + * @return addr The deployed address + */ + function deployAndMultiCall( + uint256 amount, + bytes32 salt, + bytes calldata bytecode, + bytes[] calldata data + ) external payable onlyOwner returns (address addr) { + // Deploy deterministically + addr = Create2.deploy(amount, salt, bytecode); + + uint256 dataLength = data.length; + for (uint256 i = 0; i < dataLength; ++i) { + Address.functionCall(addr, data[i]); + } + } + + /** + * @notice A helper function for predicting a deterministic address. + * @param salt The unique identifier + * @param bytecodeHash The keccak256 hash of the deployment bytecode. + * @return addr The deterministic address + */ + function computeAddress( + bytes32 salt, + bytes32 bytecodeHash + ) external view returns (address addr) { + addr = Create2.computeAddress(salt, bytecodeHash); + } +} diff --git a/src/v2/FinalityThresholds.sol b/src/v2/FinalityThresholds.sol new file mode 100644 index 0000000..7534415 --- /dev/null +++ b/src/v2/FinalityThresholds.sol @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +// The threshold at which (and above) messages are considered finalized. +uint32 constant FINALITY_THRESHOLD_FINALIZED = 2000; + +// The threshold at which (and above) messages are considered confirmed. +uint32 constant FINALITY_THRESHOLD_CONFIRMED = 1000; + +// The minimum allowed level of finality accepted by TokenMessenger +uint32 constant TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD = 500; diff --git a/src/v2/MessageTransmitterV2.sol b/src/v2/MessageTransmitterV2.sol new file mode 100644 index 0000000..bc669d4 --- /dev/null +++ b/src/v2/MessageTransmitterV2.sol @@ -0,0 +1,322 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {IMessageTransmitterV2} from "../interfaces/v2/IMessageTransmitterV2.sol"; +import {BaseMessageTransmitter} from "./BaseMessageTransmitter.sol"; +import {MessageV2} from "../messages/v2/MessageV2.sol"; +import {AddressUtils} from "../messages/v2/AddressUtils.sol"; +import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol"; +import {IMessageHandlerV2} from "../interfaces/v2/IMessageHandlerV2.sol"; +import {FINALITY_THRESHOLD_FINALIZED} from "./FinalityThresholds.sol"; + +/** + * @title MessageTransmitterV2 + * @notice Contract responsible for sending and receiving messages across chains. + */ +contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { + // ============ Events ============ + /** + * @notice Emitted when a new message is dispatched + * @param message Raw bytes of message + */ + event MessageSent(bytes message); + + /** + * @notice Emitted when a new message is received + * @param caller Caller (msg.sender) on destination domain + * @param sourceDomain The source domain this message originated from + * @param nonce The nonce unique to this message + * @param sender The sender of this message + * @param finalityThresholdExecuted The finality at which message was attested to + * @param messageBody message body bytes + */ + event MessageReceived( + address indexed caller, + uint32 sourceDomain, + bytes32 indexed nonce, + bytes32 sender, + uint32 indexed finalityThresholdExecuted, + bytes messageBody + ); + + // ============ Libraries ============ + using AddressUtils for address; + using AddressUtils for address payable; + using AddressUtils for bytes32; + using MessageV2 for bytes29; + using TypedMemView for bytes; + using TypedMemView for bytes29; + + // ============ Constructor ============ + /** + * @param _localDomain Domain of chain on which the contract is deployed + * @param _version Message Format version + */ + constructor( + uint32 _localDomain, + uint32 _version + ) BaseMessageTransmitter(_localDomain, _version) { + _disableInitializers(); + } + + // ============ Initializers ============ + /** + * @notice Initializes the contract + * @dev Owner, pauser, rescuer, attesterManager, and attesters must be non-zero. + * @dev Signature threshold must be non-zero, but not exceed the number of enabled attesters + * @param owner_ Owner address + * @param pauser_ Pauser address + * @param rescuer_ Rescuer address + * @param attesterManager_ AttesterManager address + * @param attesters_ Set of attesters to enable + * @param signatureThreshold_ Signature threshold + * @param maxMessageBodySize_ Maximum message body size + */ + function initialize( + address owner_, + address pauser_, + address rescuer_, + address attesterManager_, + address[] calldata attesters_, + uint256 signatureThreshold_, + uint256 maxMessageBodySize_ + ) external initializer { + require(owner_ != address(0), "Owner is the zero address"); + require( + attesterManager_ != address(0), + "AttesterManager is the zero address" + ); + require( + signatureThreshold_ <= attesters_.length, + "Signature threshold exceeds attesters" + ); + require(maxMessageBodySize_ > 0, "MaxMessageBodySize is zero"); + + // Roles + _transferOwnership(owner_); + _updateRescuer(rescuer_); + _updatePauser(pauser_); + _setAttesterManager(attesterManager_); + + // Max message body size + _setMaxMessageBodySize(maxMessageBodySize_); + + // Attester configuration + uint256 _attestersLength = attesters_.length; + for (uint256 i; i < _attestersLength; ++i) { + _enableAttester(attesters_[i]); + } + + // Signature threshold + _setSignatureThreshold(signatureThreshold_); + + // Claim 0-nonce + usedNonces[bytes32(0)] = NONCE_USED; + } + + // ============ External Functions ============ + /** + * @notice Send the message to the destination domain and recipient + * @dev Formats the message, and emits a `MessageSent` event with message information. + * @param destinationDomain Domain of destination chain + * @param recipient Address of message recipient on destination chain as bytes32 + * @param destinationCaller Caller on the destination domain, as bytes32 + * @param minFinalityThreshold The minimum finality at which the message should be attested to + * @param messageBody Contents of the message (bytes) + */ + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes32 destinationCaller, + uint32 minFinalityThreshold, + bytes calldata messageBody + ) external override whenNotPaused { + require(destinationDomain != localDomain, "Domain is local domain"); + // Validate message body length + require( + messageBody.length <= maxMessageBodySize, + "Message body exceeds max size" + ); + require(recipient != bytes32(0), "Recipient must be nonzero"); + + bytes32 _messageSender = msg.sender.toBytes32(); + + // serialize message + bytes memory _message = MessageV2._formatMessageForRelay( + version, + localDomain, + destinationDomain, + _messageSender, + recipient, + destinationCaller, + minFinalityThreshold, + messageBody + ); + + // Emit MessageSent event + emit MessageSent(_message); + } + + /** + * @notice Receive a message. Messages can only be broadcast once for a given nonce. + * The message body of a valid message is passed to the specified recipient for further processing. + * + * @dev Attestation format: + * A valid attestation is the concatenated 65-byte signature(s) of exactly + * `thresholdSignature` signatures, in increasing order of attester address. + * ***If the attester addresses recovered from signatures are not in + * increasing order, signature verification will fail.*** + * If incorrect number of signatures or duplicate signatures are supplied, + * signature verification will fail. + * + * Message Format: + * + * Field Bytes Type Index + * version 4 uint32 0 + * sourceDomain 4 uint32 4 + * destinationDomain 4 uint32 8 + * nonce 32 bytes32 12 + * sender 32 bytes32 44 + * recipient 32 bytes32 76 + * destinationCaller 32 bytes32 108 + * minFinalityThreshold 4 uint32 140 + * finalityThresholdExecuted 4 uint32 144 + * messageBody dynamic bytes 148 + * @param message Message bytes + * @param attestation Concatenated 65-byte signature(s) of `message`, in increasing order + * of the attester address recovered from signatures. + * @return success True, if successful; false, if not + */ + function receiveMessage( + bytes calldata message, + bytes calldata attestation + ) external override whenNotPaused returns (bool success) { + // Validate message + ( + bytes32 _nonce, + uint32 _sourceDomain, + bytes32 _sender, + address _recipient, + uint32 _finalityThresholdExecuted, + bytes memory _messageBody + ) = _validateReceivedMessage(message, attestation); + + // Mark nonce as used + usedNonces[_nonce] = NONCE_USED; + + // Handle receive message + if (_finalityThresholdExecuted < FINALITY_THRESHOLD_FINALIZED) { + require( + IMessageHandlerV2(_recipient).handleReceiveUnfinalizedMessage( + _sourceDomain, + _sender, + _finalityThresholdExecuted, + _messageBody + ), + "handleReceiveUnfinalizedMessage() failed" + ); + } else { + require( + IMessageHandlerV2(_recipient).handleReceiveFinalizedMessage( + _sourceDomain, + _sender, + _finalityThresholdExecuted, + _messageBody + ), + "handleReceiveFinalizedMessage() failed" + ); + } + + // Emit MessageReceived event + emit MessageReceived( + msg.sender, + _sourceDomain, + _nonce, + _sender, + _finalityThresholdExecuted, + _messageBody + ); + + return true; + } + + /** + * @notice Validates a received message, including the attestation signatures as well + * as the message contents. + * @param _message Message bytes + * @param _attestation Concatenated 65-byte signature(s) of `message` + * @return _nonce Message nonce, as bytes32 + * @return _sourceDomain Domain where message originated from + * @return _sender Sender of the message + * @return _recipient Recipient of the message + * @return _finalityThresholdExecuted The level of finality at which the message was attested to + * @return _messageBody The message body bytes + */ + function _validateReceivedMessage( + bytes calldata _message, + bytes calldata _attestation + ) + internal + view + returns ( + bytes32 _nonce, + uint32 _sourceDomain, + bytes32 _sender, + address _recipient, + uint32 _finalityThresholdExecuted, + bytes memory _messageBody + ) + { + // Validate each signature in the attestation + _verifyAttestationSignatures(_message, _attestation); + + bytes29 _msg = _message.ref(0); + + // Validate message format + _msg._validateMessageFormat(); + + // Validate domain + require( + _msg._getDestinationDomain() == localDomain, + "Invalid destination domain" + ); + + // Validate destination caller + if (_msg._getDestinationCaller() != bytes32(0)) { + require( + _msg._getDestinationCaller() == msg.sender.toBytes32(), + "Invalid caller for message" + ); + } + + // Validate version + require(_msg._getVersion() == version, "Invalid message version"); + + // Validate nonce is available + _nonce = _msg._getNonce(); + require(usedNonces[_nonce] == 0, "Nonce already used"); + + // Unpack remaining values + _sourceDomain = _msg._getSourceDomain(); + _sender = _msg._getSender(); + _recipient = _msg._getRecipient().toAddress(); + _finalityThresholdExecuted = _msg._getFinalityThresholdExecuted(); + _messageBody = _msg._getMessageBody().clone(); + } +} diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol new file mode 100644 index 0000000..cf8abb2 --- /dev/null +++ b/src/v2/TokenMessengerV2.sol @@ -0,0 +1,431 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {BaseTokenMessenger} from "./BaseTokenMessenger.sol"; +import {ITokenMinterV2} from "../interfaces/v2/ITokenMinterV2.sol"; +import {AddressUtils} from "../messages/v2/AddressUtils.sol"; +import {IRelayerV2} from "../interfaces/v2/IRelayerV2.sol"; +import {IMessageHandlerV2} from "../interfaces/v2/IMessageHandlerV2.sol"; +import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol"; +import {BurnMessageV2} from "../messages/v2/BurnMessageV2.sol"; +import {TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD} from "./FinalityThresholds.sol"; + +/** + * @title TokenMessengerV2 + * @notice Sends and receives messages to/from MessageTransmitters + * and to/from TokenMinters. + */ +contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { + // ============ Events ============ + /** + * @notice Emitted when a DepositForBurn message is sent + * @param burnToken address of token burnt on source domain + * @param amount deposit amount + * @param depositor address where deposit is transferred from + * @param mintRecipient address receiving minted tokens on destination domain as bytes32 + * @param destinationDomain destination domain + * @param destinationTokenMessenger address of TokenMessenger on destination domain as bytes32 + * @param destinationCaller authorized caller as bytes32 of receiveMessage() on destination domain. + * If equal to bytes32(0), any address can broadcast the message. + * @param maxFee maximum fee to pay on destination domain, in units of burnToken + * @param minFinalityThreshold the minimum finality at which the message should be attested to. + * @param hookData optional hook for execution on destination domain + */ + event DepositForBurn( + address indexed burnToken, + uint256 amount, + address indexed depositor, + bytes32 mintRecipient, + uint32 destinationDomain, + bytes32 destinationTokenMessenger, + bytes32 destinationCaller, + uint256 maxFee, + uint32 indexed minFinalityThreshold, + bytes hookData + ); + + // ============ Libraries ============ + using AddressUtils for address; + using AddressUtils for address payable; + using AddressUtils for bytes32; + using BurnMessageV2 for bytes29; + using TypedMemView for bytes; + using TypedMemView for bytes29; + + // ============ Constructor ============ + /** + * @param _messageTransmitter Message transmitter address + * @param _messageBodyVersion Message body version + */ + constructor( + address _messageTransmitter, + uint32 _messageBodyVersion + ) BaseTokenMessenger(_messageTransmitter, _messageBodyVersion) { + _disableInitializers(); + } + + // ============ Initializers ============ + /** + * @notice Initializes the contract + * @dev Reverts if `owner_` is the zero address + * @dev Reverts if `rescuer_` is the zero address + * @dev Reverts if `feeRecipient_` is the zero address + * @dev Reverts if `denylister_` is the zero address + * @dev Reverts if `tokenMinter_` is the zero address + * @dev Reverts if `remoteDomains_` and `remoteTokenMessengers_` are unequal length + * @dev Each remoteTokenMessenger address must correspond to the remote domain at the same + * index in respective arrays. + * @dev Reverts if any `remoteTokenMessengers_` entry equals bytes32(0) + * @param owner_ Owner address + * @param rescuer_ Rescuer address + * @param feeRecipient_ FeeRecipient address + * @param denylister_ Denylister address + * @param tokenMinter_ Local token minter address + * @param remoteDomains_ Array of remote domains to configure + * @param remoteTokenMessengers_ Array of remote token messenger addresses + */ + function initialize( + address owner_, + address rescuer_, + address feeRecipient_, + address denylister_, + address tokenMinter_, + uint32[] calldata remoteDomains_, + bytes32[] calldata remoteTokenMessengers_ + ) external initializer { + require(owner_ != address(0), "Owner is the zero address"); + require( + remoteDomains_.length == remoteTokenMessengers_.length, + "Invalid remote domain configuration" + ); + + // Roles + _transferOwnership(owner_); + _updateRescuer(rescuer_); + _updateDenylister(denylister_); + _setFeeRecipient(feeRecipient_); + + // Local minter configuration + _setLocalMinter(tokenMinter_); + + // Remote token messenger configuration + uint256 _remoteDomainsLength = remoteDomains_.length; + for (uint256 i; i < _remoteDomainsLength; ++i) { + _addRemoteTokenMessenger( + remoteDomains_[i], + remoteTokenMessengers_[i] + ); + } + } + + // ============ External Functions ============ + /** + * @notice Deposits and burns tokens from sender to be minted on destination domain. + * Emits a `DepositForBurn` event. + * @dev reverts if: + * - given burnToken is not supported + * - given destinationDomain has no TokenMessenger registered + * - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance + * to this contract is less than `amount`. + * - burn() reverts. For example, if `amount` is 0. + * - maxFee is greater than or equal to `amount`. + * - MessageTransmitterV2#sendMessage reverts. + * @param amount amount of tokens to burn + * @param destinationDomain destination domain to receive message on + * @param mintRecipient address of mint recipient on destination domain + * @param burnToken token to burn `amount` of, on local domain + * @param destinationCaller authorized caller on the destination domain, as bytes32. If equal to bytes32(0), + * any address can broadcast the message. + * @param maxFee maximum fee to pay on the destination domain, specified in units of burnToken + * @param minFinalityThreshold the minimum finality at which a burn message will be attested to. + */ + function depositForBurn( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller, + uint256 maxFee, + uint32 minFinalityThreshold + ) external notDenylistedCallers { + bytes calldata _emptyHookData = msg.data[0:0]; + _depositForBurn( + amount, + destinationDomain, + mintRecipient, + burnToken, + destinationCaller, + maxFee, + minFinalityThreshold, + _emptyHookData + ); + } + + /** + * @notice Deposits and burns tokens from sender to be minted on destination domain. + * Emits a `DepositForBurn` event. + * @dev reverts if: + * - `hookData` is zero-length + * - `burnToken` is not supported + * - `destinationDomain` has no TokenMessenger registered + * - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance + * to this contract is less than `amount`. + * - burn() reverts. For example, if `amount` is 0. + * - maxFee is greater than or equal to `amount`. + * - MessageTransmitterV2#sendMessage reverts. + * @param amount amount of tokens to burn + * @param destinationDomain destination domain to receive message on + * @param mintRecipient address of mint recipient on destination domain, as bytes32 + * @param burnToken token to burn `amount` of, on local domain + * @param destinationCaller authorized caller on the destination domain, as bytes32. If equal to bytes32(0), + * any address can broadcast the message. + * @param maxFee maximum fee to pay on the destination domain, specified in units of burnToken + * @param hookData hook data to append to burn message for interpretation on destination domain + */ + function depositForBurnWithHook( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller, + uint256 maxFee, + uint32 minFinalityThreshold, + bytes calldata hookData + ) external notDenylistedCallers { + require(hookData.length > 0, "Hook data is empty"); + + _depositForBurn( + amount, + destinationDomain, + mintRecipient, + burnToken, + destinationCaller, + maxFee, + minFinalityThreshold, + hookData + ); + } + + /** + * @notice Handles an incoming finalized message received by the local MessageTransmitter, + * and takes the appropriate action. For a burn message, mints the + * associated token to the requested recipient on the local domain. + * @dev Validates the local sender is the local MessageTransmitter, and the + * remote sender is a registered remote TokenMessenger for `remoteDomain`. + * @param remoteDomain The domain where the message originated from. + * @param sender The sender of the message (remote TokenMessenger). + * @param messageBody The message body bytes. + * @return success Bool, true if successful. + */ + function handleReceiveFinalizedMessage( + uint32 remoteDomain, + bytes32 sender, + uint32, + bytes calldata messageBody + ) + external + override + onlyLocalMessageTransmitter + onlyRemoteTokenMessenger(remoteDomain, sender) + returns (bool) + { + return _handleReceiveMessage(messageBody.ref(0), remoteDomain); + } + + /** + * @notice Handles an incoming unfinalized message received by the local MessageTransmitter, + * and takes the appropriate action. For a burn message, mints the + * associated token to the requested recipient on the local domain, less fees. + * Fees are separately minted to the currently set `feeRecipient` address. + * @dev Validates the local sender is the local MessageTransmitter, and the + * remote sender is a registered remote TokenMessenger for `remoteDomain`. + * @dev Validates that `finalityThresholdExecuted` is at least 500. + * @param remoteDomain The domain where the message originated from. + * @param sender The sender of the message (remote TokenMessenger). + * @param finalityThresholdExecuted The level of finality at which the message was attested to + * @param messageBody The message body bytes. + * @return success Bool, true if successful. + */ + function handleReceiveUnfinalizedMessage( + uint32 remoteDomain, + bytes32 sender, + uint32 finalityThresholdExecuted, + bytes calldata messageBody + ) + external + override + onlyLocalMessageTransmitter + onlyRemoteTokenMessenger(remoteDomain, sender) + returns (bool) + { + require( + finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD, + "Unsupported finality threshold" + ); + + return _handleReceiveMessage(messageBody.ref(0), remoteDomain); + } + + // ============ Internal Utils ============ + /** + * @notice Deposits and burns tokens from sender to be minted on destination domain. + * Emits a `DepositForBurn` event. + * @param _amount amount of tokens to burn (must be non-zero) + * @param _destinationDomain destination domain + * @param _mintRecipient address of mint recipient on destination domain + * @param _burnToken address of the token burned on the source chain + * @param _destinationCaller caller on the destination domain, as bytes32 + * @param _maxFee maximum fee to pay on destination chain + * @param _hookData optional hook data for interpretation on destination chain + */ + function _depositForBurn( + uint256 _amount, + uint32 _destinationDomain, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold, + bytes calldata _hookData + ) internal { + require(_amount > 0, "Amount must be nonzero"); + require(_mintRecipient != bytes32(0), "Mint recipient must be nonzero"); + require(_maxFee < _amount, "Max fee must be less than amount"); + + bytes32 _destinationTokenMessenger = _getRemoteTokenMessenger( + _destinationDomain + ); + + // Deposit and burn tokens + _depositAndBurn(_burnToken, msg.sender, _amount); + + // Format message body + bytes memory _burnMessage = BurnMessageV2._formatMessageForRelay( + messageBodyVersion, + _burnToken.toBytes32(), + _mintRecipient, + _amount, + msg.sender.toBytes32(), + _maxFee, + _hookData + ); + + // Send message + IRelayerV2(localMessageTransmitter).sendMessage( + _destinationDomain, + _destinationTokenMessenger, + _destinationCaller, + _minFinalityThreshold, + _burnMessage + ); + + emit DepositForBurn( + _burnToken, + _amount, + msg.sender, + _mintRecipient, + _destinationDomain, + _destinationTokenMessenger, + _destinationCaller, + _maxFee, + _minFinalityThreshold, + _hookData + ); + } + + /** + * @notice Validates a received message and mints the token to the mintRecipient, less fees. + * @dev Reverts if _validatedReceivedMessage fails to validate the message. + * @dev Reverts if the mint operation fails. + * @param _msg Received message + * @param _remoteDomain The domain where the message originated from + * @return success Bool, true if successful. + */ + function _handleReceiveMessage( + bytes29 _msg, + uint32 _remoteDomain + ) internal returns (bool) { + // Validate message and unpack fields + ( + address _mintRecipient, + bytes32 _burnToken, + uint256 _amount, + uint256 _fee + ) = _validatedReceivedMessage(_msg); + + // Mint tokens + _mintAndWithdraw( + _remoteDomain, + _burnToken, + _mintRecipient, + _amount - _fee, + _fee + ); + + return true; + } + + /** + * @notice Validates a BurnMessage and unpacks relevant fields. + * @dev Reverts if the BurnMessage is malformed + * @dev Reverts if the BurnMessage version isn't supported + * @dev Reverts if the BurnMessage has expired + * @dev Reverts if the fee equals or exceeds the amount + * @dev Reverts if the fee exceeds the max fee specified on the source chain + * @param _msg Finalized message + * @return _mintRecipient The recipient of the mint, as bytes32 + * @return _burnToken The address of the token burned on the source chain + * @return _amount The amount of burnToken burned + * @return _fee The fee executed + */ + function _validatedReceivedMessage( + bytes29 _msg + ) + internal + view + returns ( + address _mintRecipient, + bytes32 _burnToken, + uint256 _amount, + uint256 _fee + ) + { + _msg._validateBurnMessageFormat(); + require( + _msg._getVersion() == messageBodyVersion, + "Invalid message body version" + ); + + // Enforce message expiration + uint256 _expirationBlock = _msg._getExpirationBlock(); + require( + _expirationBlock == 0 || _expirationBlock > block.number, + "Message expired and must be re-signed" + ); + + // Validate fee + _amount = _msg._getAmount(); + _fee = _msg._getFeeExecuted(); + require(_fee == 0 || _fee < _amount, "Fee equals or exceeds amount"); + require(_fee <= _msg._getMaxFee(), "Fee exceeds max fee"); + + _mintRecipient = _msg._getMintRecipient().toAddress(); + _burnToken = _msg._getBurnToken(); + } +} diff --git a/src/v2/TokenMinterV2.sol b/src/v2/TokenMinterV2.sol new file mode 100644 index 0000000..8ce93f5 --- /dev/null +++ b/src/v2/TokenMinterV2.sol @@ -0,0 +1,84 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {TokenMinter} from "../TokenMinter.sol"; +import {IMintBurnToken} from "../interfaces/IMintBurnToken.sol"; +import {ITokenMinterV2} from "../interfaces/v2/ITokenMinterV2.sol"; + +/** + * @title TokenMinterV2 + * @notice Token Minter and Burner + * @dev Maintains registry of local mintable tokens and corresponding tokens on remote domains. + * This registry can be used by caller to determine which token on local domain to mint for a + * burned token on a remote domain, and vice versa. + * It is assumed that local and remote tokens are fungible at a constant 1:1 exchange rate. + */ +contract TokenMinterV2 is ITokenMinterV2, TokenMinter { + // ============ Constructor ============ + /** + * @param _tokenController Token controller address + */ + constructor(address _tokenController) TokenMinter(_tokenController) {} + + // ============ External Functions ============ + /** + * @notice Mints to multiple recipients amounts of local tokens corresponding to the + * given (`sourceDomain`, `burnToken`) pair. + * @dev reverts if the (`sourceDomain`, `burnToken`) pair does not + * map to a nonzero local token address. This mapping can be queried using + * getLocalToken(). + * @param sourceDomain Source domain where `burnToken` was burned. + * @param burnToken Burned token address as bytes32. + * @param recipientOne Address to receive `amountOne` of minted tokens + * @param recipientTwo Address to receive `amountTwo` of minted tokens + * @param amountOne Amount of tokens to mint to `recipientOne` + * @param amountTwo Amount of tokens to mint to `recipientTwo` + * @return mintToken token minted. + */ + function mint( + uint32 sourceDomain, + bytes32 burnToken, + address recipientOne, + address recipientTwo, + uint256 amountOne, + uint256 amountTwo + ) + external + override + whenNotPaused + onlyLocalTokenMessenger + returns (address) + { + address _mintToken = _getLocalToken(sourceDomain, burnToken); + require(_mintToken != address(0), "Mint token not supported"); + IMintBurnToken _token = IMintBurnToken(_mintToken); + + require( + _token.mint(recipientOne, amountOne), + "First mint operation failed" + ); + + require( + _token.mint(recipientTwo, amountTwo), + "Second mint operation failed" + ); + + return _mintToken; + } +} diff --git a/test/MessageTransmitter.t.sol b/test/MessageTransmitter.t.sol index 61a7ae7..0ee735c 100644 --- a/test/MessageTransmitter.t.sol +++ b/test/MessageTransmitter.t.sol @@ -14,6 +14,7 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import "@openzeppelin/contracts/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/math/Math.sol"; @@ -94,7 +95,7 @@ contract MessageTransmitterTest is Test, TestUtils { function testSendMessage_rejectsTooLargeMessage() public { bytes32 _recipient = bytes32(uint256(uint160(vm.addr(1505)))); - bytes memory _messageBody = new bytes(8 * 2**10 + 1); + bytes memory _messageBody = new bytes(8 * 2 ** 10 + 1); vm.expectRevert("Message body exceeds max size"); srcMessageTransmitter.sendMessage( destinationDomain, @@ -430,9 +431,9 @@ contract MessageTransmitterTest is Test, TestUtils { ); } - function testReplaceMessage_succeeds(address _newDestinationCallerAddr) - public - { + function testReplaceMessage_succeeds( + address _newDestinationCallerAddr + ) public { bytes memory _originalMessage = _getMessage(); bytes memory _expectedMessage = _replaceMessage( _originalMessage, @@ -488,10 +489,10 @@ contract MessageTransmitterTest is Test, TestUtils { ); // assert that a MessageSent event was logged with expected message bytes + vm.prank(Message.bytes32ToAddress(sender)); vm.expectEmit(true, true, true, true); emit MessageSent(_expectedMessage); - vm.prank(Message.bytes32ToAddress(sender)); srcMessageTransmitter.replaceMessage( _originalMessage, _signature, @@ -843,7 +844,7 @@ contract MessageTransmitterTest is Test, TestUtils { } function testSetMaxMessageBodySize() public { - uint32 _newMaxMessageBodySize = 10000000; + uint32 _newMaxMessageBodySize = maxMessageBodySize + 1; MessageTransmitter _messageTransmitter = new MessageTransmitter( destinationDomain, @@ -884,22 +885,25 @@ contract MessageTransmitterTest is Test, TestUtils { function testRescuable( address _rescuer, address _rescueRecipient, - uint256 _amount + uint256 _amount, + address _nonRescuer ) public { assertContractIsRescuable( address(srcMessageTransmitter), _rescuer, _rescueRecipient, - _amount + _amount, + _nonRescuer ); } - function testPausable(address _newPauser) public { + function testPausable(address _newPauser, address _nonOwner) public { assertContractIsPausable( address(srcMessageTransmitter), pauser, _newPauser, - srcMessageTransmitter.owner() + srcMessageTransmitter.owner(), + _nonOwner ); } @@ -929,11 +933,10 @@ contract MessageTransmitterTest is Test, TestUtils { destination * @return Returns hash of source and nonce */ - function _hashSourceAndNonce(uint32 _source, uint64 _nonce) - internal - pure - returns (bytes32) - { + function _hashSourceAndNonce( + uint32 _source, + uint64 _nonce + ) internal pure returns (bytes32) { return keccak256(abi.encodePacked(_source, _nonce)); } @@ -1002,10 +1005,10 @@ contract MessageTransmitterTest is Test, TestUtils { ); // assert that a MessageSent event was logged with expected message bytes + vm.prank(Message.bytes32ToAddress(_sender)); vm.expectEmit(true, true, true, true); emit MessageSent(_expectedMessage); - vm.prank(Message.bytes32ToAddress(_sender)); uint64 _nonceReserved = srcMessageTransmitter.sendMessageWithCaller( _destinationDomain, _recipient, @@ -1165,10 +1168,9 @@ contract MessageTransmitterTest is Test, TestUtils { destMessageTransmitter.setSignatureThreshold(2); } - function _sign2OfNMultisigMessage(bytes memory _message) - internal - returns (bytes memory _signature) - { + function _sign2OfNMultisigMessage( + bytes memory _message + ) internal returns (bytes memory _signature) { uint256[] memory attesterPrivateKeys = new uint256[](2); // manually sort attesters in correct order attesterPrivateKeys[1] = attesterPK; @@ -1206,9 +1208,9 @@ contract MessageTransmitterTest is Test, TestUtils { ); // assert that a MessageSent event was logged with expected message bytes + vm.prank(Message.bytes32ToAddress(sender)); vm.expectEmit(true, true, true, true); emit MessageSent(_expectedMessage); - vm.prank(Message.bytes32ToAddress(sender)); srcMessageTransmitter.replaceMessage( _originalMessage, _signature, diff --git a/test/TestUtils.sol b/test/TestUtils.sol index 7e9fddb..0d618a1 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -14,10 +14,12 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import "../src/TokenMinter.sol"; import "../lib/forge-std/src/Test.sol"; import "./mocks/MockMintBurnToken.sol"; +import {Denylistable} from "../src/roles/v2/Denylistable.sol"; contract TestUtils is Test { /** @@ -50,6 +52,8 @@ contract TestUtils is Test { event PauserChanged(address indexed newAddress); + event RescuerChanged(address indexed newRescuer); + // test keys uint256 attesterPK = 1; uint256 fakeAttesterPK = 2; @@ -80,8 +84,13 @@ contract TestUtils is Test { address owner = vm.addr(1902); address arbitraryAddress = vm.addr(1903); + // See: https://github.com/foundry-rs/foundry/blob/2cdbfaca634b284084d0f86357623aef7a0d2ce3/crates/evm/core/src/constants.rs#L9 + // This address may be passed into fuzz tests by Foundry. VM.mockCalls fail when + // specifying the cheat code address as the target. + address foundryCheatCodeAddr = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; + // 8 KiB - uint32 maxMessageBodySize = 8 * 2**10; + uint32 maxMessageBodySize = 8 * 2 ** 10; // zero signature bytes zeroSignature = "00000000000000000000000000000000000000000000000000000000000000000"; @@ -136,6 +145,11 @@ contract TestUtils is Test { vm.expectRevert("Ownable: caller is not the owner"); } + function expectRevertWithWrongOwner(address wrongOwner) public { + vm.prank(wrongOwner); + vm.expectRevert("Ownable: caller is not the owner"); + } + function expectRevertWithWrongTokenController() public { vm.prank(arbitraryAddress); vm.expectRevert("Caller is not tokenController"); @@ -145,10 +159,17 @@ contract TestUtils is Test { address _rescuableContractAddress, address _rescuer, address _rescueRecipient, - uint256 _amount + uint256 _amount, + address _nonRescuer ) public { - // Send erc20 to _rescuableContractAddress Rescuable _rescuableContract = Rescuable(_rescuableContractAddress); + + vm.assume(_rescuer != address(0)); + vm.assume(_rescueRecipient != address(0)); + vm.assume(_rescuer != _nonRescuer); + vm.assume(_nonRescuer != _rescuableContract.owner()); + + // Send erc20 to _rescuableContractAddress MockMintBurnToken _mockMintBurnToken = new MockMintBurnToken(); // _rescueRecipient's initial balance of _mockMintBurnToken is 0 @@ -165,10 +186,19 @@ contract TestUtils is Test { _amount ); + // Test updating the rescuer // (Updating rescuer to zero-address is not permitted) - if (_rescuer != address(0)) { - _rescuableContract.updateRescuer(_rescuer); - } + vm.prank(_rescuableContract.owner()); + vm.expectRevert("Rescuable: new rescuer is the zero address"); + _rescuableContract.updateRescuer(address(0)); + + assertTrue(_rescuer != address(0)); + + // Update rescuer to a valid address + vm.expectEmit(true, true, true, true); + emit RescuerChanged(_rescuer); + vm.prank(_rescuableContract.owner()); + _rescuableContract.updateRescuer(_rescuer); // Rescue erc20 to _rescueRecipient vm.prank(_rescuer); @@ -180,19 +210,40 @@ contract TestUtils is Test { // Assert funds are rescued assertEq(_mockMintBurnToken.balanceOf(_rescueRecipient), _amount); + + // Check that non-rescuer address cannot rescue funds + assertTrue(_rescuableContract.rescuer() != _nonRescuer); + vm.prank(_nonRescuer); + vm.expectRevert("Rescuable: caller is not the rescuer"); + _rescuableContract.rescueERC20( + _mockMintBurnToken, + _rescueRecipient, + _amount + ); + + // Check that non-owner cannot update rescuer + vm.prank(_nonRescuer); + vm.expectRevert("Ownable: caller is not the owner"); + _rescuableContract.updateRescuer(_nonRescuer); + vm.stopPrank(); } function assertContractIsPausable( address _pausableContractAddress, address _currentPauser, address _newPauser, - address _owner + address _owner, + address _nonOwner ) public { vm.assume(_newPauser != address(0)); + vm.assume(_owner != _nonOwner); + vm.assume(_currentPauser != _newPauser); + Pausable _pausableContract = Pausable(_pausableContractAddress); assertEq(_pausableContract.pauser(), _currentPauser); assertFalse(_pausableContract.paused()); + // Check that the current pauser can pause / unpause vm.startPrank(_currentPauser); vm.expectEmit(true, true, true, true); @@ -207,6 +258,26 @@ contract TestUtils is Test { vm.stopPrank(); + // Check that a non-pauser cannot pause / unpause + assertTrue(_newPauser != _currentPauser); + vm.startPrank(_newPauser); + + vm.expectRevert("Pausable: caller is not the pauser"); + _pausableContract.pause(); + + vm.expectRevert("Pausable: caller is not the pauser"); + _pausableContract.unpause(); + + vm.stopPrank(); + + // Check that a non-owner cannot rotate the pauser + assertTrue(_nonOwner != _owner); + vm.prank(_nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + _pausableContract.updatePauser(_newPauser); + vm.stopPrank(); + + // Check that owner can rotate pauser, and it emits an event vm.expectEmit(true, true, true, true); emit PauserChanged(_newPauser); vm.prank(_owner); @@ -215,18 +286,146 @@ contract TestUtils is Test { assertEq(_pausableContract.pauser(), _newPauser); } + function assertContractIsDenylistable( + address _denylistableContract, + address _randomAddress, + address _newDenylister, + address _nonOwner + ) public { + Denylistable _denylistable = Denylistable(_denylistableContract); + address _owner = _denylistable.owner(); + + vm.assume(_owner != _nonOwner); + vm.assume(_newDenylister != address(0)); + vm.assume(_newDenylister != _randomAddress); + + // Test rotating denylister + // Check only the owner can update the denylister + vm.prank(_nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + _denylistable.updateDenylister(_newDenylister); + + // Rotate denylister + vm.prank(_owner); + _denylistable.updateDenylister(_newDenylister); + assertEq(_denylistable.denylister(), _newDenylister); + + // Check adding and removing an address + // First check that other addresses cannot add to the denylist + assertTrue(_denylistable.denylister() != _randomAddress); + vm.prank(_randomAddress); + vm.expectRevert("Denylistable: caller is not denylister"); + _denylistable.denylist(_owner); + + // Now add + assertFalse(_denylistable.isDenylisted(_randomAddress)); + vm.prank(_newDenylister); + _denylistable.denylist(_randomAddress); + assertTrue(_denylistable.isDenylisted(_randomAddress)); + + // Now try to remove from the denylist, but as a different caller + vm.prank(_randomAddress); + vm.expectRevert("Denylistable: caller is not denylister"); + _denylistable.unDenylist(_randomAddress); + + // Now, actually remove + vm.prank(_newDenylister); + _denylistable.unDenylist(_randomAddress); + assertFalse(_denylistable.isDenylisted(_randomAddress)); + } + + function transferOwnershipFailsIfNotOwner( + address _ownableContractAddress, + address _notOwner, + address _newOwner + ) public { + Ownable2Step _ownableContract = Ownable2Step(_ownableContractAddress); + address _initialOwner = _ownableContract.owner(); + expectRevertWithWrongOwner(_notOwner); + _ownableContract.transferOwnership(_newOwner); + + // Sanity check + assertEq(_initialOwner, _ownableContract.owner()); + } + + function acceptOwnershipFailsIfNotPendingOwner( + address _ownableContractAddress, + address _newOwner, + address _otherAccount + ) public { + Ownable2Step _ownableContract = Ownable2Step(_ownableContractAddress); + address _initialOwner = _ownableContract.owner(); + _ownableContract.transferOwnership(_newOwner); + assertEq(_ownableContract.pendingOwner(), _newOwner); + + vm.prank(_otherAccount); + vm.expectRevert("Ownable2Step: caller is not the new owner"); + _ownableContract.acceptOwnership(); + + // Sanity check + assertEq(_initialOwner, _ownableContract.owner()); + assertEq(_newOwner, _ownableContract.pendingOwner()); + } + + function transferOwnership_revertsFromNonOwner( + address _ownableContractAddress, + address _newOwner, + address _nonOwner + ) public { + Ownable2Step _ownableContract = Ownable2Step(_ownableContractAddress); + address initialOwner = _ownableContract.owner(); + vm.assume(initialOwner != _nonOwner); + vm.assume(initialOwner != _newOwner); + vm.assume(_newOwner != _nonOwner); + + assertTrue(_nonOwner != initialOwner); + + // Test non-owner cannot transfer ownership + vm.prank(_nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + _ownableContract.transferOwnership(_newOwner); + vm.stopPrank(); + } + + function acceptOwnership_revertsFromNonPendingOwner( + address _ownableContractAddress, + address _newOwner, + address _nonOwner + ) public { + Ownable2Step _ownableContract = Ownable2Step(_ownableContractAddress); + address _initialOwner = _ownableContract.owner(); + vm.assume(_initialOwner != _nonOwner); + vm.assume(_initialOwner != _newOwner); + vm.assume(_newOwner != _nonOwner); + + // First, transfer ownership + vm.prank(_initialOwner); + _ownableContract.transferOwnership(_newOwner); + vm.stopPrank(); + assertEq(_ownableContract.owner(), _initialOwner); + assertEq(_ownableContract.pendingOwner(), _newOwner); + + // Test non-pending owner cannot acceptOwnership + vm.prank(_nonOwner); + vm.expectRevert("Ownable2Step: caller is not the new owner"); + _ownableContract.acceptOwnership(); + vm.stopPrank(); + } + function transferOwnershipAndAcceptOwnership( address _ownableContractAddress, address _newOwner ) public { Ownable2Step _ownableContract = Ownable2Step(_ownableContractAddress); address initialOwner = _ownableContract.owner(); + vm.assume(initialOwner != _newOwner); // assert that the owner is still unchanged assertEq(_ownableContract.owner(), initialOwner); // set pending owner vm.expectEmit(true, true, true, true); emit OwnershipTransferStarted(initialOwner, _newOwner); + vm.prank(initialOwner); _ownableContract.transferOwnership(_newOwner); // assert that the owner is still unchanged, but pending owner is changed assertEq(_ownableContract.owner(), initialOwner); @@ -264,6 +463,7 @@ contract TestUtils is Test { // set pending owner vm.expectEmit(true, true, true, true); emit OwnershipTransferStarted(initialOwner, _newOwner); + vm.prank(initialOwner); _ownableContract.transferOwnership(_newOwner); // assert that the owner is still unchanged, but pending owner is changed assertEq(_ownableContract.owner(), initialOwner); @@ -272,6 +472,7 @@ contract TestUtils is Test { // change the owner again, because we realize _newOwner cannot accept ownership vm.expectEmit(true, true, true, true); emit OwnershipTransferStarted(initialOwner, _secondNewOwner); + vm.prank(initialOwner); _ownableContract.transferOwnership(_secondNewOwner); // accept ownership @@ -284,19 +485,18 @@ contract TestUtils is Test { assertEq(_ownableContract.owner(), _secondNewOwner); } - function _signMessageWithAttesterPK(bytes memory _message) - internal - returns (bytes memory) - { + function _signMessageWithAttesterPK( + bytes memory _message + ) internal returns (bytes memory) { uint256[] memory attesterPrivateKeys = new uint256[](1); attesterPrivateKeys[0] = attesterPK; return _signMessage(_message, attesterPrivateKeys); } - function _signMessage(bytes memory _message, uint256[] memory _privKeys) - internal - returns (bytes memory) - { + function _signMessage( + bytes memory _message, + uint256[] memory _privKeys + ) internal returns (bytes memory) { bytes memory _signaturesConcatenated = ""; for (uint256 i = 0; i < _privKeys.length; i++) { diff --git a/test/TokenMessenger.t.sol b/test/TokenMessenger.t.sol index c7f5cc5..07c27fb 100644 --- a/test/TokenMessenger.t.sol +++ b/test/TokenMessenger.t.sol @@ -14,6 +14,7 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import "../lib/forge-std/src/Test.sol"; import "../src/TokenMessenger.sol"; @@ -247,9 +248,9 @@ contract TokenMessengerTest is Test, TestUtils { ); } - function testDepositForBurn_revertsIfMintRecipientIsZero(uint256 _amount) - public - { + function testDepositForBurn_revertsIfMintRecipientIsZero( + uint256 _amount + ) public { vm.assume(_amount != 0); vm.expectRevert("Mint recipient must be nonzero"); @@ -332,9 +333,9 @@ contract TokenMessengerTest is Test, TestUtils { ); } - function testDepositForBurn_revertsOnFailedTokenTransfer(uint256 _amount) - public - { + function testDepositForBurn_revertsOnFailedTokenTransfer( + uint256 _amount + ) public { vm.prank(owner); vm.mockCall( address(localToken), @@ -776,14 +777,17 @@ contract TokenMessengerTest is Test, TestUtils { assertEq(destToken.balanceOf(_mintRecipientAddr), 0); // test event is emitted + vm.startPrank(address(remoteMessageTransmitter)); + bytes32 _sender = Message.addressToBytes32( + address(localTokenMessenger) + ); vm.expectEmit(true, true, true, true); emit MintAndWithdraw(_mintRecipientAddr, _amount, address(destToken)); - vm.startPrank(address(remoteMessageTransmitter)); assertTrue( destTokenMessenger.handleReceiveMessage( localDomain, - Message.addressToBytes32(address(localTokenMessenger)), + _sender, _messageBody ) ); @@ -850,9 +854,9 @@ contract TokenMessengerTest is Test, TestUtils { vm.stopPrank(); } - function testHandleReceiveMessage_revertsOnInvalidMessage(uint256 _amount) - public - { + function testHandleReceiveMessage_revertsOnInvalidMessage( + uint256 _amount + ) public { vm.assume(_amount > 0); bytes32 _mintRecipient = Message.addressToBytes32(vm.addr(1505)); @@ -1013,9 +1017,9 @@ contract TokenMessengerTest is Test, TestUtils { localTokenMessenger.addLocalMinter(address(0)); } - function testAddLocalMinter_revertsIfAlreadySet(address _localMinter) - public - { + function testAddLocalMinter_revertsIfAlreadySet( + address _localMinter + ) public { vm.assume(_localMinter != address(0)); vm.expectRevert("Local minter is already set."); localTokenMessenger.addLocalMinter(_localMinter); @@ -1057,13 +1061,15 @@ contract TokenMessengerTest is Test, TestUtils { function testRescuable( address _rescuer, address _rescueRecipient, - uint256 _amount + uint256 _amount, + address _nonRescuer ) public { assertContractIsRescuable( address(localTokenMessenger), _rescuer, _rescueRecipient, - _amount + _amount, + _nonRescuer ); } @@ -1185,10 +1191,10 @@ contract TokenMessengerTest is Test, TestUtils { _mintAmount, _allowedBurnAmount ); - - vm.expectEmit(true, true, true, true); - emit MessageSent( - Message._formatMessage( + vm.startPrank(owner); + { + // Scoped to prevent stack too deep + bytes memory _message = Message._formatMessage( version, localDomain, remoteDomain, @@ -1197,8 +1203,10 @@ contract TokenMessengerTest is Test, TestUtils { remoteTokenMessenger, _destinationCaller, _messageBody - ) - ); + ); + vm.expectEmit(true, true, true, true); + emit MessageSent(_message); + } vm.expectEmit(true, true, true, true); emit DepositForBurn( @@ -1212,7 +1220,6 @@ contract TokenMessengerTest is Test, TestUtils { _destinationCaller ); - vm.prank(owner); uint64 _nonceReserved = localTokenMessenger.depositForBurnWithCaller( _amount, remoteDomain, @@ -1220,6 +1227,7 @@ contract TokenMessengerTest is Test, TestUtils { address(localToken), _destinationCaller ); + vm.stopPrank(); assertEq(uint256(_nonce), uint256(_nonceReserved)); diff --git a/test/TokenMinter.t.sol b/test/TokenMinter.t.sol index 8cafb62..6f24554 100644 --- a/test/TokenMinter.t.sol +++ b/test/TokenMinter.t.sol @@ -14,6 +14,7 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import "../src/messages/Message.sol"; import "../src/TokenMinter.sol"; @@ -76,7 +77,7 @@ contract TokenMinterTest is Test, TestUtils { address pauser = vm.addr(1509); function setUp() public { - tokenMinter = new TokenMinter(tokenController); + tokenMinter = TokenMinter(createTokenMinter()); localToken = new MockMintBurnToken(); localTokenAddress = address(localToken); remoteToken = new MockMintBurnToken(); @@ -85,7 +86,11 @@ contract TokenMinterTest is Test, TestUtils { tokenMinter.updatePauser(pauser); } - function testMint_succeeds(uint256 _amount, address _localToken) public { + function createTokenMinter() internal virtual returns (address) { + return address(new TokenMinter(tokenController)); + } + + function testMint_succeeds(uint256 _amount) public { _mint(_amount); } @@ -115,15 +120,14 @@ contract TokenMinterTest is Test, TestUtils { } function testMint_revertsWhenPaused( - address _mintToken, address _to, uint256 _amount, - bytes32 remoteToken + bytes32 _remoteToken ) public { vm.prank(pauser); tokenMinter.pause(); vm.expectRevert("Pausable: paused"); - tokenMinter.mint(sourceDomain, remoteToken, _to, _amount); + tokenMinter.mint(sourceDomain, _remoteToken, _to, _amount); // Mint works again after unpause vm.prank(pauser); @@ -131,9 +135,10 @@ contract TokenMinterTest is Test, TestUtils { _mint(_amount); } - function testMint_revertsOnFailedTokenMint(address _to, uint256 _amount) - public - { + function testMint_revertsOnFailedTokenMint( + address _to, + uint256 _amount + ) public { _linkTokenPair(localTokenAddress); vm.mockCall( localTokenAddress, @@ -148,7 +153,6 @@ contract TokenMinterTest is Test, TestUtils { function testBurn_succeeds( uint256 _amount, - address _localToken, uint256 _allowedBurnAmount ) public { vm.assume(_amount > 0); @@ -160,7 +164,7 @@ contract TokenMinterTest is Test, TestUtils { _allowedBurnAmount ); - _mintAndBurn(_amount, _localToken); + _mintAndBurn(_amount); } function testBurn_revertsOnUnsupportedBurnToken(uint256 _amount) public { @@ -197,7 +201,7 @@ contract TokenMinterTest is Test, TestUtils { // Mint works again after unpause vm.prank(pauser); tokenMinter.unpause(); - _mintAndBurn(_burnAmount, localTokenAddress); + _mintAndBurn(_burnAmount); } function testBurn_revertsWhenAmountExceedsNonZeroBurnLimit( @@ -302,7 +306,7 @@ contract TokenMinterTest is Test, TestUtils { _linkTokenPair(localTokenAddress); } - function testGetLocalToken_findsNoLocalToken() public { + function testGetLocalToken_findsNoLocalToken() public view { address _result = tokenMinter.getLocalToken( remoteDomain, remoteTokenBytes32 @@ -336,9 +340,9 @@ contract TokenMinterTest is Test, TestUtils { ); } - function testSetTokenController_succeeds(address newTokenController) - public - { + function testSetTokenController_succeeds( + address newTokenController + ) public { vm.assume(newTokenController != address(0)); assertEq(tokenMinter.tokenController(), tokenController); @@ -411,22 +415,25 @@ contract TokenMinterTest is Test, TestUtils { function testRescuable( address _rescuer, address _rescueRecipient, - uint256 _amount + uint256 _amount, + address _nonRescuer ) public { assertContractIsRescuable( address(tokenMinter), _rescuer, _rescueRecipient, - _amount + _amount, + _nonRescuer ); } - function testPausable(address _newPauser) public { + function testPausable(address _newPauser, address _nonOwner) public { assertContractIsPausable( address(tokenMinter), pauser, _newPauser, - tokenMinter.owner() + tokenMinter.owner(), + _nonOwner ); } @@ -477,7 +484,7 @@ contract TokenMinterTest is Test, TestUtils { assertEq(localToken.totalSupply(), _amount); } - function _mintAndBurn(uint256 _amount, address _localToken) internal { + function _mintAndBurn(uint256 _amount) internal { _mint(_amount); address mockTokenMessenger = vm.addr(1507); diff --git a/test/examples/CCTPHookWrapper.t.sol b/test/examples/CCTPHookWrapper.t.sol new file mode 100644 index 0000000..78aac7b --- /dev/null +++ b/test/examples/CCTPHookWrapper.t.sol @@ -0,0 +1,451 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {CCTPHookWrapper} from "../../src/examples/CCTPHookWrapper.sol"; +import {IReceiver} from "../../src/interfaces/v2/IReceiverV2.sol"; +import {MessageV2} from "../../src/messages/v2/MessageV2.sol"; +import {BurnMessageV2} from "../../src/messages/v2/BurnMessageV2.sol"; +import {MockHookTarget} from "../mocks/v2/MockHookTarget.sol"; +import {Test} from "forge-std/Test.sol"; +import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol"; + +contract CCTPHookWrapperTest is Test { + // Libraries + + using TypedMemView for bytes; + using TypedMemView for bytes29; + + // Test events + event HookReceived(uint256 paramOne, uint256 paramTwo); + + // Test constants + + uint32 v2MessageVersion = 1; + uint32 v2MessageBodyVersion = 1; + + address wrapperOwner = address(123); + + address localMessageTransmitter = address(10); + MockHookTarget hookTarget; + CCTPHookWrapper wrapper; + + function setUp() public { + vm.prank(wrapperOwner); + wrapper = new CCTPHookWrapper(localMessageTransmitter); + + hookTarget = new MockHookTarget(); + } + + // Tests + + function testInitialization__revertsIfMessageTransmitterIsZero() public { + vm.expectRevert("Message transmitter is the zero address"); + new CCTPHookWrapper(address(0)); + } + + function testInitialization__setsTheMessageTransmitter( + address _messageTransmitter + ) public { + vm.assume(_messageTransmitter != address(0)); + CCTPHookWrapper _wrapper = new CCTPHookWrapper(_messageTransmitter); + assertEq(address(_wrapper.messageTransmitter()), _messageTransmitter); + } + + function testInitialization__usesTheV2MessageVersion() public view { + assertEq( + uint256(address(wrapper.supportedMessageVersion())), + uint256(v2MessageVersion) + ); + } + + function testInitialization__usesTheV2MessageBodyVersion() public view { + assertEq( + uint256(address(wrapper.supportedMessageBodyVersion())), + uint256(v2MessageBodyVersion) + ); + } + + function testRelay__revertsIfNotCalledByOwner( + address _randomAddress, + bytes calldata _randomBytes + ) public { + vm.assume(_randomAddress != wrapperOwner); + + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(_randomAddress); + wrapper.relay(_randomBytes, bytes("")); + } + + function testRelay__revertsIfMessageFormatVersionIsInvalid( + uint32 _messageVersion + ) public { + vm.assume(_messageVersion != v2MessageVersion); + + vm.expectRevert("Invalid message version"); + bytes memory _message = _createMessage( + _messageVersion, + v2MessageBodyVersion, + bytes("") + ); + + vm.prank(wrapperOwner); + wrapper.relay(_message, bytes("")); + } + + function testRelay__revertsIfMessageBodyVersionIsInvalid( + uint32 _messageBodyVersion + ) public { + vm.assume(_messageBodyVersion != v2MessageBodyVersion); + + vm.expectRevert("Invalid message body version"); + bytes memory _message = _createMessage( + v2MessageVersion, + _messageBodyVersion, + bytes("") + ); + + vm.prank(wrapperOwner); + wrapper.relay(_message, bytes("")); + } + + function testRelay__revertsIfMessageValidationFails() public { + bytes memory _message = _createMessage( + v2MessageVersion, + v2MessageBodyVersion, + bytes("") + ); + + // Slice the message to make it fail validation + bytes memory _truncatedMessage = _message + .ref(0) + .slice(0, 147, 0) + .clone(); // See: MessageV2#MESSAGE_BODY_INDEX + + vm.expectRevert("Invalid message: too short"); + + vm.prank(wrapperOwner); + wrapper.relay(_truncatedMessage, bytes("")); + } + + function testRelay__revertsIfMessageBodyValidationFails() public { + bytes memory _message = _createMessage( + v2MessageVersion, + v2MessageBodyVersion, + bytes("") + ); + + // Slice the message to make it fail validation + bytes memory _truncatedMessage = _message + .ref(0) + .slice(0, 375, 0) + .clone(); // See: BurnMessageV2#HOOK_DATA_INDEX (148 + 228 = 376) + + vm.expectRevert("Invalid burn message: too short"); + + vm.prank(wrapperOwner); + wrapper.relay(_truncatedMessage, bytes("")); + } + + function testRelay__revertsIfMessageTransmitterCallReverts() public { + bytes memory _message = _createMessage( + v2MessageVersion, + v2MessageBodyVersion, + bytes("") + ); + + // Mock a reverting call to message transmitter + vm.mockCallRevert( + localMessageTransmitter, + abi.encodeWithSelector( + IReceiver.receiveMessage.selector, + _message, + bytes("") + ), + "Testing: message transmitter failed" + ); + + vm.expectRevert(); + + vm.prank(wrapperOwner); + wrapper.relay(_message, bytes("")); + } + + function testRelay__revertsIfMessageTransmitterReturnsFalse() public { + bytes memory _message = _createMessage( + v2MessageVersion, + v2MessageBodyVersion, + bytes("") + ); + + // Mock receiveMessage() returning false + vm.mockCall( + localMessageTransmitter, + abi.encodeWithSelector( + IReceiver.receiveMessage.selector, + _message, + bytes("") + ), + abi.encode(false) + ); + + vm.expectCall( + localMessageTransmitter, + abi.encodeWithSelector( + IReceiver.receiveMessage.selector, + _message, + bytes("") + ), + 1 + ); + + vm.expectRevert("Receive message failed"); + + vm.prank(wrapperOwner); + wrapper.relay(_message, bytes("")); + } + + function testRelay__succeedsWithNoHook() public { + bytes memory _message = _createMessage( + v2MessageVersion, + v2MessageBodyVersion, + bytes("") + ); + + vm.mockCall( + localMessageTransmitter, + abi.encodeWithSelector( + IReceiver.receiveMessage.selector, + _message, + bytes("") + ), + abi.encode(true) + ); + + vm.prank(wrapperOwner); + ( + bool _relaySuccess, + bool _hookSuccess, + bytes memory _returnData + ) = wrapper.relay(_message, bytes("")); + + assertTrue(_relaySuccess); + assertFalse(_hookSuccess); + assertEq(_returnData.length, 0); + } + + function testRelay__succeedsWithFailingHook() public { + // Prepare a message with hookCalldata that will fail + bytes memory _failingHookCalldata = abi.encodeWithSelector( + MockHookTarget.failingHook.selector + ); + bytes memory _message = _createMessage( + v2MessageVersion, + v2MessageBodyVersion, + abi.encodePacked(address(hookTarget), _failingHookCalldata) + ); + + // Mock successful call to MessageTransmitter + vm.mockCall( + localMessageTransmitter, + abi.encodeWithSelector( + IReceiver.receiveMessage.selector, + _message, + bytes("") + ), + abi.encode(true) + ); + + // Call wrapper + vm.prank(wrapperOwner); + ( + bool _relaySuccess, + bool _hookSuccess, + bytes memory _returnData + ) = wrapper.relay(_message, bytes("")); + + assertTrue(_relaySuccess); + assertFalse(_hookSuccess); + assertEq(_getRevertMsg(_returnData), "Hook failure"); + } + + function testRelay__succeedsAndIgnoresHooksLessThanRequiredLength( + bytes calldata randomBytes + ) public { + vm.assume(randomBytes.length > 20); + // Prepare a message with hookData less than required length (20 bytes) + bytes memory _shortCallData = randomBytes[:19]; + bytes memory _message = _createMessage( + v2MessageVersion, + v2MessageBodyVersion, + _shortCallData + ); + + // Mock successful call to MessageTransmitter + vm.mockCall( + localMessageTransmitter, + abi.encodeWithSelector( + IReceiver.receiveMessage.selector, + _message, + bytes("") + ), + abi.encode(true) + ); + + // Call wrapper + vm.prank(wrapperOwner); + ( + bool _relaySuccess, + bool _hookSuccess, + bytes memory _returnData + ) = wrapper.relay(_message, bytes("")); + + assertTrue(_relaySuccess); + assertFalse(_hookSuccess); + assertEq(_returnData.length, 0); + } + + function testRelay__succeedsWithCallToEOAHookTarget( + bytes calldata _hookCalldata + ) public { + bytes memory _message = _createMessage( + v2MessageVersion, + v2MessageBodyVersion, + abi.encodePacked(address(12345), _hookCalldata) + ); + + // Mock successful call to MessageTransmitter + vm.mockCall( + localMessageTransmitter, + abi.encodeWithSelector( + IReceiver.receiveMessage.selector, + _message, + bytes("") + ), + abi.encode(true) + ); + + // Call wrapper + vm.prank(wrapperOwner); + ( + bool _relaySuccess, + bool _hookSuccess, + bytes memory _returnData + ) = wrapper.relay(_message, bytes("")); + + assertTrue(_relaySuccess); + assertTrue(_hookSuccess); + assertEq(_returnData.length, 0); + } + + function testRelay__succeedsWithSucceedingHook() public { + // Prepare a message with hookCalldata that will succeed + uint256 _expectedReturnData = 12; + bytes memory _succeedingHookCallData = abi.encodeWithSelector( + MockHookTarget.succeedingHook.selector, + 5, + 7 + ); + bytes memory _message = _createMessage( + v2MessageVersion, + v2MessageBodyVersion, + abi.encodePacked(address(hookTarget), _succeedingHookCallData) + ); + + // Mock successful call to MessageTransmitter + vm.mockCall( + localMessageTransmitter, + abi.encodeWithSelector( + IReceiver.receiveMessage.selector, + _message, + bytes("") + ), + abi.encode(true) + ); + + vm.expectEmit(true, true, true, true); + emit HookReceived(5, 7); + + // Call wrapper + vm.prank(wrapperOwner); + ( + bool _relaySuccess, + bool _hookSuccess, + bytes memory _returnData + ) = wrapper.relay(_message, bytes("")); + + assertTrue(_relaySuccess); + assertTrue(_hookSuccess); + assertEq(abi.decode(_returnData, (uint256)), _expectedReturnData); + } + + // Test Utils + + function _createMessage( + uint32 _messageVersion, + uint32 _messageBodyVersion, + bytes memory _hookData + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + _messageVersion, // messageVersion + uint32(0), // sourceDomain + uint32(0), // destinationDomain + bytes32(0), // nonce + bytes32(0), // sender + bytes32(0), // recipient + bytes32(0), // destinationCaller + uint32(0), // minFinalityThreshold + uint32(0), // finalityThresholdExecuted + _createBurnMessage(_messageBodyVersion, _hookData) + ); + } + + function _createBurnMessage( + uint32 _burnMessageVersion, + bytes memory _hookData + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + _burnMessageVersion, // messageBodyVersion + bytes32(0), // burnToken + bytes32(0), // mintRecipient + uint256(0), // amount + bytes32(0), // messageSender + uint256(0), // maxFee + uint256(0), // feeExecuted + uint256(0), // expirationBlock + _hookData // hookData + ); + } + + // source: https://ethereum.stackexchange.com/a/83577 + function _getRevertMsg( + bytes memory _returnData + ) internal pure returns (string memory) { + // If the _res length is less than 68, then the transaction failed silently (without a revert message) + if (_returnData.length < 68) return "Transaction reverted silently"; + + assembly { + // Slice the sighash. + _returnData := add(_returnData, 0x04) + } + return abi.decode(_returnData, (string)); // All that remains is the revert string + } +} diff --git a/test/messages/BurnMessage.t.sol b/test/messages/BurnMessage.t.sol index 2bd0463..734dd52 100644 --- a/test/messages/BurnMessage.t.sol +++ b/test/messages/BurnMessage.t.sol @@ -14,6 +14,7 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import "@memview-sol/contracts/TypedMemView.sol"; import "forge-std/Test.sol"; diff --git a/test/messages/Message.t.sol b/test/messages/Message.t.sol index 14a7986..e03ea54 100644 --- a/test/messages/Message.t.sol +++ b/test/messages/Message.t.sol @@ -14,6 +14,7 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import "forge-std/Test.sol"; import "../../src/messages/Message.sol"; diff --git a/test/messages/v2/AddressUtils.t.sol b/test/messages/v2/AddressUtils.t.sol new file mode 100644 index 0000000..12283bc --- /dev/null +++ b/test/messages/v2/AddressUtils.t.sol @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {Test} from "forge-std/Test.sol"; +import {AddressUtils} from "../../../src/messages/v2/AddressUtils.sol"; + +contract AddressUtilsTest is Test { + function testAddressToBytes32Conversion(address _addr) public pure { + bytes32 _addrAsBytes32 = AddressUtils.toBytes32(_addr); + address _recoveredAddr = AddressUtils.toAddress(_addrAsBytes32); + assertEq(_recoveredAddr, _addr); + } + + function testAddressToBytes32LeftPads(address _addr) public pure { + bytes32 _addrAsBytes32 = AddressUtils.toBytes32(_addr); + + // addresses are 20 bytes, so the first 12 bytes should be 0 (left-padded) + for (uint8 i; i < 12; i++) { + assertEq(_addrAsBytes32[i], 0); + } + } +} diff --git a/test/messages/v2/AddressUtilsExternal.t.sol b/test/messages/v2/AddressUtilsExternal.t.sol new file mode 100644 index 0000000..27750ba --- /dev/null +++ b/test/messages/v2/AddressUtilsExternal.t.sol @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {Test} from "forge-std/Test.sol"; +import {AddressUtilsExternal} from "../../../src/messages/v2/AddressUtilsExternal.sol"; + +contract AddressUtilsExternalTest is Test { + function testAddressToBytes32Conversion(address _addr) public pure { + bytes32 _addrAsBytes32 = AddressUtilsExternal.addressToBytes32(_addr); + address _recoveredAddr = AddressUtilsExternal.bytes32ToAddress( + _addrAsBytes32 + ); + assertEq(_recoveredAddr, _addr); + } + + function testAddressToBytes32LeftPads(address _addr) public pure { + bytes32 _addrAsBytes32 = AddressUtilsExternal.addressToBytes32(_addr); + + // addresses are 20 bytes, so the first 12 bytes should be 0 (left-padded) + for (uint8 i; i < 12; i++) { + assertEq(_addrAsBytes32[i], 0); + } + } +} diff --git a/test/messages/v2/BurnMessageV2.t.sol b/test/messages/v2/BurnMessageV2.t.sol new file mode 100644 index 0000000..5561322 --- /dev/null +++ b/test/messages/v2/BurnMessageV2.t.sol @@ -0,0 +1,109 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol"; +import {Test} from "forge-std/Test.sol"; +import {BurnMessageV2} from "../../../src/messages/v2/BurnMessageV2.sol"; + +contract BurnMessageV2Test is Test { + using TypedMemView for bytes; + using TypedMemView for bytes29; + using BurnMessageV2 for bytes29; + + function testFormatMessageyForRelay_succeeds( + uint32 _version, + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _messageSender, + uint256 _maxFee, + bytes calldata _hookData + ) public view { + bytes memory _expectedMessageBody = abi.encodePacked( + _version, + _burnToken, + _mintRecipient, + _amount, + _messageSender, + _maxFee, + uint256(0), + uint256(0), + _hookData + ); + + bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( + _version, + _burnToken, + _mintRecipient, + _amount, + _messageSender, + _maxFee, + _hookData + ); + + bytes29 _m = _messageBody.ref(0); + assertEq(uint256(_m._getVersion()), uint256(_version)); + assertEq(_m._getBurnToken(), _burnToken); + assertEq(_m._getMintRecipient(), _mintRecipient); + assertEq(_m._getAmount(), _amount); + assertEq(_m._getMessageSender(), _messageSender); + assertEq(_m._getMaxFee(), _maxFee); + assertEq(_m._getFeeExecuted(), 0); + assertEq(_m._getExpirationBlock(), 0); + assertEq(_m._getHookData().clone(), _hookData); + + _m._validateBurnMessageFormat(); + + assertEq(_expectedMessageBody.ref(0).keccak(), _m.keccak()); + } + + function testIsValidBurnMessage_revertsForTooShortMessage( + uint32 _version, + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _messageSender, + uint256 _maxFee, + bytes calldata _hookData + ) public { + bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( + _version, + _burnToken, + _mintRecipient, + _amount, + _messageSender, + _maxFee, + _hookData + ); + bytes29 _m = _messageBody.ref(0); + + // Lop off the hookData bytes, and then one more + _m = _m.slice(0, _m.len() - _hookData.length - 1, 0); + + vm.expectRevert("Invalid burn message: too short"); + _m._validateBurnMessageFormat(); + } + + function testIsValidBurnMessage_revertsForEmptyMessage() public { + bytes29 _m = TypedMemView.nullView(); + vm.expectRevert("Malformed message"); + BurnMessageV2._validateBurnMessageFormat(_m); + } +} diff --git a/test/messages/v2/MessageV2.t.sol b/test/messages/v2/MessageV2.t.sol new file mode 100644 index 0000000..0e719f0 --- /dev/null +++ b/test/messages/v2/MessageV2.t.sol @@ -0,0 +1,105 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {Test} from "forge-std/Test.sol"; +import {MessageV2} from "../../../src/messages/v2/MessageV2.sol"; +import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol"; + +contract MessageV2Test is Test { + using TypedMemView for bytes; + using TypedMemView for bytes29; + using MessageV2 for bytes29; + + function testFormatMessage( + uint32 _version, + uint32 _sourceDomain, + uint32 _destinationDomain, + bytes32 _sender, + bytes32 _recipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _messageBody + ) public view { + bytes memory _message = MessageV2._formatMessageForRelay( + _version, + _sourceDomain, + _destinationDomain, + _sender, + _recipient, + _destinationCaller, + _minFinalityThreshold, + _messageBody + ); + + bytes29 _m = _message.ref(0); + assertEq(uint256(_m._getVersion()), uint256(_version)); + assertEq(uint256(_m._getSourceDomain()), uint256(_sourceDomain)); + assertEq( + uint256(_m._getDestinationDomain()), + uint256(_destinationDomain) + ); + + assertEq(_m._getNonce(), bytes32(0)); + assertEq(_m._getSender(), _sender); + assertEq(_m._getRecipient(), _recipient); + assertEq(_m._getDestinationCaller(), _destinationCaller); + assertEq( + uint256(_m._getMinFinalityThreshold()), + uint256(_minFinalityThreshold) + ); + assertEq(uint256(_m._getFinalityThresholdExecuted()), uint256(0)); + assertEq(_m._getMessageBody().clone(), _messageBody); + } + + function testIsValidMessage_revertsForEmptyMessage() public { + bytes29 _m = TypedMemView.nullView(); + vm.expectRevert("Malformed message"); + MessageV2._validateMessageFormat(_m); + } + + function testIsValidMessage_revertsForTooShortMessage( + uint32 _version, + uint32 _sourceDomain, + uint32 _destinationDomain, + bytes32 _sender, + bytes32 _recipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _messageBody + ) public { + bytes memory _message = MessageV2._formatMessageForRelay( + _version, + _sourceDomain, + _destinationDomain, + _sender, + _recipient, + _destinationCaller, + _minFinalityThreshold, + _messageBody + ); + bytes29 _m = _message.ref(0); + + // Lop off the _messageBody bytes, and then one more + _m = _m.slice(0, _m.len() - _messageBody.length - 1, 0); + + vm.expectRevert("Invalid message: too short"); + MessageV2._validateMessageFormat(_m); + } +} diff --git a/test/mocks/MockInitializableImplementation.sol b/test/mocks/MockInitializableImplementation.sol new file mode 100644 index 0000000..750c333 --- /dev/null +++ b/test/mocks/MockInitializableImplementation.sol @@ -0,0 +1,48 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {Initializable} from "../../src/proxy/Initializable.sol"; + +contract MockInitializableImplementation is Initializable { + address public addr; + uint256 public num; + + function initialize(address _addr, uint256 _num) external initializer { + addr = _addr; + num = _num; + } + + function initializeV2() external reinitializer(2) {} + + function initializeV3() external reinitializer(3) {} + + function supportingInitializer() public view onlyInitializing {} + + function disableInitializers() external { + _disableInitializers(); + } + + function initializedVersion() external view returns (uint64) { + return _getInitializedVersion(); + } + + function initializing() external view returns (bool) { + return _isInitializing(); + } +} diff --git a/test/mocks/MockReentrantCaller.sol b/test/mocks/MockReentrantCaller.sol index 8586071..ba61d4e 100644 --- a/test/mocks/MockReentrantCaller.sol +++ b/test/mocks/MockReentrantCaller.sol @@ -18,7 +18,6 @@ pragma solidity 0.7.6; import "../../src/interfaces/IMessageHandler.sol"; import "../../src/interfaces/IReceiver.sol"; import "../../src/messages/Message.sol"; -import "../../lib/forge-std/src/console.sol"; import "@openzeppelin/contracts/utils/Address.sol"; contract MockReentrantCaller is IMessageHandler { @@ -31,7 +30,7 @@ contract MockReentrantCaller is IMessageHandler { function setMessageAndSignature( bytes memory _message, bytes memory _signature - ) external { + ) public { message = _message; signature = _signature; } @@ -40,7 +39,7 @@ contract MockReentrantCaller is IMessageHandler { uint32 _sourceDomain, bytes32 _sender, bytes memory _messageBody - ) external override returns (bool) { + ) public override returns (bool) { // revert if _messageBody is 'revert', otherwise do nothing require( keccak256(_messageBody) != keccak256(bytes("revert")), @@ -66,11 +65,9 @@ contract MockReentrantCaller is IMessageHandler { } // source: https://ethereum.stackexchange.com/a/83577 - function _getRevertMsg(bytes memory _returnData) - internal - pure - returns (string memory) - { + function _getRevertMsg( + bytes memory _returnData + ) internal pure returns (string memory) { // If the _res length is less than 68, then the transaction failed silently (without a revert message) if (_returnData.length < 68) return "Transaction reverted silently"; @@ -81,11 +78,10 @@ contract MockReentrantCaller is IMessageHandler { return abi.decode(_returnData, (string)); // All that remains is the revert string } - function stringEquals(string memory a, string memory b) - internal - pure - returns (bool) - { + function stringEquals( + string memory a, + string memory b + ) internal pure returns (bool) { return (keccak256(abi.encodePacked((a))) == keccak256(abi.encodePacked((b)))); } diff --git a/test/mocks/v2/MockDenylistable.sol b/test/mocks/v2/MockDenylistable.sol new file mode 100644 index 0000000..4a5495f --- /dev/null +++ b/test/mocks/v2/MockDenylistable.sol @@ -0,0 +1,31 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {Denylistable} from "../../../src/roles/v2/Denylistable.sol"; + +contract MockDenylistable is Denylistable { + function sensitiveFunction() + external + view + notDenylistedCallers + returns (bool) + { + return true; + } +} diff --git a/test/mocks/v2/MockHookTarget.sol b/test/mocks/v2/MockHookTarget.sol new file mode 100644 index 0000000..67a815e --- /dev/null +++ b/test/mocks/v2/MockHookTarget.sol @@ -0,0 +1,35 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +contract MockHookTarget { + event HookReceived(uint256 paramOne, uint256 paramTwo); + + // Returns the sum of paramOne and paramTwo + function succeedingHook( + uint256 paramOne, + uint256 paramTwo + ) external returns (uint256) { + emit HookReceived(paramOne, paramTwo); + return paramOne + paramTwo; + } + + function failingHook() external pure { + revert("Hook failure"); + } +} diff --git a/test/mocks/v2/MockMessageTransmitterV3.sol b/test/mocks/v2/MockMessageTransmitterV3.sol new file mode 100644 index 0000000..84b085b --- /dev/null +++ b/test/mocks/v2/MockMessageTransmitterV3.sol @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {MessageTransmitterV2} from "../../../src/v2/MessageTransmitterV2.sol"; + +contract MockMessageTransmitterV3 is MessageTransmitterV2 { + address public newV3State; + + constructor( + uint32 _localDomain, + uint32 _version + ) MessageTransmitterV2(_localDomain, _version) {} + + function initializeV3(address newState) external reinitializer(2) { + newV3State = newState; + } + + function v3Function() external pure returns (bool) { + return true; + } +} diff --git a/test/mocks/v2/MockPayableProxyImplementation.sol b/test/mocks/v2/MockPayableProxyImplementation.sol new file mode 100644 index 0000000..d534f4c --- /dev/null +++ b/test/mocks/v2/MockPayableProxyImplementation.sol @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +contract MockPayableProxyImplementation { + receive() external payable {} +} diff --git a/test/mocks/v2/MockProxyImplementation.sol b/test/mocks/v2/MockProxyImplementation.sol new file mode 100644 index 0000000..cca155d --- /dev/null +++ b/test/mocks/v2/MockProxyImplementation.sol @@ -0,0 +1,45 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +// Test helper to use an alternate implementation to test proxy +contract MockProxyImplementation { + address public storedAddr; + + function foo() external pure returns (bytes memory response) { + response = bytes("bar"); + } + + function setStoredAddr(address _storedAddr) external { + storedAddr = _storedAddr; + } +} + +// Alternate implementation with distinct ABI +contract MockAlternateProxyImplementation { + uint256[1] __gap; + address public storedAddrAlternate; + + function baz() external pure returns (bytes memory response) { + response = bytes("qux"); + } + + function setStoredAddrAlternate(address _storedAddr) external { + storedAddrAlternate = _storedAddr; + } +} diff --git a/test/mocks/v2/MockReentrantCallerV2.sol b/test/mocks/v2/MockReentrantCallerV2.sol new file mode 100644 index 0000000..0075d4a --- /dev/null +++ b/test/mocks/v2/MockReentrantCallerV2.sol @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {IMessageHandlerV2} from "../../../src/interfaces/v2/IMessageHandlerV2.sol"; +import {MockReentrantCaller} from "../MockReentrantCaller.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +contract MockReentrantCallerV2 is IMessageHandlerV2, MockReentrantCaller { + function handleReceiveFinalizedMessage( + uint32 sourceDomain, + bytes32 sender, + uint32, + bytes calldata messageBody + ) external override returns (bool) { + return handleReceiveMessage(sourceDomain, sender, messageBody); + } + + function handleReceiveUnfinalizedMessage( + uint32 sourceDomain, + bytes32 sender, + uint32, + bytes calldata messageBody + ) external override returns (bool) { + return handleReceiveMessage(sourceDomain, sender, messageBody); + } +} diff --git a/test/mocks/v2/MockTokenMessengerV3.sol b/test/mocks/v2/MockTokenMessengerV3.sol new file mode 100644 index 0000000..02a22b0 --- /dev/null +++ b/test/mocks/v2/MockTokenMessengerV3.sol @@ -0,0 +1,31 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; + +import {TokenMessengerV2} from "../../../src/v2/TokenMessengerV2.sol"; + +contract MockTokenMessengerV3 is TokenMessengerV2 { + constructor( + address _messageTransmitter, + uint32 _messageBodyVersion + ) TokenMessengerV2(_messageTransmitter, _messageBodyVersion) {} + + function v3Function() external pure returns (bool) { + return true; + } +} diff --git a/test/roles/Attestable.t.sol b/test/roles/Attestable.t.sol index 395811c..3327547 100644 --- a/test/roles/Attestable.t.sol +++ b/test/roles/Attestable.t.sol @@ -14,6 +14,7 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import "../../src/roles/Attestable.sol"; import "../../lib/forge-std/src/Test.sol"; diff --git a/test/roles/Ownable2Step.t.sol b/test/roles/Ownable2Step.t.sol index 7e8074e..2a8593c 100644 --- a/test/roles/Ownable2Step.t.sol +++ b/test/roles/Ownable2Step.t.sol @@ -14,6 +14,7 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import "../../lib/forge-std/src/Test.sol"; import "../../src/roles/Ownable2Step.sol"; diff --git a/test/roles/v2/Denylistable.t.sol b/test/roles/v2/Denylistable.t.sol new file mode 100644 index 0000000..bdd0d03 --- /dev/null +++ b/test/roles/v2/Denylistable.t.sol @@ -0,0 +1,261 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {MockDenylistable} from "../../mocks/v2/MockDenylistable.sol"; +import {Test} from "forge-std/Test.sol"; + +contract DenylistableTest is Test { + // Test events + event DenylisterChanged( + address indexed oldDenylister, + address indexed newDenylister + ); + event Denylisted(address indexed account); + event UnDenylisted(address indexed account); + + // Test constants + address owner = address(10); + address denylister = address(20); + + MockDenylistable denylistable; + + function setUp() public { + vm.startPrank(owner); + denylistable = new MockDenylistable(); + denylistable.updateDenylister(denylister); + + assertEq(denylistable.owner(), owner); + assertEq(denylistable.denylister(), denylister); + + vm.stopPrank(); + } + + // Tests + + function testUpdateDenylister_revertsIfNotCalledByOwner( + address _notOwner, + address _otherAddress + ) public { + vm.assume(_notOwner != owner); + vm.assume(_otherAddress != address(0)); + + vm.prank(_notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + denylistable.updateDenylister(_otherAddress); + } + + function testUpdateDenylister_revertsIfDenylisterIsZeroAddress() public { + vm.prank(owner); + vm.expectRevert("Denylistable: new denylister is the zero address"); + denylistable.updateDenylister(address(0)); + } + + function testUpdateDenylister_succeeds(address _newDenylister) public { + vm.assume(_newDenylister != address(0)); + + vm.expectEmit(true, true, true, true); + emit DenylisterChanged(denylister, _newDenylister); + + vm.prank(owner); + denylistable.updateDenylister(_newDenylister); + assertEq(denylistable.denylister(), _newDenylister); + } + + function testDenylist_revertsIfNotCalledByDenylister( + address _randomCaller, + address _deniedAddress + ) public { + vm.assume(_randomCaller != denylister); + + vm.prank(_randomCaller); + vm.expectRevert("Denylistable: caller is not denylister"); + denylistable.denylist(_deniedAddress); + } + + function testDenylist_succeeds(address _deniedAddress) public { + vm.expectEmit(true, true, true, true); + emit Denylisted(_deniedAddress); + + vm.prank(denylister); + denylistable.denylist(_deniedAddress); + assertTrue(denylistable.isDenylisted(_deniedAddress)); + } + + function testUndenylist_revertsIfNotCalledByDenylister( + address _randomCaller, + address _addressToRemove + ) public { + vm.assume(_randomCaller != denylister); + + vm.prank(_randomCaller); + vm.expectRevert("Denylistable: caller is not denylister"); + denylistable.unDenylist(_addressToRemove); + } + + function testUndenylist_succeeds(address _addressToRemove) public { + // First add to denylist + vm.prank(denylister); + denylistable.denylist(_addressToRemove); + + // Verify + assertTrue(denylistable.isDenylisted(_addressToRemove)); + + vm.expectEmit(true, true, true, true); + emit UnDenylisted(_addressToRemove); + + vm.prank(denylister); + denylistable.unDenylist(_addressToRemove); + assertFalse(denylistable.isDenylisted(_addressToRemove)); + } + + function testDenylister_returnsTheCurrentDenylister( + address _newDenylister + ) public { + vm.assume(_newDenylister != denylister); + vm.assume(_newDenylister != address(0)); + + // Sanity check + assertEq(denylistable.denylister(), denylister); + + // Change to new address + vm.prank(owner); + denylistable.updateDenylister(_newDenylister); + assertEq(denylistable.denylister(), _newDenylister); + } + + function testDenylisted_returnsIfAnAddressIsOnTheDenylist( + address _deniedAddress + ) public { + // Sanity check + assertFalse(denylistable.isDenylisted(_deniedAddress)); + + // Add to deny list + vm.prank(denylister); + denylistable.denylist(_deniedAddress); + assertTrue(denylistable.isDenylisted(_deniedAddress)); + + // Remove again + vm.prank(denylister); + denylistable.unDenylist(_deniedAddress); + assertFalse(denylistable.isDenylisted(_deniedAddress)); + } + + function testNotDenylistedCallers_revertsIfMessageSenderIsDenylisted( + address _messageSender, + address _txOrigin + ) public { + vm.assume(_messageSender != _txOrigin); + + // Sanity checks + assertFalse(denylistable.isDenylisted(_messageSender)); + assertFalse(denylistable.isDenylisted(_txOrigin)); + + // Add messageSender to deny list + vm.prank(denylister); + denylistable.denylist(_messageSender); + + // Now, mock with modifier should fail + vm.prank(_messageSender, _txOrigin); + vm.expectRevert("Denylistable: account is on denylist"); + denylistable.sensitiveFunction(); + } + + function testNotDenylistedCallers_revertsIfTxOriginIsDenylisted( + address _messageSender, + address _txOrigin + ) public { + vm.assume(_messageSender != _txOrigin); + + // Sanity checks + assertFalse(denylistable.isDenylisted(_messageSender)); + assertFalse(denylistable.isDenylisted(_txOrigin)); + + // Add messageSender to deny list + vm.prank(denylister); + denylistable.denylist(_txOrigin); + + // Now, mock with modifier should fail + vm.prank(_messageSender, _txOrigin); + vm.expectRevert("Denylistable: account is on denylist"); + denylistable.sensitiveFunction(); + } + + function testNotDenylistedCallers_revertsIfBothMessageSenderAndTxOriginAreDenylistedAndDistinct( + address _messageSender, + address _txOrigin + ) public { + vm.assume(_messageSender != _txOrigin); + + // Sanity checks + assertFalse(denylistable.isDenylisted(_messageSender)); + assertFalse(denylistable.isDenylisted(_txOrigin)); + + // Add messageSender to deny list + vm.startPrank(denylister); + denylistable.denylist(_messageSender); + denylistable.denylist(_txOrigin); + vm.stopPrank(); + + // Now, mock with modifier should fail + vm.prank(_messageSender, _txOrigin); + vm.expectRevert("Denylistable: account is on denylist"); + denylistable.sensitiveFunction(); + } + + function testNotDenylistedCallers_revertsForSameCallers( + address _caller + ) public { + // Sanity check + assertFalse(denylistable.isDenylisted(_caller)); + + // Add messageSender to deny list + vm.prank(denylister); + denylistable.denylist(_caller); + + // Now, mock with modifier should fail + vm.prank(_caller, _caller); + vm.expectRevert("Denylistable: account is on denylist"); + denylistable.sensitiveFunction(); + } + + function testNotDenylistedCallers_succeedsForDistinctCallers( + address _messageSender, + address _txOrigin + ) public { + // Sanity checks + assertFalse(denylistable.isDenylisted(_messageSender)); + assertFalse(denylistable.isDenylisted(_txOrigin)); + + // Call should succeed + vm.prank(_messageSender, _txOrigin); + assertTrue(denylistable.sensitiveFunction()); + } + + function testNotDenylistedCallers_succeedsForTheSameCaller( + address _caller + ) public { + // Sanity check + assertFalse(denylistable.isDenylisted(_caller)); + + // Call should succeed + vm.prank(_caller, _caller); + assertTrue(denylistable.sensitiveFunction()); + } +} diff --git a/test/scripts/v2/DeployImplementationsV2.t.sol b/test/scripts/v2/DeployImplementationsV2.t.sol new file mode 100644 index 0000000..90f69b0 --- /dev/null +++ b/test/scripts/v2/DeployImplementationsV2.t.sol @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; +import {DeployImplementationsV2Script} from "../../../scripts/v2/DeployImplementationsV2.s.sol"; +import {MessageTransmitterV2} from "../../../src/v2/MessageTransmitterV2.sol"; +import {TokenMessengerV2} from "../../../src/v2/TokenMessengerV2.sol"; + +contract DeployImplementationsV2Test is ScriptV2TestUtils { + DeployImplementationsV2Script deployImplementationsV2Script; + + function setUp() public { + _deployCreate2Factory(); + _deployImplementations(); + deployImplementationsV2Script = new DeployImplementationsV2Script(); + } + + function testDeployImplementationsV2() public { + // MessageTransmitterV2 + assertEq(messageTransmitterV2Impl.localDomain(), uint256(sourceDomain)); + assertEq(messageTransmitterV2Impl.version(), uint256(_version)); + + // TokenMinterV2 + assertEq(tokenMinterV2.tokenController(), deployer); + assertEq(tokenMinterV2.owner(), deployer); + + // TokenMessengerV2 + assertEq( + address(tokenMessengerV2Impl.localMessageTransmitter()), + address(expectedMessageTransmitterV2ProxyAddress) + ); + assertEq( + tokenMessengerV2Impl.messageBodyVersion(), + uint256(_messageBodyVersion) + ); + } +} diff --git a/test/scripts/v2/DeployProxiesV2.t.sol b/test/scripts/v2/DeployProxiesV2.t.sol new file mode 100644 index 0000000..4f621bc --- /dev/null +++ b/test/scripts/v2/DeployProxiesV2.t.sol @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2024, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; +import {AdminUpgradableProxy} from "../../../src/proxy/AdminUpgradableProxy.sol"; +import {DeployImplementationsV2Script} from "../../../scripts/v2/DeployImplementationsV2.s.sol"; +import {DeployProxiesV2Script} from "../../../scripts/v2/DeployProxiesV2.s.sol"; +import {MessageTransmitterV2} from "../../../src/v2/MessageTransmitterV2.sol"; +import {TokenMessengerV2} from "../../../src/v2/TokenMessengerV2.sol"; +import {SALT_MESSAGE_TRANSMITTER, SALT_TOKEN_MESSENGER} from "../../../scripts/v2/Salts.sol"; + +contract DeployProxiesV2Test is ScriptV2TestUtils { + DeployProxiesV2Script deployProxiesV2Script; + + function setUp() public { + _deployCreate2Factory(); + _deployImplementations(); + _deployProxies(); + deployProxiesV2Script = new DeployProxiesV2Script(); + } + + function testDeployMessageTransmitterV2() public { + // create2 address + address predicted = create2Factory.computeAddress( + SALT_MESSAGE_TRANSMITTER, + keccak256( + deployProxiesV2Script.getProxyCreationCode( + address(create2Factory), + address(create2Factory), + "" + ) + ) + ); + assertEq(address(messageTransmitterV2), predicted); + // owner + assertEq(messageTransmitterV2.owner(), deployer); + // domain + assertEq(messageTransmitterV2.localDomain(), uint256(sourceDomain)); + // attester + assertEq(messageTransmitterV2.attesterManager(), deployer); + assertTrue(messageTransmitterV2.isEnabledAttester(attester1)); + assertTrue(messageTransmitterV2.isEnabledAttester(attester2)); + assertEq(messageTransmitterV2.signatureThreshold(), 2); + // maxMessageBodySize + assertEq(messageTransmitterV2.maxMessageBodySize(), maxMessageBodySize); + // version + assertEq(messageTransmitterV2.version(), uint256(1)); + // pauser + assertEq(messageTransmitterV2.pauser(), pauser); + // rescuer + assertEq(messageTransmitterV2.rescuer(), rescuer); + // admin + assertEq( + AdminUpgradableProxy(payable(address(messageTransmitterV2))) + .admin(), + messageTransmitterV2AdminAddress + ); + } + + function testDeployTokenMessengerV2() public { + // create2 address + address predicted = create2Factory.computeAddress( + SALT_TOKEN_MESSENGER, + keccak256( + deployProxiesV2Script.getProxyCreationCode( + address(create2Factory), + address(create2Factory), + "" + ) + ) + ); + assertEq(address(tokenMessengerV2), predicted); + // message transmitter + assertEq( + address(tokenMessengerV2.localMessageTransmitter()), + address(messageTransmitterV2) + ); + // message body version + assertEq( + tokenMessengerV2.messageBodyVersion(), + uint256(_messageBodyVersion) + ); + // owner + assertEq(tokenMessengerV2.owner(), deployer); + // rescuer + assertEq(tokenMessengerV2.rescuer(), rescuer); + // fee recipient + assertEq(tokenMessengerV2.feeRecipient(), feeRecipient); + // deny lister + assertEq(tokenMessengerV2.denylister(), denyLister); + // remote token messengers + for (uint256 i = 0; i < remoteDomains.length; i++) { + uint32 remoteDomain = remoteDomains[i]; + bytes32 remoteTokenMessengerAddress = bytes32( + uint256(uint160(address(tokenMessengerV2))) + ); + if (remoteTokenMessengerV2FromEnv) { + remoteTokenMessengerAddress = bytes32( + uint256(uint160(address(remoteTokenMessengerV2s[i]))) + ); + } + assertEq( + tokenMessengerV2.remoteTokenMessengers(remoteDomain), + remoteTokenMessengerAddress + ); + } + // admin + assertEq( + AdminUpgradableProxy(payable(address(tokenMessengerV2))).admin(), + tokenMessengerV2AdminAddress + ); + } + + function testConfigureTokenMinterV2() public { + // token controller + assertEq(tokenMinterV2.tokenController(), deployer); + // token messenger + assertEq( + tokenMinterV2.localTokenMessenger(), + address(tokenMessengerV2) + ); + // pauser + assertEq(tokenMinterV2.pauser(), pauser); + // rescuer + assertEq(tokenMinterV2.rescuer(), rescuer); + // max burn per msg + assertEq( + tokenMinterV2.burnLimitsPerMessage(token), + maxBurnAmountPerMessage + ); + // linked token pairs + for (uint256 i = 0; i < remoteDomains.length; i++) { + address remoteToken = remoteTokens[i]; + bytes32 remoteKey = keccak256( + abi.encodePacked( + remoteDomains[i], + bytes32(uint256(uint160(remoteToken))) + ) + ); + assertEq(tokenMinterV2.remoteTokensToLocalTokens(remoteKey), token); + } + } +} diff --git a/test/scripts/v2/RotateKeysV2.t.sol b/test/scripts/v2/RotateKeysV2.t.sol new file mode 100644 index 0000000..edfe9c6 --- /dev/null +++ b/test/scripts/v2/RotateKeysV2.t.sol @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; + +contract RotateKeysTest is ScriptV2TestUtils { + function setUp() public { + _deployCreate2Factory(); + _deployImplementations(); + _deployProxies(); + _setupRemoteResources(); + _rotateKeys(); + } + + function testRotateMessageTransmitterV2Owner() public { + assertEq(messageTransmitterV2.pendingOwner(), newOwner); + } + + function testRotateTokenMessengerV2Owner() public { + assertEq(tokenMessengerV2.pendingOwner(), newOwner); + } + + function testRotateTokenControllerThenTokenMinterV2Owner() public { + assertEq(tokenMinterV2.tokenController(), newOwner); + assertEq(tokenMinterV2.pendingOwner(), newOwner); + } +} diff --git a/test/scripts/v2/ScriptV2TestUtils.sol b/test/scripts/v2/ScriptV2TestUtils.sol new file mode 100644 index 0000000..5f79885 --- /dev/null +++ b/test/scripts/v2/ScriptV2TestUtils.sol @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2024, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {DeployImplementationsV2Script} from "../../../scripts/v2/DeployImplementationsV2.s.sol"; +import {DeployProxiesV2Script} from "../../../scripts/v2/DeployProxiesV2.s.sol"; +import {SetupRemoteResourcesV2Script} from "../../../scripts/v2/SetupRemoteResourcesV2.s.sol"; +import {RotateKeysV2Script} from "../../../scripts/v2/RotateKeysV2.s.sol"; +import {MessageTransmitterV2} from "../../../src/v2/MessageTransmitterV2.sol"; +import {TokenMessengerV2} from "../../../src/v2/TokenMessengerV2.sol"; +import {TokenMinterV2} from "../../../src/v2/TokenMinterV2.sol"; +import {MockMintBurnToken} from "../../mocks/MockMintBurnToken.sol"; +import {TestUtils} from "../../TestUtils.sol"; +import {Create2Factory} from "../../../src/v2/Create2Factory.sol"; +import {Message} from "../../../src/messages/Message.sol"; + +contract ScriptV2TestUtils is TestUtils { + uint32 _messageBodyVersion = 1; + uint32 _version = 1; + address token; + uint256 deployerPK; + address deployer; + address attester1; + address attester2; + address pauser; + address rescuer; + address feeRecipient; + address denyLister; + + Create2Factory create2Factory; + MessageTransmitterV2 messageTransmitterV2; + TokenMessengerV2 tokenMessengerV2; + TokenMinterV2 tokenMinterV2; + + address expectedMessageTransmitterV2ProxyAddress; + MessageTransmitterV2 messageTransmitterV2Impl; + TokenMessengerV2 tokenMessengerV2Impl; + + address[] remoteTokens; + uint32[] remoteDomains; + address[] remoteTokenMessengerV2s; + bool remoteTokenMessengerV2FromEnv = false; + uint32 anotherRemoteDomain = 5; + address anotherRemoteToken; + + uint256 newOwnerPK; + address newOwner; + address messageTransmitterV2AdminAddress; + address tokenMessengerV2AdminAddress; + + function _deployCreate2Factory() internal { + deployerPK = uint256(keccak256("DEPLOYTEST_DEPLOYER_PK")); + deployer = vm.addr(deployerPK); + vm.startBroadcast(deployerPK); + create2Factory = new Create2Factory(); + vm.stopBroadcast(); + } + + function _deployImplementations() internal { + vm.setEnv( + "CREATE2_FACTORY_CONTRACT_ADDRESS", + vm.toString(address(create2Factory)) + ); + vm.setEnv("CREATE2_FACTORY_OWNER_KEY", vm.toString(deployerPK)); + vm.setEnv("TOKEN_MINTER_V2_OWNER_ADDRESS", vm.toString(deployer)); + vm.setEnv("TOKEN_MINTER_V2_OWNER_KEY", vm.toString(deployerPK)); + vm.setEnv("TOKEN_CONTROLLER_ADDRESS", vm.toString(deployer)); + vm.setEnv("DOMAIN", vm.toString(uint256(sourceDomain))); + vm.setEnv( + "MESSAGE_BODY_VERSION", + vm.toString(uint256(_messageBodyVersion)) + ); + vm.setEnv("VERSION", vm.toString(uint256(_version))); + + DeployImplementationsV2Script deployImplScript = new DeployImplementationsV2Script(); + deployImplScript.setUp(); + deployImplScript.run(); + + messageTransmitterV2Impl = deployImplScript.messageTransmitterV2(); + tokenMinterV2 = deployImplScript.tokenMinterV2(); + tokenMessengerV2Impl = deployImplScript.tokenMessengerV2(); + expectedMessageTransmitterV2ProxyAddress = deployImplScript + .expectedMessageTransmitterV2ProxyAddress(); + } + + function _deployProxies() internal { + token = address(new MockMintBurnToken()); + remoteTokens.push(address(new MockMintBurnToken())); + remoteTokens.push(address(new MockMintBurnToken())); + remoteTokens.push(address(new MockMintBurnToken())); + remoteDomains.push(1); + remoteDomains.push(2); + remoteDomains.push(3); + remoteTokenMessengerV2s.push( + vm.addr(uint256(keccak256("REMOTE_TOKEN_MESSENGER_V2_ADDRESS_1"))) + ); + remoteTokenMessengerV2s.push( + vm.addr(uint256(keccak256("REMOTE_TOKEN_MESSENGER_V2_ADDRESS_2"))) + ); + remoteTokenMessengerV2s.push( + vm.addr(uint256(keccak256("REMOTE_TOKEN_MESSENGER_V2_ADDRESS_3"))) + ); + anotherRemoteToken = address(new MockMintBurnToken()); + + attester1 = vm.addr(uint256(keccak256("DEPLOYTEST_ATTESTER_1_PK"))); + attester2 = vm.addr(uint256(keccak256("DEPLOYTEST_ATTESTER_2_PK"))); + pauser = vm.addr(uint256(keccak256("DEPLOYTEST_PAUSER_PK"))); + rescuer = vm.addr(uint256(keccak256("DEPLOYTEST_RESCUER_PK"))); + feeRecipient = vm.addr( + uint256(keccak256("DEPLOYTEST_FEE_RECIPIENT_PK")) + ); + denyLister = vm.addr(uint256(keccak256("DEPLOYTEST_DENY_LISTER_PK"))); + + messageTransmitterV2AdminAddress = vm.addr( + uint256(keccak256("MESSAGE_TRANSMITTER_V2_ADMIN")) + ); + tokenMessengerV2AdminAddress = vm.addr( + uint256(keccak256("TOKEN_MESSENGER_V2_ADMIN")) + ); + + // Override env vars + vm.setEnv("USDC_CONTRACT_ADDRESS", vm.toString(token)); + vm.setEnv("TOKEN_CONTROLLER_ADDRESS", vm.toString(deployer)); + vm.setEnv( + "CREATE2_FACTORY_CONTRACT_ADDRESS", + vm.toString(address(create2Factory)) + ); + vm.setEnv( + "REMOTE_DOMAINS", + string( + abi.encodePacked( + vm.toString(uint256(remoteDomains[0])), + ",", + vm.toString(uint256(remoteDomains[1])), + ",", + vm.toString(uint256(remoteDomains[2])) + ) + ) + ); + vm.setEnv( + "REMOTE_USDC_CONTRACT_ADDRESSES", + string( + abi.encodePacked( + vm.toString(Message.addressToBytes32(remoteTokens[0])), + ",", + vm.toString(Message.addressToBytes32(remoteTokens[1])), + ",", + vm.toString(Message.addressToBytes32(remoteTokens[2])) + ) + ) + ); + if (remoteTokenMessengerV2FromEnv) { + // TODO: Figure out if there is a way to dynamically set this before setUp() + vm.setEnv( + "REMOTE_TOKEN_MESSENGER_V2_ADDRESSES", + string( + abi.encodePacked( + vm.toString( + Message.addressToBytes32(remoteTokenMessengerV2s[0]) + ), + ",", + vm.toString( + Message.addressToBytes32(remoteTokenMessengerV2s[1]) + ), + ",", + vm.toString( + Message.addressToBytes32(remoteTokenMessengerV2s[2]) + ) + ) + ) + ); + } + + vm.setEnv( + "MESSAGE_TRANSMITTER_V2_IMPLEMENTATION_ADDRESS", + vm.toString(address(messageTransmitterV2Impl)) + ); + vm.setEnv( + "MESSAGE_TRANSMITTER_V2_OWNER_ADDRESS", + vm.toString(deployer) + ); + vm.setEnv("MESSAGE_TRANSMITTER_V2_PAUSER_ADDRESS", vm.toString(pauser)); + vm.setEnv( + "MESSAGE_TRANSMITTER_V2_RESCUER_ADDRESS", + vm.toString(rescuer) + ); + vm.setEnv( + "MESSAGE_TRANSMITTER_V2_ATTESTER_MANAGER_ADDRESS", + vm.toString(deployer) + ); + vm.setEnv( + "MESSAGE_TRANSMITTER_V2_ATTESTER_1_ADDRESS", + vm.toString(attester1) + ); + vm.setEnv( + "MESSAGE_TRANSMITTER_V2_ATTESTER_2_ADDRESS", + vm.toString(attester2) + ); + vm.setEnv( + "MESSAGE_TRANSMITTER_V2_PROXY_ADMIN_ADDRESS", + vm.toString(messageTransmitterV2AdminAddress) + ); + + vm.setEnv( + "TOKEN_MINTER_V2_CONTRACT_ADDRESS", + vm.toString(address(tokenMinterV2)) + ); + vm.setEnv("TOKEN_MINTER_V2_PAUSER_ADDRESS", vm.toString(pauser)); + vm.setEnv("TOKEN_MINTER_V2_RESCUER_ADDRESS", vm.toString(rescuer)); + + vm.setEnv( + "TOKEN_MESSENGER_V2_IMPLEMENTATION_ADDRESS", + vm.toString(address(tokenMessengerV2Impl)) + ); + vm.setEnv("TOKEN_MESSENGER_V2_OWNER_ADDRESS", vm.toString(deployer)); + vm.setEnv("TOKEN_MESSENGER_V2_RESCUER_ADDRESS", vm.toString(rescuer)); + vm.setEnv( + "TOKEN_MESSENGER_V2_FEE_RECIPIENT_ADDRESS", + vm.toString(feeRecipient) + ); + vm.setEnv( + "TOKEN_MESSENGER_V2_DENYLISTER_ADDRESS", + vm.toString(denyLister) + ); + vm.setEnv( + "TOKEN_MESSENGER_V2_PROXY_ADMIN_ADDRESS", + vm.toString(tokenMessengerV2AdminAddress) + ); + + vm.setEnv("DOMAIN", vm.toString(uint256(sourceDomain))); + vm.setEnv( + "BURN_LIMIT_PER_MESSAGE", + vm.toString(maxBurnAmountPerMessage) + ); + + vm.setEnv("CREATE2_FACTORY_OWNER_KEY", vm.toString(deployerPK)); + vm.setEnv("TOKEN_MINTER_V2_OWNER_KEY", vm.toString(deployerPK)); + vm.setEnv("TOKEN_CONTROLLER_KEY", vm.toString(deployerPK)); + + DeployProxiesV2Script deployProxiesV2Script = new DeployProxiesV2Script(); + deployProxiesV2Script.setUp(); + deployProxiesV2Script.run(); + + messageTransmitterV2 = deployProxiesV2Script.messageTransmitterV2(); + tokenMessengerV2 = deployProxiesV2Script.tokenMessengerV2(); + } + + function _setupRemoteResources() internal { + vm.setEnv("TOKEN_MESSENGER_V2_OWNER_KEY", vm.toString(deployerPK)); + vm.setEnv( + "TOKEN_MESSENGER_V2_CONTRACT_ADDRESS", + vm.toString(address(tokenMessengerV2)) + ); + vm.setEnv( + "TOKEN_MINTER_V2_CONTRACT_ADDRESS", + vm.toString(address(tokenMinterV2)) + ); + vm.setEnv("USDC_CONTRACT_ADDRESS", vm.toString(token)); + vm.setEnv( + "REMOTE_USDC_CONTRACT_ADDRESS", + vm.toString(anotherRemoteToken) + ); + + vm.setEnv("REMOTE_DOMAIN", vm.toString(uint256(anotherRemoteDomain))); + + SetupRemoteResourcesV2Script setupRemoteResourcesV2Script = new SetupRemoteResourcesV2Script(); + setupRemoteResourcesV2Script.setUp(); + setupRemoteResourcesV2Script.run(); + } + + function _rotateKeys() internal { + vm.setEnv( + "MESSAGE_TRANSMITTER_V2_CONTRACT_ADDRESS", + vm.toString(address(messageTransmitterV2)) + ); + // [SKIP] Use same TOKEN_MESSENGER_CONTRACT_ADDRESS + // [SKIP] Use same TOKEN_MINTER_CONTRACT_ADDRESS + vm.setEnv("MESSAGE_TRANSMITTER_V2_OWNER_KEY", vm.toString(deployerPK)); + vm.setEnv("TOKEN_MINTER_V2_OWNER_KEY", vm.toString(deployerPK)); + + newOwnerPK = uint256(keccak256("ROTATEKEYSTEST_NEW_OWNER")); + newOwner = vm.addr(newOwnerPK); + + vm.setEnv( + "MESSAGE_TRANSMITTER_V2_NEW_OWNER_ADDRESS", + vm.toString(newOwner) + ); + vm.setEnv( + "TOKEN_MESSENGER_V2_NEW_OWNER_ADDRESS", + vm.toString(newOwner) + ); + vm.setEnv("TOKEN_MINTER_V2_NEW_OWNER_ADDRESS", vm.toString(newOwner)); + vm.setEnv("NEW_TOKEN_CONTROLLER_ADDRESS", vm.toString(newOwner)); + + RotateKeysV2Script rotateKeysV2Script = new RotateKeysV2Script(); + rotateKeysV2Script.setUp(); + rotateKeysV2Script.run(); + } +} diff --git a/test/scripts/v2/SetupRemoteResourcesV2.t.sol b/test/scripts/v2/SetupRemoteResourcesV2.t.sol new file mode 100644 index 0000000..b6b7c2c --- /dev/null +++ b/test/scripts/v2/SetupRemoteResourcesV2.t.sol @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; + +contract SetupRemoteResourcesTest is ScriptV2TestUtils { + function setUp() public { + _deployCreate2Factory(); + _deployImplementations(); + _deployProxies(); + _setupRemoteResources(); + } + + function testLinkTokenPair() public { + bytes32 remoteKey = keccak256( + abi.encodePacked( + anotherRemoteDomain, + bytes32(uint256(uint160(anotherRemoteToken))) + ) + ); + assertEq(tokenMinterV2.remoteTokensToLocalTokens(remoteKey), token); + } + + function testAddRemoteTokenMessenger() public { + assertEq( + tokenMessengerV2.remoteTokenMessengers(anotherRemoteDomain), + bytes32(uint256(uint160(address(tokenMessengerV2)))) + ); + } +} diff --git a/test/v2/AdminUpgradableProxy.t.sol b/test/v2/AdminUpgradableProxy.t.sol new file mode 100644 index 0000000..348ae8e --- /dev/null +++ b/test/v2/AdminUpgradableProxy.t.sol @@ -0,0 +1,332 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {AdminUpgradableProxy} from "../../src/proxy/AdminUpgradableProxy.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {MockInitializableImplementation} from "../mocks/MockInitializableImplementation.sol"; +import {MockProxyImplementation, MockAlternateProxyImplementation} from "../mocks/v2/MockProxyImplementation.sol"; +import {MockPayableProxyImplementation} from "../mocks/v2/MockPayableProxyImplementation.sol"; +import {Test} from "forge-std/Test.sol"; + +contract AdminUpgradableProxyTest is Test { + // Events + + event AdminChanged(address previousAdmin, address newAdmin); + event Upgraded(address indexed implementation); + + // Constants + + address proxyAdmin = address(1); + + AdminUpgradableProxy proxy; + + MockProxyImplementation impl; + MockAlternateProxyImplementation alternateImpl; + MockInitializableImplementation initializableImpl; + MockPayableProxyImplementation payableImpl; + + function setUp() public { + impl = new MockProxyImplementation(); + alternateImpl = new MockAlternateProxyImplementation(); + initializableImpl = new MockInitializableImplementation(); + payableImpl = new MockPayableProxyImplementation(); + + proxy = new AdminUpgradableProxy(address(impl), proxyAdmin, bytes("")); + } + + // Tests + + function testConstructor_setsTheImplementation() public view { + assertEq(proxy.implementation(), address(impl)); + } + + function testConstructor_setsTheProxyAdmin() public view { + assertEq(proxy.admin(), proxyAdmin); + } + + function testConstructor_revertsIfInitializationCallFails() public { + bytes4 badSelector = bytes4(keccak256("notafunction()")); + + vm.expectRevert("Address: low-level delegate call failed"); + new AdminUpgradableProxy( + address(impl), + proxyAdmin, + abi.encodeWithSelector(badSelector) + ); + } + + function testConstructor_initializesWithExtraData( + address _randomAddress, + uint256 _randomNumber + ) public { + bytes memory _initializationData = abi.encodeWithSelector( + MockInitializableImplementation.initialize.selector, + _randomAddress, + _randomNumber + ); + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(initializableImpl), + proxyAdmin, + _initializationData + ); + MockInitializableImplementation _proxyAsImpl = MockInitializableImplementation( + address(_proxy) + ); + + assertEq(_proxyAsImpl.addr(), _randomAddress); + assertEq(_proxyAsImpl.num(), _randomNumber); + } + + function testImplementation_returnsTheImplementationContractAddress( + address _randomCaller + ) public { + vm.prank(_randomCaller); + assertEq(proxy.implementation(), address(impl)); + + vm.prank(proxyAdmin); + proxy.upgradeTo(address(alternateImpl)); + + vm.prank(_randomCaller); + assertEq(proxy.implementation(), address(alternateImpl)); + } + + function testAdmin_returnsTheProxyAdminAddress( + address _randomCaller, + address _newAdmin + ) public { + vm.assume(_randomCaller != _newAdmin); + vm.assume(_newAdmin != address(0)); + + vm.prank(_randomCaller); + assertEq(proxy.admin(), proxyAdmin); + + vm.prank(proxyAdmin); + proxy.changeAdmin(_newAdmin); + + vm.prank(_randomCaller); + assertEq(proxy.admin(), _newAdmin); + } + + function testChangeAdmin_revertsIfNotCalledByProxyAdmin( + address _randomCaller, + address _newAdmin + ) public { + vm.assume(_randomCaller != proxyAdmin); + + // This reverts because changeAdmin() does not exist on the implementation, so + // the delegate call fails + vm.expectRevert(); + vm.prank(_randomCaller); + proxy.changeAdmin(_newAdmin); + + // sanity check + assertEq(proxy.admin(), proxyAdmin); + } + + function testChangeAdmin_revertsIfNewAdminIsZeroAddress() public { + vm.prank(proxyAdmin); + vm.expectRevert("AdminUpgradableProxy: new admin is the zero address"); + proxy.changeAdmin(address(0)); + } + + function testChangeAdmin_succeeds(address _newAdmin) public { + vm.assume(_newAdmin != proxyAdmin); + vm.assume(_newAdmin != address(0)); + + vm.expectEmit(true, true, true, true); + emit AdminChanged(proxyAdmin, _newAdmin); + + vm.prank(proxyAdmin); + proxy.changeAdmin(_newAdmin); + + assertEq(proxy.admin(), _newAdmin); + } + + function testReceiveNative_revertsIfImplementationDoesntHaveReceiveFunction( + address _spender + ) public { + vm.assume(_spender != address(0)); + vm.assume(_spender != address(proxy)); + + vm.deal(_spender, 10 ether); + vm.startPrank(_spender); + + // MockProxyImplementation.sol does not have a receive function; sanity-check this + // by sending native token directly to the implementation address + bool ok; + (ok, ) = address(impl).call{value: 10 ether}(""); + assertFalse(ok); + + // Transfer native token to the proxy using call(); see: https://github.com/foundry-rs/foundry/discussions/4508 + // This should fail, as the impl does not have a receive function + (ok, ) = address(proxy).call{value: 10 ether}(""); + assertFalse(ok); + + vm.stopPrank(); + assertEq(address(proxy).balance, 0); + } + + function testReceiveNative_succeedsIfImplementationHasReceiveFunction( + address _spender + ) public { + vm.assume(_spender != address(0)); + vm.assume(_spender != address(proxy)); + + vm.deal(_spender, 10 ether); + + // MockPayableProxyImplementation.sol DOES have a receive function; sanity-check this + // by sending native token directly to the implementation address + bool ok; + vm.prank(_spender); + (ok, ) = address(payableImpl).call{value: 5 ether}(""); + assertTrue(ok); + + // Now switch over to use the payable impl + vm.prank(proxyAdmin); + proxy.upgradeTo(address(payableImpl)); + + // Transfer native token using call(); see: https://github.com/foundry-rs/foundry/discussions/4508 + vm.prank(_spender); + (ok, ) = address(proxy).call{value: 5 ether}(""); + assertTrue(ok); + + assertEq(address(proxy).balance, 5 ether); + } + + function testUpgradeTo_revertsWhenNotCalledByProxyAdmin( + address _sender + ) public { + vm.assume(_sender != proxyAdmin); + + vm.prank(_sender); + vm.expectRevert(); // reverts because upgradeTo() does not exist on the implementation + proxy.upgradeTo(address(alternateImpl)); + } + + function testUpgradeTo_revertsIfNewImplementationIsNotAContract( + address _randomAddress + ) public { + vm.assume(!Address.isContract(_randomAddress)); + + vm.prank(proxyAdmin); + vm.expectRevert( + "UpgradeableProxy: new implementation is not a contract" + ); + proxy.upgradeTo(address(_randomAddress)); + } + + function testUpgradeTo_succeeds() public { + // Sanity check + assertEq(proxy.implementation(), address(impl)); + assertEq(MockProxyImplementation(address(proxy)).foo(), bytes("bar")); + + vm.expectEmit(true, true, true, true); + emit Upgraded(address(alternateImpl)); + + // Upgrade + vm.prank(proxyAdmin); + proxy.upgradeTo(address(alternateImpl)); + + assertEq(proxy.implementation(), address(alternateImpl)); + assertEq( + MockAlternateProxyImplementation(address(proxy)).baz(), + bytes("qux") + ); + } + + function testUpgradeToAndCall_revertsWhenNotCalledByProxyAdmin( + address _sender + ) public { + vm.assume(_sender != proxyAdmin); + + vm.prank(_sender); + vm.expectRevert(); // reverts because upgradeToAndCall() does not exist on the implementation + proxy.upgradeToAndCall( + address(alternateImpl), + abi.encodeWithSelector( + MockAlternateProxyImplementation + .setStoredAddrAlternate + .selector, + address(123) + ) + ); + } + + function testUpgradeToAndCall_revertsIfNewImplementationIsNotAContract( + address _randomAddress + ) public { + vm.assume(!Address.isContract(_randomAddress)); + + vm.prank(proxyAdmin); + vm.expectRevert( + "UpgradeableProxy: new implementation is not a contract" + ); + proxy.upgradeToAndCall(_randomAddress, bytes("")); + } + + function testUpgradeToAndCall_succeeds(address _randomAddress) public { + // Sanity check + assertEq(proxy.implementation(), address(impl)); + assertEq( + MockProxyImplementation(address(proxy)).storedAddr(), + address(0) + ); + + vm.expectEmit(true, true, true, true); + emit Upgraded(address(alternateImpl)); + + // Upgrade + vm.prank(proxyAdmin); + proxy.upgradeToAndCall( + address(alternateImpl), + // Encode a call to set a storage value atomically + abi.encodeWithSelector( + MockAlternateProxyImplementation + .setStoredAddrAlternate + .selector, + _randomAddress + ) + ); + + assertEq(proxy.implementation(), address(alternateImpl)); + // Check that the proxy delegates to the new impl + assertEq( + MockAlternateProxyImplementation(address(proxy)).baz(), + bytes("qux") + ); + // Check that the value was stored + assertEq( + MockAlternateProxyImplementation(address(proxy)) + .storedAddrAlternate(), + _randomAddress + ); + } + + function testDelegatesToImplementationContract() public { + assertEq(MockProxyImplementation(address(proxy)).foo(), bytes("bar")); + + vm.prank(proxyAdmin); + proxy.upgradeTo(address(alternateImpl)); + assertEq( + MockAlternateProxyImplementation(address(proxy)).baz(), + bytes("qux") + ); + } +} diff --git a/test/v2/BaseTokenMessenger.t.sol b/test/v2/BaseTokenMessenger.t.sol new file mode 100644 index 0000000..777883c --- /dev/null +++ b/test/v2/BaseTokenMessenger.t.sol @@ -0,0 +1,358 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {BaseTokenMessenger} from "../../src/v2/BaseTokenMessenger.sol"; +import {TestUtils} from "../TestUtils.sol"; + +abstract contract BaseTokenMessengerTest is Test, TestUtils { + // Events + + event RemoteTokenMessengerAdded(uint32 domain, bytes32 tokenMessenger); + event RemoteTokenMessengerRemoved(uint32 domain, bytes32 tokenMessenger); + event LocalMinterAdded(address localMinter); + event LocalMinterRemoved(address localMinter); + event FeeRecipientSet(address feeRecipient); + + BaseTokenMessenger baseTokenMessenger; + + function setUp() public virtual { + baseTokenMessenger = BaseTokenMessenger(setUpBaseTokenMessenger()); + } + + function setUpBaseTokenMessenger() internal virtual returns (address); + + function createBaseTokenMessenger( + address _messageTransmitter, + uint32 _messageBodyVersion + ) internal virtual returns (address); + + // Initialization tests + function testConstructor_setsLocalMessageTransmitter( + address _messageTransmitter, + uint32 _messageBodyVersion + ) public { + vm.assume(_messageTransmitter != address(0)); + address _tokenMessenger = createBaseTokenMessenger( + _messageTransmitter, + _messageBodyVersion + ); + + assertEq( + BaseTokenMessenger(_tokenMessenger).localMessageTransmitter(), + _messageTransmitter + ); + } + + function testConstructor_setsMessageVersion( + address _messageTransmitter, + uint32 _messageBodyVersion + ) public { + vm.assume(_messageTransmitter != address(0)); + address _tokenMessenger = createBaseTokenMessenger( + _messageTransmitter, + _messageBodyVersion + ); + + assertEq( + uint256(BaseTokenMessenger(_tokenMessenger).messageBodyVersion()), + uint256(_messageBodyVersion) + ); + } + + function testConstructor_rejectsZeroAddressLocalMessageTransmitter( + uint32 _messageBodyVersion + ) public { + vm.expectRevert("MessageTransmitter not set"); + createBaseTokenMessenger(address(0), _messageBodyVersion); + } + + function testAddRemoteTokenMessenger_succeeds( + uint32 _remoteDomain, + bytes32 _remoteTokenMessengerAddr + ) public { + vm.assume(_remoteTokenMessengerAddr != bytes32(0)); + // Sanity check that there is not a token messenger already registered + assertEq( + baseTokenMessenger.remoteTokenMessengers(_remoteDomain), + bytes32(0) + ); + + vm.expectEmit(true, true, true, true); + emit RemoteTokenMessengerAdded( + _remoteDomain, + _remoteTokenMessengerAddr + ); + baseTokenMessenger.addRemoteTokenMessenger( + _remoteDomain, + _remoteTokenMessengerAddr + ); + + assertEq( + baseTokenMessenger.remoteTokenMessengers(_remoteDomain), + _remoteTokenMessengerAddr + ); + } + + function testAddRemoteTokenMessenger_revertsOnExistingRemoteTokenMessenger( + uint32 _remoteDomain, + bytes32 _remoteTokenMessengerAddr + ) public { + vm.assume(_remoteTokenMessengerAddr != bytes32(0)); + baseTokenMessenger.addRemoteTokenMessenger( + _remoteDomain, + _remoteTokenMessengerAddr + ); + + vm.expectRevert("TokenMessenger already set"); + baseTokenMessenger.addRemoteTokenMessenger( + _remoteDomain, + _remoteTokenMessengerAddr + ); + } + + function testAddRemoteTokenMessenger_revertsOnZeroAddress( + uint32 _domain + ) public { + vm.expectRevert("bytes32(0) not allowed"); + baseTokenMessenger.addRemoteTokenMessenger(_domain, bytes32(0)); + } + + function testAddRemoteTokenMessenger_revertsOnNonOwner( + uint32 _domain, + bytes32 _tokenMessenger, + address _wrongOwner + ) public { + vm.assume(_wrongOwner != baseTokenMessenger.owner()); + expectRevertWithWrongOwner(_wrongOwner); + baseTokenMessenger.addRemoteTokenMessenger(_domain, _tokenMessenger); + } + + function testRemoveRemoteTokenMessenger_succeeds( + uint32 _remoteDomain, + bytes32 _remoteTokenMessenger + ) public { + vm.assume(_remoteTokenMessenger != bytes32(0)); + + baseTokenMessenger.addRemoteTokenMessenger( + _remoteDomain, + _remoteTokenMessenger + ); + + vm.expectEmit(true, true, true, true); + emit RemoteTokenMessengerRemoved(_remoteDomain, _remoteTokenMessenger); + baseTokenMessenger.removeRemoteTokenMessenger(_remoteDomain); + } + + function testRemoveRemoteTokenMessenger_revertsOnNoTokenMessengerSet( + uint32 _remoteDomain + ) public { + vm.assume( + baseTokenMessenger.remoteTokenMessengers(_remoteDomain) == + bytes32(0) + ); + + vm.expectRevert("No TokenMessenger set"); + baseTokenMessenger.removeRemoteTokenMessenger(_remoteDomain); + } + + function testRemoveRemoteTokenMessenger_revertsOnNonOwner( + uint32 _remoteDomain, + address _wrongOwner + ) public { + vm.assume( + baseTokenMessenger.remoteTokenMessengers(_remoteDomain) == + bytes32(0) + ); + vm.assume(_wrongOwner != baseTokenMessenger.owner()); + + expectRevertWithWrongOwner(_wrongOwner); + baseTokenMessenger.removeRemoteTokenMessenger(_remoteDomain); + } + + function testAddLocalMinter_succeeds(address _localMinter) public { + vm.assume(_localMinter != address(0)); + + assertEq(address(baseTokenMessenger.localMinter()), address(0)); + + _addLocalMinter(_localMinter, baseTokenMessenger); + } + + function testAddLocalMinter_revertsIfZeroAddress() public { + vm.expectRevert("Zero address not allowed"); + baseTokenMessenger.addLocalMinter(address(0)); + } + + function testAddLocalMinter_revertsIfAlreadySet( + address _localMinter + ) public { + vm.assume(_localMinter != address(0)); + + _addLocalMinter(_localMinter, baseTokenMessenger); + + vm.expectRevert("Local minter is already set."); + baseTokenMessenger.addLocalMinter(_localMinter); + } + + function testAddLocalMinter_revertsOnNonOwner( + address _localMinter, + address _notOwner + ) public { + vm.assume(_localMinter != address(0)); + vm.assume(_notOwner != baseTokenMessenger.owner()); + + expectRevertWithWrongOwner(_notOwner); + baseTokenMessenger.addLocalMinter(_localMinter); + } + + function testRemoveLocalMinter_succeeds(address _localMinter) public { + vm.assume(_localMinter != address(0)); + + _addLocalMinter(_localMinter, baseTokenMessenger); + + vm.expectEmit(true, true, true, true); + emit LocalMinterRemoved(_localMinter); + baseTokenMessenger.removeLocalMinter(); + } + + function testRemoveLocalMinter_revertsIfNoLocalMinterSet() public { + vm.expectRevert("No local minter is set."); + baseTokenMessenger.removeLocalMinter(); + } + + function testRemoveLocalMinter_revertsOnNonOwner(address _notOwner) public { + vm.assume(_notOwner != baseTokenMessenger.owner()); + expectRevertWithWrongOwner(_notOwner); + baseTokenMessenger.removeLocalMinter(); + } + + function testSetFeeRecipient_revertsOnNonOwner( + address _notOwner, + address _feeRecipient + ) public { + vm.assume(_notOwner != baseTokenMessenger.owner()); + expectRevertWithWrongOwner(_notOwner); + baseTokenMessenger.setFeeRecipient(_feeRecipient); + } + + function testSetFeeRecipient_revertsIfFeeRecipientIsZeroAddress() public { + vm.expectRevert("Zero address not allowed"); + baseTokenMessenger.setFeeRecipient(address(0)); + } + + function testSetFeeRecipient_succeeds(address _feeRecipient) public { + vm.assume(_feeRecipient != address(0)); + + vm.expectEmit(true, true, true, true); + emit FeeRecipientSet(_feeRecipient); + baseTokenMessenger.setFeeRecipient(_feeRecipient); + } + + // Ownable tests + + function testTransferOwnershipAndAcceptOwnership_succeeds( + address _newOwner + ) public { + vm.assume(_newOwner != baseTokenMessenger.owner()); + transferOwnershipAndAcceptOwnership( + address(baseTokenMessenger), + _newOwner + ); + } + + function testTransferOwnership_revertsOnNonOwner( + address _notOwner, + address _newOwner + ) public { + vm.assume(_notOwner != baseTokenMessenger.owner()); + transferOwnershipFailsIfNotOwner( + address(baseTokenMessenger), + _notOwner, + _newOwner + ); + } + + function testAcceptOwnership_revertsOnNonPendingOwner( + address _newOwner, + address _otherAccount + ) public { + vm.assume(_newOwner != _otherAccount); + acceptOwnershipFailsIfNotPendingOwner( + address(baseTokenMessenger), + _newOwner, + _otherAccount + ); + } + + function testTransferOwnershipWithoutAcceptingThenTransferToNewOwner_succeeds( + address _newOwner, + address _secondNewOwner + ) public { + transferOwnershipWithoutAcceptingThenTransferToNewOwner( + address(baseTokenMessenger), + _newOwner, + _secondNewOwner + ); + } + + // Rescuable tests + + function testRescuable( + address _rescuer, + address _rescueRecipient, + uint256 _amount, + address _nonRescuer + ) public { + assertContractIsRescuable( + address(baseTokenMessenger), + _rescuer, + _rescueRecipient, + _amount, + _nonRescuer + ); + } + + // Denylistable Tests + + function testDenylistable( + address _randomAddress, + address _newDenylister, + address _nonOwner + ) public { + assertContractIsDenylistable( + address(baseTokenMessenger), + _randomAddress, + _newDenylister, + _nonOwner + ); + } + + // Test utils + + function _addLocalMinter( + address _localMinter, + BaseTokenMessenger _tokenMessenger + ) internal { + vm.expectEmit(true, true, true, true); + emit LocalMinterAdded(_localMinter); + _tokenMessenger.addLocalMinter(_localMinter); + assertEq(address(_tokenMessenger.localMinter()), _localMinter); + } +} diff --git a/test/v2/Create2Factory.t.sol b/test/v2/Create2Factory.t.sol new file mode 100644 index 0000000..51f5c30 --- /dev/null +++ b/test/v2/Create2Factory.t.sol @@ -0,0 +1,117 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {Create2Factory} from "../../src/v2/Create2Factory.sol"; +import {MockInitializableImplementation} from "../mocks/MockInitializableImplementation.sol"; +import {UpgradeableProxy} from "@openzeppelin/contracts/proxy/UpgradeableProxy.sol"; +import {Test} from "forge-std/Test.sol"; + +contract Create2FactoryTest is Test { + Create2Factory private create2Factory; + MockInitializableImplementation private impl; + + event Upgraded(address indexed implementation); + + function setUp() public { + create2Factory = new Create2Factory(); + impl = new MockInitializableImplementation(); + } + + function test_SetUpState() public { + // Check owners + assertEq(create2Factory.owner(), address(this)); + } + + function testDeploy(address addr, uint256 num, bytes32 salt) public { + // Construct initializer + bytes memory initializer = abi.encodeWithSelector( + MockInitializableImplementation.initialize.selector, + addr, + num + ); + // Construct bytecode + bytes memory bytecode = abi.encodePacked( + type(UpgradeableProxy).creationCode, + abi.encode(address(impl), initializer) + ); + // Deploy proxy + address expectedAddr = create2Factory.computeAddress( + salt, + keccak256(bytecode) + ); + address proxyAddr = create2Factory.deploy(0, salt, bytecode); + + // Verify deterministic + assertEq(proxyAddr, expectedAddr); + // Check initialized vars + assertEq(MockInitializableImplementation(proxyAddr).addr(), addr); + assertEq(MockInitializableImplementation(proxyAddr).num(), num); + } + + function testDeployAndMultiCall( + address addr, + uint256 num, + uint256 amount, + bytes32 salt + ) public { + // Construct initializers + bytes memory initializer1 = abi.encodeWithSelector( + MockInitializableImplementation.initialize.selector, + addr, + num + ); + bytes memory initializer2 = abi.encodeWithSelector( + MockInitializableImplementation.initializeV2.selector + ); + bytes[] memory data = new bytes[](2); + data[0] = initializer1; + data[1] = initializer2; + // Construct bytecode + bytes memory bytecode = abi.encodePacked( + type(UpgradeableProxy).creationCode, + abi.encode(address(impl), "") + ); + // Deploy proxy + address expectedAddr = create2Factory.computeAddress( + salt, + keccak256(bytecode) + ); + vm.deal(address(this), amount); + + // Expect calls + vm.expectCall(expectedAddr, initializer1); + vm.expectCall(expectedAddr, initializer2); + + address proxyAddr = create2Factory.deployAndMultiCall{value: amount}( + amount, + salt, + bytecode, + data + ); + + // Verify deterministic + assertEq(proxyAddr, expectedAddr); + // Check initialized vars + assertEq(MockInitializableImplementation(proxyAddr).addr(), addr); + assertEq(MockInitializableImplementation(proxyAddr).num(), num); + // Verify balance + assertEq(proxyAddr.balance, amount); + } +} diff --git a/test/v2/Initializable.t.sol b/test/v2/Initializable.t.sol new file mode 100644 index 0000000..a202f9f --- /dev/null +++ b/test/v2/Initializable.t.sol @@ -0,0 +1,169 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {Initializable} from "../../src/proxy/Initializable.sol"; +import {MockInitializableImplementation} from "../mocks/MockInitializableImplementation.sol"; +import {Test} from "forge-std/Test.sol"; + +contract InitializableTest is Test { + MockInitializableImplementation private impl; + + event Initialized(uint64 version); + + struct InitializableStorage { + uint64 _initialized; + bool _initializing; + } + + bytes32 private constant INITIALIZABLE_STORAGE = + 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + + function setUp() public { + impl = new MockInitializableImplementation(); + } + + function test_canBeInitializedToNextIncrementedVersionFromZero() public { + assertEq(uint256(impl.initializedVersion()), 0); + + // Upgrade 0 -> 1 + vm.expectEmit(true, true, true, true); + emit Initialized(1); + impl.initialize(address(10), 1); + assertEq(uint256(impl.initializedVersion()), 1); + } + + function test_canBeReinitializedToNextIncrementedVersionFromNonZero() + public + { + impl.initialize(address(10), 1); + assertEq(uint256(impl.initializedVersion()), 1); + + // Upgrade 1 -> 2 + vm.expectEmit(true, true, true, true); + emit Initialized(2); + impl.initializeV2(); + assertEq(uint256(impl.initializedVersion()), 2); + } + + function test_canJumpToLaterVersionFromZero() public { + assertEq(uint256(impl.initializedVersion()), 0); + + // Upgrade 0 -> 2 + vm.expectEmit(true, true, true, true); + emit Initialized(2); + impl.initializeV2(); + assertEq(uint256(impl.initializedVersion()), 2); + } + + function test_canJumpToLaterVersionFromNonZero() public { + impl.initialize(address(10), 1); + assertEq(uint256(impl.initializedVersion()), 1); + + // Upgrade 1 -> 3 + vm.expectEmit(true, true, true, true); + emit Initialized(3); + impl.initializeV3(); + assertEq(uint256(impl.initializedVersion()), 3); + } + + function test_revertsIfInitializerIsCalledTwice() public { + impl.initialize(address(10), 1); + + vm.expectRevert("Initializable: invalid initialization"); + impl.initialize(address(10), 1); + } + + function test_revertsIfReinitializerIsCalledTwice() public { + impl.initializeV2(); + + vm.expectRevert("Initializable: invalid initialization"); + impl.initializeV2(); + } + + function test_revertsIfInitializersAreDisabled() public { + impl.disableInitializers(); + + vm.expectRevert("Initializable: invalid initialization"); + impl.initialize(address(10), 1); + } + + function testDisableInitializers_revertsIfInitializing() public { + // Set the initializing storage slot to 'true' directly + _setInitializableStorage(impl.initializedVersion(), true); + + // Sanity check + assertTrue(impl.initializing()); + + vm.expectRevert("Initializable: invalid initialization"); + impl.disableInitializers(); + } + + function test_revertsIfDowngraded() public { + impl.initializeV3(); + assertEq(uint256(impl.initializedVersion()), 3); + + // Downgrade 3 -> 2 + vm.expectRevert("Initializable: invalid initialization"); + impl.initializeV2(); + + assertEq(uint256(impl.initializedVersion()), 3); + } + + function testOnlyInitializing_revertsIfCalledOutsideOfInitialization() + public + { + assertFalse(impl.initializing()); + + vm.expectRevert("Initializable: not initializing"); + impl.supportingInitializer(); + } + + function testOnlyInitializing_succeedsIfCalledWhileInitializing() public { + // Set 'initializing' to true directly + _setInitializableStorage(impl.initializedVersion(), true); + assertTrue(impl.initializing()); + + // Should be callable now + impl.supportingInitializer(); + } + + // Test utils + + function _setInitializableStorage( + uint64 _initializedVersion, + bool _initializing + ) internal { + // Write it to a slot, and then copy over the slot contents to the implementation address + // There might be a better way to do this + InitializableStorage storage $; + assembly { + $.slot := INITIALIZABLE_STORAGE + } + $._initialized = _initializedVersion; + $._initializing = _initializing; + + // Copy over slot contents to implementation + vm.store( + address(impl), + INITIALIZABLE_STORAGE, + vm.load(address(this), INITIALIZABLE_STORAGE) + ); + } +} diff --git a/test/v2/MessageTransmitterV2.t.sol b/test/v2/MessageTransmitterV2.t.sol new file mode 100644 index 0000000..646e503 --- /dev/null +++ b/test/v2/MessageTransmitterV2.t.sol @@ -0,0 +1,1606 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {MessageTransmitterV2} from "../../src/v2/MessageTransmitterV2.sol"; +import {TestUtils} from "../TestUtils.sol"; +import {MessageV2} from "../../src/messages/v2/MessageV2.sol"; +import {AddressUtils} from "../../src/messages/v2/AddressUtils.sol"; +import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol"; +import {IMessageHandlerV2} from "../../src/interfaces/v2/IMessageHandlerV2.sol"; +import {MockReentrantCallerV2} from "../mocks/v2/MockReentrantCallerV2.sol"; +import {AdminUpgradableProxy} from "../../src/proxy/AdminUpgradableProxy.sol"; +import {MockMessageTransmitterV3} from "../mocks/v2/MockMessageTransmitterV3.sol"; +import {FINALITY_THRESHOLD_FINALIZED} from "../../src/v2/FinalityThresholds.sol"; + +contract MessageTransmitterV2Test is TestUtils { + event MessageSent(bytes message); + + event MessageReceived( + address indexed caller, + uint32 sourceDomain, + bytes32 indexed nonce, + bytes32 sender, + uint32 indexed finalityThresholdExecuted, + bytes messageBody + ); + + event MaxMessageBodySizeUpdated(uint256 newMaxMessageBodySize); + + event Upgraded(address indexed implementation); + + event AttesterEnabled(address indexed attester); + + event AttesterManagerUpdated( + address indexed previousAttesterManager, + address indexed newAttesterManager + ); + + event SignatureThresholdUpdated( + uint256 oldSignatureThreshold, + uint256 newSignatureThreshold + ); + + // ============ Libraries ============ + using TypedMemView for bytes; + using TypedMemView for bytes29; + using MessageV2 for bytes29; + using AddressUtils for address; + using AddressUtils for bytes32; + + // Test constants + uint256 constant SIGNATURE_LENGTH = 65; + + uint32 localDomain = 1; + uint32 remoteDomain = 2; + + address deployer = address(10); + address pauser = address(20); + address rescuer = address(30); + address attesterManager = address(40); + address proxyAdmin = address(50); + + MessageTransmitterV2 messageTransmitter; + MessageTransmitterV2 messageTransmitterImpl; + + function setUp() public { + vm.startPrank(deployer); + + // Deploy implementation + messageTransmitterImpl = new MessageTransmitterV2(localDomain, version); + + // Deploy proxy + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(messageTransmitterImpl), + proxyAdmin, + bytes("") + ); + + // Initialize MessageTransmitter + messageTransmitter = MessageTransmitterV2(address(_proxy)); + address[] memory _attesters = new address[](1); + _attesters[0] = attester; + messageTransmitter.initialize( + owner, + pauser, + rescuer, + attesterManager, + _attesters, + 1, + maxMessageBodySize + ); + + vm.stopPrank(); + } + + function testStorageSlots_hasAGapForAttestableV2Additions() public view { + // AttestableV2 slots are arranged at slots 4-8 + // Sanity check this by reading from an AttestableV2 storage var + // attesterManager is stored at slot 7 + address _attesterManager = vm + .load(address(messageTransmitter), bytes32(uint256(7))) + .toAddress(); + + assertEq(_attesterManager, messageTransmitter.attesterManager()); + + // Check that the next storage vars, defined in BaseMessageTransmitter, are gapped + // by 20 slots + // + uint256 _maxMessageBodySize = uint256( + vm.load(address(messageTransmitter), bytes32(uint256(28))) + ); + assertEq(_maxMessageBodySize, messageTransmitter.maxMessageBodySize()); + } + + function testInitialize_revertsIfOwnerIsZero() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(messageTransmitterImpl), + proxyAdmin, + bytes("") + ); + + vm.expectRevert("Owner is the zero address"); + MessageTransmitterV2(address(_proxy)).initialize( + address(0), + pauser, + rescuer, + attesterManager, + new address[](0), + 1, + maxMessageBodySize + ); + } + + function testInitialize_revertsIfPauserIsZero() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(messageTransmitterImpl), + proxyAdmin, + bytes("") + ); + + address[] memory _attesters = new address[](1); + _attesters[0] = attester; + + vm.expectRevert("Pausable: new pauser is the zero address"); + MessageTransmitterV2(address(_proxy)).initialize( + owner, + address(0), + rescuer, + attesterManager, + _attesters, + 1, + maxMessageBodySize + ); + } + + function testInitialize_revertsIfRescuerIsZero() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(messageTransmitterImpl), + proxyAdmin, + bytes("") + ); + + address[] memory _attesters = new address[](1); + _attesters[0] = attester; + + vm.expectRevert("Rescuable: new rescuer is the zero address"); + MessageTransmitterV2(address(_proxy)).initialize( + owner, + pauser, + address(0), + attesterManager, + _attesters, + 1, + maxMessageBodySize + ); + } + + function testInitialize_revertsIfAttesterManagerIsZero() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(messageTransmitterImpl), + proxyAdmin, + bytes("") + ); + + vm.expectRevert("AttesterManager is the zero address"); + MessageTransmitterV2(address(_proxy)).initialize( + owner, + pauser, + rescuer, + address(0), + new address[](0), + 1, + maxMessageBodySize + ); + } + + function testInitialize_revertsIfSignatureThresholdZero() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(messageTransmitterImpl), + proxyAdmin, + bytes("") + ); + + address[] memory _attesters = new address[](1); + _attesters[0] = attester; + + vm.expectRevert("Invalid signature threshold"); + MessageTransmitterV2(address(_proxy)).initialize( + owner, + pauser, + rescuer, + attesterManager, + _attesters, + 0, + maxMessageBodySize + ); + } + + function testInitialize_revertsIfSignatureThresholdExceedsAttestersCount() + public + { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(messageTransmitterImpl), + proxyAdmin, + bytes("") + ); + + address[] memory _attesters = new address[](2); + _attesters[0] = address(10); + _attesters[1] = address(20); + + vm.expectRevert("Signature threshold exceeds attesters"); + MessageTransmitterV2(address(_proxy)).initialize( + owner, + pauser, + rescuer, + attesterManager, + _attesters, + 3, // signature threshold + maxMessageBodySize + ); + } + + function testInitialize_revertsIfMaxMessageBodySizeIsZero() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(messageTransmitterImpl), + proxyAdmin, + bytes("") + ); + + address[] memory _attesters = new address[](1); + _attesters[0] = attester; + + vm.expectRevert("MaxMessageBodySize is zero"); + MessageTransmitterV2(address(_proxy)).initialize( + owner, + pauser, + rescuer, + attesterManager, + _attesters, + 1, + 0 + ); + } + + function testInitialize_canBeCalledAtomicallyByTheProxy() public { + // Deploy proxy and initialize it atomically + address[] memory _attesters = new address[](1); + _attesters[0] = attester; + + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(messageTransmitterImpl), + proxyAdmin, + abi.encodeWithSelector( + MessageTransmitterV2.initialize.selector, + owner, + pauser, + rescuer, + attesterManager, + _attesters, + 1, + maxMessageBodySize + ) + ); + + MessageTransmitterV2 _messageTransmitter = MessageTransmitterV2( + address(_proxy) + ); + assertEq(_messageTransmitter.owner(), owner); + assertEq(_messageTransmitter.pauser(), pauser); + assertEq(_messageTransmitter.rescuer(), rescuer); + assertEq(_messageTransmitter.attesterManager(), attesterManager); + assertTrue(_messageTransmitter.isEnabledAttester(attester)); + assertEq(_messageTransmitter.maxMessageBodySize(), maxMessageBodySize); + assertEq(_messageTransmitter.signatureThreshold(), 1); + } + + function testInitialize_emitsEvents() public { + // Deploy proxy and initialize it atomically + address[] memory _attesters = new address[](1); + _attesters[0] = attester; + + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(messageTransmitterImpl), + proxyAdmin, + bytes("") + ); + + vm.expectEmit(true, true, true, true); + emit OwnershipTransferred(address(0), owner); + + vm.expectEmit(true, true, true, true); + emit RescuerChanged(rescuer); + + vm.expectEmit(true, true, true, true); + emit PauserChanged(pauser); + + vm.expectEmit(true, true, true, true); + emit AttesterManagerUpdated(address(0), attesterManager); + + vm.expectEmit(true, true, true, true); + emit MaxMessageBodySizeUpdated(maxMessageBodySize); + + vm.expectEmit(true, true, true, true); + emit AttesterEnabled(attester); + + vm.expectEmit(true, true, true, true); + emit SignatureThresholdUpdated(0, 1); + + MessageTransmitterV2 _messageTransmitter = MessageTransmitterV2( + address(_proxy) + ); + _messageTransmitter.initialize( + owner, + pauser, + rescuer, + attesterManager, + _attesters, + 1, + maxMessageBodySize + ); + assertEq(_messageTransmitter.owner(), owner); + assertEq(_messageTransmitter.pauser(), pauser); + assertEq(_messageTransmitter.rescuer(), rescuer); + assertEq(_messageTransmitter.attesterManager(), attesterManager); + assertTrue(_messageTransmitter.isEnabledAttester(attester)); + assertEq(_messageTransmitter.maxMessageBodySize(), maxMessageBodySize); + assertEq(_messageTransmitter.signatureThreshold(), 1); + } + + function testInitializedVersion_returnsTheInitializedVersion() public { + assertEq(uint256(messageTransmitter.initializedVersion()), 1); + + // Upgrade to the next version + AdminUpgradableProxy _proxy = AdminUpgradableProxy( + payable(address(messageTransmitter)) + ); + + // Deploy v3 implementation + MockMessageTransmitterV3 _implV3 = new MockMessageTransmitterV3( + localDomain, + version + ); + + // Upgrade + vm.prank(proxyAdmin); + vm.expectEmit(true, true, true, true); + emit Upgraded(address(_implV3)); + _proxy.upgradeTo(address(_implV3)); + + // Call initializer on the new implementation + MockMessageTransmitterV3(address(_proxy)).initializeV3(address(123)); + + // Check initialized version + assertEq( + uint256( + MockMessageTransmitterV3(address(_proxy)).initializedVersion() + ), + 2 + ); + } + + function testUpgrade_succeeds() public { + AdminUpgradableProxy _proxy = AdminUpgradableProxy( + payable(address(messageTransmitter)) + ); + + // Sanity check + assertEq(_proxy.implementation(), address(messageTransmitterImpl)); + + // Test that we can upgrade to a v3 MessageTransmitter + // Deploy v3 implementation + MockMessageTransmitterV3 _implV3 = new MockMessageTransmitterV3( + localDomain, + version + 1 + ); + + // Upgrade + vm.prank(proxyAdmin); + vm.expectEmit(true, true, true, true); + emit Upgraded(address(_implV3)); + _proxy.upgradeTo(address(_implV3)); + + // Sanity checks + assertEq(_proxy.implementation(), address(_implV3)); + assertTrue(MockMessageTransmitterV3(address(_proxy)).v3Function()); + // Check that the MessageTransmitter Message Format version has changed + assertEq(uint256(messageTransmitter.version()), uint256(version + 1)); + } + + function testInitialize_setsTheOwner() public view { + assertEq(messageTransmitter.owner(), owner); + } + + function testInitialize_setsThePauser() public view { + assertEq(messageTransmitter.pauser(), pauser); + } + + function testInitialize_setsTheRescuer() public view { + assertEq(messageTransmitter.rescuer(), rescuer); + } + + function testInitialize_setsTheAttesterManager() public view { + assertEq(messageTransmitter.attesterManager(), attesterManager); + } + + function testInitialize_setsTheAttester() public view { + assertEq(messageTransmitter.getNumEnabledAttesters(), 1); + assertTrue(messageTransmitter.isEnabledAttester(attester)); + address _enabledAttester = messageTransmitter.getEnabledAttester(0); + assertEq(_enabledAttester, attester); + } + + function testInitialize_setsMultipleAttesters() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(messageTransmitterImpl), + proxyAdmin, + bytes("") + ); + MessageTransmitterV2 _newMessageTransmitter = MessageTransmitterV2( + address(_proxy) + ); + + address _attesterOne = address(123); + address _attesterTwo = address(456); + + address[] memory _attesters = new address[](2); + _attesters[0] = _attesterOne; + _attesters[1] = _attesterTwo; + _newMessageTransmitter.initialize( + owner, + pauser, + rescuer, + attesterManager, + _attesters, + 1, + maxMessageBodySize + ); + + assertEq(_newMessageTransmitter.getNumEnabledAttesters(), 2); + assertTrue(_newMessageTransmitter.isEnabledAttester(_attesterOne)); + assertTrue(_newMessageTransmitter.isEnabledAttester(_attesterTwo)); + address _enabledAttester = _newMessageTransmitter.getEnabledAttester(0); + assertEq(_enabledAttester, _attesterOne); + _enabledAttester = _newMessageTransmitter.getEnabledAttester(1); + assertEq(_enabledAttester, _attesterTwo); + } + + function testInitialize_setsTheSignatureThreshold() public view { + assertEq(messageTransmitter.signatureThreshold(), 1); + } + + function testInitialize_setsTheMaxMessageBodySize() public view { + assertEq(messageTransmitter.maxMessageBodySize(), maxMessageBodySize); + } + + function testInitialize_setsZeroNonceAsUsed() public view { + assertEq( + messageTransmitter.usedNonces(bytes32(0)), + messageTransmitter.NONCE_USED() + ); + } + + function testSendMessage_revertsWhenPaused( + uint32 _destinationDomain, + bytes32 _recipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _messageBody, + address _pauser + ) public { + vm.assume(_recipient != bytes32(0)); + vm.assume(_messageBody.length < maxMessageBodySize); + vm.assume(_pauser != address(0)); + vm.assume(_destinationDomain != localDomain); + + vm.prank(owner); + messageTransmitter.updatePauser(_pauser); + + vm.prank(_pauser); + messageTransmitter.pause(); + assertTrue(messageTransmitter.paused()); + + vm.expectRevert("Pausable: paused"); + messageTransmitter.sendMessage( + _destinationDomain, + _recipient, + _destinationCaller, + _minFinalityThreshold, + _messageBody + ); + } + + function testSendMessage_revertsWhenSendingToLocalDomain( + bytes32 _recipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _messageBody + ) public { + vm.assume(_recipient != bytes32(0)); + vm.assume(_messageBody.length < maxMessageBodySize); + + vm.expectRevert("Domain is local domain"); + messageTransmitter.sendMessage( + localDomain, + _recipient, + _destinationCaller, + _minFinalityThreshold, + _messageBody + ); + } + + function testSendMessage_rejectsTooLargeMessage( + uint32 _destinationDomain, + bytes32 _recipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold + ) public { + vm.assume(_recipient != bytes32(0)); + vm.assume(_destinationDomain != localDomain); + + bytes memory _messageBody = new bytes(maxMessageBodySize + 1); + + vm.expectRevert("Message body exceeds max size"); + messageTransmitter.sendMessage( + _destinationDomain, + _recipient, + _destinationCaller, + _minFinalityThreshold, + _messageBody + ); + } + + function testSendMessage_rejectsZeroRecipient( + uint32 _destinationDomain, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _messageBody + ) public { + vm.assume(_messageBody.length < maxMessageBodySize); + vm.assume(_destinationDomain != localDomain); + + vm.expectRevert("Recipient must be nonzero"); + messageTransmitter.sendMessage( + _destinationDomain, + bytes32(0), // recipient + _destinationCaller, + _minFinalityThreshold, + _messageBody + ); + } + + function testSendMessage_succeeds( + uint32 _destinationDomain, + bytes32 _recipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _messageBody, + address _sender + ) public { + vm.assume(_recipient != bytes32(0)); + vm.assume(_messageBody.length < maxMessageBodySize); + vm.assume(_destinationDomain != localDomain); + + _sendMessage( + _destinationDomain, + _recipient, + _destinationCaller, + _minFinalityThreshold, + _messageBody, + _sender + ); + } + + function testReceiveMessage_revertsWhenPaused( + bytes calldata _message, + bytes calldata _attestation, + address _pauser + ) public { + vm.assume(_pauser != address(0)); + + // Pause + vm.prank(owner); + messageTransmitter.updatePauser(_pauser); + vm.prank(_pauser); + messageTransmitter.pause(); + vm.stopPrank(); + + // Sanity check + assertTrue(messageTransmitter.paused()); + + vm.expectRevert("Pausable: paused"); + messageTransmitter.receiveMessage(_message, _attestation); + } + + function testReceiveMessage_revertsWithZeroLengthAttestation( + bytes calldata _message + ) public { + vm.expectRevert("Invalid attestation length"); + messageTransmitter.receiveMessage(_message, ""); + } + + function testReceiveMessage_revertsWithTooShortAttestation( + bytes calldata _message, + bytes calldata _attestation + ) public { + _setup2of3Multisig(); + + uint256 _expectedAttestationLength = 2 * SIGNATURE_LENGTH; + vm.assume( + _attestation.length > 0 && + _attestation.length < _expectedAttestationLength + ); + + vm.expectRevert("Invalid attestation length"); + messageTransmitter.receiveMessage(_message, _attestation); + } + + function testReceiveMessage_revertsWithTooLongAttestation( + bytes calldata _message + ) public { + _setup2of3Multisig(); + + uint256 _expectedAttestationLength = 2 * SIGNATURE_LENGTH; + bytes memory _attestation = new bytes(_expectedAttestationLength + 1); + + vm.expectRevert("Invalid attestation length"); + messageTransmitter.receiveMessage(_message, _attestation); + } + + function testReceiveMessage_revertsWhenSignerIsNotEnabled( + bytes calldata _message + ) public { + uint256[] memory _fakeAttesterPrivateKeys = new uint256[](1); + _fakeAttesterPrivateKeys[0] = fakeAttesterPK; + bytes memory _signature = _signMessage( + _message, + _fakeAttesterPrivateKeys + ); + + vm.expectRevert("Invalid signature: not attester"); + messageTransmitter.receiveMessage(_message, _signature); + } + + function testReceiveMessage_revertsIfAttestationSignaturesAreOutOfOrder( + bytes calldata _message + ) public { + _setup2of2Multisig(); + + uint256[] memory attesterPrivateKeys = new uint256[](2); + // manually sign, with attesters in reverse order + attesterPrivateKeys[0] = attesterPK; + attesterPrivateKeys[1] = secondAttesterPK; + bytes memory _signature = _signMessage(_message, attesterPrivateKeys); + + vm.expectRevert("Invalid signature order or dupe"); + messageTransmitter.receiveMessage(_message, _signature); + } + + function testReceiveMessage_revertsIfFirstSignatureIsEmpty( + bytes calldata _message + ) public { + _setup2of3Multisig(); + + uint256[] memory _attesterPrivateKeys = new uint256[](1); + _attesterPrivateKeys[0] = attesterPK; + bytes memory _signature = _signMessage(_message, _attesterPrivateKeys); + + bytes memory _validSignaturePrependedWithEmptySig = abi.encodePacked( + zeroSignature, + _signature + ); + + vm.expectRevert("ECDSA: invalid signature 'v' value"); + messageTransmitter.receiveMessage( + _message, + _validSignaturePrependedWithEmptySig + ); + } + + function testReceiveMessage_revertsIfLastSignatureIsEmpty( + bytes calldata _message + ) public { + _setup2of3Multisig(); + + uint256[] memory _attesterPrivateKeys = new uint256[](1); + _attesterPrivateKeys[0] = attesterPK; + bytes memory _signature = _signMessage(_message, _attesterPrivateKeys); + + bytes memory _validSignaturePrependedWithEmptySig = abi.encodePacked( + _signature, + zeroSignature + ); + + vm.expectRevert("ECDSA: invalid signature 'v' value"); + messageTransmitter.receiveMessage( + _message, + _validSignaturePrependedWithEmptySig + ); + } + + function testReceiveMessage_revertsIfAttestationHasDuplicatedSignatures( + bytes calldata _message + ) public { + _setup2of3Multisig(); + + uint256[] memory _attesterPrivateKeys = new uint256[](2); + // attempt to use same private key to sign twice (disallowed) + _attesterPrivateKeys[0] = attesterPK; + _attesterPrivateKeys[1] = attesterPK; + bytes memory _signature = _signMessage(_message, _attesterPrivateKeys); + + vm.expectRevert("Invalid signature order or dupe"); + messageTransmitter.receiveMessage(_message, _signature); + } + + function testReceiveMessage_revertsIfAttestationSignaturesAreAllEmpty( + bytes calldata _message + ) public { + _setup2of3Multisig(); + bytes memory _emptySigs = abi.encodePacked( + zeroSignature, + zeroSignature + ); + + vm.expectRevert("ECDSA: invalid signature 'v' value"); + messageTransmitter.receiveMessage(_message, _emptySigs); + } + + function testReceiveMessage_revertsIfMessageIsTooShort( + bytes calldata _message + ) public { + // See: MessageV2.sol#MESSAGE_BODY_INDEX + vm.assume(_message.length < 148); + + // Produce a valid signature + bytes memory _signature = _sign1of1Message(_message); + + vm.expectRevert("Invalid message: too short"); + messageTransmitter.receiveMessage(_message, _signature); + } + + function testReceiveMessage_revertsIfMessageHasInvalidDestinationDomain( + uint32 _version, + uint32 _sourceDomain, + uint32 _destinationDomain, + bytes32 _nonce, + bytes32 _sender, + bytes32 _recipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_destinationDomain != messageTransmitter.localDomain()); + bytes memory _message = _formatMessageForReceive( + _version, + _sourceDomain, + _destinationDomain, + _nonce, + _sender, + _recipient, + _destinationCaller, + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + + bytes memory _attestation = _sign1of1Message(_message); + + vm.expectRevert("Invalid destination domain"); + messageTransmitter.receiveMessage(_message, _attestation); + } + + function testReceiveMessage_revertsIfCallerIsNotNonZeroDestinationCaller( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + bytes32 _recipient, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody, + address _caller + ) public { + vm.assume(_caller != destinationCallerAddr); + + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + _recipient, + destinationCaller, + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + + vm.prank(_caller); + vm.expectRevert("Invalid caller for message"); + messageTransmitter.receiveMessage(_message, _signature); + } + + function testReceiveMessage_revertsIfMessageVersionIsInvalid( + uint32 _version, + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + bytes32 _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_version != version); + + bytes memory _message = _formatMessageForReceive( + _version, + _sourceDomain, + destinationDomain, + _nonce, + _sender, + _recipient, + _destinationCaller.toBytes32(), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + + bytes memory _attestation = _sign1of1Message(_message); + + if (_destinationCaller != address(0)) { + vm.prank(_destinationCaller); + } + vm.expectRevert("Invalid message version"); + messageTransmitter.receiveMessage(_message, _attestation); + } + + function testReceiveMessage_revertsIfHandleReceiveFinalizedMessageReverts( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); + + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + _recipient.toBytes32(), + _destinationCaller.toBytes32(), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + + // Mock a revert + bytes memory _call = abi.encodeWithSelector( + IMessageHandlerV2.handleReceiveFinalizedMessage.selector, + _sourceDomain, + _sender, + _finalityThresholdExecuted, + _messageBody + ); + vm.mockCallRevert(_recipient, _call, "Testing"); + + vm.prank(_destinationCaller); + vm.expectRevert(); + messageTransmitter.receiveMessage(_message, _signature); + } + + function testReceiveMessage_revertsIfHandleReceiveUnfinalizedMessageReverts( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_finalityThresholdExecuted < FINALITY_THRESHOLD_FINALIZED); + + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + _recipient.toBytes32(), + _destinationCaller.toBytes32(), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + + // Mock a revert + bytes memory _call = abi.encodeWithSelector( + IMessageHandlerV2.handleReceiveUnfinalizedMessage.selector, + _sourceDomain, + _sender, + _finalityThresholdExecuted, + _messageBody + ); + vm.mockCallRevert(_recipient, _call, "Testing"); + + vm.prank(_destinationCaller); + vm.expectRevert(); + messageTransmitter.receiveMessage(_message, _signature); + } + + function testReceiveMessage_revertsIfHandleReceiveFinalizedMessageReturnsFalse( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); + vm.assume(_recipient != foundryCheatCodeAddr); + vm.assume(_nonce != bytes32(0)); + + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + _recipient.toBytes32(), + _destinationCaller.toBytes32(), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + + // Mock returning false + bytes memory _call = abi.encodeWithSelector( + IMessageHandlerV2.handleReceiveFinalizedMessage.selector, + _sourceDomain, + _sender, + _finalityThresholdExecuted, + _messageBody + ); + vm.mockCall(_recipient, _call, abi.encode(false)); + vm.expectCall(_recipient, _call); + + vm.prank(_destinationCaller); + vm.expectRevert("handleReceiveFinalizedMessage() failed"); + messageTransmitter.receiveMessage(_message, _signature); + } + + function testReceiveMessage_revertsIfHandleReceiveUnfinalizedMessageReturnsFalse( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_nonce != bytes32(0)); + vm.assume(_finalityThresholdExecuted < FINALITY_THRESHOLD_FINALIZED); + vm.assume(_recipient != foundryCheatCodeAddr); + + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + _recipient.toBytes32(), + _destinationCaller.toBytes32(), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + + // Mock returning false + bytes memory _call = abi.encodeWithSelector( + IMessageHandlerV2.handleReceiveUnfinalizedMessage.selector, + _sourceDomain, + _sender, + _finalityThresholdExecuted, + _messageBody + ); + vm.mockCall(_recipient, _call, abi.encode(false)); + vm.expectCall(_recipient, _call, 1); + + vm.prank(_destinationCaller); + vm.expectRevert("handleReceiveUnfinalizedMessage() failed"); + messageTransmitter.receiveMessage(_message, _signature); + } + + function testReceiveMessage_revertsIfNonceIsZero( + uint32 _sourceDomain, + bytes32 _sender, + bytes32 _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + destinationDomain, + bytes32(0), // nonce + _sender, + _recipient, + _destinationCaller.toBytes32(), + _minFinalityThreshold, // minFinalityThreshold + _finalityThresholdExecuted, + _messageBody + ); + + bytes memory _attestation = _sign1of1Message(_message); + + if (_destinationCaller != address(0)) { + vm.prank(_destinationCaller); + } + vm.expectRevert("Nonce already used"); + messageTransmitter.receiveMessage(_message, _attestation); + } + + function testReceiveMessage_revertsIfNonceIsAlreadyUsed( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody, + address _randomCaller + ) public { + vm.assume(_nonce != bytes32(0)); + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + _recipient.toBytes32(), + _destinationCaller.toBytes32(), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + _receiveMessage(_message, _signature, _randomCaller); + + // Try again + vm.prank(_destinationCaller); + vm.expectRevert("Nonce already used"); + messageTransmitter.receiveMessage(_message, _signature); + } + + function testReceiveMessage_rejectsReusedNonceFromReentrantCaller( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted + ) public { + vm.assume(_nonce != bytes32(0)); + MockReentrantCallerV2 _mockReentrantCaller = new MockReentrantCallerV2(); + + // Encode mockReentrantCaller as recipient + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + address(_mockReentrantCaller).toBytes32(), + bytes32(0), + _minFinalityThreshold, + _finalityThresholdExecuted, + bytes("reenter") + ); + bytes memory _signature = _sign1of1Message(_message); + _mockReentrantCaller.setMessageAndSignature(_message, _signature); + + // fail to call receiveMessage twice in same transaction + vm.expectRevert("Re-entrant call failed due to reused nonce"); + messageTransmitter.receiveMessage(_message, _signature); + + // Check that nonce was not consumed + assertEq(messageTransmitter.usedNonces(_nonce), 0); + } + + function testReceiveMessage_succeedsWith1of1Signing( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody, + address _randomCaller + ) public { + vm.assume(_nonce != bytes32(0)); + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + _recipient.toBytes32(), + _destinationCaller.toBytes32(), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + _receiveMessage(_message, _signature, _randomCaller); + } + + function testReceiveMessage_succeedsWith2of2Signing( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody, + address _randomCaller + ) public { + vm.assume(_nonce != bytes32(0)); + _setup2of2Multisig(); + + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + _recipient.toBytes32(), + _destinationCaller.toBytes32(), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign2OfNMultisigMessage(_message); + _receiveMessage(_message, _signature, _randomCaller); + } + + function testReceiveMessage_succeedsWith2of3Signing( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody, + address _randomCaller + ) public { + vm.assume(_nonce != bytes32(0)); + _setup2of3Multisig(); + + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + _recipient.toBytes32(), + _destinationCaller.toBytes32(), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign2OfNMultisigMessage(_message); + _receiveMessage(_message, _signature, _randomCaller); + } + + function testReceiveMessage_succeedsWithFinalizedMessage( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody, + address _randomCaller + ) public { + vm.assume(_nonce != bytes32(0)); + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + _recipient.toBytes32(), + _destinationCaller.toBytes32(), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + _receiveMessage(_message, _signature, _randomCaller); + } + + function testReceiveMessage_succeedsWithUnfinalizedMessage( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody, + address _randomCaller + ) public { + vm.assume(_nonce != bytes32(0)); + vm.assume(_finalityThresholdExecuted < FINALITY_THRESHOLD_FINALIZED); + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + _recipient.toBytes32(), + _destinationCaller.toBytes32(), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + _receiveMessage(_message, _signature, _randomCaller); + } + + function testReceiveMessage_succeedsWithNonZeroDestinationCaller( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_nonce != bytes32(0)); + vm.assume(_destinationCaller != address(0)); + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + _recipient.toBytes32(), + _destinationCaller.toBytes32(), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + _receiveMessage(_message, _signature, _destinationCaller); + } + + function testReceiveMessage_succeedsWithZeroDestinationCaller( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody, + address _randomCaller + ) public { + vm.assume(_nonce != bytes32(0)); + vm.assume(_randomCaller != address(0)); + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + _recipient.toBytes32(), + bytes32(0), // destinationCaller + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + _receiveMessage(_message, _signature, _randomCaller); + } + + function testSendMaxMessageBodySize_revertsOnNonOwner( + uint256 _newMaxMessageBodySize, + address _notOwner + ) public { + vm.assume(_notOwner != messageTransmitter.owner()); + expectRevertWithWrongOwner(_notOwner); + messageTransmitter.setMaxMessageBodySize(_newMaxMessageBodySize); + } + + function testSetMaxMessageBodySize_succeeds( + uint256 _newMaxMessageBodySize + ) public { + vm.assume(_newMaxMessageBodySize != maxMessageBodySize); + + // Set new max size + vm.expectEmit(true, true, true, true); + emit MaxMessageBodySizeUpdated(_newMaxMessageBodySize); + vm.prank(owner); + messageTransmitter.setMaxMessageBodySize(_newMaxMessageBodySize); + assertEq( + messageTransmitter.maxMessageBodySize(), + _newMaxMessageBodySize + ); + } + + function testRescuable( + address _rescuer, + address _rescueRecipient, + uint256 _amount, + address _nonRescuer + ) public { + assertContractIsRescuable( + address(messageTransmitter), + _rescuer, + _rescueRecipient, + _amount, + _nonRescuer + ); + } + + function testPausable( + address _currentPauser, + address _newPauser, + address _nonOwner + ) public { + vm.assume(_currentPauser != address(0)); + vm.prank(owner); + messageTransmitter.updatePauser(_currentPauser); + + assertContractIsPausable( + address(messageTransmitter), + _currentPauser, + _newPauser, + messageTransmitter.owner(), + _nonOwner + ); + } + + function testTransferOwnership_revertsFromNonOwner( + address _newOwner, + address _nonOwner + ) public { + transferOwnership_revertsFromNonOwner( + address(messageTransmitter), + _newOwner, + _nonOwner + ); + } + + function testAcceptOwnership_revertsFromNonPendingOwner( + address _newOwner, + address _nonOwner + ) public { + acceptOwnership_revertsFromNonPendingOwner( + address(messageTransmitter), + _newOwner, + _nonOwner + ); + } + + function testTransferOwnershipAndAcceptOwnership(address _newOwner) public { + transferOwnershipAndAcceptOwnership( + address(messageTransmitter), + _newOwner + ); + } + + function testTransferOwnershipWithoutAcceptingThenTransferToNewOwner( + address _newOwner, + address _secondNewOwner + ) public { + transferOwnershipWithoutAcceptingThenTransferToNewOwner( + address(messageTransmitter), + _newOwner, + _secondNewOwner + ); + } + + // Internal utility functions + + function _sendMessage( + uint32 _destinationDomain, + bytes32 _recipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _messageBody, + address _sender + ) internal { + bytes memory _expectedMessage = MessageV2._formatMessageForRelay( + version, + localDomain, + _destinationDomain, + _sender.toBytes32(), + _recipient, + _destinationCaller, + _minFinalityThreshold, + _messageBody + ); + + assertFalse(messageTransmitter.paused()); + + // assert that a MessageSent event was logged with expected message bytes + vm.prank(_sender); + vm.expectEmit(true, true, true, true); + emit MessageSent(_expectedMessage); + messageTransmitter.sendMessage( + _destinationDomain, + _recipient, + _destinationCaller, + _minFinalityThreshold, + _messageBody + ); + vm.stopPrank(); + } + + // Calls receiveMessage with msg.destinationCaller if set; otherwise + // with `_randomCaller` + function _receiveMessage( + bytes memory _message, + bytes memory _signature, + address _randomCaller + ) internal { + bytes29 _msg = _message.ref(0); + address _recipient = _msg._getRecipient().toAddress(); + vm.assume(_recipient != foundryCheatCodeAddr); + + // Mock a successful response from IMessageHandlerV2 to message.recipient, + // and expect it to be called once. + bytes memory _encodedMessageHandlerCall = abi.encodeWithSelector( + _msg._getFinalityThresholdExecuted() >= FINALITY_THRESHOLD_FINALIZED + ? IMessageHandlerV2.handleReceiveFinalizedMessage.selector + : IMessageHandlerV2.handleReceiveUnfinalizedMessage.selector, + _msg._getSourceDomain(), + _msg._getSender(), + _msg._getFinalityThresholdExecuted(), + _msg._getMessageBody().clone() + ); + vm.mockCall(_recipient, _encodedMessageHandlerCall, abi.encode(true)); + vm.expectCall(_recipient, _encodedMessageHandlerCall, 1); + + // Spoof the destination caller if needed + address _caller; + if (_msg._getDestinationCaller() == bytes32(0)) { + // Don't spoof the 0-address; defeats the purpose of the test + vm.assume(_randomCaller != address(0)); + _caller = _randomCaller; + } else { + _caller = _msg._getDestinationCaller().toAddress(); + } + + // assert that a MessageReceive event was logged with expected message bytes + vm.expectEmit(true, true, true, true); + emit MessageReceived( + _caller, + _msg._getSourceDomain(), + _msg._getNonce(), + _msg._getSender(), + _msg._getFinalityThresholdExecuted(), + _msg._getMessageBody().clone() + ); + + // Receive message + vm.prank(_caller); + assertTrue(messageTransmitter.receiveMessage(_message, _signature)); + vm.stopPrank(); + + // Check that the nonce is now used + assertEq( + messageTransmitter.usedNonces(_msg._getNonce()), + messageTransmitter.NONCE_USED() + ); + } + + // setup second and third attester (first set in setUp()); set sig threshold at 2 + function _setup2of3Multisig() internal { + vm.startPrank(messageTransmitter.attesterManager()); + messageTransmitter.enableAttester(secondAttester); + messageTransmitter.enableAttester(thirdAttester); + messageTransmitter.setSignatureThreshold(2); + vm.stopPrank(); + } + + // setup second attester (first set in setUp()); set sig threshold at 2 + function _setup2of2Multisig() internal { + vm.startPrank(messageTransmitter.attesterManager()); + messageTransmitter.enableAttester(secondAttester); + messageTransmitter.setSignatureThreshold(2); + vm.stopPrank(); + } + + function _sign1of1Message( + bytes memory _message + ) internal returns (bytes memory) { + uint256[] memory _privateKeys = new uint256[](1); + _privateKeys[0] = attesterPK; + return _signMessage(_message, _privateKeys); + } + + function _sign2OfNMultisigMessage( + bytes memory _message + ) internal returns (bytes memory _signature) { + uint256[] memory attesterPrivateKeys = new uint256[](2); + // manually sort attesters in correct order + attesterPrivateKeys[1] = attesterPK; + // attester == 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf + attesterPrivateKeys[0] = secondAttesterPK; + // second attester = 0x6813eb9362372eef6200f3b1dbc3f819671cba69 + // sanity check order + assertTrue(attester > secondAttester); + assertTrue(secondAttester > address(0)); + return _signMessage(_message, attesterPrivateKeys); + } + + function _formatMessageForReceive( + uint32 _version, + uint32 _sourceDomain, + uint32 _destinationDomain, + bytes32 _nonce, + bytes32 _sender, + bytes32 _recipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes memory _messageBody + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + _version, + _sourceDomain, + _destinationDomain, + _nonce, + _sender, + _recipient, + _destinationCaller, + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + } +} diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol new file mode 100644 index 0000000..fa8552d --- /dev/null +++ b/test/v2/TokenMessengerV2.t.sol @@ -0,0 +1,2904 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {BaseTokenMessengerTest} from "./BaseTokenMessenger.t.sol"; +import {TokenMessengerV2} from "../../src/v2/TokenMessengerV2.sol"; +import {AddressUtils} from "../../src/messages/v2/AddressUtils.sol"; +import {MockMintBurnToken} from "../mocks/MockMintBurnToken.sol"; +import {TokenMinterV2} from "../../src/v2/TokenMinterV2.sol"; +import {TokenMinter} from "../../src/TokenMinter.sol"; +import {MessageTransmitterV2} from "../../src/v2/MessageTransmitterV2.sol"; +import {BurnMessageV2} from "../../src/messages/v2/BurnMessageV2.sol"; +import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol"; +import {AdminUpgradableProxy} from "../../src/proxy/AdminUpgradableProxy.sol"; +import {MockTokenMessengerV3} from "../mocks/v2/MockTokenMessengerV3.sol"; +import {TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD, FINALITY_THRESHOLD_FINALIZED, FINALITY_THRESHOLD_CONFIRMED} from "../../src/v2/FinalityThresholds.sol"; + +contract TokenMessengerV2Test is BaseTokenMessengerTest { + // Events + event DepositForBurn( + address indexed burnToken, + uint256 amount, + address indexed depositor, + bytes32 mintRecipient, + uint32 destinationDomain, + bytes32 destinationTokenMessenger, + bytes32 destinationCaller, + uint256 maxFee, + uint32 indexed minFinalityThreshold, + bytes hookData + ); + + event MintAndWithdraw( + address indexed mintRecipient, + uint256 amount, + address indexed mintToken, + uint256 feeCollected + ); + + event Upgraded(address indexed implementation); + + event DenylisterChanged( + address indexed oldDenylister, + address indexed newDenylister + ); + + // Libraries + using TypedMemView for bytes; + using TypedMemView for bytes29; + using BurnMessageV2 for bytes29; + using AddressUtils for address; + using AddressUtils for bytes32; + + // Constants + uint32 remoteDomain = 1; + uint32 messageBodyVersion = 2; + + address localMessageTransmitter = address(10); + address remoteMessageTransmitter = address(20); + + TokenMessengerV2 localTokenMessenger; + TokenMessengerV2 tokenMessengerImpl; + + address remoteTokenMessenger = address(30); + bytes32 remoteTokenMessengerAddr; + + address remoteTokenAddr = address(40); + + // TokenMessengerV2 Roles + address feeRecipient = address(50); + address denylister = address(60); + address proxyAdmin = address(70); + address rescuer = address(80); + + MockMintBurnToken localToken = new MockMintBurnToken(); + TokenMinterV2 localTokenMinter = new TokenMinterV2(tokenController); + + function setUp() public override { + // Deploy implementation + tokenMessengerImpl = new TokenMessengerV2( + localMessageTransmitter, + messageBodyVersion + ); + + // Deploy and initialize proxy + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + bytes("") + ); + + remoteTokenMessengerAddr = remoteTokenMessenger.toBytes32(); + + ( + uint32[] memory _remoteDomains, + bytes32[] memory _remoteTokenMessengers + ) = _defaultRemoteTokenMessengers(); + + TokenMessengerV2(address(_proxy)).initialize( + owner, + rescuer, + feeRecipient, + denylister, + address(localTokenMinter), + _remoteDomains, + _remoteTokenMessengers + ); + localTokenMessenger = TokenMessengerV2(address(_proxy)); + + linkTokenPair( + localTokenMinter, + address(localToken), + remoteDomain, + remoteTokenAddr.toBytes32() + ); + + localTokenMinter.addLocalTokenMessenger(address(localTokenMessenger)); + + super.setUp(); + } + + // BaseTokenMessengerTest overrides + + function setUpBaseTokenMessenger() internal override returns (address) { + TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( + localMessageTransmitter, + messageBodyVersion + ); + return address(_tokenMessenger); + } + + function createBaseTokenMessenger( + address _localMessageTransmitter, + uint32 _messageBodyVersion + ) internal override returns (address) { + TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( + _localMessageTransmitter, + _messageBodyVersion + ); + return address(_tokenMessenger); + } + + // Tests + + function testStorageSlots_hasAGapForDenylistableAdditions() public view { + // Denylistable slots are arranged at slots 3-5 + // Sanity check this by reading from a Denylistable storage var + // the denylister is stored at slot 3 + address _denylister = vm + .load(address(localTokenMessenger), bytes32(uint256(3))) + .toAddress(); + assertEq(_denylister, localTokenMessenger.denylister()); + + // Check that the next storage vars, defined in BaseTokenMessenger, are gapped + // by 20 slots + // The localMinter is stored at slot 55 + address _localMinter = vm + .load(address(localTokenMessenger), bytes32(uint256(25))) + .toAddress(); + + assertEq(_localMinter, address(localTokenMessenger.localMinter())); + } + + function testInitialize_revertsIfOwnerIsZeroAddress() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + bytes("") + ); + vm.expectRevert("Owner is the zero address"); + + ( + uint32[] memory _remoteDomains, + bytes32[] memory _remoteTokenMessengers + ) = _defaultRemoteTokenMessengers(); + + TokenMessengerV2(address(_proxy)).initialize( + address(0), + rescuer, + feeRecipient, + denylister, + address(localTokenMinter), + _remoteDomains, + _remoteTokenMessengers + ); + } + + function testInitialize_revertsIfRescuerIsZeroAddress() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + bytes("") + ); + + ( + uint32[] memory _remoteDomains, + bytes32[] memory _remoteTokenMessengers + ) = _defaultRemoteTokenMessengers(); + + vm.expectRevert("Rescuable: new rescuer is the zero address"); + TokenMessengerV2(address(_proxy)).initialize( + owner, + address(0), + feeRecipient, + denylister, + address(localTokenMinter), + _remoteDomains, + _remoteTokenMessengers + ); + } + + function testInitialize_revertsIfFeeRecipientIsZeroAddress() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + bytes("") + ); + + ( + uint32[] memory _remoteDomains, + bytes32[] memory _remoteTokenMessengers + ) = _defaultRemoteTokenMessengers(); + + vm.expectRevert("Zero address not allowed"); + TokenMessengerV2(address(_proxy)).initialize( + owner, + rescuer, + address(0), + denylister, + address(localTokenMinter), + _remoteDomains, + _remoteTokenMessengers + ); + } + + function testInitialize_revertsIfDenylisterIsZeroAddress() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + bytes("") + ); + + ( + uint32[] memory _remoteDomains, + bytes32[] memory _remoteTokenMessengers + ) = _defaultRemoteTokenMessengers(); + + vm.expectRevert("Denylistable: new denylister is the zero address"); + TokenMessengerV2(address(_proxy)).initialize( + owner, + rescuer, + feeRecipient, + address(0), + address(localTokenMinter), + _remoteDomains, + _remoteTokenMessengers + ); + } + + function testInitialize_revertsIfTokenMinterIsZeroAddress() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + bytes("") + ); + + ( + uint32[] memory _remoteDomains, + bytes32[] memory _remoteTokenMessengers + ) = _defaultRemoteTokenMessengers(); + + vm.expectRevert("Zero address not allowed"); + TokenMessengerV2(address(_proxy)).initialize( + owner, + rescuer, + feeRecipient, + denylister, + address(0), + _remoteDomains, + _remoteTokenMessengers + ); + } + + function testInitialize_revertsIfRemoteDomainsDoNotMatchRemoteMessengers() + public + { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + bytes("") + ); + + (uint32[] memory _remoteDomains, ) = _defaultRemoteTokenMessengers(); + + // Add an extra remote token messenger + bytes32[] memory _remoteTokenMessengers = new bytes32[]( + _remoteDomains.length + 1 + ); + for (uint256 i; i < _remoteTokenMessengers.length; i++) { + _remoteTokenMessengers[i] = bytes32("test"); + } + + vm.expectRevert("Invalid remote domain configuration"); + TokenMessengerV2(address(_proxy)).initialize( + owner, + rescuer, + feeRecipient, + denylister, + address(localTokenMinter), + _remoteDomains, + _remoteTokenMessengers + ); + } + + function testInitialize_revertsIfRemoteTokenMessengerIsZeroAddress() + public + { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + bytes("") + ); + + bytes32[] memory _remoteTokenMessengers = new bytes32[](2); + _remoteTokenMessengers[0] = bytes32("1"); + _remoteTokenMessengers[1] = bytes32(""); // empty + + uint32[] memory _remoteDomains = new uint32[](2); + _remoteDomains[0] = 1; + _remoteDomains[1] = 2; + + vm.expectRevert("bytes32(0) not allowed"); + TokenMessengerV2(address(_proxy)).initialize( + owner, + rescuer, + feeRecipient, + denylister, + address(localTokenMinter), + _remoteDomains, + _remoteTokenMessengers + ); + } + + function testInitialize_succeedsIfRemoteDomainsIsEmpty() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + bytes("") + ); + + uint32[] memory _remoteDomains = new uint32[](0); + bytes32[] memory _remoteTokenMessengers = new bytes32[](0); + + TokenMessengerV2(address(_proxy)).initialize( + owner, + rescuer, + feeRecipient, + denylister, + address(localTokenMinter), + _remoteDomains, + _remoteTokenMessengers + ); + } + + function testInitialize_revertsIfCalledTwice() public { + ( + uint32[] memory _remoteDomains, + bytes32[] memory _remoteTokenMessengers + ) = _defaultRemoteTokenMessengers(); + + vm.expectRevert("Initializable: invalid initialization"); + localTokenMessenger.initialize( + owner, + rescuer, + feeRecipient, + denylister, + address(localTokenMinter), + _remoteDomains, + _remoteTokenMessengers + ); + } + + function testInitialize_setsTheOwner() public view { + assertEq(localTokenMessenger.owner(), owner); + } + + function testInitialize_setsTheRescuer() public view { + assertEq(localTokenMessenger.rescuer(), rescuer); + } + + function testInitialize_setsTheFeeRecipient() public view { + assertEq(localTokenMessenger.feeRecipient(), feeRecipient); + } + + function testInitialize_setsTheDenylister() public view { + assertEq(localTokenMessenger.denylister(), denylister); + } + + function testInitialize_setsTheTokenMinter() public view { + assertEq( + address(localTokenMessenger.localMinter()), + address(localTokenMinter) + ); + } + + function testInitialize_setsSingleRemoteTokenMessenger() public view { + assertEq( + bytes32(localTokenMessenger.remoteTokenMessengers(remoteDomain)), + remoteTokenMessengerAddr + ); + } + + function testInitialize_setsMultipleRemoteTokenMessengers() public { + bytes32[] memory _remoteTokenMessengers = new bytes32[](2); + _remoteTokenMessengers[0] = bytes32("1"); + _remoteTokenMessengers[1] = bytes32("2"); + + uint32[] memory _remoteDomains = new uint32[](2); + _remoteDomains[0] = 1; + _remoteDomains[1] = 2; + + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + bytes("") + ); + + TokenMessengerV2(address(_proxy)).initialize( + owner, + rescuer, + feeRecipient, + denylister, + address(localTokenMinter), + _remoteDomains, + _remoteTokenMessengers + ); + assertEq( + bytes32(TokenMessengerV2(address(_proxy)).remoteTokenMessengers(1)), + bytes32("1") + ); + assertEq( + bytes32(TokenMessengerV2(address(_proxy)).remoteTokenMessengers(2)), + bytes32("2") + ); + } + + function testInitialize_setsTheInitializedVersion() public view { + assertEq(uint256(localTokenMessenger.initializedVersion()), 1); + } + + function testInitialize_canBeCalledAtomicallyByTheProxy() public { + ( + uint32[] memory _remoteDomains, + bytes32[] memory _remoteTokenMessengers + ) = _defaultRemoteTokenMessengers(); + + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + abi.encodeWithSelector( + TokenMessengerV2.initialize.selector, + owner, + rescuer, + feeRecipient, + denylister, + address(localTokenMinter), + _remoteDomains, + _remoteTokenMessengers + ) + ); + assertEq(TokenMessengerV2(address(_proxy)).owner(), owner); + assertEq(TokenMessengerV2(address(_proxy)).rescuer(), rescuer); + assertEq( + TokenMessengerV2(address(_proxy)).feeRecipient(), + feeRecipient + ); + assertEq( + uint256(TokenMessengerV2(address(_proxy)).initializedVersion()), + 1 + ); + assertEq(TokenMessengerV2(address(_proxy)).denylister(), denylister); + assertEq( + address(TokenMessengerV2(address(_proxy)).localMinter()), + address(localTokenMinter) + ); + assertEq( + TokenMessengerV2(address(_proxy)).remoteTokenMessengers( + remoteDomain + ), + remoteTokenMessengerAddr + ); + } + + function testInitialize_emitsEvents() public { + ( + uint32[] memory _remoteDomains, + bytes32[] memory _remoteTokenMessengers + ) = _defaultRemoteTokenMessengers(); + + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + bytes("") + ); + + TokenMessengerV2 _tokenMessenger = TokenMessengerV2(address(_proxy)); + + vm.expectEmit(true, true, true, true); + emit OwnershipTransferred(address(0), owner); + + vm.expectEmit(true, true, true, true); + emit RescuerChanged(rescuer); + + vm.expectEmit(true, true, true, true); + emit DenylisterChanged(address(0), denylister); + + vm.expectEmit(true, true, true, true); + emit FeeRecipientSet(feeRecipient); + + vm.expectEmit(true, true, true, true); + emit LocalMinterAdded(address(localTokenMinter)); + + vm.expectEmit(true, true, true, true); + emit RemoteTokenMessengerAdded(remoteDomain, remoteTokenMessengerAddr); + + _tokenMessenger.initialize( + owner, + rescuer, + feeRecipient, + denylister, + address(localTokenMinter), + _remoteDomains, + _remoteTokenMessengers + ); + } + + function testUpgrade_succeeds() public { + AdminUpgradableProxy _proxy = AdminUpgradableProxy( + payable(address(localTokenMessenger)) + ); + + // Sanity check + assertEq(_proxy.implementation(), address(tokenMessengerImpl)); + + // Test that we can upgrade to a v3 TokenMessenger + // Deploy v3 implementation + MockTokenMessengerV3 _implV3 = new MockTokenMessengerV3( + localMessageTransmitter, + messageBodyVersion + 1 + ); + + // Upgrade + vm.prank(proxyAdmin); + vm.expectEmit(true, true, true, true); + emit Upgraded(address(_implV3)); + _proxy.upgradeTo(address(_implV3)); + + // Sanity checks + assertEq(_proxy.implementation(), address(_implV3)); + assertTrue(MockTokenMessengerV3(address(_proxy)).v3Function()); + // Check that the message body version is updated + assertEq( + uint256(localTokenMessenger.messageBodyVersion()), + uint256(messageBodyVersion + 1) + ); + } + + function testDepositForBurn_revertsIfMsgSenderIsOnDenylist( + address _messageSender, + address _txOriginator, + uint256 _amount, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_amount > 0); + vm.assume(_messageSender != _txOriginator); + + // Add messageSender to deny list + vm.prank(denylister); + localTokenMessenger.denylist(_messageSender); + assertTrue(localTokenMessenger.isDenylisted(_messageSender)); + assertFalse(localTokenMessenger.isDenylisted(_txOriginator)); + + vm.prank(_messageSender, _txOriginator); + vm.expectRevert("Denylistable: account is on denylist"); + localTokenMessenger.depositForBurn( + _amount, + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + 0, + _minFinalityThreshold + ); + } + + function testDepositForBurn_revertsIfTxOriginatorIsOnDenylist( + address _messageSender, + address _txOriginator, + uint256 _amount, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_amount > 0); + vm.assume(_messageSender != _txOriginator); + + // Add _txOriginator to deny list + vm.prank(denylister); + localTokenMessenger.denylist(_txOriginator); + assertTrue(localTokenMessenger.isDenylisted(_txOriginator)); + assertFalse(localTokenMessenger.isDenylisted(_messageSender)); + + vm.prank(_messageSender, _txOriginator); + vm.expectRevert("Denylistable: account is on denylist"); + localTokenMessenger.depositForBurn( + _amount, + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + 0, + _minFinalityThreshold + ); + } + + function testDepositForBurn_revertsIfBothTxOriginatorAndMsgSenderAreOnDenylist( + address _messageSender, + address _txOriginator, + uint256 _amount, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold + ) public { + vm.assume(_messageSender != _txOriginator); + + // Add both to deny list + vm.startPrank(denylister); + localTokenMessenger.denylist(_messageSender); + localTokenMessenger.denylist(_txOriginator); + assertTrue(localTokenMessenger.isDenylisted(_messageSender)); + assertTrue(localTokenMessenger.isDenylisted(_txOriginator)); + vm.stopPrank(); + + vm.prank(_messageSender, _txOriginator); + vm.expectRevert("Denylistable: account is on denylist"); + localTokenMessenger.depositForBurn( + _amount, + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + 0, + _minFinalityThreshold + ); + } + + function testDepositForBurn_revertsIfTransferAmountIsZero( + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold + ) public { + vm.assume(_mintRecipient != bytes32(0)); + + vm.expectRevert("Amount must be nonzero"); + localTokenMessenger.depositForBurn( + 0, // amount + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + 0, + _minFinalityThreshold + ); + } + + function testDepositForBurn_revertsIfMintRecipientIsZero( + uint256 _amount, + address _burnToken, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold + ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee < _amount); + + vm.expectRevert("Mint recipient must be nonzero"); + localTokenMessenger.depositForBurn( + _amount, + remoteDomain, + bytes32(0), // mintRecipient + _burnToken, + _destinationCaller, + _maxFee, + _minFinalityThreshold + ); + } + + function testDepositForBurn_revertsIfFeeEqualsTransferAmount( + uint256 _amount, + address _burnToken, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold + ) public { + vm.assume(_amount > 0); + vm.assume(_mintRecipient != bytes32(0)); + + vm.expectRevert("Max fee must be less than amount"); + localTokenMessenger.depositForBurn( + _amount, + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + _amount, // maxFee + _minFinalityThreshold + ); + } + + function testDepositForBurn_revertsIfFeeExceedsTransferAmount( + uint256 _amount, + address _burnToken, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold + ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee > _amount); + vm.assume(_mintRecipient != bytes32(0)); + + vm.expectRevert("Max fee must be less than amount"); + localTokenMessenger.depositForBurn( + _amount, + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + _maxFee, + _minFinalityThreshold + ); + } + + function testDepositForBurn_revertsIfNoRemoteTokenMessengerExistsForDomain( + uint256 _amount, + uint32 _remoteDomain, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold + ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee < _amount); + vm.assume(_mintRecipient != bytes32(0)); + + TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( + localMessageTransmitter, + messageBodyVersion + ); + + vm.expectRevert("No TokenMessenger for domain"); + _tokenMessenger.depositForBurn( + _amount, + _remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + _maxFee, + _minFinalityThreshold + ); + } + + function testDepositForBurn_revertsIfLocalMinterIsNotSet( + uint256 _amount, + uint32 _remoteDomain, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold + ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee < _amount); + vm.assume(_mintRecipient != bytes32(0)); + + TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( + localMessageTransmitter, + messageBodyVersion + ); + + _tokenMessenger.addRemoteTokenMessenger( + _remoteDomain, + remoteTokenMessengerAddr + ); + + vm.expectRevert("Local minter is not set"); + _tokenMessenger.depositForBurn( + _amount, + _remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + _maxFee, + _minFinalityThreshold + ); + } + + function testDepositForBurn_revertsOnFailedTokenTransfer( + uint256 _amount, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold + ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee < _amount); + vm.assume(_mintRecipient != bytes32(0)); + + vm.mockCall( + address(localToken), + abi.encodeWithSelector(MockMintBurnToken.transferFrom.selector), + abi.encode(false) + ); + vm.expectRevert("Transfer operation failed"); + localTokenMessenger.depositForBurn( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _maxFee, + _minFinalityThreshold + ); + } + + function testDepositForBurn_revertsIfTokenTransferReverts( + address _caller, + uint256 _amount, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold + ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee < _amount); + vm.assume(_mintRecipient != bytes32(0)); + + // TransferFrom will revert, as localTokenMessenger has no allowance + assertEq( + localToken.allowance(_caller, address(localTokenMessenger)), + 0 + ); + + vm.expectRevert("ERC20: transfer amount exceeds allowance"); + localTokenMessenger.depositForBurn( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _maxFee, + _minFinalityThreshold + ); + } + + function testDepositForBurn_revertsIfTransferAmountExceedsMaxBurnAmountPerMessage( + uint256 _amount, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold, + address _caller + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_maxFee < _amount); + vm.assume(_amount > 1); + vm.assume(_caller != address(0)); + + _setupDepositForBurn(_caller, _amount, _amount - 1); + + vm.prank(_caller); + vm.expectRevert("Burn amount exceeds per tx limit"); + localTokenMessenger.depositForBurn( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _maxFee, + _minFinalityThreshold + ); + } + + function testDepositForBurn_succeeds( + uint256 _amount, + uint256 _burnLimit, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + address _caller + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_amount > 1); + vm.assume(_amount < _burnLimit); + vm.assume(_caller != address(0)); + + uint256 _maxFee = _amount - 1; + + _setupDepositForBurn(_caller, _amount, _burnLimit); + + _depositForBurn( + _caller, + _mintRecipient, + _destinationCaller, + _amount, + _maxFee, + _minFinalityThreshold, + msg.data[0:0] + ); + } + + function testDepositForBurnWithHook_revertsIfMsgSenderIsOnDenylist( + address _messageSender, + address _txOriginator, + uint256 _amount, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _hookData + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_amount > 0); + vm.assume(_messageSender != _txOriginator); + + // Add messageSender to deny list + vm.prank(denylister); + localTokenMessenger.denylist(_messageSender); + assertTrue(localTokenMessenger.isDenylisted(_messageSender)); + assertFalse(localTokenMessenger.isDenylisted(_txOriginator)); + + vm.prank(_messageSender, _txOriginator); + vm.expectRevert("Denylistable: account is on denylist"); + localTokenMessenger.depositForBurnWithHook( + _amount, + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + 0, + _minFinalityThreshold, + _hookData + ); + } + + function testDepositForBurnWithHook_revertsIfTxOriginatorIsOnDenylist( + address _messageSender, + address _txOriginator, + uint256 _amount, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _hookData + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_amount > 0); + vm.assume(_messageSender != _txOriginator); + + // Add txOriginator to deny list + vm.prank(denylister); + localTokenMessenger.denylist(_txOriginator); + assertTrue(localTokenMessenger.isDenylisted(_txOriginator)); + assertFalse(localTokenMessenger.isDenylisted(_messageSender)); + + vm.prank(_messageSender, _txOriginator); + vm.expectRevert("Denylistable: account is on denylist"); + localTokenMessenger.depositForBurnWithHook( + _amount, + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + 0, + _minFinalityThreshold, + _hookData + ); + } + + function testDepositForBurnWithHook_revertsIfBothTxOriginatorAndMsgSenderAreOnDenylist( + address _messageSender, + address _txOriginator, + uint256 _amount, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _hookData + ) public { + vm.assume(_messageSender != _txOriginator); + + // Add both to deny list + vm.startPrank(denylister); + localTokenMessenger.denylist(_messageSender); + localTokenMessenger.denylist(_txOriginator); + assertTrue(localTokenMessenger.isDenylisted(_messageSender)); + assertTrue(localTokenMessenger.isDenylisted(_txOriginator)); + vm.stopPrank(); + + vm.prank(_messageSender, _txOriginator); + vm.expectRevert("Denylistable: account is on denylist"); + localTokenMessenger.depositForBurnWithHook( + _amount, + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + 0, + _minFinalityThreshold, + _hookData + ); + } + + function testDepositForBurnWithHook_revertsIfHookIsEmpty( + uint256 _amount, + uint256 _maxFee, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_maxFee < _amount); + vm.assume(_amount > 1); + + vm.expectRevert("Hook data is empty"); + localTokenMessenger.depositForBurnWithHook( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _maxFee, + _minFinalityThreshold, + bytes("") + ); + } + + function testDepositForBurnWithHook_revertsIfTransferAmountIsZero( + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _hookData + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hookData.length > 0); + + vm.expectRevert("Amount must be nonzero"); + localTokenMessenger.depositForBurnWithHook( + 0, // amount + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + 0, + _minFinalityThreshold, + _hookData + ); + } + + function testDepositForBurnWithHook_revertsIfMintRecipientIsZero( + uint256 _amount, + address _burnToken, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold, + bytes calldata _hookData + ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee < _amount); + vm.assume(_hookData.length > 0); + + vm.expectRevert("Mint recipient must be nonzero"); + localTokenMessenger.depositForBurnWithHook( + _amount, + remoteDomain, + bytes32(0), // mintRecipient + _burnToken, + _destinationCaller, + _maxFee, + _minFinalityThreshold, + _hookData + ); + } + + function testDepositForBurnWithHook_revertsIfFeeEqualsTransferAmount( + uint256 _amount, + address _burnToken, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _hookData + ) public { + vm.assume(_amount > 0); + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hookData.length > 0); + + vm.expectRevert("Max fee must be less than amount"); + localTokenMessenger.depositForBurnWithHook( + _amount, + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + _amount, // maxFee + _minFinalityThreshold, + _hookData + ); + } + + function testDepositForBurnWithHook_revertsIfFeeExceedsTransferAmount( + uint256 _amount, + address _burnToken, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold, + bytes calldata _hookData + ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee > _amount); + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hookData.length > 0); + + vm.expectRevert("Max fee must be less than amount"); + localTokenMessenger.depositForBurnWithHook( + _amount, + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + _maxFee, + _minFinalityThreshold, + _hookData + ); + } + + function testDepositForBurnWithHook_revertsIfNoRemoteTokenMessengerExistsForDomain( + uint256 _amount, + uint32 _remoteDomain, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold, + bytes calldata _hookData + ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee < _amount); + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hookData.length > 0); + + TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( + localMessageTransmitter, + messageBodyVersion + ); + + vm.expectRevert("No TokenMessenger for domain"); + _tokenMessenger.depositForBurnWithHook( + _amount, + _remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + _maxFee, + _minFinalityThreshold, + _hookData + ); + } + + function testDepositForBurnWithHook_revertsIfLocalMinterIsNotSet( + uint256 _amount, + uint32 _remoteDomain, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold, + bytes calldata _hookData + ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee < _amount); + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hookData.length > 0); + + TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( + localMessageTransmitter, + messageBodyVersion + ); + + _tokenMessenger.addRemoteTokenMessenger( + _remoteDomain, + remoteTokenMessengerAddr + ); + + vm.expectRevert("Local minter is not set"); + _tokenMessenger.depositForBurnWithHook( + _amount, + _remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + _maxFee, + _minFinalityThreshold, + _hookData + ); + } + + function testDepositForBurnWithHook_revertsOnFailedTokenTransfer( + uint256 _amount, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold, + bytes calldata _hookData + ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee < _amount); + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hookData.length > 0); + + vm.mockCall( + address(localToken), + abi.encodeWithSelector(MockMintBurnToken.transferFrom.selector), + abi.encode(false) + ); + vm.expectRevert("Transfer operation failed"); + localTokenMessenger.depositForBurnWithHook( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _maxFee, + _minFinalityThreshold, + _hookData + ); + } + + function testDepositForBurnWithHook_revertsIfTokenTransferReverts( + address _caller, + uint256 _amount, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold, + bytes calldata _hookData + ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee < _amount); + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hookData.length > 0); + + // TransferFrom will revert, as localTokenMessenger has no allowance + assertEq( + localToken.allowance(_caller, address(localTokenMessenger)), + 0 + ); + + vm.expectRevert("ERC20: transfer amount exceeds allowance"); + localTokenMessenger.depositForBurnWithHook( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _maxFee, + _minFinalityThreshold, + _hookData + ); + } + + function testDepositForBurnWithHook_revertsIfTransferAmountExceedsMaxBurnAmountPerMessage( + uint256 _amount, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint256 _maxFee, + uint32 _minFinalityThreshold, + bytes calldata _hookData, + address _caller + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hookData.length > 0); + vm.assume(_maxFee < _amount); + vm.assume(_amount > 1); + vm.assume(_caller != address(0)); + + _setupDepositForBurn(_caller, _amount, _amount - 1); + + vm.prank(_caller); + vm.expectRevert("Burn amount exceeds per tx limit"); + localTokenMessenger.depositForBurnWithHook( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _maxFee, + _minFinalityThreshold, + _hookData + ); + } + + function testDepositForBurnWithHook_succeeds( + uint256 _amount, + uint256 _burnLimit, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _hookData, + address _caller + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_amount > 1); + vm.assume(_amount < _burnLimit); + vm.assume(_hookData.length > 0); + vm.assume(_caller != address(0)); + + uint256 _maxFee = _amount - 1; + + _setupDepositForBurn(_caller, _amount, _burnLimit); + + _depositForBurn( + _caller, + _mintRecipient, + _destinationCaller, + _amount, + _maxFee, + _minFinalityThreshold, + _hookData + ); + } + + function testHandleReceiveFinalizedMessage_revertsIfCallerIsNotLocalMessageTransmitter( + uint32 _remoteDomain, + bytes32 _sender, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody, + address _caller + ) public { + vm.assume(_caller != localMessageTransmitter); + + vm.expectRevert("Invalid message transmitter"); + localTokenMessenger.handleReceiveFinalizedMessage( + _remoteDomain, + _sender, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_revertsIfMessageSenderIsNotRemoteTokenMessengerForKnownDomain( + bytes32 _sender, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_sender != remoteTokenMessengerAddr); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Remote TokenMessenger unsupported"); + localTokenMessenger.handleReceiveFinalizedMessage( + remoteDomain, // known domain, but unknown remote token messenger addr + _sender, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_revertsIfMessageSenderIsKnownRemoteTokenMessengerForUnknownDomain( + uint32 _remoteDomain, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_remoteDomain != remoteDomain); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Remote TokenMessenger unsupported"); + localTokenMessenger.handleReceiveFinalizedMessage( + _remoteDomain, + remoteTokenMessengerAddr, // known token messenger, but unknown domain + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_revertsForUnknownRemoteTokenMessengersAndRemoteDomains( + uint32 _remoteDomain, + bytes32 _remoteTokenMessenger, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_remoteDomain != remoteDomain); + vm.assume(_remoteTokenMessenger != remoteTokenMessengerAddr); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Remote TokenMessenger unsupported"); + localTokenMessenger.handleReceiveFinalizedMessage( + _remoteDomain, + _remoteTokenMessenger, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_revertsOnTooShortMessage( + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + // See: BurnMessageV2#HOOK_DATA_INDEX + vm.assume(_messageBody.length < 228); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Invalid burn message: too short"); + localTokenMessenger.handleReceiveFinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_revertsOnInvalidMessageBodyVersion( + uint32 _version, + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume(_version != localTokenMessenger.messageBodyVersion()); + + bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( + _version, + _burnToken, + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _hookData + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Invalid message body version"); + localTokenMessenger.handleReceiveFinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_revertsIfNonZeroExpirationBlockIsLessThanCurrentBlock( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _feeExecuted, + uint256 _expirationBlock, + uint32 _finalityThresholdExecuted, + bytes calldata _hookData + ) public { + vm.assume(_expirationBlock > 0); + vm.assume(_expirationBlock < type(uint256).max - 1); + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + _expirationBlock, + _hookData + ); + + // Overwrite current block number to be greater than expirationBlock + vm.roll(_expirationBlock + 1); + assertTrue(vm.getBlockNumber() > _expirationBlock); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Message expired and must be re-signed"); + localTokenMessenger.handleReceiveFinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_revertsIfNonZeroExpirationBlockEqualsCurrentBlock( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _feeExecuted, + uint256 _expirationBlock, + uint32 _finalityThresholdExecuted, + bytes calldata _hookData + ) public { + vm.assume(_expirationBlock > 0); + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + _expirationBlock, + _hookData + ); + + // Overwrite current block number to equal expirationBlock + vm.roll(_expirationBlock); + assertEq(vm.getBlockNumber(), _expirationBlock); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Message expired and must be re-signed"); + localTokenMessenger.handleReceiveFinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_revertsIfFeeIsGreaterThanAmount( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _feeExecuted, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); + vm.assume(_feeExecuted > _amount); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + 0, + _hookData + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Fee equals or exceeds amount"); + localTokenMessenger.handleReceiveFinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_revertsIfFeeEqualsAmount( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); + vm.assume(_amount > 0); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _amount, // feeExecuted == amount + 0, + _hookData + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Fee equals or exceeds amount"); + localTokenMessenger.handleReceiveFinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_revertsIfFeeExecutedExceedsMaxFee( + bytes32 _mintRecipient, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _feeExecuted, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); + vm.assume(_maxFee > 0); + vm.assume(_feeExecuted > _maxFee); + vm.assume(_feeExecuted < type(uint256).max - 1); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _feeExecuted + 1, // amount + _burnMessageSender, + _maxFee, + _feeExecuted, + 0, + _hookData + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Fee exceeds max fee"); + localTokenMessenger.handleReceiveFinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_revertsIfNoLocalMinterIsSet( + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( + localTokenMessenger.messageBodyVersion(), + _burnToken, + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _hookData + ); + + assertTrue(address(localTokenMessenger.localMinter()) != address(0)); + + // Remove local minter + vm.prank(owner); + localTokenMessenger.removeLocalMinter(); + + assertEq(address(localTokenMessenger.localMinter()), address(0)); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Local minter is not set"); + localTokenMessenger.handleReceiveFinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_revertsIfMintRevertsWithZeroFees( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + 0, // 0 fee executed, meaning that we'll use the regular mint() on TokenMinter + 0, + _hookData + ); + + // Mock a failing call to TokenMinter mint() for amount + bytes memory _call = abi.encodeWithSelector( + TokenMinter.mint.selector, + remoteDomain, + remoteTokenAddr.toBytes32(), + _mintRecipient.toAddress(), + _amount + ); + vm.mockCallRevert( + address(localTokenMinter), + _call, + "Testing: mint() failed" + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Testing: mint() failed"); + localTokenMessenger.handleReceiveFinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_revertsIfMintRevertsWithNonZeroFees( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _feeExecuted, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume( + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD + ); + vm.assume(_amount > 0); + vm.assume(_feeExecuted > 0); + vm.assume(_feeExecuted < _amount); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _feeExecuted, // maxFee + _feeExecuted, // non-zero fee, meaning we'll try to mint() on TokenMinterV2, passing in multiple recipients + 0, + _hookData + ); + + // Mock a failing call to TokenMinter mint() for amount, less fees + bytes memory _call = abi.encodeWithSelector( + TokenMinterV2.mint.selector, + remoteDomain, + remoteTokenAddr.toBytes32(), + _mintRecipient.toAddress(), + feeRecipient, + _amount - _feeExecuted, + _feeExecuted + ); + vm.mockCallRevert( + address(localTokenMinter), + _call, + "Testing: mint() failed" + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Testing: mint() failed"); + localTokenMessenger.handleReceiveFinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_succeedsForZeroExpirationBlock( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _feeExecuted, + bytes calldata _hookData + ) public { + vm.assume(_amount > 0); + vm.assume(_feeExecuted < _amount); + vm.assume(_maxFee >= _feeExecuted); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + 0, // expiration + _hookData + ); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + FINALITY_THRESHOLD_FINALIZED, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_succeedsForNonZeroExpirationBlock( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _feeExecuted, + uint256 _expirationBlock, + bytes calldata _hookData + ) public { + vm.assume(_amount > 0); + vm.assume(_feeExecuted < _amount); + vm.assume(_expirationBlock > 0); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _feeExecuted, // maxFee + _feeExecuted, + _expirationBlock, + _hookData + ); + + // Jump to a block height lower than the expiration block + vm.roll(_expirationBlock - 1); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + FINALITY_THRESHOLD_FINALIZED, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_succeedsForZeroFee( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _expirationBlock, + bytes calldata _hookData + ) public { + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + 0, // feeExecuted + _expirationBlock, + _hookData + ); + + // Jump to a block height lower than the expiration block + vm.roll(_expirationBlock - 1); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + FINALITY_THRESHOLD_FINALIZED, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_succeedsForNonZeroFee( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _expirationBlock, + uint256 _feeExecuted, + bytes calldata _hookData + ) public { + vm.assume(_feeExecuted < _amount); + vm.assume(_feeExecuted > 0); + vm.assume(_expirationBlock > 0); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _feeExecuted, // maxFee + _feeExecuted, + _expirationBlock, + _hookData + ); + + // Jump to a block height lower than the expiration block + vm.roll(_expirationBlock - 1); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + FINALITY_THRESHOLD_FINALIZED, + _messageBody + ); + } + + function testHandleReceiveFinalizedMessage_succeeds( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _expirationBlock, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); + vm.assume(_maxFee < _amount); + vm.assume(_expirationBlock > 0); + + // Jump to a block height lower than the expiration block + vm.roll(_expirationBlock - 1); + + uint256 _feeExecuted = _maxFee; + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + _expirationBlock, + _hookData + ); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsIfCallerIsNotLocalMessageTransmitter( + uint32 _remoteDomain, + bytes32 _sender, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody, + address _caller + ) public { + vm.assume(_caller != localMessageTransmitter); + + vm.expectRevert("Invalid message transmitter"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + _remoteDomain, + _sender, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsIfMessageSenderIsNotRemoteTokenMessengerForKnownDomain( + bytes32 _sender, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_sender != remoteTokenMessengerAddr); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Remote TokenMessenger unsupported"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + remoteDomain, // known domain, but unknown remote token messenger addr + _sender, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsIfMessageSenderIsKnownRemoteTokenMessengerForUnknownDomain( + uint32 _remoteDomain, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_remoteDomain != remoteDomain); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Remote TokenMessenger unsupported"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + _remoteDomain, + remoteTokenMessengerAddr, // known token messenger, but unknown domain + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsForUnknownRemoteTokenMessengersAndRemoteDomains( + uint32 _remoteDomain, + bytes32 _remoteTokenMessenger, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_remoteDomain != remoteDomain); + vm.assume(_remoteTokenMessenger != remoteTokenMessengerAddr); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Remote TokenMessenger unsupported"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + _remoteDomain, + _remoteTokenMessenger, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsOnTooLowFinalityThreshold( + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume( + _finalityThresholdExecuted < TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Unsupported finality threshold"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsOnTooShortMessage( + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + // See: BurnMessageV2#HOOK_DATA_INDEX + vm.assume(_messageBody.length < 228); + vm.assume( + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Invalid burn message: too short"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsOnInvalidMessageBodyVersion( + uint32 _version, + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume(_version != localTokenMessenger.messageBodyVersion()); + vm.assume( + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD + ); + + bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( + _version, + _burnToken, + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _hookData + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Invalid message body version"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsIfNonZeroExpirationBlockIsLessThanCurrentBlock( + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _feeExecuted, + uint256 _expirationBlock, + bytes calldata _hookData + ) public { + vm.assume(_expirationBlock > 0); + vm.assume(_expirationBlock < type(uint256).max - 1); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + _burnToken, + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + _expirationBlock, + _hookData + ); + + // Overwrite current block number to be greater than expirationBlock + vm.roll(_expirationBlock + 1); + assertTrue(vm.getBlockNumber() > _expirationBlock); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Message expired and must be re-signed"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + FINALITY_THRESHOLD_CONFIRMED, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsIfNonZeroExpirationBlockEqualsCurrentBlock( + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _feeExecuted, + uint256 _expirationBlock, + bytes calldata _hookData + ) public { + vm.assume(_expirationBlock > 0); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + _burnToken, + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + _expirationBlock, + _hookData + ); + + // Overwrite current block number to equal expirationBlock + vm.roll(_expirationBlock); + assertEq(vm.getBlockNumber(), _expirationBlock); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Message expired and must be re-signed"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + FINALITY_THRESHOLD_CONFIRMED, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsIfFeeIsGreaterThanAmount( + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _feeExecuted, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume( + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD + ); + vm.assume(_feeExecuted > _amount); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + _burnToken, + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + 0, + _hookData + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Fee equals or exceeds amount"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsIfFeeEqualsAmount( + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume( + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD + ); + vm.assume(_amount > 0); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + _burnToken, + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _amount, // feeExecuted == amount + 0, + _hookData + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Fee equals or exceeds amount"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsIfFeeExecutedExceedsMaxFee( + bytes32 _mintRecipient, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _feeExecuted, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume( + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD + ); + vm.assume(_maxFee > 0); + vm.assume(_feeExecuted > _maxFee); + vm.assume(_feeExecuted < type(uint256).max - 1); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _feeExecuted + 1, // amount + _burnMessageSender, + _maxFee, + _feeExecuted, + 0, + _hookData + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Fee exceeds max fee"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsIfNoLocalMinterIsSet( + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _feeExecuted, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume( + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD + ); + vm.assume(_feeExecuted < _amount); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + _burnToken, + _mintRecipient, + _amount, + _burnMessageSender, + _feeExecuted, // maxFee + _feeExecuted, + 0, + _hookData + ); + + assertTrue(address(localTokenMessenger.localMinter()) != address(0)); + + // Remove local minter + vm.prank(owner); + localTokenMessenger.removeLocalMinter(); + + assertEq(address(localTokenMessenger.localMinter()), address(0)); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Local minter is not set"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsIfMintRevertsWithZeroFees( + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume( + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD + ); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + _burnToken, + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + 0, // 0 fee executed, meaning that we'll use the regular mint() on TokenMinter + 0, + _hookData + ); + + // Mock a failing call to TokenMinter mint() for amount, less fees + bytes memory _call = abi.encodeWithSelector( + TokenMinter.mint.selector, + remoteDomain, + _burnToken, + _mintRecipient.toAddress(), + _amount + ); + vm.mockCallRevert( + address(localTokenMinter), + _call, + "Testing: mint() failed" + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Testing: mint() failed"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_revertsIfMintRevertsWithNonZeroFees( + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _feeExecuted, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume( + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD + ); + vm.assume(_amount > 0); + vm.assume(_feeExecuted > 0); + vm.assume(_feeExecuted < _amount); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + _burnToken, + _mintRecipient, + _amount, + _burnMessageSender, + _feeExecuted, // maxFee + _feeExecuted, // non-zero fee, meaning we'll try to mint() on TokenMinterV2, passing in multiple recipients + 0, + _hookData + ); + + // Mock a failing call to TokenMinter mint() for amount, less fees + bytes memory _call = abi.encodeWithSelector( + TokenMinterV2.mint.selector, + remoteDomain, + _burnToken, + _mintRecipient.toAddress(), + feeRecipient, + _amount - _feeExecuted, + _feeExecuted + ); + vm.mockCallRevert( + address(localTokenMinter), + _call, + "Testing: mint() failed" + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Testing: mint() failed"); + localTokenMessenger.handleReceiveUnfinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_succeedsForZeroExpirationBlock( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _feeExecuted, + bytes calldata _hookData + ) public { + vm.assume(_amount > 0); + vm.assume(_feeExecuted < _amount); + vm.assume(_maxFee >= _feeExecuted); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + 0, // expiration + _hookData + ); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + FINALITY_THRESHOLD_CONFIRMED, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_succeedsForNonZeroExpirationBlock( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _feeExecuted, + uint256 _expirationBlock, + bytes calldata _hookData + ) public { + vm.assume(_amount > 0); + vm.assume(_feeExecuted < _amount); + vm.assume(_expirationBlock > 0); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _feeExecuted, // maxFee + _feeExecuted, + _expirationBlock, + _hookData + ); + + // Jump to a block height lower than the expiration block + vm.roll(_expirationBlock - 1); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + FINALITY_THRESHOLD_CONFIRMED, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_succeedsForZeroFee( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _expirationBlock, + bytes calldata _hookData + ) public { + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + 0, // feeExecuted + _expirationBlock, + _hookData + ); + + // Jump to a block height lower than the expiration block + vm.roll(_expirationBlock - 1); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + FINALITY_THRESHOLD_CONFIRMED, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_succeedsForNonZeroFee( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _expirationBlock, + uint256 _feeExecuted, + bytes calldata _hookData + ) public { + vm.assume(_feeExecuted < _amount); + vm.assume(_feeExecuted > 0); + vm.assume(_expirationBlock > 0); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _feeExecuted, // maxFee + _feeExecuted, + _expirationBlock, + _hookData + ); + + // Jump to a block height lower than the expiration block + vm.roll(_expirationBlock - 1); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + FINALITY_THRESHOLD_CONFIRMED, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_succeeds( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _expirationBlock, + uint256 _feeExecuted, + bytes calldata _hookData + ) public { + vm.assume(_feeExecuted < _amount); + vm.assume(_expirationBlock > 0); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _feeExecuted, // maxFee + _feeExecuted, + _expirationBlock, + _hookData + ); + + // Jump to a block height lower than the expiration block + vm.roll(_expirationBlock - 1); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + FINALITY_THRESHOLD_CONFIRMED, + _messageBody + ); + } + + // Overall fuzz test for both finalized and unfinalized messages + function testHandleReceivedMessage_succeeds( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _expirationBlock, + uint256 _feeExecuted, + uint32 _finalityThresholdExecuted, + bytes calldata _hookData + ) public { + vm.assume(_feeExecuted < _amount); + vm.assume(_expirationBlock > 0); + vm.assume( + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD + ); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + remoteTokenAddr.toBytes32(), + _mintRecipient, + _amount, + _burnMessageSender, + _feeExecuted, // maxFee + _feeExecuted, + _expirationBlock, + _hookData + ); + + // Jump to a block height lower than the expiration block + vm.roll(_expirationBlock - 1); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + // Test helpers + + function _defaultRemoteTokenMessengers() + internal + view + returns ( + uint32[] memory _remoteDomains, + bytes32[] memory _remoteTokenMessengers + ) + { + _remoteDomains = new uint32[](1); + _remoteDomains[0] = remoteDomain; + + _remoteTokenMessengers = new bytes32[](1); + _remoteTokenMessengers[0] = remoteTokenMessengerAddr; + } + + function _formatBurnMessageForReceive( + uint32 _version, + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _messageSender, + uint256 _maxFee, + uint256 _feeExecuted, + uint256 _expirationBlock, + bytes calldata _hookData + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + _version, + _burnToken, + _mintRecipient, + _amount, + _messageSender, + _maxFee, + _feeExecuted, + _expirationBlock, + _hookData + ); + } + + function _setupDepositForBurn( + address _caller, + uint256 _amount, + uint256 _maxBurnAmount + ) internal { + localToken.mint(_caller, _amount); + + vm.prank(_caller); + localToken.approve(address(localTokenMessenger), _amount); + + vm.prank(tokenController); + localTokenMinter.setMaxBurnAmountPerMessage( + address(localToken), + _maxBurnAmount + ); + } + + function _depositForBurn( + address _caller, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint256 _amount, + uint256 _maxFee, + uint32 _minFinalityThreshold, + bytes calldata _hookData + ) internal { + bytes memory _expectedBurnMessage = BurnMessageV2 + ._formatMessageForRelay( + localTokenMessenger.messageBodyVersion(), // version + address(localToken).toBytes32(), // burn token + _mintRecipient, // mint recipient + _amount, // amount + _caller.toBytes32(), // sender + _maxFee, // max fee + _hookData + ); + + // expect burn() on localTokenMinter + vm.expectCall( + address(localTokenMinter), + abi.encodeWithSelector( + localTokenMinter.burn.selector, + address(localToken), + _amount + ) + ); + + // expect sendMessage() on localMessageTransmitter + vm.expectCall( + address(localMessageTransmitter), + abi.encodeWithSelector( + MessageTransmitterV2.sendMessage.selector, + destinationDomain, + remoteTokenMessengerAddr, + _destinationCaller, + _minFinalityThreshold, + _expectedBurnMessage + ) + ); + + // Mock an empty response from messageTransmitter + vm.mockCall( + address(localMessageTransmitter), + abi.encodeWithSelector(MessageTransmitterV2.sendMessage.selector), + bytes("") + ); + + // Expect event emission + vm.expectEmit(true, true, true, true); + emit DepositForBurn( + address(localToken), + _amount, + _caller, + _mintRecipient, + destinationDomain, + remoteTokenMessengerAddr, + _destinationCaller, + _maxFee, + _minFinalityThreshold, + _hookData + ); + + vm.prank(_caller); + + if (_hookData.length == 0) { + localTokenMessenger.depositForBurn( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _maxFee, + _minFinalityThreshold + ); + } else { + localTokenMessenger.depositForBurnWithHook( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _maxFee, + _minFinalityThreshold, + _hookData + ); + } + } + + function _handleReceiveMessage( + uint32 _remoteDomain, + bytes32 _sender, + uint32 _finalityThresholdExecuted, + bytes memory _messageBody + ) internal { + bytes29 _msg = _messageBody.ref(0); + address _mintRecipient = _msg._getMintRecipient().toAddress(); + uint256 _amount = _msg._getAmount(); + uint256 _fee = _msg._getFeeExecuted(); + + // Sanity checks to ensure this is being called with appropriate inputs + assertEq( + uint256(localTokenMessenger.messageBodyVersion()), + uint256(_msg._getVersion()) + ); + assertEq(uint256(_remoteDomain), uint256(remoteDomain)); + assertEq(_sender, remoteTokenMessengerAddr); + assertEq(_msg._getBurnToken().toAddress(), remoteTokenAddr); + assertTrue(_fee == 0 || _amount > _fee); + assertTrue(feeRecipient != address(0)); + vm.assume(_mintRecipient != feeRecipient); + + // Sanity check that the starting balances are 0 + assertEq(localToken.balanceOf(_mintRecipient), 0); + assertEq(localToken.balanceOf(feeRecipient), 0); + + // Expect that TokenMinter be called 1x + { + bytes memory _encodedMinterCall; + if (_fee == 0) { + _encodedMinterCall = abi.encodeWithSelector( + TokenMinter.mint.selector, + _remoteDomain, + _msg._getBurnToken(), + _mintRecipient, + _amount + ); + } else { + _encodedMinterCall = abi.encodeWithSelector( + TokenMinterV2.mint.selector, + _remoteDomain, + _msg._getBurnToken(), + _mintRecipient, + feeRecipient, + _amount - _fee, + _fee + ); + } + vm.expectCall(address(localTokenMinter), _encodedMinterCall, 1); + } + + vm.expectEmit(true, true, true, true); // Expect MintAndWithdraw to be emitted + emit MintAndWithdraw( + _mintRecipient, + _amount - _fee, + address(localToken), + _fee + ); + + // Execute handleReceive() + vm.prank(localMessageTransmitter); + + bool _result; + if (_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED) { + _result = localTokenMessenger.handleReceiveFinalizedMessage( + _remoteDomain, + _sender, + _finalityThresholdExecuted, + _messageBody + ); + } else { + _result = localTokenMessenger.handleReceiveUnfinalizedMessage( + _remoteDomain, + _sender, + _finalityThresholdExecuted, + _messageBody + ); + } + + assertTrue(_result); + + // Check balances after + assertEq( + _amount - _fee, + localToken.balanceOf(_mintRecipient), + "Mint recipient received incorrect amount" + ); + assertEq( + _fee, + localToken.balanceOf(feeRecipient), + "Fee recipient received incorrect amount" + ); + } +} diff --git a/test/v2/TokenMessengerV2IT.t.sol b/test/v2/TokenMessengerV2IT.t.sol new file mode 100644 index 0000000..882bc41 --- /dev/null +++ b/test/v2/TokenMessengerV2IT.t.sol @@ -0,0 +1,360 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {TokenMessengerV2} from "../../src/v2/TokenMessengerV2.sol"; +import {AddressUtils} from "../../src/messages/v2/AddressUtils.sol"; +import {MockMintBurnToken} from "../mocks/MockMintBurnToken.sol"; +import {TokenMinterV2} from "../../src/v2/TokenMinterV2.sol"; +import {MessageTransmitterV2} from "../../src/v2/MessageTransmitterV2.sol"; +import {BurnMessageV2} from "../../src/messages/v2/BurnMessageV2.sol"; +import {MessageV2} from "../../src/messages/v2/MessageV2.sol"; +import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol"; +import {AdminUpgradableProxy} from "../../src/proxy/AdminUpgradableProxy.sol"; +import {MessageTransmitterV2} from "../../src/v2/MessageTransmitterV2.sol"; +import {TestUtils} from "../TestUtils.sol"; +import {Vm} from "forge-std/Vm.sol"; + +contract TokenMessengerV2IntegrationTest is TestUtils { + event MessageSent(bytes message); + + // Libraries + using TypedMemView for bytes; + using TypedMemView for bytes29; + using BurnMessageV2 for bytes29; + using MessageV2 for bytes29; + using AddressUtils for address; + + // Constants + uint32 localDomain = 0; + uint32 remoteDomain = 1; + uint32 messageVersion = 1; + uint32 messageBodyVersion = 1; + + MessageTransmitterV2 localMessageTransmitter; + MessageTransmitterV2 remoteMessageTransmitter; + + TokenMessengerV2 localTokenMessenger; + TokenMessengerV2 remoteTokenMessenger; + + MockMintBurnToken localToken = new MockMintBurnToken(); + MockMintBurnToken remoteToken = new MockMintBurnToken(); + + TokenMinterV2 localTokenMinter = new TokenMinterV2(tokenController); + TokenMinterV2 remoteTokenMinter = new TokenMinterV2(tokenController); + + // Roles + address localDepositor = + address(0xBcd4042DE499D14e55001CcbB24a551F3b954096); + address localMintRecipient = + address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); + + address remoteDepositor = + address(0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC); + address remoteMintRecipient = + address(0x90F79bf6EB2c4f870365E785982E1f101E93b906); + + address localDeployer = address(0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65); + address remoteDeployer = + address(0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f); + + address tokenDeployer = address(0x15d34Aaf54267dB7d7c367839aaF71A00A2C6a64); + + // Constants + + uint256 localDepositAmount = 50_000_000_000; + uint256 localFeeExecuted = 50_000_000; + + // Precomputed messages + // Reformatted MessageSent from local domain depositForBurn with: + // --nonce: keccak256("LocalNonce") + // --finalityThresholdExecuted: 1000 + // --feeExecuted: 50_000_000 + // --expirationBlock: 0 + function _localMessageSent() internal pure returns (bytes memory) { + return + abi.encodePacked( + hex"00000001000000000000000109ac09a5866905247c049066d77ced39929878c828a4198405db6608023c54fb00000000000000000000000093c7a6d00849c44ef3e92e95dceffccd447909ae000000000000000000000000ca8b49076d1a8039599e24979abf819af784c27a00000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b906000003e8000003e80000000100000000000000000000000024FA1F38FfE8bE6711872c6e0D662D83E524f0cE00000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b9060000000000000000000000000000000000000000000000000000000ba43b7400000000000000000000000000bcd4042de499d14e55001ccbb24a551f3b9540960000000000000000000000000000000000000000000000000000000002faf0800000000000000000000000000000000000000000000000000000000002faf0800000000000000000000000000000000000000000000000000000000000000000" + ); + } + + function setUp() public { + // Attesters + address[] memory _attesters = new address[](1); + _attesters[0] = attester; + + vm.startPrank(tokenDeployer); + localToken = new MockMintBurnToken(); + remoteToken = new MockMintBurnToken(); + vm.stopPrank(); + + // Local MessageTransmitterV2 + vm.startPrank(localDeployer); + MessageTransmitterV2 localMessageTransmitterImpl = new MessageTransmitterV2( + localDomain, + messageVersion + ); + AdminUpgradableProxy proxy = new AdminUpgradableProxy( + address(localMessageTransmitterImpl), + localDeployer, + abi.encodeWithSelector( + MessageTransmitterV2.initialize.selector, + localDeployer, + localDeployer, + localDeployer, + localDeployer, + _attesters, + 1, + maxMessageBodySize + ) + ); + localMessageTransmitter = MessageTransmitterV2(address(proxy)); + + // Local TokenMessengerV2 + TokenMessengerV2 localTokenMessengerImpl = new TokenMessengerV2( + address(localMessageTransmitter), + messageBodyVersion + ); + proxy = new AdminUpgradableProxy( + address(localTokenMessengerImpl), + localDeployer, + abi.encodeWithSelector( + TokenMessengerV2.initialize.selector, + localDeployer, // owner + localDeployer, // rescuer + localDeployer, // feeRecipient + localDeployer, // denylister + address(localTokenMinter), + new uint32[](0), + new bytes32[](0) + ) + ); + localTokenMessenger = TokenMessengerV2(address(proxy)); + vm.stopPrank(); + + // Remote MessageTransmitterV2 + vm.startPrank(remoteDeployer); + MessageTransmitterV2 remoteMessageTransmitterImpl = new MessageTransmitterV2( + remoteDomain, + messageVersion + ); + proxy = new AdminUpgradableProxy( + address(remoteMessageTransmitterImpl), + remoteDeployer, + abi.encodeWithSelector( + MessageTransmitterV2.initialize.selector, + remoteDeployer, + remoteDeployer, + remoteDeployer, + remoteDeployer, + _attesters, + 1, + maxMessageBodySize + ) + ); + remoteMessageTransmitter = MessageTransmitterV2(address(proxy)); + + // Remote TokenMessengerV2 + TokenMessengerV2 remoteTokenMessengerImpl = new TokenMessengerV2( + address(remoteMessageTransmitter), + messageBodyVersion + ); + uint32[] memory _remoteDomains = new uint32[](1); + bytes32[] memory _remoteTokenMessengerAddresses = new bytes32[](1); + + _remoteDomains[0] = 0; // configure localDomain, on remoteDomain + _remoteTokenMessengerAddresses[0] = address(localTokenMessenger) + .toBytes32(); + + proxy = new AdminUpgradableProxy( + address(remoteTokenMessengerImpl), + remoteDeployer, + abi.encodeWithSelector( + TokenMessengerV2.initialize.selector, + remoteDeployer, // owner + remoteDeployer, // rescuer + remoteDeployer, // feeRecipient + remoteDeployer, // denylister + address(remoteTokenMinter), + _remoteDomains, + _remoteTokenMessengerAddresses + ) + ); + remoteTokenMessenger = TokenMessengerV2(address(proxy)); + vm.stopPrank(); + + // Configure remote TokenMessenger on local domain + vm.startPrank(localDeployer); + localTokenMessenger.addRemoteTokenMessenger( + remoteDomain, + address(remoteTokenMessenger).toBytes32() + ); + vm.stopPrank(); + + // Link token pair on local domain + linkTokenPair( + localTokenMinter, + address(localToken), + remoteDomain, + address(remoteToken).toBytes32() + ); + + // Link token pair on remote domain + linkTokenPair( + remoteTokenMinter, + address(remoteToken), + localDomain, + address(localToken).toBytes32() + ); + + // Set maxBurnAmountPerMessage + vm.startPrank(tokenController); + localTokenMinter.setMaxBurnAmountPerMessage( + address(localToken), + 1_000_000_000_000 + ); + remoteTokenMinter.setMaxBurnAmountPerMessage( + address(remoteToken), + 1_000_000_000_000 + ); + vm.stopPrank(); + + // Configure TokenMessengers on TokenMinters + localTokenMinter.addLocalTokenMessenger(address(localTokenMessenger)); + remoteTokenMinter.addLocalTokenMessenger(address(remoteTokenMessenger)); + + // Mint ERC20 tokens and setup allowances + localToken.mint(localDepositor, localDepositAmount); + + vm.prank(localDepositor); + localToken.approve(address(localTokenMessenger), localDepositAmount); + } + + // Tests + + function testDepositForBurn_succeedsFromLocalDomain() public { + assertEq(localToken.totalSupply(), localDepositAmount); + assertEq(localToken.balanceOf(localDepositor), localDepositAmount); + + vm.startPrank(localDepositor); + localTokenMessenger.depositForBurn( + localDepositAmount, + remoteDomain, + remoteMintRecipient.toBytes32(), + address(localToken), + remoteMintRecipient.toBytes32(), + localFeeExecuted, + 1000 + ); + vm.stopPrank(); + + assertEq(localToken.totalSupply(), 0); + assertEq(localToken.balanceOf(localDepositor), 0); + } + + function testReceiveMessage_succeedsOnRemoteDomain() public { + bytes memory _message = _localMessageSent(); + _sanityCheckMessageSent(_message); + + bytes memory _attestation = _signMessageWithAttesterPK(_message); + + assertEq(remoteToken.totalSupply(), 0); + assertEq(remoteToken.balanceOf(remoteMintRecipient), 0); + + vm.prank(remoteMintRecipient); + remoteMessageTransmitter.receiveMessage(_message, _attestation); + + assertEq(remoteToken.totalSupply(), localDepositAmount); + assertEq( + remoteToken.balanceOf(remoteMintRecipient), + localDepositAmount - localFeeExecuted + ); + assertEq( + remoteToken.balanceOf(remoteDeployer), // feeRecipient + localFeeExecuted + ); + } + + // Test utils + + // Helper to validate that the message doesn't have unexpected values + // according to the test harness, since we precompute the MessageSent for delivery + // on the destination chain, with nonce, finalityThresholdExecuted, feeExecuted, and + // expirationBlock encoded off-chain. + function _sanityCheckMessageSent(bytes memory _message) internal view { + bytes29 _msg = _message.ref(0); + + assertEq(uint256(MessageV2._getVersion(_msg)), uint256(messageVersion)); + assertEq( + uint256(MessageV2._getSourceDomain(_msg)), + uint256(localDomain) + ); + assertEq( + uint256(MessageV2._getDestinationDomain(_msg)), + uint256(remoteDomain) + ); + assertTrue(MessageV2._getNonce(_msg) > 0); + assertEq( + MessageV2._getSender(_msg), + address(localTokenMessenger).toBytes32() + ); + assertEq( + MessageV2._getRecipient(_msg), + address(remoteTokenMessenger).toBytes32() + ); + assertEq( + MessageV2._getDestinationCaller(_msg), + remoteMintRecipient.toBytes32() + ); + assertEq( + uint256(MessageV2._getMinFinalityThreshold(_msg)), + uint256(1000) + ); + assertEq( + uint256(MessageV2._getFinalityThresholdExecuted(_msg)), + uint256(1000) + ); + + bytes29 _burnMessageV2 = _msg._getMessageBody(); + + assertEq( + uint256(BurnMessageV2._getVersion(_burnMessageV2)), + uint256(messageBodyVersion) + ); + assertEq( + BurnMessageV2._getBurnToken(_burnMessageV2), + address(localToken).toBytes32() + ); + assertEq( + BurnMessageV2._getMintRecipient(_burnMessageV2), + remoteMintRecipient.toBytes32() + ); + assertEq(BurnMessageV2._getAmount(_burnMessageV2), localDepositAmount); + assertEq( + BurnMessageV2._getMessageSender(_burnMessageV2), + localDepositor.toBytes32() + ); + assertEq(BurnMessageV2._getMaxFee(_burnMessageV2), localFeeExecuted); + assertEq( + BurnMessageV2._getFeeExecuted(_burnMessageV2), + localFeeExecuted + ); + assertEq(BurnMessageV2._getExpirationBlock(_burnMessageV2), 0); + } +} diff --git a/test/v2/TokenMinterV2.t.sol b/test/v2/TokenMinterV2.t.sol new file mode 100644 index 0000000..4f6363d --- /dev/null +++ b/test/v2/TokenMinterV2.t.sol @@ -0,0 +1,330 @@ +/* + * Copyright 2024 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity 0.7.6; +pragma abicoder v2; + +import {TokenMinterTest} from "../TokenMinter.t.sol"; +import {TokenMinterV2} from "../../src/v2/TokenMinterV2.sol"; +import {MockMintBurnToken} from "../mocks/MockMintBurnToken.sol"; + +contract TokenMinterV2Test is TokenMinterTest { + // Test constant + address account1 = address(123); + address account2 = address(456); + address account3 = address(789); + + // Overrides + + function createTokenMinter() internal override returns (address) { + return address(new TokenMinterV2(tokenController)); + } + + // Tests + + function testMint_revertsWhenPaused( + uint32 _sourceDomain, + bytes32 _burnToken, + address _recipientOne, + address _recipientTwo, + uint256 _amountOne, + uint256 _amountTwo + ) public { + vm.prank(pauser); + tokenMinter.pause(); + // Sanity check + assertTrue(tokenMinter.paused()); + + vm.expectRevert("Pausable: paused"); + _getTokenMinterV2().mint( + _sourceDomain, + _burnToken, + _recipientOne, + _recipientTwo, + _amountOne, + _amountTwo + ); + } + + function testMint_revertsWhenNotCalledByLocalTokenMessenger( + address _mockCaller, + uint32 _sourceDomain, + bytes32 _burnToken, + address _recipientOne, + address _recipientTwo, + uint256 _amountOne, + uint256 _amountTwo + ) public { + vm.assume(_mockCaller != localTokenMessenger); + + vm.expectRevert("Caller not local TokenMessenger"); + vm.prank(_mockCaller); + _getTokenMinterV2().mint( + _sourceDomain, + _burnToken, + _recipientOne, + _recipientTwo, + _amountOne, + _amountTwo + ); + } + + function testMint_revertsIfBurnTokenDoesntMatchRemoteDomain( + bytes32 _burnToken, + address _recipientOne, + address _recipientTwo, + uint256 _amountOne, + uint256 _amountTwo + ) public { + vm.assume(_burnToken != remoteTokenBytes32); // unrecognized burnToken for recognized remote domain + + vm.expectRevert("Mint token not supported"); + vm.prank(localTokenMessenger); + _getTokenMinterV2().mint( + remoteDomain, + _burnToken, + _recipientOne, + _recipientTwo, + _amountOne, + _amountTwo + ); + } + + function testMint_revertsIfRemoteDomainDoesntMatchBurnToken( + uint32 _remoteDomain, + address _recipientOne, + address _recipientTwo, + uint256 _amountOne, + uint256 _amountTwo + ) public { + vm.assume(_remoteDomain != remoteDomain); // unrecognized domain for recognized burn token + + vm.expectRevert("Mint token not supported"); + vm.prank(localTokenMessenger); + _getTokenMinterV2().mint( + _remoteDomain, + remoteTokenBytes32, + _recipientOne, + _recipientTwo, + _amountOne, + _amountTwo + ); + } + + function testMint_revertsIfNeitherRemoteDomainOrBurnTokenAreRecognized( + uint32 _remoteDomain, + bytes32 _burnToken, + address _recipientOne, + address _recipientTwo, + uint256 _amountOne, + uint256 _amountTwo + ) public { + vm.assume(_remoteDomain != remoteDomain); + vm.assume(_burnToken != remoteTokenBytes32); + + vm.expectRevert("Mint token not supported"); + vm.prank(localTokenMessenger); + _getTokenMinterV2().mint( + _remoteDomain, + _burnToken, + _recipientOne, + _recipientTwo, + _amountOne, + _amountTwo + ); + } + + function testMint_revertsIfFirstMintReverts( + address _recipientOne, + address _recipientTwo, + uint256 _amountOne, + uint256 _amountTwo + ) public { + _linkTokenPair(localTokenAddress); + + // Fail the 1st underlying mint() + vm.mockCallRevert( + localTokenAddress, + abi.encodeWithSelector( + MockMintBurnToken.mint.selector, + _recipientOne, + _amountOne + ), + "Testing - 1st mint failed" + ); + + vm.expectRevert("Testing - 1st mint failed"); + vm.prank(localTokenMessenger); + _getTokenMinterV2().mint( + remoteDomain, + remoteTokenBytes32, + _recipientOne, + _recipientTwo, + _amountOne, + _amountTwo + ); + } + + function testMint_revertsIfSecondMintReverts( + address _recipientOne, + address _recipientTwo, + uint256 _amountOne, + uint256 _amountTwo + ) public { + _linkTokenPair(localTokenAddress); + + // Fail the 2nd underlying mint() + vm.mockCallRevert( + localTokenAddress, + abi.encodeWithSelector( + MockMintBurnToken.mint.selector, + _recipientTwo, + _amountTwo + ), + "Testing - 2nd mint failed" + ); + + vm.expectRevert("Testing - 2nd mint failed"); + vm.prank(localTokenMessenger); + _getTokenMinterV2().mint( + remoteDomain, + remoteTokenBytes32, + _recipientOne, + _recipientTwo, + _amountOne, + _amountTwo + ); + } + + function testMint_revertsIfFirstMintReturnsFalse( + address _recipientOne, + address _recipientTwo, + uint256 _amountOne, + uint256 _amountTwo + ) public { + _linkTokenPair(localTokenAddress); + + // Return false from the 1st mint + vm.mockCall( + localTokenAddress, + abi.encodeWithSelector( + MockMintBurnToken.mint.selector, + _recipientOne, + _amountOne + ), + abi.encode(false) + ); + + vm.expectRevert("First mint operation failed"); + vm.prank(localTokenMessenger); + _getTokenMinterV2().mint( + remoteDomain, + remoteTokenBytes32, + _recipientOne, + _recipientTwo, + _amountOne, + _amountTwo + ); + } + + function testMint_revertsIfSecondMintReturnsFalse( + address _recipientOne, + address _recipientTwo, + uint256 _amountOne, + uint256 _amountTwo + ) public { + _linkTokenPair(localTokenAddress); + + // Return false from the 2nd mint + vm.mockCall( + localTokenAddress, + abi.encodeWithSelector( + MockMintBurnToken.mint.selector, + _recipientTwo, + _amountTwo + ), + abi.encode(false) + ); + + vm.expectRevert("Second mint operation failed"); + vm.prank(localTokenMessenger); + _getTokenMinterV2().mint( + remoteDomain, + remoteTokenBytes32, + _recipientOne, + _recipientTwo, + _amountOne, + _amountTwo + ); + } + + function testMint_succeeds( + address _recipientOne, + address _recipientTwo, + uint256 _amountOne, + uint256 _amountTwo + ) public { + vm.assume(type(uint256).max - _amountOne > _amountTwo); + vm.assume(_recipientOne != _recipientTwo); + + _linkTokenPair(localTokenAddress); + + // Sanity check + assertEq(localToken.balanceOf(_recipientOne), 0); + assertEq(localToken.balanceOf(_recipientTwo), 0); + + vm.expectCall( + localTokenAddress, + abi.encodeWithSelector( + MockMintBurnToken.mint.selector, + _recipientOne, + _amountOne + ), + 1 + ); + + vm.expectCall( + localTokenAddress, + abi.encodeWithSelector( + MockMintBurnToken.mint.selector, + _recipientTwo, + _amountTwo + ), + 1 + ); + + vm.prank(localTokenMessenger); + _getTokenMinterV2().mint( + remoteDomain, + remoteTokenBytes32, + _recipientOne, + _recipientTwo, + _amountOne, + _amountTwo + ); + + // Sanity check ending balances + assertEq(localToken.balanceOf(_recipientOne), _amountOne); + assertEq(localToken.balanceOf(_recipientTwo), _amountTwo); + } + + // Test Helpers + + function _getTokenMinterV2() internal view returns (TokenMinterV2) { + return TokenMinterV2(address(tokenMinter)); + } +}