From 3818bedaf90b99c8843eab532434d9e415abfaca Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:43:31 -0400 Subject: [PATCH 01/40] STABLE-6895 (Part 1): Add initial BaseTokenMessenger implementation and tests (#10) As discussed offline with @walkerq, breaking apart STABLE-6895 into 2 parts: 1) Extract base token messenger behavior and tests, copied from v1, into a separate type (`BaseTokenMessenger`). This base type handles adding / removing remote token messengers, the local minter, and encodes the local message transmitter / version. `TokenMessengerV2` derives from this type and layers on the v2-specific, messaging-layer differences. I think this is more flexible for a potential v3 and is a natural separation of concerns, but definitely open to further discussion! 2) (Future) integrate hooks, the first V2-specific functionality, into `TokenMessengerV2`. Some misc callouts: - Intention is leave the v1 contract code unmodified (optional: in the future we can refactor `TokenMessenger.t.sol` to use these shared baseTokenMessenger tests to avoid duplication). - Added some additional Ownable2Step test cases into `TestUtils.sol`. - Copied over the v1 unit tests from `TokenMessenger.t.sol`, but injected more fuzzable inputs into some of the test cases. Further callouts below in-line. --- src/v2/BaseTokenMessenger.sol | 227 +++++++++++++++++++++ src/v2/TokenMessengerV2.sol | 42 ++++ test/TestUtils.sol | 55 ++++- test/v2/BaseTokenMessenger.t.sol | 339 +++++++++++++++++++++++++++++++ test/v2/TokenMessengerV2.t.sol | 58 ++++++ 5 files changed, 712 insertions(+), 9 deletions(-) create mode 100644 src/v2/BaseTokenMessenger.sol create mode 100644 src/v2/TokenMessengerV2.sol create mode 100644 test/v2/BaseTokenMessenger.t.sol create mode 100644 test/v2/TokenMessengerV2.t.sol diff --git a/src/v2/BaseTokenMessenger.sol b/src/v2/BaseTokenMessenger.sol new file mode 100644 index 0000000..0921e22 --- /dev/null +++ b/src/v2/BaseTokenMessenger.sol @@ -0,0 +1,227 @@ +/* + * 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 "../roles/Ownable2Step.sol"; +import {ITokenMinter} from "../interfaces/ITokenMinter.sol"; +import {Rescuable} from "../roles/Rescuable.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 { + // ============ 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 + * @notice Emitted when the local minter is added + */ + event LocalMinterAdded(address localMinter); + + /** + * @notice Emitted when the local minter is removed + * @param localMinter address of local minter + * @notice Emitted when the local minter is removed + */ + event LocalMinterRemoved(address localMinter); + + // ============ 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 + ITokenMinter public localMinter; + + // Valid TokenMessengers on remote domains + mapping(uint32 => bytes32) public remoteTokenMessengers; + + // ============ 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 { + require(tokenMessenger != bytes32(0), "bytes32(0) not allowed"); + + require( + remoteTokenMessengers[domain] == bytes32(0), + "TokenMessenger already set" + ); + + remoteTokenMessengers[domain] = tokenMessenger; + emit RemoteTokenMessengerAdded(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 { + require(newLocalMinter != address(0), "Zero address not allowed"); + + require( + address(localMinter) == address(0), + "Local minter is already set." + ); + + localMinter = ITokenMinter(newLocalMinter); + + emit LocalMinterAdded(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); + } + + // ============ 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 (ITokenMinter) { + 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 + address(localMessageTransmitter) != address(0) && + msg.sender == address(localMessageTransmitter); + } +} diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol new file mode 100644 index 0000000..6c65957 --- /dev/null +++ b/src/v2/TokenMessengerV2.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 {BaseTokenMessenger} from "./BaseTokenMessenger.sol"; + +contract TokenMessengerV2 is BaseTokenMessenger { + // ============ Events ============ + + // ============ State Variables ============ + + // ============ Modifiers ============ + + // ============ Constructor ============ + /** + * @param _messageTransmitter Message transmitter address + * @param _messageBodyVersion Message body version + */ + constructor( + address _messageTransmitter, + uint32 _messageBodyVersion + ) BaseTokenMessenger(_messageTransmitter, _messageBodyVersion) {} + + // ============ External Functions ============ + + // ============ Internal Utils ============ +} diff --git a/test/TestUtils.sol b/test/TestUtils.sol index 7e9fddb..a75e3a9 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -81,7 +81,7 @@ contract TestUtils is Test { address arbitraryAddress = vm.addr(1903); // 8 KiB - uint32 maxMessageBodySize = 8 * 2**10; + uint32 maxMessageBodySize = 8 * 2 ** 10; // zero signature bytes zeroSignature = "00000000000000000000000000000000000000000000000000000000000000000"; @@ -136,6 +136,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"); @@ -215,6 +220,39 @@ contract TestUtils is Test { assertEq(_pausableContract.pauser(), _newPauser); } + 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 transferOwnershipAndAcceptOwnership( address _ownableContractAddress, address _newOwner @@ -284,19 +322,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/v2/BaseTokenMessenger.t.sol b/test/v2/BaseTokenMessenger.t.sol new file mode 100644 index 0000000..9b60d3a --- /dev/null +++ b/test/v2/BaseTokenMessenger.t.sol @@ -0,0 +1,339 @@ +/* + * 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 {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 + /** + * @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 + * @notice Emitted when the local minter is added + */ + event LocalMinterAdded(address localMinter); + + /** + * @notice Emitted when the local minter is removed + * @param localMinter address of local minter + * @notice Emitted when the local minter is removed + */ + event LocalMinterRemoved(address localMinter); + + 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(); + } + + // 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 + ) public { + assertContractIsRescuable( + address(baseTokenMessenger), + _rescuer, + _rescueRecipient, + _amount + ); + } + + // 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/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol new file mode 100644 index 0000000..7014657 --- /dev/null +++ b/test/v2/TokenMessengerV2.t.sol @@ -0,0 +1,58 @@ +/* + * 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 {BaseTokenMessengerTest} from "./BaseTokenMessenger.t.sol"; +import {TokenMessengerV2} from "../../src/v2/TokenMessengerV2.sol"; + +contract TokenMessengerV2Test is BaseTokenMessengerTest { + // Constants + uint32 messageBodyVersion = 2; + address localMessageTransmitter = address(10); + + TokenMessengerV2 tokenMessenger; + + function setUp() public override { + tokenMessenger = new TokenMessengerV2( + localMessageTransmitter, + messageBodyVersion + ); + + super.setUp(); + } + + function setUpBaseTokenMessenger() + internal + view + override + returns (address) + { + return address(tokenMessenger); + } + + function createBaseTokenMessenger( + address _localMessageTransmitter, + uint32 _messageBodyVersion + ) internal override returns (address) { + TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( + _localMessageTransmitter, + _messageBodyVersion + ); + return address(_tokenMessenger); + } +} From a6ff5ee779f664070241e5d1fdb7acccb8c43d5c Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:43:22 -0400 Subject: [PATCH 02/40] STABLE-6895: Part 2/3, implement depositForBurnWithHook relay-side (#13) ## Summary This implements `depositForBurnWithHook` on the relay side. This includes: - Updating `TokenMessengerV2` with a new `depositForBurnWithHook` function - Validating inputs - Using `BurnMessageV2` to format the burn message correctly (note: the Iris-inserted fields are omitted). - Calling message transmitter with the formatted message. It does _not_ cover: - MessageTransmitt-ing - Handling receive messages or executing hooks ^ will be taken care of in part 3. ## Testing This extends the `TokenMessengerV2` tests, using many of the same test cases from `TokenMessenger.t.sol`. Some callouts and questions below in-line. ## Discussion Open to any and all ideas for further code re-use between v1 and v2. The principle has been to leave v1 as-is, so re-use as focused on implementations and tests, vs strictly sharing code. --- src/interfaces/v2/IRelayerV2.sol | 44 +++ src/messages/v2/BurnMessageV2.sol | 62 ++++ src/v2/MessageTransmitterV2.sol | 35 ++ src/v2/TokenMessengerV2.sol | 154 ++++++++ test/messages/v2/BurnMessageV2.t.sol | 61 ++++ test/v2/TokenMessengerV2.t.sol | 520 ++++++++++++++++++++++++++- 6 files changed, 867 insertions(+), 9 deletions(-) create mode 100644 src/interfaces/v2/IRelayerV2.sol create mode 100644 src/messages/v2/BurnMessageV2.sol create mode 100644 src/v2/MessageTransmitterV2.sol create mode 100644 test/messages/v2/BurnMessageV2.t.sol diff --git a/src/interfaces/v2/IRelayerV2.sol b/src/interfaces/v2/IRelayerV2.sol new file mode 100644 index 0000000..5cb0add --- /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 source domain to 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 Caller on destination domain. + * @param minFinalityThreshold Minimum finality threshold requested. + * @param messageBody Raw bytes content of message + */ + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes32 destinationCaller, + uint32 minFinalityThreshold, + bytes calldata messageBody + ) external; +} diff --git a/src/messages/v2/BurnMessageV2.sol b/src/messages/v2/BurnMessageV2.sol new file mode 100644 index 0000000..d58eba0 --- /dev/null +++ b/src/messages/v2/BurnMessageV2.sol @@ -0,0 +1,62 @@ +/* + * 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 TODO: STABLE-6895 + * @dev TODO: STABLE-6895 + **/ +library BurnMessageV2 { + /** + * @notice Formats a burn message + * @param _version The message body version + * @param _burnToken The burn token address on source domain as bytes32 + * @param _mintRecipient The mint recipient address as bytes32 + * @param _amount The burn amount + * @param _messageSender The message sender + * @param _feeRequested The fee requested for the burn + * @param _hook Optional hook to execute on destination domain + * @return Formatted message. + */ + function _formatMessage( + uint32 _version, + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _messageSender, + uint256 _feeRequested, + bytes calldata _hook + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + _version, + _burnToken, + _mintRecipient, + _amount, + _messageSender, + _feeRequested, + _hook + ); + } + + // TODO: STABLE-6895, complete implementation, including message validation. +} diff --git a/src/v2/MessageTransmitterV2.sol b/src/v2/MessageTransmitterV2.sol new file mode 100644 index 0000000..dc051e5 --- /dev/null +++ b/src/v2/MessageTransmitterV2.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; + +import {IRelayerV2} from "../interfaces/v2/IRelayerV2.sol"; + +/** + * @title MessageTransmitterV2 + * @notice Contract responsible for sending and receiving messages across chains. + */ +contract MessageTransmitterV2 is IRelayerV2 { + // TODO: STABLE-6895 + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes32 destinationCaller, + uint32 minFinalityThreshold, + bytes calldata messageBody + ) external override {} +} diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol index 6c65957..12804d8 100644 --- a/src/v2/TokenMessengerV2.sol +++ b/src/v2/TokenMessengerV2.sol @@ -18,11 +18,50 @@ pragma solidity 0.7.6; import {BaseTokenMessenger} from "./BaseTokenMessenger.sol"; +import {ITokenMinter} from "../interfaces/ITokenMinter.sol"; +import {IMintBurnToken} from "../interfaces/IMintBurnToken.sol"; +import {BurnMessageV2} from "../messages/v2/BurnMessageV2.sol"; +import {Message} from "../messages/Message.sol"; +import {MessageTransmitterV2} from "./MessageTransmitterV2.sol"; +/** + * @title TokenMessengerV2 + * @notice Sends messages and receives messages to/from MessageTransmitters + * and to/from TokenMinters + */ contract TokenMessengerV2 is 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 fee requested fee to pay on destination domain, in units of burnToken + * @param minFinalityThreshold the minimum finality at which the message should be attested to. + * @param hook target and calldata for execution on destination domain + */ + event DepositForBurn( + address indexed burnToken, + uint256 amount, + address indexed depositor, + bytes32 mintRecipient, + uint32 destinationDomain, + bytes32 destinationTokenMessenger, + bytes32 destinationCaller, + uint256 fee, + uint32 indexed minFinalityThreshold, + bytes hook + ); + + // ============ Libraries ============ // ============ State Variables ============ + uint32 public immutable MIN_HOOK_LENGTH = 32; // ============ Modifiers ============ @@ -37,6 +76,121 @@ contract TokenMessengerV2 is BaseTokenMessenger { ) BaseTokenMessenger(_messageTransmitter, _messageBodyVersion) {} // ============ External Functions ============ + /** + * @notice Deposits and burns tokens from sender to be minted on destination domain. + * Emits a `DepositForBurn` event. + * @dev reverts if: + * - hook appears invalid, such as being less than 32 bytes in length + * - 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. + * - fee is greater than or equal to the amount. + * - MessageTransmitter#sendMessage reverts. + * @dev Note that even if the hook reverts on the destination domain, the mint will still proceed. + * @dev Hook formatting: + * - TODO: STABLE-7280 + * @param amount amount of tokens to burn + * @param destinationDomain destination domain + * @param mintRecipient address of mint recipient on destination domain + * @param burnToken address of contract to burn deposited tokens, on local domain + * @param destinationCaller caller on the destination domain, as bytes32 + * @param fee requested 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. + * @param hook hook to execute on destination domain. Must be 32-bytes length or more. + */ + function depositForBurnWithHook( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller, + uint256 fee, + uint32 minFinalityThreshold, + bytes calldata hook + ) external { + require(hook.length >= MIN_HOOK_LENGTH, "Invalid hook length"); + + _depositForBurn( + amount, + destinationDomain, + mintRecipient, + burnToken, + destinationCaller, + fee, + minFinalityThreshold, + hook + ); + } // ============ Internal Utils ============ + function _depositForBurn( + uint256 _amount, + uint32 _destinationDomain, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint256 _fee, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) internal { + require(_amount > 0, "Amount must be nonzero"); + require(_mintRecipient != bytes32(0), "Mint recipient must be nonzero"); + require(_fee < _amount, "Fee must be less than amount"); + + bytes32 _destinationTokenMessenger = _getRemoteTokenMessenger( + _destinationDomain + ); + + // Deposit and burn tokens + _depositAndBurnTokens(_burnToken, msg.sender, _amount); + + // Format message body + bytes memory _burnMessage = BurnMessageV2._formatMessage( + messageBodyVersion, + Message.addressToBytes32(_burnToken), + _mintRecipient, + _amount, + Message.addressToBytes32(msg.sender), + _fee, + _hook + ); + + // Send message + MessageTransmitterV2(localMessageTransmitter).sendMessage( + _destinationDomain, + _destinationTokenMessenger, + _destinationCaller, + _minFinalityThreshold, + _burnMessage + ); + + emit DepositForBurn( + _burnToken, + _amount, + msg.sender, + _mintRecipient, + _destinationDomain, + _destinationTokenMessenger, + _destinationCaller, + _fee, + _minFinalityThreshold, + _hook + ); + } + + function _depositAndBurnTokens( + address _burnToken, + address _from, + uint256 _amount + ) internal { + ITokenMinter _localMinter = _getLocalMinter(); + IMintBurnToken _mintBurnToken = IMintBurnToken(_burnToken); + require( + _mintBurnToken.transferFrom(_from, address(_localMinter), _amount), + "Transfer operation failed" + ); + _localMinter.burn(_burnToken, _amount); + } } diff --git a/test/messages/v2/BurnMessageV2.t.sol b/test/messages/v2/BurnMessageV2.t.sol new file mode 100644 index 0000000..3576719 --- /dev/null +++ b/test/messages/v2/BurnMessageV2.t.sol @@ -0,0 +1,61 @@ +/* + * 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 {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 testFormatMessage_succeeds( + uint32 _version, + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _messageSender, + uint256 _feeRequested, + bytes calldata _hook + ) public { + bytes memory _expectedMessageBody = abi.encodePacked( + _version, + _burnToken, + _mintRecipient, + _amount, + _messageSender, + _feeRequested, + _hook + ); + + bytes memory _messageBody = BurnMessageV2._formatMessage( + _version, + _burnToken, + _mintRecipient, + _amount, + _messageSender, + _feeRequested, + _hook + ); + + bytes29 _m = _messageBody.ref(0); + assertEq(_expectedMessageBody.ref(0).keccak(), _m.keccak()); + } +} diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index 7014657..387817c 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -19,30 +19,96 @@ pragma solidity 0.7.6; import {BaseTokenMessengerTest} from "./BaseTokenMessenger.t.sol"; import {TokenMessengerV2} from "../../src/v2/TokenMessengerV2.sol"; +import {Message} from "../../src/messages/Message.sol"; +import {MockMintBurnToken} from "../mocks/MockMintBurnToken.sol"; +import {TokenMinter} from "../../src/TokenMinter.sol"; +import {MessageTransmitterV2} from "../../src/v2/MessageTransmitterV2.sol"; +import {BurnMessageV2} from "../../src/messages/v2/BurnMessageV2.sol"; contract TokenMessengerV2Test is BaseTokenMessengerTest { + // 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 not equal to bytes32(0). + * @param fee fee paid for fast burn on destination domain, in burnToken + * @param minFinalityThreshold the minimum finality at which the message should be attested to. + * @param hook hook target and calldata for execution on destination domain + */ + event DepositForBurn( + address indexed burnToken, + uint256 amount, + address indexed depositor, + bytes32 mintRecipient, + uint32 destinationDomain, + bytes32 destinationTokenMessenger, + bytes32 destinationCaller, + uint256 fee, + uint32 indexed minFinalityThreshold, + bytes hook + ); + // Constants + uint32 remoteDomain = 1; uint32 messageBodyVersion = 2; + address localMessageTransmitter = address(10); + address remoteMessageTransmitter = address(20); + + TokenMessengerV2 localTokenMessenger; + + address remoteTokenMessageger = address(30); + bytes32 remoteTokenMessengerAddr; - TokenMessengerV2 tokenMessenger; + address remoteTokenAddr = address(40); + + MockMintBurnToken localToken = new MockMintBurnToken(); + TokenMinter localTokenMinter = new TokenMinter(tokenController); function setUp() public override { - tokenMessenger = new TokenMessengerV2( + // TokenMessenger under test + localTokenMessenger = new TokenMessengerV2( localMessageTransmitter, messageBodyVersion ); + // Add a local minter + localTokenMessenger.addLocalMinter(address(localTokenMinter)); + + remoteTokenMessengerAddr = Message.addressToBytes32( + remoteTokenMessageger + ); + + // Register remote token messenger + localTokenMessenger.addRemoteTokenMessenger( + remoteDomain, + remoteTokenMessengerAddr + ); + + linkTokenPair( + localTokenMinter, + address(localToken), + remoteDomain, + Message.addressToBytes32(remoteTokenAddr) + ); + + localTokenMinter.addLocalTokenMessenger(address(localTokenMessenger)); super.setUp(); } - function setUpBaseTokenMessenger() - internal - view - override - returns (address) - { - return address(tokenMessenger); + // BaseTokenMessengerTest overrides + + function setUpBaseTokenMessenger() internal override returns (address) { + TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( + localMessageTransmitter, + messageBodyVersion + ); + return address(_tokenMessenger); } function createBaseTokenMessenger( @@ -55,4 +121,440 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); return address(_tokenMessenger); } + + // Tests + + function testDepositForBurnWithHook_revertsIfNoRemoteTokenMessengerExistsForDomain( + uint256 _amount, + uint32 _remoteDomain, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint256 _fee, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) public { + vm.assume(_amount > 0); + vm.assume(_fee < _amount); + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hook.length > 32); + + TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( + localMessageTransmitter, + messageBodyVersion + ); + + vm.expectRevert("No TokenMessenger for domain"); + _tokenMessenger.depositForBurnWithHook( + _amount, + _remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + _fee, + _minFinalityThreshold, + _hook + ); + } + + function testDepositForBurnWithHook_revertsIfLocalMinterIsNotSet( + uint256 _amount, + uint32 _remoteDomain, + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint256 _fee, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) public { + vm.assume(_amount > 0); + vm.assume(_fee < _amount); + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hook.length > 32); + + TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( + localMessageTransmitter, + messageBodyVersion + ); + + _tokenMessenger.addRemoteTokenMessenger( + _remoteDomain, + remoteTokenMessengerAddr + ); + + vm.expectRevert("Local minter is not set"); + _tokenMessenger.depositForBurnWithHook( + _amount, + _remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + _fee, + _minFinalityThreshold, + _hook + ); + } + + function testDepositForBurnWithHook_revertsIfTransferAmountIsZero( + bytes32 _mintRecipient, + address _burnToken, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hook.length > 32); + + vm.expectRevert("Amount must be nonzero"); + localTokenMessenger.depositForBurnWithHook( + 0, // amount + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + 0, + _minFinalityThreshold, + _hook + ); + } + + function testDepositForBurnWithHook_revertsIfMintRecipientIsZero( + uint256 _amount, + address _burnToken, + bytes32 _destinationCaller, + uint256 _fee, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) public { + vm.assume(_amount > 0); + vm.assume(_fee < _amount); + vm.assume(_hook.length > 32); + + vm.expectRevert("Mint recipient must be nonzero"); + localTokenMessenger.depositForBurnWithHook( + _amount, + remoteDomain, + bytes32(0), // mintRecipient + _burnToken, + _destinationCaller, + _fee, + _minFinalityThreshold, + _hook + ); + } + + function testDepositForBurnWithHook_revertsIfFeeEqualsTransferAmount( + uint256 _amount, + address _burnToken, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) public { + vm.assume(_amount > 0); + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hook.length > 32); + + vm.expectRevert("Fee must be less than amount"); + localTokenMessenger.depositForBurnWithHook( + _amount, + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + _amount, // fee + _minFinalityThreshold, + _hook + ); + } + + function testDepositForBurnWithHook_revertsIfFeeExceedsTransferAmount( + uint256 _amount, + address _burnToken, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint256 _fee, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) public { + vm.assume(_amount > 0); + vm.assume(_fee > _amount); + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hook.length > 32); + + vm.expectRevert("Fee must be less than amount"); + localTokenMessenger.depositForBurnWithHook( + _amount, + remoteDomain, + _mintRecipient, + _burnToken, + _destinationCaller, + _fee, + _minFinalityThreshold, + _hook + ); + } + + function testDepositForBurnWithHook_revertsOnFailedTokenTransfer( + uint256 _amount, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint256 _fee, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) public { + vm.assume(_amount > 0); + vm.assume(_fee < _amount); + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hook.length > 32); + + 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, + _fee, + _minFinalityThreshold, + _hook + ); + } + + function testDepositForBurnWithHook_revertsIfTokenTransferReverts( + address _caller, + uint256 _amount, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint256 _fee, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) public { + vm.assume(_amount > 0); + vm.assume(_fee < _amount); + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hook.length > 32); + + // 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, + _fee, + _minFinalityThreshold, + _hook + ); + } + + function testDepositForBurnWithHook_revertsIfTransferAmountExceedsMaxBurnAmountPerMessage( + uint256 _amount, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint256 _fee, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hook.length > 32); + vm.assume(_fee < _amount); + vm.assume(_amount > 1); + + _setupDepositForBurn(arbitraryAddress, _amount, _amount - 1); + + vm.prank(arbitraryAddress); + vm.expectRevert("Burn amount exceeds per tx limit"); + localTokenMessenger.depositForBurnWithHook( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _fee, + _minFinalityThreshold, + _hook + ); + } + + function testDepositForBurnWithHook_revertsIfHookIsEmpty( + uint256 _amount, + uint256 _fee, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_fee < _amount); + vm.assume(_amount > 1); + + vm.expectRevert("Invalid hook length"); + localTokenMessenger.depositForBurnWithHook( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _fee, + _minFinalityThreshold, + bytes("") + ); + } + + function testDepositForBurnWithHook_revertsIfHookIsTooShort( + uint256 _amount, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_amount > 1); + vm.assume(_hook.length > 0 && _hook.length < 32); + + vm.expectRevert("Invalid hook length"); + localTokenMessenger.depositForBurnWithHook( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _amount - 1, // fee + _minFinalityThreshold, + _hook + ); + } + + function testDepositForBurnWithHook_succeeds( + uint256 _amount, + uint256 _burnLimit, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_amount > 1); + vm.assume(_amount < _burnLimit); + vm.assume(_hook.length > 32); + + uint256 _fee = _amount - 1; + + _setupDepositForBurn(arbitraryAddress, _amount, _burnLimit); + + _depositForBurn( + arbitraryAddress, + _mintRecipient, + _destinationCaller, + _amount, + _fee, + _minFinalityThreshold, + _hook + ); + } + + 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 _fee, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) internal { + bytes memory _expectedBurnMessage = BurnMessageV2._formatMessage( + messageBodyVersion, + Message.addressToBytes32(address(localToken)), + _mintRecipient, + _amount, + Message.addressToBytes32(_caller), + _fee, + _hook + ); + + // 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, + _fee, + _minFinalityThreshold, + _hook + ); + + vm.prank(_caller); + localTokenMessenger.depositForBurnWithHook( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _fee, + _minFinalityThreshold, + _hook + ); + } } From 8b7aa906b2b2b0ed324e0e5f23c8c0290c8cd0ce Mon Sep 17 00:00:00 2001 From: epoon-circle Date: Wed, 11 Sep 2024 06:57:57 -0700 Subject: [PATCH 03/40] Stable 7247 evm contract deploy process refactor (#14) As described by [STABLE-7247](https://circlepay.atlassian.net/browse/STABLE-7247?atlOrigin=eyJpIjoiY2ZjMzRmZGI5Y2Y5NDgyN2JhNjJlM2E5ZGU5NjVlOWMiLCJwIjoiaiJ9): - Copied over deploy scripts 1-4 from evm-bridge-contracts - Added tests to verify deployed contracts' parameters [STABLE-7247]: https://circlepay.atlassian.net/browse/STABLE-7247?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- Makefile | 30 ++- README.md | 123 +++++++++- anvil/Counter.sol | 20 -- anvil/scripts/Counter.s.sol | 14 -- scripts/{ => v1}/deploy.s.sol | 14 +- scripts/v2/1_deploy.s.sol | 274 ++++++++++++++++++++++ scripts/v2/2_setupSecondAttester.s.sol | 63 +++++ scripts/v2/3_setupRemoteResources.s.sol | 108 +++++++++ scripts/v2/4_rotateKeys.s.sol | 114 +++++++++ test/scripts/1_deploy.t.sol | 104 ++++++++ test/scripts/2_setupSecondAttester.t.sol | 36 +++ test/scripts/3_setupRemoteResources.t.sol | 43 ++++ test/scripts/4_rotateKeys.t.sol | 40 ++++ test/scripts/ScriptV2TestUtils.sol | 170 ++++++++++++++ 14 files changed, 1104 insertions(+), 49 deletions(-) delete mode 100644 anvil/Counter.sol delete mode 100644 anvil/scripts/Counter.s.sol rename scripts/{ => v1}/deploy.s.sol (97%) create mode 100644 scripts/v2/1_deploy.s.sol create mode 100644 scripts/v2/2_setupSecondAttester.s.sol create mode 100644 scripts/v2/3_setupRemoteResources.s.sol create mode 100644 scripts/v2/4_rotateKeys.s.sol create mode 100644 test/scripts/1_deploy.t.sol create mode 100644 test/scripts/2_setupSecondAttester.t.sol create mode 100644 test/scripts/3_setupRemoteResources.t.sol create mode 100644 test/scripts/4_rotateKeys.t.sol create mode 100644 test/scripts/ScriptV2TestUtils.sol diff --git a/Makefile b/Makefile index 3fa14cf..5e7f5f3 100644 --- a/Makefile +++ b/Makefile @@ -9,11 +9,35 @@ 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-deployv2: + forge script scripts/v2/1_deploy.s.sol:DeployV2Script --rpc-url ${RPC_URL} --sender ${SENDER} + +deployv2: + forge script scripts/v2/1_deploy.s.sol:DeployV2Script --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast + +simulate-setup-second-attester: + forge script scripts/v2/2_setupSecondAttester.s.sol:SetupSecondAttesterScript --rpc-url ${RPC_URL} --sender ${SENDER} + +setup-second-attester: + forge script scripts/v2/2_setupSecondAttester.s.sol:SetupSecondAttesterScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast + +simulate-setup-remote-resources: + forge script scripts/v2/3_setupRemoteResources.s.sol:SetupRemoteResourcesScript --rpc-url ${RPC_URL} --sender ${SENDER} + +setup-remote-resources: + forge script scripts/v2/3_setupRemoteResources.s.sol:SetupRemoteResourcesScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast + +simulate-rotate-keys: + forge script scripts/v2/4_rotateKeys.s.sol:RotateKeysScript --rpc-url ${RPC_URL} --sender ${SENDER} + +rotate-keys: + forge script scripts/v2/4_rotateKeys.s.sol:RotateKeysScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast anvil: docker rm -f anvil || true diff --git a/README.md b/README.md index 2e0cdd9..734e8d3 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,53 @@ - 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. ). + +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). ## 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. ### 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) ### 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 +91,105 @@ 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 + +The contracts are deployed using [Forge Scripts](https://book.getfoundry.sh/tutorials/solidity-scripting). The script is located in [scripts/v2/1_deploy.s.sol](/scripts/v2/1_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` + - `TOKEN_MINTER_DEPLOYER_KEY` + - `TOKEN_CONTROLLER_DEPLOYER_KEY` + - `ATTESTER_ADDRESS` + - `USDC_CONTRACT_ADDRESS` + - `REMOTE_USDC_CONTRACT_ADDRESS` + - `MESSAGE_TRANSMITTER_PAUSER_ADDRESS` + - `TOKEN_MINTER_PAUSER_ADDRESS` + - `MESSAGE_TRANSMITTER_RESCUER_ADDRESS` + - `TOKEN_MESSENGER_RESCUER_ADDRESS` + - `TOKEN_MINTER_RESCUER_ADDRESS` + - `TOKEN_CONTROLLER_ADDRESS` + - `DOMAIN` + - `REMOTE_DOMAIN` + - `BURN_LIMIT_PER_MESSAGE` + + In addition, to link the remote bridge, one of two steps needs to be followed: + + - 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-deployv2 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 deployv2 RPC_URL= SENDER=` to deploy the contracts + +4. Replace the environment variables in your [env](.env) file with: + + - `MESSAGE_TRANSMITTER_CONTRACT_ADDRESS` + - `MESSAGE_TRANSMITTER_DEPLOYER_KEY` + - `NEW_ATTESTER_MANAGER_ADDRESS` + - `SECOND_ATTESTER_ADDRESS` + +5. Run `make simulate-setup-second-attester RPC_URL= SENDER=` to perform a dry run of setting up the second attester. + +6. Run `make setup-second-attester RPC_URL= SENDER=` to setup the second attester. + +7. 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_DEPLOYER_KEY` + - `TOKEN_CONTROLLER_KEY` + - `REMOTE_TOKEN_MESSENGER_ADDRESS` + - `TOKEN_MINTER_CONTRACT_ADDRESS` + - `TOKEN_MESSENGER_CONTRACT_ADDRESS` + - `REMOTE_USDC_CONTRACT_ADDRESS` + - `USDC_CONTRACT_ADDRESS` + - `REMOTE_DOMAIN` + +8. Run `make simulate-setup-remote-resources RPC_URL= SENDER=` to perform a dry run of adding remote resources. + +9. Run `make setup-remote-resources RPC_URL= SENDER=` to setup the remote resources. + +10. Repeat steps 7-9 for all remote resources. This needs to be done for all existing remote chains at + contract setup except for the initial remote chain used in `1_deploy.s.sol`. + +**[Only execute the following if replacing remote resources for an existing chain]** + +11. Replace the environment variables in your [env](.env) file with the following. We'll replace one set of remote resources for a given chain (e.g. changing the remote token messenger and remote usdc contract addresses) at a time so it will need to be repeated for each applicable chain. + - `TOKEN_MESSENGER_DEPLOYER_KEY` + - `TOKEN_CONTROLLER_KEY` + - `REMOTE_TOKEN_MESSENGER_ADDRESS` + - `REMOTE_TOKEN_MESSENGER_ADDRESS_DEPRECATED` + - `TOKEN_MINTER_CONTRACT_ADDRESS` + - `TOKEN_MESSENGER_CONTRACT_ADDRESS` + - `REMOTE_USDC_CONTRACT_ADDRESS` + - `REMOTE_USDC_CONTRACT_ADDRESS_DEPRECATED` + - `USDC_CONTRACT_ADDRESS` + - `REMOTE_DOMAIN` +12. Run `make simulate-replace-remote-resources RPC_URL= SENDER=` to perform a dry run of replacing remote resources. + +13. Run `make replace-remote-resources RPC_URL= SENDER=` to replace the remote resources. + +**[Remaining steps are only for mainnet]** + +14. Replace the environment variables in your [env](.env) file with: + + - `MESSAGE_TRANSMITTER_CONTRACT_ADDRESS` + - `TOKEN_MESSENGER_CONTRACT_ADDRESS` + - `TOKEN_MINTER_CONTRACT_ADDRESS` + - `MESSAGE_TRANSMITTER_DEPLOYER_KEY` + - `TOKEN_MESSENGER_DEPLOYER_KEY` + - `TOKEN_MINTER_DEPLOYER_KEY` + - `MESSAGE_TRANSMITTER_NEW_OWNER_ADDRESS` + - `TOKEN_MESSENGER_NEW_OWNER_ADDRESS` + - `TOKEN_MINTER_NEW_OWNER_ADDRESS` + - `NEW_TOKEN_CONTROLLER_ADDRESS` + +15. Run `make simulate-rotate-keys RPC_URL= SENDER=` to perform a dry run of rotating the keys. + +16. Run `make rotate-keys RPC_URL= SENDER=` to rotate keys. + ## 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/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/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/scripts/deploy.s.sol b/scripts/v1/deploy.s.sol similarity index 97% rename from scripts/deploy.s.sol rename to scripts/v1/deploy.s.sol index 2574545..470489f 100644 --- a/scripts/deploy.s.sol +++ b/scripts/v1/deploy.s.sol @@ -35,10 +35,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 +139,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/1_deploy.s.sol b/scripts/v2/1_deploy.s.sol new file mode 100644 index 0000000..0bf07bd --- /dev/null +++ b/scripts/v2/1_deploy.s.sol @@ -0,0 +1,274 @@ +pragma solidity 0.7.6; + +import "forge-std/Script.sol"; +import "../../src/TokenMessenger.sol"; +import "../../src/TokenMinter.sol"; +import "../../src/MessageTransmitter.sol"; +import "../../src/messages/Message.sol"; + +contract DeployV2Script is Script { + // Expose for tests + MessageTransmitter public messageTransmitter; + TokenMessenger public tokenMessenger; + TokenMinter public tokenMinter; + + address private attesterAddress; + address private usdcContractAddress; + address private usdcRemoteContractAddress; + address private remoteTokenMessengerAddress; + address private tokenControllerAddress; + address private messageTransmitterPauserAddress; + address private tokenMinterPauserAddress; + address private messageTransmitterRescuerAddress; + address private tokenMessengerRescuerAddress; + address private tokenMinterRescuerAddress; + + uint32 private messageBodyVersion = 0; + uint32 private version = 0; + uint32 private domain; + uint32 private remoteDomain; + uint32 private maxMessageBodySize = 8192; + uint256 private burnLimitPerMessage; + + uint256 private messageTransmitterDeployerPrivateKey; + uint256 private tokenMessengerDeployerPrivateKey; + uint256 private tokenMinterDeployerPrivateKey; + uint256 private tokenControllerPrivateKey; + + /** + * @notice deploys Message Transmitter + * @param privateKey Private Key for signing the transactions + * @return MessageTransmitter instance + */ + function deployMessageTransmitter( + uint256 privateKey + ) private returns (MessageTransmitter) { + // Start recording transactions + vm.startBroadcast(privateKey); + + // Deploy MessageTransmitter + MessageTransmitter _messageTransmitter = new MessageTransmitter( + domain, + attesterAddress, + maxMessageBodySize, + version + ); + + // Add Pauser + _messageTransmitter.updatePauser(messageTransmitterPauserAddress); + + // Add Rescuer + _messageTransmitter.updateRescuer(messageTransmitterRescuerAddress); + + // Stop recording transactions + vm.stopBroadcast(); + return _messageTransmitter; + } + + /** + * @notice deploys TokenMessenger + * @param privateKey Private Key for signing the transactions + * @param messageTransmitterAddress Message Transmitter Contract address + * @return TokenMessenger instance + */ + function deployTokenMessenger( + uint256 privateKey, + address messageTransmitterAddress + ) private returns (TokenMessenger) { + // Start recording transations + vm.startBroadcast(privateKey); + + // Deploy TokenMessenger + TokenMessenger _tokenMessenger = new TokenMessenger( + messageTransmitterAddress, + messageBodyVersion + ); + + // Add Rescuer + _tokenMessenger.updateRescuer(tokenMessengerRescuerAddress); + + // Stop recording transations + vm.stopBroadcast(); + + return _tokenMessenger; + } + + /** + * @notice deploys TokenMinter + * @param privateKey Private Key for signing the transactions + * @param tokenMessengerAddress TokenMessenger Contract address + * @return TokenMinter instance + */ + function deployTokenMinter( + uint256 privateKey, + address tokenMessengerAddress + ) private returns (TokenMinter) { + // Start recording transations + vm.startBroadcast(privateKey); + + // Deploy TokenMinter + TokenMinter _tokenMinter = new TokenMinter(tokenControllerAddress); + + // Add Local TokenMessenger + _tokenMinter.addLocalTokenMessenger(tokenMessengerAddress); + + // Add Pauser + _tokenMinter.updatePauser(tokenMinterPauserAddress); + + // Add Rescuer + _tokenMinter.updateRescuer(tokenMinterRescuerAddress); + + // Stop recording transations + vm.stopBroadcast(); + + return _tokenMinter; + } + + /** + * @notice add local minter to the TokenMessenger + */ + function addMinterAddressToTokenMessenger( + TokenMessenger _tokenMessenger, + uint256 privateKey, + address minterAddress + ) private { + // Start recording transations + vm.startBroadcast(privateKey); + + _tokenMessenger.addLocalMinter(minterAddress); + + // Stop recording transations + vm.stopBroadcast(); + } + + /** + * @notice link current chain and remote chain tokens + */ + function linkTokenPair( + TokenMinter _tokenMinter, + uint256 privateKey + ) private { + // Start recording transations + vm.startBroadcast(privateKey); + + bytes32 remoteUsdcContractAddressInBytes32 = Message.addressToBytes32( + usdcRemoteContractAddress + ); + + _tokenMinter.setMaxBurnAmountPerMessage( + usdcContractAddress, + burnLimitPerMessage + ); + + _tokenMinter.linkTokenPair( + usdcContractAddress, + remoteDomain, + remoteUsdcContractAddressInBytes32 + ); + + // Stop recording transations + vm.stopBroadcast(); + } + + /** + * @notice add address of TokenMessenger deployed on another chain + */ + function addRemoteTokenMessenger( + TokenMessenger _tokenMessenger, + uint256 privateKey + ) private { + // Start recording transations + vm.startBroadcast(privateKey); + bytes32 remoteTokenMessengerAddressInBytes32 = Message.addressToBytes32( + remoteTokenMessengerAddress + ); + _tokenMessenger.addRemoteTokenMessenger( + remoteDomain, + remoteTokenMessengerAddressInBytes32 + ); + + // Stop recording transations + vm.stopBroadcast(); + } + + /** + * @notice initialize variables from environment + */ + function setUp() public { + messageTransmitterDeployerPrivateKey = vm.envUint( + "MESSAGE_TRANSMITTER_DEPLOYER_KEY" + ); + tokenMessengerDeployerPrivateKey = vm.envUint( + "TOKEN_MESSENGER_DEPLOYER_KEY" + ); + tokenMinterDeployerPrivateKey = vm.envUint("TOKEN_MINTER_DEPLOYER_KEY"); + tokenControllerPrivateKey = vm.envUint("TOKEN_CONTROLLER_DEPLOYER_KEY"); + + attesterAddress = vm.envAddress("ATTESTER_ADDRESS"); + usdcContractAddress = vm.envAddress("USDC_CONTRACT_ADDRESS"); + tokenControllerAddress = vm.envAddress("TOKEN_CONTROLLER_ADDRESS"); + burnLimitPerMessage = vm.envUint("BURN_LIMIT_PER_MESSAGE"); + + usdcRemoteContractAddress = vm.envAddress( + "REMOTE_USDC_CONTRACT_ADDRESS" + ); + + remoteTokenMessengerAddress = vm.envAddress( + "REMOTE_TOKEN_MESSENGER_ADDRESS" + ); + + domain = uint32(vm.envUint("DOMAIN")); + remoteDomain = uint32(vm.envUint("REMOTE_DOMAIN")); + + messageTransmitterPauserAddress = vm.envAddress( + "MESSAGE_TRANSMITTER_PAUSER_ADDRESS" + ); + tokenMinterPauserAddress = vm.envAddress("TOKEN_MINTER_PAUSER_ADDRESS"); + + messageTransmitterRescuerAddress = vm.envAddress( + "MESSAGE_TRANSMITTER_RESCUER_ADDRESS" + ); + tokenMessengerRescuerAddress = vm.envAddress( + "TOKEN_MESSENGER_RESCUER_ADDRESS" + ); + tokenMinterRescuerAddress = vm.envAddress( + "TOKEN_MINTER_RESCUER_ADDRESS" + ); + } + + /** + * @notice main function that will be run by forge + */ + function run() public { + // Deploy MessageTransmitter + messageTransmitter = deployMessageTransmitter( + messageTransmitterDeployerPrivateKey + ); + + // Deploy TokenMessenger + tokenMessenger = deployTokenMessenger( + tokenMessengerDeployerPrivateKey, + address(messageTransmitter) + ); + + // Deploy TokenMinter + tokenMinter = deployTokenMinter( + tokenMinterDeployerPrivateKey, + address(tokenMessenger) + ); + + // Add Local Minter + addMinterAddressToTokenMessenger( + tokenMessenger, + tokenMessengerDeployerPrivateKey, + address(tokenMinter) + ); + + // Link token pair and add remote token messenger + linkTokenPair(tokenMinter, tokenControllerPrivateKey); + addRemoteTokenMessenger( + tokenMessenger, + tokenMessengerDeployerPrivateKey + ); + } +} diff --git a/scripts/v2/2_setupSecondAttester.s.sol b/scripts/v2/2_setupSecondAttester.s.sol new file mode 100644 index 0000000..65fbc86 --- /dev/null +++ b/scripts/v2/2_setupSecondAttester.s.sol @@ -0,0 +1,63 @@ +pragma solidity 0.7.6; + +import "forge-std/Script.sol"; +import "../../src/MessageTransmitter.sol"; + +contract SetupSecondAttesterScript is Script { + address private secondAttesterAddress; + address private newAttesterManagerAddress; + address private messageTransmitterContractAddress; + + uint256 private attesterManagerPrivateKey; + + function configureSecondAttesterThenRotateAttesterManager( + uint256 privateKey + ) public { + MessageTransmitter messageTransmitter = MessageTransmitter( + messageTransmitterContractAddress + ); + + vm.startBroadcast(privateKey); + + // enable second attester + messageTransmitter.enableAttester(secondAttesterAddress); + + // setSignatureThreshold to 2 + messageTransmitter.setSignatureThreshold(2); + + // updateAttesterManager + messageTransmitter.updateAttesterManager(newAttesterManagerAddress); + + vm.stopBroadcast(); + } + + /** + * @notice initialize variables from environment + */ + function setUp() public { + messageTransmitterContractAddress = vm.envAddress( + "MESSAGE_TRANSMITTER_CONTRACT_ADDRESS" + ); + + attesterManagerPrivateKey = vm.envUint( + "MESSAGE_TRANSMITTER_DEPLOYER_KEY" + ); + + newAttesterManagerAddress = vm.envAddress( + "NEW_ATTESTER_MANAGER_ADDRESS" + ); + + secondAttesterAddress = vm.envAddress("SECOND_ATTESTER_ADDRESS"); + } + + /** + * @notice main function that will be run by forge + */ + function run() public { + setUp(); + + configureSecondAttesterThenRotateAttesterManager( + attesterManagerPrivateKey + ); + } +} diff --git a/scripts/v2/3_setupRemoteResources.s.sol b/scripts/v2/3_setupRemoteResources.s.sol new file mode 100644 index 0000000..3645278 --- /dev/null +++ b/scripts/v2/3_setupRemoteResources.s.sol @@ -0,0 +1,108 @@ +pragma solidity 0.7.6; + +import "forge-std/Script.sol"; +import "../../src/TokenMessenger.sol"; +import "../../src/TokenMinter.sol"; +import "../../src/messages/Message.sol"; + +contract SetupRemoteResourcesScript is Script { + address private usdcRemoteContractAddress; + address private usdcContractAddress; + address private remoteTokenMessengerAddress; + address private tokenMessengerContractAddress; + address private tokenMinterContractAddress; + + uint32 private remoteDomain; + + uint256 private tokenMessengerDeployerPrivateKey; + uint256 private tokenControllerPrivateKey; + + /** + * @notice link current chain and remote chain tokens + */ + function linkTokenPair( + TokenMinter tokenMinter, + uint256 privateKey + ) private { + // Start recording transactions + vm.startBroadcast(privateKey); + + bytes32 remoteUsdcContractAddressInBytes32 = Message.addressToBytes32( + usdcRemoteContractAddress + ); + + tokenMinter.linkTokenPair( + usdcContractAddress, + remoteDomain, + remoteUsdcContractAddressInBytes32 + ); + + // Stop recording transactions + vm.stopBroadcast(); + } + + /** + * @notice add address of TokenMessenger deployed on another chain + */ + function addRemoteTokenMessenger( + TokenMessenger tokenMessenger, + uint256 privateKey + ) private { + // Start recording transactions + vm.startBroadcast(privateKey); + bytes32 remoteTokenMessengerAddressInBytes32 = Message.addressToBytes32( + remoteTokenMessengerAddress + ); + tokenMessenger.addRemoteTokenMessenger( + remoteDomain, + remoteTokenMessengerAddressInBytes32 + ); + + // Stop recording transactions + vm.stopBroadcast(); + } + + /** + * @notice initialize variables from environment + */ + function setUp() public { + tokenMessengerDeployerPrivateKey = vm.envUint( + "TOKEN_MESSENGER_DEPLOYER_KEY" + ); + tokenControllerPrivateKey = vm.envUint("TOKEN_CONTROLLER_KEY"); + + tokenMessengerContractAddress = vm.envAddress( + "TOKEN_MESSENGER_CONTRACT_ADDRESS" + ); + tokenMinterContractAddress = vm.envAddress( + "TOKEN_MINTER_CONTRACT_ADDRESS" + ); + usdcContractAddress = vm.envAddress("USDC_CONTRACT_ADDRESS"); + usdcRemoteContractAddress = vm.envAddress( + "REMOTE_USDC_CONTRACT_ADDRESS" + ); + remoteTokenMessengerAddress = vm.envAddress( + "REMOTE_TOKEN_MESSENGER_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 { + TokenMessenger tokenMessenger = TokenMessenger( + tokenMessengerContractAddress + ); + TokenMinter tokenMinter = TokenMinter(tokenMinterContractAddress); + + // Link token pair and add remote token messenger + linkTokenPair(tokenMinter, tokenControllerPrivateKey); + addRemoteTokenMessenger( + tokenMessenger, + tokenMessengerDeployerPrivateKey + ); + } +} diff --git a/scripts/v2/4_rotateKeys.s.sol b/scripts/v2/4_rotateKeys.s.sol new file mode 100644 index 0000000..591f4a2 --- /dev/null +++ b/scripts/v2/4_rotateKeys.s.sol @@ -0,0 +1,114 @@ +pragma solidity 0.7.6; + +import "forge-std/Script.sol"; +import "../../src/TokenMessenger.sol"; +import "../../src/TokenMinter.sol"; +import "../../src/MessageTransmitter.sol"; + +contract RotateKeysScript is Script { + address private messageTransmitterContractAddress; + address private tokenMessengerContractAddress; + address private tokenMinterContractAddress; + address private newTokenControllerAddress; + + uint256 private messageTransmitterDeployerPrivateKey; + uint256 private tokenMessengerDeployerPrivateKey; + uint256 private tokenMinterDeployerPrivateKey; + + address private messageTransmitterNewOwnerAddress; + address private tokenMessengerNewOwnerAddress; + address private tokenMinterNewOwnerAddress; + + function rotateMessageTransmitterOwner(uint256 privateKey) public { + // load messageTransmitter + MessageTransmitter messageTransmitter = MessageTransmitter( + messageTransmitterContractAddress + ); + + vm.startBroadcast(privateKey); + + messageTransmitter.transferOwnership(messageTransmitterNewOwnerAddress); + + vm.stopBroadcast(); + } + + function rotateTokenMessengerOwner(uint256 privateKey) public { + TokenMessenger tokenMessenger = TokenMessenger( + tokenMessengerContractAddress + ); + + vm.startBroadcast(privateKey); + + tokenMessenger.transferOwnership(tokenMessengerNewOwnerAddress); + + vm.stopBroadcast(); + } + + function rotateTokenControllerThenTokenMinterOwner( + uint256 privateKey + ) public { + TokenMinter tokenMinter = TokenMinter(tokenMinterContractAddress); + + vm.startBroadcast(privateKey); + + tokenMinter.setTokenController(newTokenControllerAddress); + + tokenMinter.transferOwnership(tokenMinterNewOwnerAddress); + + vm.stopBroadcast(); + } + + /** + * @notice initialize variables from environment + */ + function setUp() public { + messageTransmitterContractAddress = vm.envAddress( + "MESSAGE_TRANSMITTER_CONTRACT_ADDRESS" + ); + + tokenMessengerContractAddress = vm.envAddress( + "TOKEN_MESSENGER_CONTRACT_ADDRESS" + ); + + tokenMinterContractAddress = vm.envAddress( + "TOKEN_MINTER_CONTRACT_ADDRESS" + ); + + messageTransmitterDeployerPrivateKey = vm.envUint( + "MESSAGE_TRANSMITTER_DEPLOYER_KEY" + ); + tokenMessengerDeployerPrivateKey = vm.envUint( + "TOKEN_MESSENGER_DEPLOYER_KEY" + ); + tokenMinterDeployerPrivateKey = vm.envUint("TOKEN_MINTER_DEPLOYER_KEY"); + + messageTransmitterNewOwnerAddress = vm.envAddress( + "MESSAGE_TRANSMITTER_NEW_OWNER_ADDRESS" + ); + + tokenMessengerNewOwnerAddress = vm.envAddress( + "TOKEN_MESSENGER_NEW_OWNER_ADDRESS" + ); + + tokenMinterNewOwnerAddress = vm.envAddress( + "TOKEN_MINTER_NEW_OWNER_ADDRESS" + ); + + newTokenControllerAddress = vm.envAddress( + "NEW_TOKEN_CONTROLLER_ADDRESS" + ); + } + + /** + * @notice main function that will be run by forge + */ + function run() public { + setUp(); + + rotateMessageTransmitterOwner(messageTransmitterDeployerPrivateKey); + rotateTokenMessengerOwner(tokenMessengerDeployerPrivateKey); + rotateTokenControllerThenTokenMinterOwner( + tokenMinterDeployerPrivateKey + ); + } +} diff --git a/test/scripts/1_deploy.t.sol b/test/scripts/1_deploy.t.sol new file mode 100644 index 0000000..991adb9 --- /dev/null +++ b/test/scripts/1_deploy.t.sol @@ -0,0 +1,104 @@ +/* + * 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 {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; + +contract DeployTest is ScriptV2TestUtils { + function setUp() public { + _deploy(); + } + + function testDeployMessageTransmitter() public { + // domain + assertEq(messageTransmitter.localDomain(), uint256(sourceDomain)); + + // attester + assertEq(messageTransmitter.attesterManager(), deployer); + assertTrue(messageTransmitter.isEnabledAttester(deployer)); + + // maxMessageBodySize + assertEq(messageTransmitter.maxMessageBodySize(), maxMessageBodySize); + + // version + assertEq(messageTransmitter.version(), uint256(version)); + + // pauser + assertEq(messageTransmitter.pauser(), pauser); + + // rescuer + assertEq(messageTransmitter.rescuer(), rescuer); + } + + function testDeployTokenMessenger() public { + // message transmitter + assertEq( + address(tokenMessenger.localMessageTransmitter()), + address(messageTransmitter) + ); + + // message body version + assertEq( + tokenMessenger.messageBodyVersion(), + uint256(_messageBodyVersion) + ); + + // rescuer + assertEq(tokenMessenger.rescuer(), rescuer); + } + + function testDeployTokenMinter() public { + // token controller + assertEq(tokenMinter.tokenController(), deployer); + + // token messenger + assertEq(tokenMinter.localTokenMessenger(), address(tokenMessenger)); + + // pauser + assertEq(tokenMinter.pauser(), pauser); + + // rescuer + assertEq(tokenMinter.rescuer(), rescuer); + } + + function testAddMinterAddressToTokenMessenger() public { + assertEq(address(tokenMessenger.localMinter()), address(tokenMinter)); + } + + function testLinkTokenPair() public { + // max burn per msg + assertEq( + tokenMinter.burnLimitsPerMessage(token), + maxBurnAmountPerMessage + ); + + // linked token pair + bytes32 remoteKey = keccak256( + abi.encodePacked( + destinationDomain, + bytes32(uint256(uint160(remoteToken))) + ) + ); + assertEq(tokenMinter.remoteTokensToLocalTokens(remoteKey), token); + } + + function testAddRemoteTokenMessenger() public { + assertEq( + tokenMessenger.remoteTokenMessengers(destinationDomain), + bytes32(uint256(uint160(remoteTokenMessengerAddress))) + ); + } +} diff --git a/test/scripts/2_setupSecondAttester.t.sol b/test/scripts/2_setupSecondAttester.t.sol new file mode 100644 index 0000000..acb1d62 --- /dev/null +++ b/test/scripts/2_setupSecondAttester.t.sol @@ -0,0 +1,36 @@ +/* + * 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 {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; + +contract SetupSecondAttesterTest is ScriptV2TestUtils { + function setUp() public { + _deploy(); + _setupSecondAttester(); + } + + function testConfigureSecondAttester() public { + // second attester enabled + assertTrue(messageTransmitter.isEnabledAttester(secondAttester)); + + // sig threshold + assertEq(messageTransmitter.signatureThreshold(), 2); + + // attester manager, didn't change + assertEq(messageTransmitter.attesterManager(), deployer); + } +} diff --git a/test/scripts/3_setupRemoteResources.t.sol b/test/scripts/3_setupRemoteResources.t.sol new file mode 100644 index 0000000..6fbd191 --- /dev/null +++ b/test/scripts/3_setupRemoteResources.t.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 {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; + +contract SetupRemoteResourcesTest is ScriptV2TestUtils { + function setUp() public { + _deploy(); + _setupSecondAttester(); + _setupRemoteResources(); + } + + function testLinkTokenPair() public { + bytes32 remoteKey = keccak256( + abi.encodePacked( + anotherRemoteDomain, + bytes32(uint256(uint160(anotherRemoteToken))) + ) + ); + assertEq(tokenMinter.remoteTokensToLocalTokens(remoteKey), token); + } + + function testAddRemoteTokenMessenger() public { + assertEq( + tokenMessenger.remoteTokenMessengers(anotherRemoteDomain), + bytes32(uint256(uint160(anotherRemoteTokenMessengerAddress))) + ); + } +} diff --git a/test/scripts/4_rotateKeys.t.sol b/test/scripts/4_rotateKeys.t.sol new file mode 100644 index 0000000..460e5a2 --- /dev/null +++ b/test/scripts/4_rotateKeys.t.sol @@ -0,0 +1,40 @@ +/* + * 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 {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; + +contract RotateKeysTest is ScriptV2TestUtils { + function setUp() public { + _deploy(); + _setupSecondAttester(); + _setupRemoteResources(); + _rotateKeys(); + } + + function testRotateMessageTransmitterOwner() public { + assertEq(messageTransmitter.pendingOwner(), newOwner); + } + + function testRotateTokenMessengerOwner() public { + assertEq(tokenMessenger.pendingOwner(), newOwner); + } + + function testRotateTokenControllerThenTokenMinterOwner() public { + assertEq(tokenMinter.tokenController(), newOwner); + assertEq(tokenMinter.pendingOwner(), newOwner); + } +} diff --git a/test/scripts/ScriptV2TestUtils.sol b/test/scripts/ScriptV2TestUtils.sol new file mode 100644 index 0000000..df809fc --- /dev/null +++ b/test/scripts/ScriptV2TestUtils.sol @@ -0,0 +1,170 @@ +/* + * 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 {DeployV2Script} from "../../scripts/v2/1_deploy.s.sol"; +import {SetupSecondAttesterScript} from "../../scripts/v2/2_setupSecondAttester.s.sol"; +import {SetupRemoteResourcesScript} from "../../scripts/v2/3_setupRemoteResources.s.sol"; +import {RotateKeysScript} from "../../scripts/v2/4_rotateKeys.s.sol"; +import {MessageTransmitter} from "../../src/MessageTransmitter.sol"; +import {TokenMessenger} from "../../src/TokenMessenger.sol"; +import {TokenMinter} from "../../src/TokenMinter.sol"; +import {MockMintBurnToken} from "../mocks/MockMintBurnToken.sol"; +import {TestUtils} from "../TestUtils.sol"; + +contract ScriptV2TestUtils is TestUtils { + uint32 _messageBodyVersion = 0; + address token; + address remoteToken; + address remoteTokenMessengerAddress; + uint256 deployerPK; + address deployer; + address pauser; + address rescuer; + MessageTransmitter messageTransmitter; + TokenMessenger tokenMessenger; + TokenMinter tokenMinter; + + uint32 anotherRemoteDomain = 2; + address anotherRemoteToken; + address anotherRemoteTokenMessengerAddress; + + uint256 newOwnerPK; + address newOwner; + + function _deploy() internal { + token = address(new MockMintBurnToken()); + remoteToken = address(new MockMintBurnToken()); + + deployerPK = uint256(keccak256("DEPLOYTEST_DEPLOYER_PK")); + deployer = vm.addr(deployerPK); + pauser = vm.addr(uint256(keccak256("DEPLOYTEST_PAUSER_PK"))); + rescuer = vm.addr(uint256(keccak256("DEPLOYTEST_RESCUER_PK"))); + + // Override env vars + vm.setEnv("MESSAGE_TRANSMITTER_DEPLOYER_KEY", vm.toString(deployerPK)); + vm.setEnv("TOKEN_MESSENGER_DEPLOYER_KEY", vm.toString(deployerPK)); + vm.setEnv("TOKEN_MINTER_DEPLOYER_KEY", vm.toString(deployerPK)); + vm.setEnv("TOKEN_CONTROLLER_DEPLOYER_KEY", vm.toString(deployerPK)); + + vm.setEnv("ATTESTER_ADDRESS", vm.toString(deployer)); + vm.setEnv("USDC_CONTRACT_ADDRESS", vm.toString(token)); + vm.setEnv("TOKEN_CONTROLLER_ADDRESS", vm.toString(deployer)); + vm.setEnv( + "BURN_LIMIT_PER_MESSAGE", + vm.toString(maxBurnAmountPerMessage) + ); + + vm.setEnv("REMOTE_USDC_CONTRACT_ADDRESS", vm.toString(remoteToken)); + + remoteTokenMessengerAddress = vm.addr( + uint256(keccak256("REMOTE_TOKEN_MESSENGER_ADDRESS")) + ); + vm.setEnv( + "REMOTE_TOKEN_MESSENGER_ADDRESS", + vm.toString(remoteTokenMessengerAddress) + ); + + vm.setEnv("DOMAIN", vm.toString(uint256(sourceDomain))); + vm.setEnv("REMOTE_DOMAIN", vm.toString(uint256(destinationDomain))); + + vm.setEnv("MESSAGE_TRANSMITTER_PAUSER_ADDRESS", vm.toString(pauser)); + vm.setEnv("TOKEN_MINTER_PAUSER_ADDRESS", vm.toString(pauser)); + + vm.setEnv("MESSAGE_TRANSMITTER_RESCUER_ADDRESS", vm.toString(rescuer)); + vm.setEnv("TOKEN_MESSENGER_RESCUER_ADDRESS", vm.toString(rescuer)); + vm.setEnv("TOKEN_MINTER_RESCUER_ADDRESS", vm.toString(rescuer)); + + DeployV2Script deployScript = new DeployV2Script(); + deployScript.setUp(); + deployScript.run(); + + messageTransmitter = deployScript.messageTransmitter(); + tokenMessenger = deployScript.tokenMessenger(); + tokenMinter = deployScript.tokenMinter(); + } + + function _setupSecondAttester() internal { + vm.setEnv( + "MESSAGE_TRANSMITTER_CONTRACT_ADDRESS", + vm.toString(address(messageTransmitter)) + ); + // [SKIP] Use same MESSAGE_TRANSMITTER_DEPLOYER_KEY + // Use same attester manager, deployer + vm.setEnv("NEW_ATTESTER_MANAGER_ADDRESS", vm.toString(deployer)); + vm.setEnv("SECOND_ATTESTER_ADDRESS", vm.toString(secondAttester)); + + SetupSecondAttesterScript setupSecondAttesterScript = new SetupSecondAttesterScript(); + setupSecondAttesterScript.setUp(); + setupSecondAttesterScript.run(); + } + + function _setupRemoteResources() internal { + // [SKIP] Use same TOKEN_MESSENGER_DEPLOYER_KEY + // Use same TOKEN_CONTROLLER_DEPLOYER_KEY as TOKEN_CONTROLLER_KEY + vm.setEnv("TOKEN_CONTROLLER_KEY", vm.toString(deployerPK)); + vm.setEnv( + "TOKEN_MESSENGER_CONTRACT_ADDRESS", + vm.toString(address(tokenMessenger)) + ); + vm.setEnv( + "TOKEN_MINTER_CONTRACT_ADDRESS", + vm.toString(address(tokenMinter)) + ); + vm.setEnv("USDC_CONTRACT_ADDRESS", vm.toString(token)); + vm.setEnv( + "REMOTE_USDC_CONTRACT_ADDRESS", + vm.toString(anotherRemoteToken) + ); + + anotherRemoteTokenMessengerAddress = vm.addr( + uint256(keccak256("ANOTHER_REMOTE_TOKEN_MESSENGER_ADDRESS")) + ); + vm.setEnv( + "REMOTE_TOKEN_MESSENGER_ADDRESS", + vm.toString(anotherRemoteTokenMessengerAddress) + ); + vm.setEnv("REMOTE_DOMAIN", vm.toString(uint256(anotherRemoteDomain))); + + SetupRemoteResourcesScript setupRemoteResourcesScript = new SetupRemoteResourcesScript(); + setupRemoteResourcesScript.setUp(); + setupRemoteResourcesScript.run(); + } + + function _rotateKeys() internal { + // [SKIP] Use same MESSAGE_TRANSMITTER_CONTRACT_ADDRESS + // [SKIP] Use same TOKEN_MESSENGER_CONTRACT_ADDRESS + // [SKIP] Use same TOKEN_MINTER_CONTRACT_ADDRESS + // [SKIP] Use same MESSAGE_TRANSMITTER_DEPLOYER_KEY + // [SKIP] Use same TOKEN_MESSENGER_DEPLOYER_KEY + // [SKIP] Use same TOKEN_MINTER_DEPLOYER_KEY + + newOwnerPK = uint256(keccak256("ROTATEKEYSTEST_NEW_OWNER")); + newOwner = vm.addr(newOwnerPK); + + vm.setEnv( + "MESSAGE_TRANSMITTER_NEW_OWNER_ADDRESS", + vm.toString(newOwner) + ); + vm.setEnv("TOKEN_MESSENGER_NEW_OWNER_ADDRESS", vm.toString(newOwner)); + vm.setEnv("TOKEN_MINTER_NEW_OWNER_ADDRESS", vm.toString(newOwner)); + vm.setEnv("NEW_TOKEN_CONTROLLER_ADDRESS", vm.toString(newOwner)); + + RotateKeysScript rotateKeysScript = new RotateKeysScript(); + rotateKeysScript.setUp(); + rotateKeysScript.run(); + } +} From 727af10f2f05492bd91c4459f0f0856d861bad04 Mon Sep 17 00:00:00 2001 From: epoon-circle Date: Wed, 11 Sep 2024 08:41:08 -0700 Subject: [PATCH 04/40] Stable 7269 eccp update foundry version forge std (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As described by [STABLE-7268](https://circlepay.atlassian.net/browse/STABLE-7269?atlOrigin=eyJpIjoiZjgzMjBiMjEyMGRhNDc1OGFkNWQxM2NlNzhhMDU3YjUiLCJwIjoiaiJ9): - Updated Dockerfile to (2024-03-15) nightly Foundry build, which is the last version that still builds locally on Mac. Version is still 0.2.0. **NOTE: Docker images that are even newer do not support arm64 (macos) and only supports linux/amd64.** There is an open task to address this issue [here](https://github.com/foundry-rs/foundry/pull/7512). - Updated lib/forge-std submodule version to v1.9.2. As a result, changed all tests to `pragma abicoder v2` for compatibility with forge-std/Test.sol. - Fixed tests that were failing due to vm cheatcodes registering as a call, thus catching vm.expectEmit(). - Fixed a test that failed due to OOG. ## [REFERENCE] AbiCoder V2 fixes/changes since solidity 0.7.6 ### 0.8.15 * ABI Encoder: When encoding an empty string coming from storage do not add a superfluous empty slot for data. ### 0.8.14 * ABI Encoder: When ABI-encoding values from calldata that contain nested arrays, correctly validate the nested array length against calldatasize() in all cases. ### 0.8.10 * Code Generator: Skip existence check for external contract if return data is expected. In this case, the ABI decoder will revert if the contract does not exist. ### 0.8.4 * ABI Decoder V2: For two-dimensional arrays and specially crafted data in memory, the result of abi.decode can depend on data elsewhere in memory. Calldata decoding is not affected. ### 0.8.0 * General: Enable ABI coder v2 by default. [STABLE-7268]: https://circlepay.atlassian.net/browse/STABLE-7268?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Co-authored-by: ams9198 <111915188+ams9198@users.noreply.github.com> --- Dockerfile | 3 +- lib/forge-std | 2 +- test/MessageTransmitter.t.sol | 33 ++++++++++--------- test/TestUtils.sol | 1 + test/TokenMessenger.t.sol | 48 ++++++++++++++++------------ test/TokenMinter.t.sol | 14 ++++---- test/messages/BurnMessage.t.sol | 1 + test/messages/Message.t.sol | 1 + test/messages/v2/BurnMessageV2.t.sol | 1 + test/roles/Attestable.t.sol | 1 + test/roles/Ownable2Step.t.sol | 1 + test/v2/BaseTokenMessenger.t.sol | 1 + test/v2/TokenMessengerV2.t.sol | 1 + 13 files changed, 61 insertions(+), 47 deletions(-) diff --git a/Dockerfile b/Dockerfile index e240985..c77c2a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,5 @@ # Use fixed foundry image - -FROM ghcr.io/foundry-rs/foundry:nightly-4a8c7d0e26a1befa526222e22737740f80a7f1c5 +FROM ghcr.io/foundry-rs/foundry:nightly-3fa02706ca732c994715ba42d923605692062375 # Copy our source code into the container WORKDIR /app 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/test/MessageTransmitter.t.sol b/test/MessageTransmitter.t.sol index 61a7ae7..6ca2d99 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, @@ -929,11 +930,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 +1002,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 +1165,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 +1205,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 a75e3a9..7c51dff 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -14,6 +14,7 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import "../src/TokenMinter.sol"; import "../lib/forge-std/src/Test.sol"; diff --git a/test/TokenMessenger.t.sol b/test/TokenMessenger.t.sol index c7f5cc5..0bbdf35 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); @@ -1185,10 +1189,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 +1201,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 +1218,6 @@ contract TokenMessengerTest is Test, TestUtils { _destinationCaller ); - vm.prank(owner); uint64 _nonceReserved = localTokenMessenger.depositForBurnWithCaller( _amount, remoteDomain, @@ -1220,6 +1225,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..9685e6c 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"; @@ -131,9 +132,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, @@ -336,9 +338,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); 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/BurnMessageV2.t.sol b/test/messages/v2/BurnMessageV2.t.sol index 3576719..5c971af 100644 --- a/test/messages/v2/BurnMessageV2.t.sol +++ b/test/messages/v2/BurnMessageV2.t.sol @@ -16,6 +16,7 @@ * 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"; 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/v2/BaseTokenMessenger.t.sol b/test/v2/BaseTokenMessenger.t.sol index 9b60d3a..3af8e41 100644 --- a/test/v2/BaseTokenMessenger.t.sol +++ b/test/v2/BaseTokenMessenger.t.sol @@ -16,6 +16,7 @@ * 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"; diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index 387817c..972bfbc 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -16,6 +16,7 @@ * 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"; From b0e480ecbcd3444e8f4691495f1e9953147d28b8 Mon Sep 17 00:00:00 2001 From: epoon-circle Date: Wed, 11 Sep 2024 09:20:31 -0700 Subject: [PATCH 05/40] Stable 7269 bugfix new v2 tests abicoder version (#17) A prior PR merge added new v2 script tests that were not updated with the `pragma abicoder v2` specification. --- test/scripts/1_deploy.t.sol | 1 + test/scripts/2_setupSecondAttester.t.sol | 1 + test/scripts/3_setupRemoteResources.t.sol | 1 + test/scripts/4_rotateKeys.t.sol | 1 + test/scripts/ScriptV2TestUtils.sol | 1 + 5 files changed, 5 insertions(+) diff --git a/test/scripts/1_deploy.t.sol b/test/scripts/1_deploy.t.sol index 991adb9..a9747ff 100644 --- a/test/scripts/1_deploy.t.sol +++ b/test/scripts/1_deploy.t.sol @@ -14,6 +14,7 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; diff --git a/test/scripts/2_setupSecondAttester.t.sol b/test/scripts/2_setupSecondAttester.t.sol index acb1d62..7310ffb 100644 --- a/test/scripts/2_setupSecondAttester.t.sol +++ b/test/scripts/2_setupSecondAttester.t.sol @@ -14,6 +14,7 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; diff --git a/test/scripts/3_setupRemoteResources.t.sol b/test/scripts/3_setupRemoteResources.t.sol index 6fbd191..a5bfc3d 100644 --- a/test/scripts/3_setupRemoteResources.t.sol +++ b/test/scripts/3_setupRemoteResources.t.sol @@ -14,6 +14,7 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; diff --git a/test/scripts/4_rotateKeys.t.sol b/test/scripts/4_rotateKeys.t.sol index 460e5a2..64fdc1b 100644 --- a/test/scripts/4_rotateKeys.t.sol +++ b/test/scripts/4_rotateKeys.t.sol @@ -14,6 +14,7 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; diff --git a/test/scripts/ScriptV2TestUtils.sol b/test/scripts/ScriptV2TestUtils.sol index df809fc..167dc6d 100644 --- a/test/scripts/ScriptV2TestUtils.sol +++ b/test/scripts/ScriptV2TestUtils.sol @@ -14,6 +14,7 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; import {DeployV2Script} from "../../scripts/v2/1_deploy.s.sol"; import {SetupSecondAttesterScript} from "../../scripts/v2/2_setupSecondAttester.s.sol"; From 5ba32e2f7b7f09525608e69621cd67c1fb0c4005 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Wed, 11 Sep 2024 17:53:52 -0400 Subject: [PATCH 06/40] STABLE-6926: (Part 1) Add depositForBurn + tests on relay side (#15) ## Summary 1) Adds the `depositForBurn` external function on `TokenMessengerV2` + tests on the relay side, building on patterns in the past PRs + v1. 2) Updates `fee` --> `maxFee` per the latest [in the designs](https://circlepay.atlassian.net/wiki/spaces/~5ab29c9a45a2562a4c64de77/pages/1476788228/CCTP+V2+Contract+Requirements). Part 2) will fill out the `MessageTransmitterV2` implementation and the receiving side. Breaking this into 2 parts to manage PR size. ## Tests Add tests for `depositForBurn` validations and event emission. ## Misc Left 1 callout below. --- src/messages/v2/BurnMessageV2.sol | 6 +- src/v2/TokenMessengerV2.sol | 63 +++- test/messages/v2/BurnMessageV2.t.sol | 6 +- test/v2/TokenMessengerV2.t.sol | 472 +++++++++++++++++++++------ 4 files changed, 437 insertions(+), 110 deletions(-) diff --git a/src/messages/v2/BurnMessageV2.sol b/src/messages/v2/BurnMessageV2.sol index d58eba0..eebb683 100644 --- a/src/messages/v2/BurnMessageV2.sol +++ b/src/messages/v2/BurnMessageV2.sol @@ -33,7 +33,7 @@ library BurnMessageV2 { * @param _mintRecipient The mint recipient address as bytes32 * @param _amount The burn amount * @param _messageSender The message sender - * @param _feeRequested The fee requested for the burn + * @param _maxFee The maximum fee to be paid on destination domain * @param _hook Optional hook to execute on destination domain * @return Formatted message. */ @@ -43,7 +43,7 @@ library BurnMessageV2 { bytes32 _mintRecipient, uint256 _amount, bytes32 _messageSender, - uint256 _feeRequested, + uint256 _maxFee, bytes calldata _hook ) internal pure returns (bytes memory) { return @@ -53,7 +53,7 @@ library BurnMessageV2 { _mintRecipient, _amount, _messageSender, - _feeRequested, + _maxFee, _hook ); } diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol index 12804d8..e21e252 100644 --- a/src/v2/TokenMessengerV2.sol +++ b/src/v2/TokenMessengerV2.sol @@ -41,7 +41,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { * @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 fee requested fee to pay on destination domain, in units of burnToken + * @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 hook target and calldata for execution on destination domain */ @@ -53,7 +53,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { uint32 destinationDomain, bytes32 destinationTokenMessenger, bytes32 destinationCaller, - uint256 fee, + uint256 maxFee, uint32 indexed minFinalityThreshold, bytes hook ); @@ -76,6 +76,48 @@ contract TokenMessengerV2 is BaseTokenMessenger { ) BaseTokenMessenger(_messageTransmitter, _messageBodyVersion) {} // ============ 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 the amount. + * - MessageTransmitter#sendMessage reverts. + * @param amount amount of tokens to burn + * @param destinationDomain destination domain + * @param mintRecipient address of mint recipient on destination domain + * @param burnToken address of contract to burn deposited tokens, 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 { + bytes calldata _emptyHook = msg.data[0:0]; + _depositForBurn( + amount, + destinationDomain, + mintRecipient, + burnToken, + destinationCaller, + maxFee, + minFinalityThreshold, + _emptyHook + ); + } + /** * @notice Deposits and burns tokens from sender to be minted on destination domain. * Emits a `DepositForBurn` event. @@ -95,8 +137,9 @@ contract TokenMessengerV2 is BaseTokenMessenger { * @param destinationDomain destination domain * @param mintRecipient address of mint recipient on destination domain * @param burnToken address of contract to burn deposited tokens, on local domain - * @param destinationCaller caller on the destination domain, as bytes32 - * @param fee requested fee to pay on the destination domain, specified in units of burnToken + * @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. * @param hook hook to execute on destination domain. Must be 32-bytes length or more. */ @@ -106,7 +149,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, - uint256 fee, + uint256 maxFee, uint32 minFinalityThreshold, bytes calldata hook ) external { @@ -118,7 +161,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { mintRecipient, burnToken, destinationCaller, - fee, + maxFee, minFinalityThreshold, hook ); @@ -131,13 +174,13 @@ contract TokenMessengerV2 is BaseTokenMessenger { bytes32 _mintRecipient, address _burnToken, bytes32 _destinationCaller, - uint256 _fee, + uint256 _maxFee, uint32 _minFinalityThreshold, bytes calldata _hook ) internal { require(_amount > 0, "Amount must be nonzero"); require(_mintRecipient != bytes32(0), "Mint recipient must be nonzero"); - require(_fee < _amount, "Fee must be less than amount"); + require(_maxFee < _amount, "Max fee must be less than amount"); bytes32 _destinationTokenMessenger = _getRemoteTokenMessenger( _destinationDomain @@ -153,7 +196,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { _mintRecipient, _amount, Message.addressToBytes32(msg.sender), - _fee, + _maxFee, _hook ); @@ -174,7 +217,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { _destinationDomain, _destinationTokenMessenger, _destinationCaller, - _fee, + _maxFee, _minFinalityThreshold, _hook ); diff --git a/test/messages/v2/BurnMessageV2.t.sol b/test/messages/v2/BurnMessageV2.t.sol index 5c971af..5c864cd 100644 --- a/test/messages/v2/BurnMessageV2.t.sol +++ b/test/messages/v2/BurnMessageV2.t.sol @@ -33,7 +33,7 @@ contract BurnMessageV2Test is Test { bytes32 _mintRecipient, uint256 _amount, bytes32 _messageSender, - uint256 _feeRequested, + uint256 _maxFee, bytes calldata _hook ) public { bytes memory _expectedMessageBody = abi.encodePacked( @@ -42,7 +42,7 @@ contract BurnMessageV2Test is Test { _mintRecipient, _amount, _messageSender, - _feeRequested, + _maxFee, _hook ); @@ -52,7 +52,7 @@ contract BurnMessageV2Test is Test { _mintRecipient, _amount, _messageSender, - _feeRequested, + _maxFee, _hook ); diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index 972bfbc..e06c79f 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -37,7 +37,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { * @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 not equal to bytes32(0). - * @param fee fee paid for fast burn on destination domain, in burnToken + * @param maxFee maximum fee to pay on destination domain, in burnToken * @param minFinalityThreshold the minimum finality at which the message should be attested to. * @param hook hook target and calldata for execution on destination domain */ @@ -49,7 +49,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint32 destinationDomain, bytes32 destinationTokenMessenger, bytes32 destinationCaller, - uint256 fee, + uint256 maxFee, uint32 indexed minFinalityThreshold, bytes hook ); @@ -125,20 +125,106 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { // Tests - function testDepositForBurnWithHook_revertsIfNoRemoteTokenMessengerExistsForDomain( + 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 _fee, - uint32 _minFinalityThreshold, - bytes calldata _hook + uint256 _maxFee, + uint32 _minFinalityThreshold ) public { vm.assume(_amount > 0); - vm.assume(_fee < _amount); + vm.assume(_maxFee < _amount); vm.assume(_mintRecipient != bytes32(0)); - vm.assume(_hook.length > 32); TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( localMessageTransmitter, @@ -146,32 +232,29 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); vm.expectRevert("No TokenMessenger for domain"); - _tokenMessenger.depositForBurnWithHook( + _tokenMessenger.depositForBurn( _amount, _remoteDomain, _mintRecipient, _burnToken, _destinationCaller, - _fee, - _minFinalityThreshold, - _hook + _maxFee, + _minFinalityThreshold ); } - function testDepositForBurnWithHook_revertsIfLocalMinterIsNotSet( + function testDepositForBurn_revertsIfLocalMinterIsNotSet( uint256 _amount, uint32 _remoteDomain, bytes32 _mintRecipient, address _burnToken, bytes32 _destinationCaller, - uint256 _fee, - uint32 _minFinalityThreshold, - bytes calldata _hook + uint256 _maxFee, + uint32 _minFinalityThreshold ) public { vm.assume(_amount > 0); - vm.assume(_fee < _amount); + vm.assume(_maxFee < _amount); vm.assume(_mintRecipient != bytes32(0)); - vm.assume(_hook.length > 32); TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( localMessageTransmitter, @@ -184,13 +267,174 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); vm.expectRevert("Local minter is not set"); - _tokenMessenger.depositForBurnWithHook( + _tokenMessenger.depositForBurn( _amount, _remoteDomain, _mintRecipient, _burnToken, _destinationCaller, - _fee, + _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_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("Invalid hook length"); + localTokenMessenger.depositForBurnWithHook( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _maxFee, + _minFinalityThreshold, + bytes("") + ); + } + + function testDepositForBurnWithHook_revertsIfHookIsTooShort( + uint256 _amount, + bytes32 _mintRecipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes calldata _hook + ) public { + vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_amount > 1); + vm.assume(_hook.length > 0 && _hook.length < 32); + + vm.expectRevert("Invalid hook length"); + localTokenMessenger.depositForBurnWithHook( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _amount - 1, // maxFee _minFinalityThreshold, _hook ); @@ -223,12 +467,12 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint256 _amount, address _burnToken, bytes32 _destinationCaller, - uint256 _fee, + uint256 _maxFee, uint32 _minFinalityThreshold, bytes calldata _hook ) public { vm.assume(_amount > 0); - vm.assume(_fee < _amount); + vm.assume(_maxFee < _amount); vm.assume(_hook.length > 32); vm.expectRevert("Mint recipient must be nonzero"); @@ -238,7 +482,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32(0), // mintRecipient _burnToken, _destinationCaller, - _fee, + _maxFee, _minFinalityThreshold, _hook ); @@ -256,14 +500,14 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { vm.assume(_mintRecipient != bytes32(0)); vm.assume(_hook.length > 32); - vm.expectRevert("Fee must be less than amount"); + vm.expectRevert("Max fee must be less than amount"); localTokenMessenger.depositForBurnWithHook( _amount, remoteDomain, _mintRecipient, _burnToken, _destinationCaller, - _amount, // fee + _amount, // maxFee _minFinalityThreshold, _hook ); @@ -274,164 +518,189 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { address _burnToken, bytes32 _mintRecipient, bytes32 _destinationCaller, - uint256 _fee, + uint256 _maxFee, uint32 _minFinalityThreshold, bytes calldata _hook ) public { vm.assume(_amount > 0); - vm.assume(_fee > _amount); + vm.assume(_maxFee > _amount); vm.assume(_mintRecipient != bytes32(0)); vm.assume(_hook.length > 32); - vm.expectRevert("Fee must be less than amount"); + vm.expectRevert("Max fee must be less than amount"); localTokenMessenger.depositForBurnWithHook( _amount, remoteDomain, _mintRecipient, _burnToken, _destinationCaller, - _fee, + _maxFee, _minFinalityThreshold, _hook ); } - function testDepositForBurnWithHook_revertsOnFailedTokenTransfer( + function testDepositForBurnWithHook_revertsIfNoRemoteTokenMessengerExistsForDomain( uint256 _amount, + uint32 _remoteDomain, bytes32 _mintRecipient, + address _burnToken, bytes32 _destinationCaller, - uint256 _fee, + uint256 _maxFee, uint32 _minFinalityThreshold, bytes calldata _hook ) public { vm.assume(_amount > 0); - vm.assume(_fee < _amount); + vm.assume(_maxFee < _amount); vm.assume(_mintRecipient != bytes32(0)); vm.assume(_hook.length > 32); - vm.mockCall( - address(localToken), - abi.encodeWithSelector(MockMintBurnToken.transferFrom.selector), - abi.encode(false) + TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( + localMessageTransmitter, + messageBodyVersion ); - vm.expectRevert("Transfer operation failed"); - localTokenMessenger.depositForBurnWithHook( + + vm.expectRevert("No TokenMessenger for domain"); + _tokenMessenger.depositForBurnWithHook( _amount, - destinationDomain, + _remoteDomain, _mintRecipient, - address(localToken), + _burnToken, _destinationCaller, - _fee, + _maxFee, _minFinalityThreshold, _hook ); } - function testDepositForBurnWithHook_revertsIfTokenTransferReverts( - address _caller, + function testDepositForBurnWithHook_revertsIfLocalMinterIsNotSet( uint256 _amount, + uint32 _remoteDomain, bytes32 _mintRecipient, + address _burnToken, bytes32 _destinationCaller, - uint256 _fee, + uint256 _maxFee, uint32 _minFinalityThreshold, bytes calldata _hook ) public { vm.assume(_amount > 0); - vm.assume(_fee < _amount); + vm.assume(_maxFee < _amount); vm.assume(_mintRecipient != bytes32(0)); vm.assume(_hook.length > 32); - // TransferFrom will revert, as localTokenMessenger has no allowance - assertEq( - localToken.allowance(_caller, address(localTokenMessenger)), - 0 + TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( + localMessageTransmitter, + messageBodyVersion ); - vm.expectRevert("ERC20: transfer amount exceeds allowance"); - localTokenMessenger.depositForBurnWithHook( + _tokenMessenger.addRemoteTokenMessenger( + _remoteDomain, + remoteTokenMessengerAddr + ); + + vm.expectRevert("Local minter is not set"); + _tokenMessenger.depositForBurnWithHook( _amount, - destinationDomain, + _remoteDomain, _mintRecipient, - address(localToken), + _burnToken, _destinationCaller, - _fee, + _maxFee, _minFinalityThreshold, _hook ); } - function testDepositForBurnWithHook_revertsIfTransferAmountExceedsMaxBurnAmountPerMessage( + function testDepositForBurnWithHook_revertsOnFailedTokenTransfer( uint256 _amount, bytes32 _mintRecipient, bytes32 _destinationCaller, - uint256 _fee, + uint256 _maxFee, uint32 _minFinalityThreshold, bytes calldata _hook ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee < _amount); vm.assume(_mintRecipient != bytes32(0)); vm.assume(_hook.length > 32); - vm.assume(_fee < _amount); - vm.assume(_amount > 1); - _setupDepositForBurn(arbitraryAddress, _amount, _amount - 1); - - vm.prank(arbitraryAddress); - vm.expectRevert("Burn amount exceeds per tx limit"); + 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, - _fee, + _maxFee, _minFinalityThreshold, _hook ); } - function testDepositForBurnWithHook_revertsIfHookIsEmpty( + function testDepositForBurnWithHook_revertsIfTokenTransferReverts( + address _caller, uint256 _amount, - uint256 _fee, bytes32 _mintRecipient, bytes32 _destinationCaller, - uint32 _minFinalityThreshold + uint256 _maxFee, + uint32 _minFinalityThreshold, + bytes calldata _hook ) public { + vm.assume(_amount > 0); + vm.assume(_maxFee < _amount); vm.assume(_mintRecipient != bytes32(0)); - vm.assume(_fee < _amount); - vm.assume(_amount > 1); + vm.assume(_hook.length > 32); - vm.expectRevert("Invalid hook length"); + // 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, - _fee, + _maxFee, _minFinalityThreshold, - bytes("") + _hook ); } - function testDepositForBurnWithHook_revertsIfHookIsTooShort( + function testDepositForBurnWithHook_revertsIfTransferAmountExceedsMaxBurnAmountPerMessage( uint256 _amount, bytes32 _mintRecipient, bytes32 _destinationCaller, + uint256 _maxFee, uint32 _minFinalityThreshold, - bytes calldata _hook + bytes calldata _hook, + address _caller ) public { vm.assume(_mintRecipient != bytes32(0)); + vm.assume(_hook.length > 32); + vm.assume(_maxFee < _amount); vm.assume(_amount > 1); - vm.assume(_hook.length > 0 && _hook.length < 32); + vm.assume(_caller != address(0)); - vm.expectRevert("Invalid hook length"); + _setupDepositForBurn(_caller, _amount, _amount - 1); + + vm.prank(_caller); + vm.expectRevert("Burn amount exceeds per tx limit"); localTokenMessenger.depositForBurnWithHook( _amount, destinationDomain, _mintRecipient, address(localToken), _destinationCaller, - _amount - 1, // fee + _maxFee, _minFinalityThreshold, _hook ); @@ -443,23 +712,25 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _mintRecipient, bytes32 _destinationCaller, uint32 _minFinalityThreshold, - bytes calldata _hook + bytes calldata _hook, + address _caller ) public { vm.assume(_mintRecipient != bytes32(0)); vm.assume(_amount > 1); vm.assume(_amount < _burnLimit); vm.assume(_hook.length > 32); + vm.assume(_caller != address(0)); - uint256 _fee = _amount - 1; + uint256 _maxFee = _amount - 1; - _setupDepositForBurn(arbitraryAddress, _amount, _burnLimit); + _setupDepositForBurn(_caller, _amount, _burnLimit); _depositForBurn( - arbitraryAddress, + _caller, _mintRecipient, _destinationCaller, _amount, - _fee, + _maxFee, _minFinalityThreshold, _hook ); @@ -487,7 +758,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _mintRecipient, bytes32 _destinationCaller, uint256 _amount, - uint256 _fee, + uint256 _maxFee, uint32 _minFinalityThreshold, bytes calldata _hook ) internal { @@ -497,7 +768,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _mintRecipient, _amount, Message.addressToBytes32(_caller), - _fee, + _maxFee, _hook ); @@ -541,21 +812,34 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { destinationDomain, remoteTokenMessengerAddr, _destinationCaller, - _fee, + _maxFee, _minFinalityThreshold, _hook ); vm.prank(_caller); - localTokenMessenger.depositForBurnWithHook( - _amount, - destinationDomain, - _mintRecipient, - address(localToken), - _destinationCaller, - _fee, - _minFinalityThreshold, - _hook - ); + + if (_hook.length == 0) { + localTokenMessenger.depositForBurn( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _maxFee, + _minFinalityThreshold + ); + } else { + localTokenMessenger.depositForBurnWithHook( + _amount, + destinationDomain, + _mintRecipient, + address(localToken), + _destinationCaller, + _maxFee, + _minFinalityThreshold, + _hook + ); + } } } From 6f02d5d340e3b35c9d80766d7212b6523352ba84 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Fri, 13 Sep 2024 17:15:13 -0400 Subject: [PATCH 07/40] STABLE-6926: (Part 2) Implement MessageTransmitterV2 message relay (#18) ## Summary This continues on Part 1, building on `depositForBurn` and `depositForBurnWithHook` TokenMessenger implementations to build out the `MessageTransmitterV2.sendMessage()`. Changes: - Add MessageTransmitter.receiveMessage, along with Rescuable, Pausable inheritance and tests - Add MessageV2 and BurnMessageV2, now that source <> destination chain format questions have been resolved, along with tests - Add some missing Ownable, Rescuable, and Pausable tests around access control ## Testing This adds new tests to `BurnMessageV2.t.sol`, `MessageV2.t.sol`, and `MessageTransmitterV2.t.sol`, along with small tweaks to v1 test files to incorporate the small changes made to the shared Rescuable, Ownable, Pausable tests. --- src/interfaces/v2/IMessageTransmitterV2.sol | 29 ++ src/interfaces/v2/IReceiverV2.sol | 28 ++ src/messages/v2/AddressUtils.sol | 42 +++ src/messages/v2/BurnMessageV2.sol | 94 ++++++- src/messages/v2/MessageV2.sol | 177 ++++++++++++ src/v2/MessageTransmitterV2.sol | 115 +++++++- src/v2/TokenMessengerV2.sol | 8 +- test/MessageTransmitter.t.sol | 11 +- test/TestUtils.sol | 115 +++++++- test/TokenMessenger.t.sol | 6 +- test/TokenMinter.t.sol | 11 +- test/messages/v2/AddressUtils.t.sol | 39 +++ test/messages/v2/BurnMessageV2.t.sol | 52 +++- test/messages/v2/MessageV2.t.sol | 105 +++++++ test/v2/BaseTokenMessenger.t.sol | 6 +- test/v2/MessageTransmitterV2.t.sol | 294 ++++++++++++++++++++ test/v2/TokenMessengerV2.t.sol | 25 +- 17 files changed, 1112 insertions(+), 45 deletions(-) create mode 100644 src/interfaces/v2/IMessageTransmitterV2.sol create mode 100644 src/interfaces/v2/IReceiverV2.sol create mode 100644 src/messages/v2/AddressUtils.sol create mode 100644 src/messages/v2/MessageV2.sol create mode 100644 test/messages/v2/AddressUtils.t.sol create mode 100644 test/messages/v2/MessageV2.t.sol create mode 100644 test/v2/MessageTransmitterV2.t.sol diff --git a/src/interfaces/v2/IMessageTransmitterV2.sol b/src/interfaces/v2/IMessageTransmitterV2.sol new file mode 100644 index 0000000..4c6143c --- /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 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..b9a95bd --- /dev/null +++ b/src/interfaces/v2/IReceiverV2.sol @@ -0,0 +1,28 @@ +/* + * 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 destination chain and forwards them to IMessageDestinationHandler + */ +interface IReceiverV2 is IReceiver { + +} diff --git a/src/messages/v2/AddressUtils.sol b/src/messages/v2/AddressUtils.sol new file mode 100644 index 0000000..70bed18 --- /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 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) public pure returns (address) { + return address(uint160(uint256(_buf))); + } +} diff --git a/src/messages/v2/BurnMessageV2.sol b/src/messages/v2/BurnMessageV2.sol index eebb683..06cfbfc 100644 --- a/src/messages/v2/BurnMessageV2.sol +++ b/src/messages/v2/BurnMessageV2.sol @@ -22,10 +22,38 @@ import {BurnMessage} from "../BurnMessage.sol"; /** * @title BurnMessageV2 Library - * @notice TODO: STABLE-6895 - * @dev TODO: STABLE-6895 + * @notice Library for formatted 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 Differences from v1: + * - maxFee is added + * - feeExecuted is added + * - expirationBlock is added + * - hookData is added **/ 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 burn message * @param _version The message body version @@ -37,7 +65,7 @@ library BurnMessageV2 { * @param _hook Optional hook to execute on destination domain * @return Formatted message. */ - function _formatMessage( + function _formatMessageForRelay( uint32 _version, bytes32 _burnToken, bytes32 _mintRecipient, @@ -54,9 +82,67 @@ library BurnMessageV2 { _amount, _messageSender, _maxFee, + EMPTY_FEE_EXECUTED, + EMPTY_EXPIRATION_BLOCK, _hook ); } - // TODO: STABLE-6895, complete implementation, including message validation. + // @notice Returns _message's version field + function _getVersion(bytes29 _message) internal pure returns (uint32) { + return _message._getVersion(); + } + + // @notice Returns _message's burn token 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 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 message: too short" + ); + } } diff --git a/src/messages/v2/MessageV2.sol b/src/messages/v2/MessageV2.sol new file mode 100644 index 0000000..7b444c0 --- /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 memory _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/v2/MessageTransmitterV2.sol b/src/v2/MessageTransmitterV2.sol index dc051e5..1ac4d20 100644 --- a/src/v2/MessageTransmitterV2.sol +++ b/src/v2/MessageTransmitterV2.sol @@ -16,20 +16,127 @@ * limitations under the License. */ pragma solidity 0.7.6; +pragma abicoder v2; -import {IRelayerV2} from "../interfaces/v2/IRelayerV2.sol"; +import {IMessageTransmitterV2} from "../interfaces/v2/IMessageTransmitterV2.sol"; +import {Attestable} from "../roles/Attestable.sol"; +import {Pausable} from "../roles/Pausable.sol"; +import {Rescuable} from "../roles/Rescuable.sol"; +import {MessageV2} from "../messages/v2/MessageV2.sol"; +import {AddressUtils} from "../messages/v2/AddressUtils.sol"; /** * @title MessageTransmitterV2 * @notice Contract responsible for sending and receiving messages across chains. */ -contract MessageTransmitterV2 is IRelayerV2 { - // TODO: STABLE-6895 +// TODO STABLE-6894 & STABLE-STABLE-7293: refactor inheritance +// as-needed to work with Proxy pattern. +contract MessageTransmitterV2 is + IMessageTransmitterV2, + Pausable, + Rescuable, + Attestable +{ + // ============ Events ============ + /** + * @notice Emitted when a new message is dispatched + * @param message Raw bytes of message + */ + event MessageSent(bytes message); + + /** + * @notice Emitted when max message body size is updated + * @param newMaxMessageBodySize new maximum message body size, in bytes + */ + event MaxMessageBodySizeUpdated(uint256 newMaxMessageBodySize); + + // ============ Libraries ============ + + // ============ 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; + + // ============ Constructor ============ + // TODO STABLE-6894 & STABLE-STABLE-7293: refactor constructor + // as-needed to work with Proxy pattern. + constructor( + uint32 _localDomain, + address _attester, + uint32 _maxMessageBodySize, + uint32 _version + ) Attestable(_attester) { + localDomain = _localDomain; + version = _version; + maxMessageBodySize = _maxMessageBodySize; + } + + // ============ External Functions ============ + /** + * @notice Send the message to the destination domain and recipient + * @dev Formats the message, and emit `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 raw bytes content of message + */ function sendMessage( uint32 destinationDomain, bytes32 recipient, bytes32 destinationCaller, uint32 minFinalityThreshold, bytes calldata messageBody - ) external override {} + ) 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 = AddressUtils.addressToBytes32(msg.sender); + + // serialize message + bytes memory _message = MessageV2._formatMessageForRelay( + version, + localDomain, + destinationDomain, + _messageSender, + recipient, + destinationCaller, + minFinalityThreshold, + messageBody + ); + + // Emit MessageSent event + emit MessageSent(_message); + } + + function receiveMessage( + bytes calldata message, + bytes calldata signature + ) external override returns (bool success) {} + + /** + * @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 { + maxMessageBodySize = newMaxMessageBodySize; + emit MaxMessageBodySizeUpdated(maxMessageBodySize); + } + + // ============ Internal Utils ============ } diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol index e21e252..8a604f9 100644 --- a/src/v2/TokenMessengerV2.sol +++ b/src/v2/TokenMessengerV2.sol @@ -21,7 +21,7 @@ import {BaseTokenMessenger} from "./BaseTokenMessenger.sol"; import {ITokenMinter} from "../interfaces/ITokenMinter.sol"; import {IMintBurnToken} from "../interfaces/IMintBurnToken.sol"; import {BurnMessageV2} from "../messages/v2/BurnMessageV2.sol"; -import {Message} from "../messages/Message.sol"; +import {AddressUtils} from "../messages/v2/AddressUtils.sol"; import {MessageTransmitterV2} from "./MessageTransmitterV2.sol"; /** @@ -190,12 +190,12 @@ contract TokenMessengerV2 is BaseTokenMessenger { _depositAndBurnTokens(_burnToken, msg.sender, _amount); // Format message body - bytes memory _burnMessage = BurnMessageV2._formatMessage( + bytes memory _burnMessage = BurnMessageV2._formatMessageForRelay( messageBodyVersion, - Message.addressToBytes32(_burnToken), + AddressUtils.addressToBytes32(_burnToken), _mintRecipient, _amount, - Message.addressToBytes32(msg.sender), + AddressUtils.addressToBytes32(msg.sender), _maxFee, _hook ); diff --git a/test/MessageTransmitter.t.sol b/test/MessageTransmitter.t.sol index 6ca2d99..0ee735c 100644 --- a/test/MessageTransmitter.t.sol +++ b/test/MessageTransmitter.t.sol @@ -885,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 ); } diff --git a/test/TestUtils.sol b/test/TestUtils.sol index 7c51dff..daffeca 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -51,6 +51,8 @@ contract TestUtils is Test { event PauserChanged(address indexed newAddress); + event RescuerChanged(address indexed newRescuer); + // test keys uint256 attesterPK = 1; uint256 fakeAttesterPK = 2; @@ -151,10 +153,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 @@ -171,10 +180,17 @@ 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.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); + _rescuableContract.updateRescuer(_rescuer); // Rescue erc20 to _rescueRecipient vm.prank(_rescuer); @@ -186,19 +202,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); @@ -213,6 +250,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); @@ -254,12 +311,58 @@ contract TestUtils is Test { 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); diff --git a/test/TokenMessenger.t.sol b/test/TokenMessenger.t.sol index 0bbdf35..07c27fb 100644 --- a/test/TokenMessenger.t.sol +++ b/test/TokenMessenger.t.sol @@ -1061,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 ); } diff --git a/test/TokenMinter.t.sol b/test/TokenMinter.t.sol index 9685e6c..ad71c7f 100644 --- a/test/TokenMinter.t.sol +++ b/test/TokenMinter.t.sol @@ -413,22 +413,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 ); } diff --git a/test/messages/v2/AddressUtils.t.sol b/test/messages/v2/AddressUtils.t.sol new file mode 100644 index 0000000..90b57dd --- /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.addressToBytes32(_addr); + address _recoveredAddr = AddressUtils.bytes32ToAddress(_addrAsBytes32); + assertEq(_recoveredAddr, _addr); + } + + function testAddressToBytes32LeftPads(address _addr) public pure { + bytes32 _addrAsBytes32 = AddressUtils.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 index 5c864cd..f42e309 100644 --- a/test/messages/v2/BurnMessageV2.t.sol +++ b/test/messages/v2/BurnMessageV2.t.sol @@ -27,7 +27,7 @@ contract BurnMessageV2Test is Test { using TypedMemView for bytes29; using BurnMessageV2 for bytes29; - function testFormatMessage_succeeds( + function testFormatMessageyForRelay_succeeds( uint32 _version, bytes32 _burnToken, bytes32 _mintRecipient, @@ -35,7 +35,7 @@ contract BurnMessageV2Test is Test { bytes32 _messageSender, uint256 _maxFee, bytes calldata _hook - ) public { + ) public pure { bytes memory _expectedMessageBody = abi.encodePacked( _version, _burnToken, @@ -43,10 +43,12 @@ contract BurnMessageV2Test is Test { _amount, _messageSender, _maxFee, + uint256(0), + uint256(0), _hook ); - bytes memory _messageBody = BurnMessageV2._formatMessage( + bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( _version, _burnToken, _mintRecipient, @@ -57,6 +59,50 @@ contract BurnMessageV2Test is Test { ); 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); + + _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 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..8a81daf --- /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 memory _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 memory _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/v2/BaseTokenMessenger.t.sol b/test/v2/BaseTokenMessenger.t.sol index 3af8e41..cdc8206 100644 --- a/test/v2/BaseTokenMessenger.t.sol +++ b/test/v2/BaseTokenMessenger.t.sol @@ -316,13 +316,15 @@ abstract contract BaseTokenMessengerTest is Test, TestUtils { function testRescuable( address _rescuer, address _rescueRecipient, - uint256 _amount + uint256 _amount, + address _nonRescuer ) public { assertContractIsRescuable( address(baseTokenMessenger), _rescuer, _rescueRecipient, - _amount + _amount, + _nonRescuer ); } diff --git a/test/v2/MessageTransmitterV2.t.sol b/test/v2/MessageTransmitterV2.t.sol new file mode 100644 index 0000000..6ff9575 --- /dev/null +++ b/test/v2/MessageTransmitterV2.t.sol @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2022, 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 {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"; + +contract MessageTransmitterV2Test is TestUtils { + /** + * @notice Emitted when a new message is dispatched + * @param message Raw bytes of message + */ + event MessageSent(bytes message); + + /** + * @notice Emitted when max message body size is updated + * @param newMaxMessageBodySize new maximum message body size, in bytes + */ + event MaxMessageBodySizeUpdated(uint256 newMaxMessageBodySize); + + // ============ Libraries ============ + + // Test constants + uint32 localDomain = 1; + + MessageTransmitterV2 srcMessageTransmitter; + + function setUp() public { + // message transmitter on source domain + srcMessageTransmitter = new MessageTransmitterV2( + localDomain, + attester, + maxMessageBodySize, + version + ); + } + + 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); + + srcMessageTransmitter.updatePauser(_pauser); + + vm.prank(_pauser); + srcMessageTransmitter.pause(); + assertTrue(srcMessageTransmitter.paused()); + + vm.expectRevert("Pausable: paused"); + srcMessageTransmitter.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"); + srcMessageTransmitter.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"); + srcMessageTransmitter.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"); + srcMessageTransmitter.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 testSendMaxMessageBodySize_revertsOnNonOwner( + uint256 _newMaxMessageBodySize, + address _notOwner + ) public { + vm.assume(_notOwner != srcMessageTransmitter.owner()); + expectRevertWithWrongOwner(_notOwner); + srcMessageTransmitter.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); + srcMessageTransmitter.setMaxMessageBodySize(_newMaxMessageBodySize); + assertEq( + srcMessageTransmitter.maxMessageBodySize(), + _newMaxMessageBodySize + ); + } + + function testRescuable( + address _rescuer, + address _rescueRecipient, + uint256 _amount, + address _nonRescuer + ) public { + assertContractIsRescuable( + address(srcMessageTransmitter), + _rescuer, + _rescueRecipient, + _amount, + _nonRescuer + ); + } + + function testPausable( + address _currentPauser, + address _newPauser, + address _nonOwner + ) public { + vm.assume(_currentPauser != address(0)); + srcMessageTransmitter.updatePauser(_currentPauser); + + assertContractIsPausable( + address(srcMessageTransmitter), + _currentPauser, + _newPauser, + srcMessageTransmitter.owner(), + _nonOwner + ); + } + + function testTransferOwnership_revertsFromNonOwner( + address _newOwner, + address _nonOwner + ) public { + transferOwnership_revertsFromNonOwner( + address(srcMessageTransmitter), + _newOwner, + _nonOwner + ); + } + + function testAcceptOwnership_revertsFromNonPendingOwner( + address _newOwner, + address _nonOwner + ) public { + acceptOwnership_revertsFromNonPendingOwner( + address(srcMessageTransmitter), + _newOwner, + _nonOwner + ); + } + + function testTransferOwnershipAndAcceptOwnership(address _newOwner) public { + transferOwnershipAndAcceptOwnership( + address(srcMessageTransmitter), + _newOwner + ); + } + + function testTransferOwnershipWithoutAcceptingThenTransferToNewOwner( + address _newOwner, + address _secondNewOwner + ) public { + transferOwnershipWithoutAcceptingThenTransferToNewOwner( + address(srcMessageTransmitter), + _newOwner, + _secondNewOwner + ); + } + + function _sendMessage( + uint32 _destinationDomain, + bytes32 _recipient, + bytes32 _destinationCaller, + uint32 _minFinalityThreshold, + bytes memory _messageBody, + address _sender + ) internal { + bytes memory _expectedMessage = MessageV2._formatMessageForRelay( + version, + localDomain, + _destinationDomain, + AddressUtils.addressToBytes32(_sender), + _recipient, + _destinationCaller, + _minFinalityThreshold, + _messageBody + ); + + assertFalse(srcMessageTransmitter.paused()); + + // assert that a MessageSent event was logged with expected message bytes + vm.prank(_sender); + vm.expectEmit(true, true, true, true); + emit MessageSent(_expectedMessage); + srcMessageTransmitter.sendMessage( + _destinationDomain, + _recipient, + _destinationCaller, + _minFinalityThreshold, + _messageBody + ); + vm.stopPrank(); + } +} diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index e06c79f..6c56f2b 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -20,7 +20,7 @@ pragma abicoder v2; import {BaseTokenMessengerTest} from "./BaseTokenMessenger.t.sol"; import {TokenMessengerV2} from "../../src/v2/TokenMessengerV2.sol"; -import {Message} from "../../src/messages/Message.sol"; +import {AddressUtils} from "../../src/messages/v2/AddressUtils.sol"; import {MockMintBurnToken} from "../mocks/MockMintBurnToken.sol"; import {TokenMinter} from "../../src/TokenMinter.sol"; import {MessageTransmitterV2} from "../../src/v2/MessageTransmitterV2.sol"; @@ -80,7 +80,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { // Add a local minter localTokenMessenger.addLocalMinter(address(localTokenMinter)); - remoteTokenMessengerAddr = Message.addressToBytes32( + remoteTokenMessengerAddr = AddressUtils.addressToBytes32( remoteTokenMessageger ); @@ -94,7 +94,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { localTokenMinter, address(localToken), remoteDomain, - Message.addressToBytes32(remoteTokenAddr) + AddressUtils.addressToBytes32(remoteTokenAddr) ); localTokenMinter.addLocalTokenMessenger(address(localTokenMessenger)); @@ -762,15 +762,16 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint32 _minFinalityThreshold, bytes calldata _hook ) internal { - bytes memory _expectedBurnMessage = BurnMessageV2._formatMessage( - messageBodyVersion, - Message.addressToBytes32(address(localToken)), - _mintRecipient, - _amount, - Message.addressToBytes32(_caller), - _maxFee, - _hook - ); + bytes memory _expectedBurnMessage = BurnMessageV2 + ._formatMessageForRelay( + messageBodyVersion, // version + AddressUtils.addressToBytes32(address(localToken)), // burn token + _mintRecipient, // mint recipient + _amount, // amount + AddressUtils.addressToBytes32(_caller), // sender + _maxFee, // max fee + _hook + ); // expect burn() on localTokenMinter vm.expectCall( From 6d7d1185d555be30f6b07978e2df64ef1af68952 Mon Sep 17 00:00:00 2001 From: epoon-circle Date: Wed, 18 Sep 2024 10:09:51 -0700 Subject: [PATCH 08/40] Stable 7248 update cctpv2 deploy using create2 (#16) As per [STABLE-7248](https://circlepay.atlassian.net/browse/STABLE-7248?atlOrigin=eyJpIjoiMGI5ZDI3OTc2ZWZhNDIzYjg5NTQ2YzQ5YzViYTgzMzgiLCJwIjoiaiJ9): - Created new `ProxyFactory` that needs to be deployed traditionally via CREATE - Added deploy script and tests for `ProxyFactory`. ## NOTE This PR does not yet integrate v1/v2 deployment scripts. The v2 contracts will need to be updated to use an initialize pattern. [STABLE-7248]: https://circlepay.atlassian.net/browse/STABLE-7248?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- Makefile | 6 + README.md | 22 +++- scripts/DeployCreate2Factory.s.sol | 43 +++++++ src/v2/Create2Factory.sol | 75 +++++++++++++ .../mocks/MockInitializableImplementation.sol | 30 +++++ test/v2/Create2Factory.t.sol | 106 ++++++++++++++++++ 6 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 scripts/DeployCreate2Factory.s.sol create mode 100644 src/v2/Create2Factory.sol create mode 100644 test/mocks/MockInitializableImplementation.sol create mode 100644 test/v2/Create2Factory.t.sol diff --git a/Makefile b/Makefile index 5e7f5f3..79e01b0 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,12 @@ simulate-deploy: deploy: forge script scripts/v1/deploy.s.sol:DeployScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast +simulate-deploy-proxy-factory: + forge script scripts/DeployProxyFactory.s.sol:DeployProxyFactoryScript --rpc-url ${RPC_URL} --sender ${SENDER} + +deploy-proxy-factory: + forge script scripts/DeployProxyFactory.s.sol:DeployProxyFactoryScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast + simulate-deployv2: forge script scripts/v2/1_deploy.s.sol:DeployV2Script --rpc-url ${RPC_URL} --sender ${SENDER} diff --git a/README.md b/README.md index 734e8d3..b5b526a 100644 --- a/README.md +++ b/README.md @@ -96,9 +96,26 @@ The contracts are deployed using [Forge Scripts](https://book.getfoundry.sh/tuto ### V2 -The contracts are deployed using [Forge Scripts](https://book.getfoundry.sh/tutorials/solidity-scripting). The script is located in [scripts/v2/1_deploy.s.sol](/scripts/v2/1_deploy.s.sol). Follow the below steps to deploy the contracts: +Deploy Create2Factory first if not yet deployed. -1. Add the below environment variables to your [env](.env) file +
    +
  1. + + Add the below environment variable to your [env](.env) file: + - `CREATE2_FACTORY_DEPLOYER_KEY`
  2. +
  3. + + Run `make simulate-deploy-create2-factory RPC_URL= SENDER=` to perform a dry run.
  4. +
  5. + + Run + ```make deploy-create2-factory RPC_URL= SENDER=``` + to deploy the Create2Factory.
  6. +
+ +The contracts are deployed via `CREATE2` through Create2Factory. Follow the below steps to deploy the contracts: + +1. Replace the environment variables in your [env](.env) file with the following: - `MESSAGE_TRANSMITTER_DEPLOYER_KEY` - `TOKEN_MESSENGER_DEPLOYER_KEY` @@ -116,6 +133,7 @@ The contracts are deployed using [Forge Scripts](https://book.getfoundry.sh/tuto - `DOMAIN` - `REMOTE_DOMAIN` - `BURN_LIMIT_PER_MESSAGE` + - `CREATE2_FACTORY_ADDRESS` In addition, to link the remote bridge, one of two steps needs to be followed: 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/src/v2/Create2Factory.sol b/src/v2/Create2Factory.sol new file mode 100644 index 0000000..3b9fdb8 --- /dev/null +++ b/src/v2/Create2Factory.sol @@ -0,0 +1,75 @@ +/* + * 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 {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 deployAndCall( + uint256 amount, + bytes32 salt, + bytes calldata bytecode, + bytes calldata data + ) external payable onlyOwner returns (address addr) { + // Deploy deterministically + addr = Create2.deploy(amount, salt, bytecode); + + Address.functionCall(addr, data); + } + + /** + * @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/test/mocks/MockInitializableImplementation.sol b/test/mocks/MockInitializableImplementation.sol new file mode 100644 index 0000000..d85b435 --- /dev/null +++ b/test/mocks/MockInitializableImplementation.sol @@ -0,0 +1,30 @@ +/* + * 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 "@openzeppelin/contracts/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; + } +} diff --git a/test/v2/Create2Factory.t.sol b/test/v2/Create2Factory.t.sol new file mode 100644 index 0000000..0f98707 --- /dev/null +++ b/test/v2/Create2Factory.t.sol @@ -0,0 +1,106 @@ +/* + * 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 testDeployAndCall( + address addr, + uint256 num, + uint256 amount, + 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), "") + ); + // Deploy proxy + address expectedAddr = create2Factory.computeAddress( + salt, + keccak256(bytecode) + ); + vm.deal(address(this), amount); + address proxyAddr = create2Factory.deployAndCall{value: amount}( + amount, + salt, + bytecode, + initializer + ); + + // Verify deterministic + assertEq(proxyAddr, expectedAddr); + // Check initialized vars + assertEq(MockInitializableImplementation(proxyAddr).addr(), addr); + assertEq(MockInitializableImplementation(proxyAddr).num(), num); + // Verify balance + assertEq(proxyAddr.balance, amount); + } +} From 220b071ecf7d3c221dfd71ad6f8802b25611ac89 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:51:07 -0400 Subject: [PATCH 09/40] STABLE-6895 (Part 4) / STABLE-6926: Add deterministic nonce handling in MessageTransmitterV2 (#19) ### Summary This adds deterministic nonce handling in MessageTransmitterV2. It completes the `receiveMessage` implementation and adds a bunch of tests derived from the v1 testcases. A few callouts below in-line. The vast majority of the diff is in tests and license headers for new files--the actual contract changes are fairly compact. ### Testing See `v2/MessageTransmitterV2.t.sol` for test additions. Please scrutinize the test cases and compare them to the v1 tests in `MessageTransmitter.t.sol` and see if anything is missing. I opted to focus on fuzzing additional attributes and using mocks to focus on the contract under test. --- src/interfaces/v2/IMessageHandlerV2.sol | 57 ++ src/v2/MessageTransmitterV2.sol | 141 +++- test/TestUtils.sol | 5 + test/mocks/MockReentrantCaller.sol | 21 +- test/mocks/v2/MockReentrantCallerV2.sol | 42 ++ test/v2/MessageTransmitterV2.t.sol | 896 +++++++++++++++++++++++- 6 files changed, 1115 insertions(+), 47 deletions(-) create mode 100644 src/interfaces/v2/IMessageHandlerV2.sol create mode 100644 test/mocks/v2/MockReentrantCallerV2.sol diff --git a/src/interfaces/v2/IMessageHandlerV2.sol b/src/interfaces/v2/IMessageHandlerV2.sol new file mode 100644 index 0000000..33e3838 --- /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 destination domain forwarded from + * an IReceiverV2 + */ +interface IMessageHandlerV2 { + /** + * @notice handles an incoming finalized message from a Receiver + * @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 message raw bytes + * @return success bool, true if successful + */ + function handleReceiveFinalizedMessage( + uint32 sourceDomain, + bytes32 sender, + uint32 finalityThresholdExecuted, + bytes calldata messageBody + ) external returns (bool); + + /** + * @notice handles an incoming unfinalized message from a Receiver + * @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 (sub)finality threshold at which the message was attested to + * @param messageBody The message raw bytes + * @return success bool, true if successful + */ + function handleReceiveUnfinalizedMessage( + uint32 sourceDomain, + bytes32 sender, + uint32 finalityThresholdExecuted, + bytes calldata messageBody + ) external returns (bool); +} diff --git a/src/v2/MessageTransmitterV2.sol b/src/v2/MessageTransmitterV2.sol index 1ac4d20..ebe0fd7 100644 --- a/src/v2/MessageTransmitterV2.sol +++ b/src/v2/MessageTransmitterV2.sol @@ -24,6 +24,8 @@ import {Pausable} from "../roles/Pausable.sol"; import {Rescuable} from "../roles/Rescuable.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"; /** * @title MessageTransmitterV2 @@ -44,6 +46,24 @@ contract MessageTransmitterV2 is */ 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 + ); + /** * @notice Emitted when max message body size is updated * @param newMaxMessageBodySize new maximum message body size, in bytes @@ -51,6 +71,9 @@ contract MessageTransmitterV2 is event MaxMessageBodySizeUpdated(uint256 newMaxMessageBodySize); // ============ Libraries ============ + using TypedMemView for bytes; + using TypedMemView for bytes29; + using MessageV2 for bytes29; // ============ State Variables ============ // Domain of chain on which the contract is deployed @@ -59,10 +82,16 @@ contract MessageTransmitterV2 is // Message Format version uint32 public immutable version; + // The threshold at which messages are considered finalized + uint32 public immutable finalizedMessageThreshold = 2000; + // 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 ============ // TODO STABLE-6894 & STABLE-STABLE-7293: refactor constructor // as-needed to work with Proxy pattern. @@ -120,10 +149,118 @@ contract MessageTransmitterV2 is 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 bool, true if successful + */ function receiveMessage( bytes calldata message, - bytes calldata signature - ) external override returns (bool success) {} + bytes calldata attestation + ) external override whenNotPaused returns (bool success) { + // 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() == + AddressUtils.addressToBytes32(msg.sender), + "Invalid caller for message" + ); + } + + // Validate version + require(_msg._getVersion() == version, "Invalid message version"); + + // Validate nonce is available + bytes32 _nonce = _msg._getNonce(); + require(usedNonces[_nonce] == 0, "Nonce already used"); + // Mark nonce used + usedNonces[_nonce] = 1; + + // Unpack remaining values + uint32 _sourceDomain = _msg._getSourceDomain(); + bytes32 _sender = _msg._getSender(); + address _recipient = AddressUtils.bytes32ToAddress( + _msg._getRecipient() + ); + uint32 _finalityThresholdExecuted = _msg + ._getFinalityThresholdExecuted(); + bytes memory _messageBody = _msg._getMessageBody().clone(); + + // Handle receive message + if (_finalityThresholdExecuted < finalizedMessageThreshold) { + 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 Sets the max message body size diff --git a/test/TestUtils.sol b/test/TestUtils.sol index daffeca..2a0948a 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -83,6 +83,11 @@ 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; // zero signature diff --git a/test/mocks/MockReentrantCaller.sol b/test/mocks/MockReentrantCaller.sol index 8586071..6342ff8 100644 --- a/test/mocks/MockReentrantCaller.sol +++ b/test/mocks/MockReentrantCaller.sol @@ -31,7 +31,7 @@ contract MockReentrantCaller is IMessageHandler { function setMessageAndSignature( bytes memory _message, bytes memory _signature - ) external { + ) public { message = _message; signature = _signature; } @@ -40,7 +40,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 +66,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 +79,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/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/v2/MessageTransmitterV2.t.sol b/test/v2/MessageTransmitterV2.t.sol index 6ff9575..ce07245 100644 --- a/test/v2/MessageTransmitterV2.t.sol +++ b/test/v2/MessageTransmitterV2.t.sol @@ -1,11 +1,13 @@ /* - * Copyright (c) 2022, Circle Internet Financial Limited. + * 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 + * 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, @@ -20,30 +22,40 @@ 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"; contract MessageTransmitterV2Test is TestUtils { - /** - * @notice Emitted when a new message is dispatched - * @param message Raw bytes of message - */ event MessageSent(bytes message); - /** - * @notice Emitted when max message body size is updated - * @param newMaxMessageBodySize new maximum message body size, in bytes - */ + event MessageReceived( + address indexed caller, + uint32 sourceDomain, + bytes32 indexed nonce, + bytes32 sender, + uint32 indexed finalityThresholdExecuted, + bytes messageBody + ); + event MaxMessageBodySizeUpdated(uint256 newMaxMessageBodySize); // ============ Libraries ============ + using TypedMemView for bytes; + using TypedMemView for bytes29; + using MessageV2 for bytes29; // Test constants + uint256 constant SIGNATURE_LENGTH = 65; + uint32 localDomain = 1; + uint32 remoteDomain = 2; - MessageTransmitterV2 srcMessageTransmitter; + MessageTransmitterV2 messageTransmitter; function setUp() public { // message transmitter on source domain - srcMessageTransmitter = new MessageTransmitterV2( + messageTransmitter = new MessageTransmitterV2( localDomain, attester, maxMessageBodySize, @@ -64,14 +76,14 @@ contract MessageTransmitterV2Test is TestUtils { vm.assume(_pauser != address(0)); vm.assume(_destinationDomain != localDomain); - srcMessageTransmitter.updatePauser(_pauser); + messageTransmitter.updatePauser(_pauser); vm.prank(_pauser); - srcMessageTransmitter.pause(); - assertTrue(srcMessageTransmitter.paused()); + messageTransmitter.pause(); + assertTrue(messageTransmitter.paused()); vm.expectRevert("Pausable: paused"); - srcMessageTransmitter.sendMessage( + messageTransmitter.sendMessage( _destinationDomain, _recipient, _destinationCaller, @@ -90,7 +102,7 @@ contract MessageTransmitterV2Test is TestUtils { vm.assume(_messageBody.length < maxMessageBodySize); vm.expectRevert("Domain is local domain"); - srcMessageTransmitter.sendMessage( + messageTransmitter.sendMessage( localDomain, _recipient, _destinationCaller, @@ -111,7 +123,7 @@ contract MessageTransmitterV2Test is TestUtils { bytes memory _messageBody = new bytes(maxMessageBodySize + 1); vm.expectRevert("Message body exceeds max size"); - srcMessageTransmitter.sendMessage( + messageTransmitter.sendMessage( _destinationDomain, _recipient, _destinationCaller, @@ -130,7 +142,7 @@ contract MessageTransmitterV2Test is TestUtils { vm.assume(_destinationDomain != localDomain); vm.expectRevert("Recipient must be nonzero"); - srcMessageTransmitter.sendMessage( + messageTransmitter.sendMessage( _destinationDomain, bytes32(0), // recipient _destinationCaller, @@ -161,13 +173,716 @@ contract MessageTransmitterV2Test is TestUtils { ); } + function testReceiveMessage_revertsWhenPaused( + bytes calldata _message, + bytes calldata _attestation, + address _pauser + ) public { + vm.assume(_pauser != address(0)); + + // Pause + 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, + AddressUtils.addressToBytes32(_destinationCaller), + _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 >= + messageTransmitter.finalizedMessageThreshold() + ); + + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + AddressUtils.addressToBytes32(_recipient), + AddressUtils.addressToBytes32(_destinationCaller), + _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 < + messageTransmitter.finalizedMessageThreshold() + ); + + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + AddressUtils.addressToBytes32(_recipient), + AddressUtils.addressToBytes32(_destinationCaller), + _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 >= + messageTransmitter.finalizedMessageThreshold() + ); + vm.assume(_recipient != foundryCheatCodeAddr); + + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + AddressUtils.addressToBytes32(_recipient), + AddressUtils.addressToBytes32(_destinationCaller), + _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( + _finalityThresholdExecuted < + messageTransmitter.finalizedMessageThreshold() + ); + vm.assume(_recipient != foundryCheatCodeAddr); + + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + AddressUtils.addressToBytes32(_recipient), + AddressUtils.addressToBytes32(_destinationCaller), + _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_revertsIfNonceIsAlreadyUsed( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + AddressUtils.addressToBytes32(_recipient), + AddressUtils.addressToBytes32(_destinationCaller), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + _receiveMessage(_message, _signature); + + // 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 { + MockReentrantCallerV2 _mockReentrantCaller = new MockReentrantCallerV2(); + + // Encode mockReentrantCaller as recipient + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + AddressUtils.addressToBytes32(address(_mockReentrantCaller)), + 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 + ) public { + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + AddressUtils.addressToBytes32(_recipient), + AddressUtils.addressToBytes32(_destinationCaller), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + _receiveMessage(_message, _signature); + } + + function testReceiveMessage_succeedsWith2of2Signing( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + _setup2of2Multisig(); + + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + AddressUtils.addressToBytes32(_recipient), + AddressUtils.addressToBytes32(_destinationCaller), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign2OfNMultisigMessage(_message); + _receiveMessage(_message, _signature); + } + + function testReceiveMessage_succeedsWith2of3Signing( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + _setup2of3Multisig(); + + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + AddressUtils.addressToBytes32(_recipient), + AddressUtils.addressToBytes32(_destinationCaller), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign2OfNMultisigMessage(_message); + _receiveMessage(_message, _signature); + } + + function testReceiveMessage_succeedsWithFinalizedMessage( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume( + _finalityThresholdExecuted >= + messageTransmitter.finalizedMessageThreshold() + ); + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + AddressUtils.addressToBytes32(_recipient), + AddressUtils.addressToBytes32(_destinationCaller), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + _receiveMessage(_message, _signature); + } + + function testReceiveMessage_succeedsWithUnfinalizedMessage( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume( + _finalityThresholdExecuted < + messageTransmitter.finalizedMessageThreshold() + ); + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + AddressUtils.addressToBytes32(_recipient), + AddressUtils.addressToBytes32(_destinationCaller), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + _receiveMessage(_message, _signature); + } + + function testReceiveMessage_succeedsWithNonZeroDestinationCaller( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + address _destinationCaller, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + vm.assume(_destinationCaller != address(0)); + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + AddressUtils.addressToBytes32(_recipient), + AddressUtils.addressToBytes32(_destinationCaller), + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + _receiveMessage(_message, _signature); + } + + function testReceiveMessage_succeedsWithZeroDestinationCaller( + uint32 _sourceDomain, + bytes32 _nonce, + bytes32 _sender, + address _recipient, + uint32 _minFinalityThreshold, + uint32 _finalityThresholdExecuted, + bytes calldata _messageBody + ) public { + bytes memory _message = _formatMessageForReceive( + version, + _sourceDomain, + localDomain, + _nonce, + _sender, + AddressUtils.addressToBytes32(_recipient), + bytes32(0), // destinationCaller + _minFinalityThreshold, + _finalityThresholdExecuted, + _messageBody + ); + bytes memory _signature = _sign1of1Message(_message); + _receiveMessage(_message, _signature); + } + function testSendMaxMessageBodySize_revertsOnNonOwner( uint256 _newMaxMessageBodySize, address _notOwner ) public { - vm.assume(_notOwner != srcMessageTransmitter.owner()); + vm.assume(_notOwner != messageTransmitter.owner()); expectRevertWithWrongOwner(_notOwner); - srcMessageTransmitter.setMaxMessageBodySize(_newMaxMessageBodySize); + messageTransmitter.setMaxMessageBodySize(_newMaxMessageBodySize); } function testSetMaxMessageBodySize_succeeds( @@ -178,9 +893,9 @@ contract MessageTransmitterV2Test is TestUtils { // Set new max size vm.expectEmit(true, true, true, true); emit MaxMessageBodySizeUpdated(_newMaxMessageBodySize); - srcMessageTransmitter.setMaxMessageBodySize(_newMaxMessageBodySize); + messageTransmitter.setMaxMessageBodySize(_newMaxMessageBodySize); assertEq( - srcMessageTransmitter.maxMessageBodySize(), + messageTransmitter.maxMessageBodySize(), _newMaxMessageBodySize ); } @@ -192,7 +907,7 @@ contract MessageTransmitterV2Test is TestUtils { address _nonRescuer ) public { assertContractIsRescuable( - address(srcMessageTransmitter), + address(messageTransmitter), _rescuer, _rescueRecipient, _amount, @@ -206,13 +921,13 @@ contract MessageTransmitterV2Test is TestUtils { address _nonOwner ) public { vm.assume(_currentPauser != address(0)); - srcMessageTransmitter.updatePauser(_currentPauser); + messageTransmitter.updatePauser(_currentPauser); assertContractIsPausable( - address(srcMessageTransmitter), + address(messageTransmitter), _currentPauser, _newPauser, - srcMessageTransmitter.owner(), + messageTransmitter.owner(), _nonOwner ); } @@ -222,7 +937,7 @@ contract MessageTransmitterV2Test is TestUtils { address _nonOwner ) public { transferOwnership_revertsFromNonOwner( - address(srcMessageTransmitter), + address(messageTransmitter), _newOwner, _nonOwner ); @@ -233,7 +948,7 @@ contract MessageTransmitterV2Test is TestUtils { address _nonOwner ) public { acceptOwnership_revertsFromNonPendingOwner( - address(srcMessageTransmitter), + address(messageTransmitter), _newOwner, _nonOwner ); @@ -241,7 +956,7 @@ contract MessageTransmitterV2Test is TestUtils { function testTransferOwnershipAndAcceptOwnership(address _newOwner) public { transferOwnershipAndAcceptOwnership( - address(srcMessageTransmitter), + address(messageTransmitter), _newOwner ); } @@ -251,12 +966,14 @@ contract MessageTransmitterV2Test is TestUtils { address _secondNewOwner ) public { transferOwnershipWithoutAcceptingThenTransferToNewOwner( - address(srcMessageTransmitter), + address(messageTransmitter), _newOwner, _secondNewOwner ); } + // Internal utility functions + function _sendMessage( uint32 _destinationDomain, bytes32 _recipient, @@ -276,13 +993,13 @@ contract MessageTransmitterV2Test is TestUtils { _messageBody ); - assertFalse(srcMessageTransmitter.paused()); + 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); - srcMessageTransmitter.sendMessage( + messageTransmitter.sendMessage( _destinationDomain, _recipient, _destinationCaller, @@ -291,4 +1008,117 @@ contract MessageTransmitterV2Test is TestUtils { ); vm.stopPrank(); } + + function _receiveMessage( + bytes memory _message, + bytes memory _signature + ) internal { + bytes29 _msg = _message.ref(0); + address _recipient = AddressUtils.bytes32ToAddress( + _msg._getRecipient() + ); + 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() >= + messageTransmitter.finalizedMessageThreshold() + ? 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 + address _caller = AddressUtils.bytes32ToAddress( + _msg._getDestinationCaller() + ); + + // 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()), 1); + } + + // setup second and third attester (first set in setUp()); set sig threshold at 2 + function _setup2of3Multisig() internal { + messageTransmitter.enableAttester(secondAttester); + messageTransmitter.enableAttester(thirdAttester); + messageTransmitter.setSignatureThreshold(2); + } + + // setup second attester (first set in setUp()); set sig threshold at 2 + function _setup2of2Multisig() internal { + messageTransmitter.enableAttester(secondAttester); + messageTransmitter.setSignatureThreshold(2); + } + + 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 + ); + } } From bb17db2a08c1e2a491a619e040634cb1341b9527 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:54:14 -0400 Subject: [PATCH 10/40] [No ticket] Fast-follow to PR 19: improve fuzz tests (#20) ### Summary In PR 19 we added a bunch of fuzz-based tests for `MessageTransmitterV2`: https://github.com/circlefin/evm-cctp-contracts-private/pull/19. I realized that we were not capturing the condition where: `destinationCaller is 0, so we should use random addresses to call receiveMessage()`. Instead, we were always just spoofing the 0-address due to this line: https://github.com/circlefin/evm-cctp-contracts-private/blob/220b071ecf7d3c221dfd71ad6f8802b25611ac89/test/v2/MessageTransmitterV2.t.sol#L1039, which is equivalent to the tests for non-zero value destinationCallers. ### Changes This updates the fuzz tests to take in a random caller. If the destinationCaller is set, we spoof that address, otherwise we call with a random fuzzed address. --- test/v2/MessageTransmitterV2.t.sol | 58 +++++++++++++++++++----------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/test/v2/MessageTransmitterV2.t.sol b/test/v2/MessageTransmitterV2.t.sol index ce07245..b41199f 100644 --- a/test/v2/MessageTransmitterV2.t.sol +++ b/test/v2/MessageTransmitterV2.t.sol @@ -626,7 +626,8 @@ contract MessageTransmitterV2Test is TestUtils { address _destinationCaller, uint32 _minFinalityThreshold, uint32 _finalityThresholdExecuted, - bytes calldata _messageBody + bytes calldata _messageBody, + address _randomCaller ) public { bytes memory _message = _formatMessageForReceive( version, @@ -641,7 +642,7 @@ contract MessageTransmitterV2Test is TestUtils { _messageBody ); bytes memory _signature = _sign1of1Message(_message); - _receiveMessage(_message, _signature); + _receiveMessage(_message, _signature, _randomCaller); // Try again vm.prank(_destinationCaller); @@ -690,7 +691,8 @@ contract MessageTransmitterV2Test is TestUtils { address _destinationCaller, uint32 _minFinalityThreshold, uint32 _finalityThresholdExecuted, - bytes calldata _messageBody + bytes calldata _messageBody, + address _randomCaller ) public { bytes memory _message = _formatMessageForReceive( version, @@ -705,7 +707,7 @@ contract MessageTransmitterV2Test is TestUtils { _messageBody ); bytes memory _signature = _sign1of1Message(_message); - _receiveMessage(_message, _signature); + _receiveMessage(_message, _signature, _randomCaller); } function testReceiveMessage_succeedsWith2of2Signing( @@ -716,7 +718,8 @@ contract MessageTransmitterV2Test is TestUtils { address _destinationCaller, uint32 _minFinalityThreshold, uint32 _finalityThresholdExecuted, - bytes calldata _messageBody + bytes calldata _messageBody, + address _randomCaller ) public { _setup2of2Multisig(); @@ -733,7 +736,7 @@ contract MessageTransmitterV2Test is TestUtils { _messageBody ); bytes memory _signature = _sign2OfNMultisigMessage(_message); - _receiveMessage(_message, _signature); + _receiveMessage(_message, _signature, _randomCaller); } function testReceiveMessage_succeedsWith2of3Signing( @@ -744,7 +747,8 @@ contract MessageTransmitterV2Test is TestUtils { address _destinationCaller, uint32 _minFinalityThreshold, uint32 _finalityThresholdExecuted, - bytes calldata _messageBody + bytes calldata _messageBody, + address _randomCaller ) public { _setup2of3Multisig(); @@ -761,7 +765,7 @@ contract MessageTransmitterV2Test is TestUtils { _messageBody ); bytes memory _signature = _sign2OfNMultisigMessage(_message); - _receiveMessage(_message, _signature); + _receiveMessage(_message, _signature, _randomCaller); } function testReceiveMessage_succeedsWithFinalizedMessage( @@ -772,7 +776,8 @@ contract MessageTransmitterV2Test is TestUtils { address _destinationCaller, uint32 _minFinalityThreshold, uint32 _finalityThresholdExecuted, - bytes calldata _messageBody + bytes calldata _messageBody, + address _randomCaller ) public { vm.assume( _finalityThresholdExecuted >= @@ -791,7 +796,7 @@ contract MessageTransmitterV2Test is TestUtils { _messageBody ); bytes memory _signature = _sign1of1Message(_message); - _receiveMessage(_message, _signature); + _receiveMessage(_message, _signature, _randomCaller); } function testReceiveMessage_succeedsWithUnfinalizedMessage( @@ -802,7 +807,8 @@ contract MessageTransmitterV2Test is TestUtils { address _destinationCaller, uint32 _minFinalityThreshold, uint32 _finalityThresholdExecuted, - bytes calldata _messageBody + bytes calldata _messageBody, + address _randomCaller ) public { vm.assume( _finalityThresholdExecuted < @@ -821,7 +827,7 @@ contract MessageTransmitterV2Test is TestUtils { _messageBody ); bytes memory _signature = _sign1of1Message(_message); - _receiveMessage(_message, _signature); + _receiveMessage(_message, _signature, _randomCaller); } function testReceiveMessage_succeedsWithNonZeroDestinationCaller( @@ -848,7 +854,7 @@ contract MessageTransmitterV2Test is TestUtils { _messageBody ); bytes memory _signature = _sign1of1Message(_message); - _receiveMessage(_message, _signature); + _receiveMessage(_message, _signature, _destinationCaller); } function testReceiveMessage_succeedsWithZeroDestinationCaller( @@ -858,8 +864,10 @@ contract MessageTransmitterV2Test is TestUtils { address _recipient, uint32 _minFinalityThreshold, uint32 _finalityThresholdExecuted, - bytes calldata _messageBody + bytes calldata _messageBody, + address _randomCaller ) public { + vm.assume(_randomCaller != address(0)); bytes memory _message = _formatMessageForReceive( version, _sourceDomain, @@ -873,7 +881,7 @@ contract MessageTransmitterV2Test is TestUtils { _messageBody ); bytes memory _signature = _sign1of1Message(_message); - _receiveMessage(_message, _signature); + _receiveMessage(_message, _signature, _randomCaller); } function testSendMaxMessageBodySize_revertsOnNonOwner( @@ -1009,9 +1017,12 @@ contract MessageTransmitterV2Test is TestUtils { vm.stopPrank(); } + // Calls receiveMessage with msg.destinationCaller if set; otherwise + // with `_randomCaller` function _receiveMessage( bytes memory _message, - bytes memory _signature + bytes memory _signature, + address _randomCaller ) internal { bytes29 _msg = _message.ref(0); address _recipient = AddressUtils.bytes32ToAddress( @@ -1034,10 +1045,17 @@ contract MessageTransmitterV2Test is TestUtils { vm.mockCall(_recipient, _encodedMessageHandlerCall, abi.encode(true)); vm.expectCall(_recipient, _encodedMessageHandlerCall, 1); - // Spoof the destination caller - address _caller = AddressUtils.bytes32ToAddress( - _msg._getDestinationCaller() - ); + // 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 = AddressUtils.bytes32ToAddress( + _msg._getDestinationCaller() + ); + } // assert that a MessageReceive event was logged with expected message bytes vm.expectEmit(true, true, true, true); From caa5c52754c0decbde5c68c8c147920914734e14 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:20:28 -0400 Subject: [PATCH 11/40] STABLE-6895: Finish hooks implementation (#22) --- src/messages/v2/BurnMessageV2.sol | 6 +- src/v2/BaseTokenMessenger.sol | 59 ++++ src/v2/MessageTransmitterV2.sol | 1 - src/v2/TokenMessengerV2.sol | 119 ++++++-- test/messages/v2/BurnMessageV2.t.sol | 6 +- test/v2/TokenMessengerV2.t.sol | 421 ++++++++++++++++++++++----- 6 files changed, 498 insertions(+), 114 deletions(-) diff --git a/src/messages/v2/BurnMessageV2.sol b/src/messages/v2/BurnMessageV2.sol index 06cfbfc..46139a7 100644 --- a/src/messages/v2/BurnMessageV2.sol +++ b/src/messages/v2/BurnMessageV2.sol @@ -62,7 +62,7 @@ library BurnMessageV2 { * @param _amount The burn amount * @param _messageSender The message sender * @param _maxFee The maximum fee to be paid on destination domain - * @param _hook Optional hook to execute on destination domain + * @param _hookData Optional hook to execute on destination domain * @return Formatted message. */ function _formatMessageForRelay( @@ -72,7 +72,7 @@ library BurnMessageV2 { uint256 _amount, bytes32 _messageSender, uint256 _maxFee, - bytes calldata _hook + bytes calldata _hookData ) internal pure returns (bytes memory) { return abi.encodePacked( @@ -84,7 +84,7 @@ library BurnMessageV2 { _maxFee, EMPTY_FEE_EXECUTED, EMPTY_EXPIRATION_BLOCK, - _hook + _hookData ); } diff --git a/src/v2/BaseTokenMessenger.sol b/src/v2/BaseTokenMessenger.sol index 0921e22..d518c96 100644 --- a/src/v2/BaseTokenMessenger.sol +++ b/src/v2/BaseTokenMessenger.sol @@ -20,6 +20,7 @@ pragma solidity 0.7.6; import {Ownable2Step} from "../roles/Ownable2Step.sol"; import {ITokenMinter} from "../interfaces/ITokenMinter.sol"; import {Rescuable} from "../roles/Rescuable.sol"; +import {IMintBurnToken} from "../interfaces/IMintBurnToken.sol"; /** * @title BaseTokenMessenger @@ -56,6 +57,18 @@ abstract contract BaseTokenMessenger is Rescuable { */ event LocalMinterRemoved(address localMinter); + /** + * @notice Emitted when tokens are minted + * @param mintRecipient recipient address of minted tokens + * @param amount amount of minted tokens + * @param mintToken contract address of minted token + */ + event MintAndWithdraw( + address indexed mintRecipient, + uint256 amount, + address indexed mintToken + ); + // ============ State Variables ============ // Local Message Transmitter responsible for sending and receiving messages to/from remote domains address public immutable localMessageTransmitter; @@ -224,4 +237,50 @@ abstract contract BaseTokenMessenger is Rescuable { address(localMessageTransmitter) != address(0) && msg.sender == address(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 { + ITokenMinter _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 + * @param _tokenMinter address of TokenMinter contract + * @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 minted tokens + */ + function _mintAndWithdraw( + address _tokenMinter, + uint32 _remoteDomain, + bytes32 _burnToken, + address _mintRecipient, + uint256 _amount + ) internal { + ITokenMinter _minter = ITokenMinter(_tokenMinter); + address _mintToken = _minter.mint( + _remoteDomain, + _burnToken, + _mintRecipient, + _amount + ); + + emit MintAndWithdraw(_mintRecipient, _amount, _mintToken); + } } diff --git a/src/v2/MessageTransmitterV2.sol b/src/v2/MessageTransmitterV2.sol index ebe0fd7..ebd49a4 100644 --- a/src/v2/MessageTransmitterV2.sol +++ b/src/v2/MessageTransmitterV2.sol @@ -16,7 +16,6 @@ * limitations under the License. */ pragma solidity 0.7.6; -pragma abicoder v2; import {IMessageTransmitterV2} from "../interfaces/v2/IMessageTransmitterV2.sol"; import {Attestable} from "../roles/Attestable.sol"; diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol index 8a604f9..abdb77a 100644 --- a/src/v2/TokenMessengerV2.sol +++ b/src/v2/TokenMessengerV2.sol @@ -23,13 +23,16 @@ import {IMintBurnToken} from "../interfaces/IMintBurnToken.sol"; import {BurnMessageV2} from "../messages/v2/BurnMessageV2.sol"; import {AddressUtils} from "../messages/v2/AddressUtils.sol"; import {MessageTransmitterV2} from "./MessageTransmitterV2.sol"; +import {IMessageHandlerV2} from "../interfaces/v2/IMessageHandlerV2.sol"; +import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol"; +import {BurnMessageV2} from "../messages/v2/BurnMessageV2.sol"; /** * @title TokenMessengerV2 * @notice Sends messages and receives messages to/from MessageTransmitters * and to/from TokenMinters */ -contract TokenMessengerV2 is BaseTokenMessenger { +contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { // ============ Events ============ /** * @notice Emitted when a DepositForBurn message is sent @@ -43,7 +46,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { * 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 hook target and calldata for execution on destination domain + * @param hookData optional hook for execution on destination domain */ event DepositForBurn( address indexed burnToken, @@ -55,13 +58,15 @@ contract TokenMessengerV2 is BaseTokenMessenger { bytes32 destinationCaller, uint256 maxFee, uint32 indexed minFinalityThreshold, - bytes hook + bytes hookData ); // ============ Libraries ============ + using TypedMemView for bytes; + using TypedMemView for bytes29; + using BurnMessageV2 for bytes29; // ============ State Variables ============ - uint32 public immutable MIN_HOOK_LENGTH = 32; // ============ Modifiers ============ @@ -105,7 +110,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { uint256 maxFee, uint32 minFinalityThreshold ) external { - bytes calldata _emptyHook = msg.data[0:0]; + bytes calldata _emptyHookData = msg.data[0:0]; _depositForBurn( amount, destinationDomain, @@ -114,7 +119,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { destinationCaller, maxFee, minFinalityThreshold, - _emptyHook + _emptyHookData ); } @@ -122,7 +127,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { * @notice Deposits and burns tokens from sender to be minted on destination domain. * Emits a `DepositForBurn` event. * @dev reverts if: - * - hook appears invalid, such as being less than 32 bytes in length + * - hookData is zero-length * - given burnToken is not supported * - given destinationDomain has no TokenMessenger registered * - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance @@ -130,9 +135,6 @@ contract TokenMessengerV2 is BaseTokenMessenger { * - burn() reverts. For example, if `amount` is 0. * - fee is greater than or equal to the amount. * - MessageTransmitter#sendMessage reverts. - * @dev Note that even if the hook reverts on the destination domain, the mint will still proceed. - * @dev Hook formatting: - * - TODO: STABLE-7280 * @param amount amount of tokens to burn * @param destinationDomain destination domain * @param mintRecipient address of mint recipient on destination domain @@ -141,7 +143,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { * 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. - * @param hook hook to execute on destination domain. Must be 32-bytes length or more. + * @param hookData hook data to append to burn message for interpretation on destination domain */ function depositForBurnWithHook( uint256 amount, @@ -151,9 +153,9 @@ contract TokenMessengerV2 is BaseTokenMessenger { bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold, - bytes calldata hook + bytes calldata hookData ) external { - require(hook.length >= MIN_HOOK_LENGTH, "Invalid hook length"); + require(hookData.length > 0, "Hook data is empty"); _depositForBurn( amount, @@ -163,11 +165,78 @@ contract TokenMessengerV2 is BaseTokenMessenger { destinationCaller, maxFee, minFinalityThreshold, - hook + 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) + { + bytes29 _msg = messageBody.ref(0); + _msg._validateBurnMessageFormat(); + require( + _msg._getVersion() == messageBodyVersion, + "Invalid message body version" ); + + bytes32 _mintRecipient = _msg._getMintRecipient(); + bytes32 _burnToken = _msg._getBurnToken(); + uint256 _amount = _msg._getAmount(); + + ITokenMinter _localMinter = _getLocalMinter(); + + _mintAndWithdraw( + address(_localMinter), + remoteDomain, + _burnToken, + AddressUtils.bytes32ToAddress(_mintRecipient), + _amount + ); + + return true; + } + + function handleReceiveUnfinalizedMessage( + uint32 sourceDomain, + bytes32 sender, + uint32 finalityThresholdExecuted, + bytes calldata messageBody + ) external override returns (bool) { + // TODO: STABLE-7292 } // ============ 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 contract to burn deposited tokens, on local domain + * @param _destinationCaller caller on the destination domain, as bytes32 + * @param _maxFee maximum fee to pay on destination chain + * @param _hookData optional hook data for execution on destination chain + */ function _depositForBurn( uint256 _amount, uint32 _destinationDomain, @@ -176,7 +245,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { bytes32 _destinationCaller, uint256 _maxFee, uint32 _minFinalityThreshold, - bytes calldata _hook + bytes calldata _hookData ) internal { require(_amount > 0, "Amount must be nonzero"); require(_mintRecipient != bytes32(0), "Mint recipient must be nonzero"); @@ -187,7 +256,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { ); // Deposit and burn tokens - _depositAndBurnTokens(_burnToken, msg.sender, _amount); + _depositAndBurn(_burnToken, msg.sender, _amount); // Format message body bytes memory _burnMessage = BurnMessageV2._formatMessageForRelay( @@ -197,7 +266,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { _amount, AddressUtils.addressToBytes32(msg.sender), _maxFee, - _hook + _hookData ); // Send message @@ -219,21 +288,7 @@ contract TokenMessengerV2 is BaseTokenMessenger { _destinationCaller, _maxFee, _minFinalityThreshold, - _hook - ); - } - - function _depositAndBurnTokens( - address _burnToken, - address _from, - uint256 _amount - ) internal { - ITokenMinter _localMinter = _getLocalMinter(); - IMintBurnToken _mintBurnToken = IMintBurnToken(_burnToken); - require( - _mintBurnToken.transferFrom(_from, address(_localMinter), _amount), - "Transfer operation failed" + _hookData ); - _localMinter.burn(_burnToken, _amount); } } diff --git a/test/messages/v2/BurnMessageV2.t.sol b/test/messages/v2/BurnMessageV2.t.sol index f42e309..d8e3ff4 100644 --- a/test/messages/v2/BurnMessageV2.t.sol +++ b/test/messages/v2/BurnMessageV2.t.sol @@ -34,7 +34,7 @@ contract BurnMessageV2Test is Test { uint256 _amount, bytes32 _messageSender, uint256 _maxFee, - bytes calldata _hook + bytes calldata _hookData ) public pure { bytes memory _expectedMessageBody = abi.encodePacked( _version, @@ -45,7 +45,7 @@ contract BurnMessageV2Test is Test { _maxFee, uint256(0), uint256(0), - _hook + _hookData ); bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( @@ -55,7 +55,7 @@ contract BurnMessageV2Test is Test { _amount, _messageSender, _maxFee, - _hook + _hookData ); bytes29 _m = _messageBody.ref(0); diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index 6c56f2b..a562e5e 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -25,22 +25,10 @@ import {MockMintBurnToken} from "../mocks/MockMintBurnToken.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"; contract TokenMessengerV2Test is BaseTokenMessengerTest { // 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 not equal to bytes32(0). - * @param maxFee maximum fee to pay on destination domain, in burnToken - * @param minFinalityThreshold the minimum finality at which the message should be attested to. - * @param hook hook target and calldata for execution on destination domain - */ event DepositForBurn( address indexed burnToken, uint256 amount, @@ -51,9 +39,20 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 destinationCaller, uint256 maxFee, uint32 indexed minFinalityThreshold, - bytes hook + bytes hookData ); + event MintAndWithdraw( + address indexed mintRecipient, + uint256 amount, + address indexed mintToken + ); + + // Libraries + using TypedMemView for bytes; + using TypedMemView for bytes29; + using BurnMessageV2 for bytes29; + // Constants uint32 remoteDomain = 1; uint32 messageBodyVersion = 2; @@ -403,7 +402,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { vm.assume(_maxFee < _amount); vm.assume(_amount > 1); - vm.expectRevert("Invalid hook length"); + vm.expectRevert("Hook data is empty"); localTokenMessenger.depositForBurnWithHook( _amount, destinationDomain, @@ -416,39 +415,15 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); } - function testDepositForBurnWithHook_revertsIfHookIsTooShort( - uint256 _amount, - bytes32 _mintRecipient, - bytes32 _destinationCaller, - uint32 _minFinalityThreshold, - bytes calldata _hook - ) public { - vm.assume(_mintRecipient != bytes32(0)); - vm.assume(_amount > 1); - vm.assume(_hook.length > 0 && _hook.length < 32); - - vm.expectRevert("Invalid hook length"); - localTokenMessenger.depositForBurnWithHook( - _amount, - destinationDomain, - _mintRecipient, - address(localToken), - _destinationCaller, - _amount - 1, // maxFee - _minFinalityThreshold, - _hook - ); - } - function testDepositForBurnWithHook_revertsIfTransferAmountIsZero( bytes32 _mintRecipient, address _burnToken, bytes32 _destinationCaller, uint32 _minFinalityThreshold, - bytes calldata _hook + bytes calldata _hookData ) public { vm.assume(_mintRecipient != bytes32(0)); - vm.assume(_hook.length > 32); + vm.assume(_hookData.length > 0); vm.expectRevert("Amount must be nonzero"); localTokenMessenger.depositForBurnWithHook( @@ -459,7 +434,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _destinationCaller, 0, _minFinalityThreshold, - _hook + _hookData ); } @@ -469,11 +444,11 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _destinationCaller, uint256 _maxFee, uint32 _minFinalityThreshold, - bytes calldata _hook + bytes calldata _hookData ) public { vm.assume(_amount > 0); vm.assume(_maxFee < _amount); - vm.assume(_hook.length > 32); + vm.assume(_hookData.length > 0); vm.expectRevert("Mint recipient must be nonzero"); localTokenMessenger.depositForBurnWithHook( @@ -484,7 +459,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _destinationCaller, _maxFee, _minFinalityThreshold, - _hook + _hookData ); } @@ -494,11 +469,11 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _mintRecipient, bytes32 _destinationCaller, uint32 _minFinalityThreshold, - bytes calldata _hook + bytes calldata _hookData ) public { vm.assume(_amount > 0); vm.assume(_mintRecipient != bytes32(0)); - vm.assume(_hook.length > 32); + vm.assume(_hookData.length > 0); vm.expectRevert("Max fee must be less than amount"); localTokenMessenger.depositForBurnWithHook( @@ -509,7 +484,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _destinationCaller, _amount, // maxFee _minFinalityThreshold, - _hook + _hookData ); } @@ -520,12 +495,12 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _destinationCaller, uint256 _maxFee, uint32 _minFinalityThreshold, - bytes calldata _hook + bytes calldata _hookData ) public { vm.assume(_amount > 0); vm.assume(_maxFee > _amount); vm.assume(_mintRecipient != bytes32(0)); - vm.assume(_hook.length > 32); + vm.assume(_hookData.length > 0); vm.expectRevert("Max fee must be less than amount"); localTokenMessenger.depositForBurnWithHook( @@ -536,7 +511,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _destinationCaller, _maxFee, _minFinalityThreshold, - _hook + _hookData ); } @@ -548,12 +523,12 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _destinationCaller, uint256 _maxFee, uint32 _minFinalityThreshold, - bytes calldata _hook + bytes calldata _hookData ) public { vm.assume(_amount > 0); vm.assume(_maxFee < _amount); vm.assume(_mintRecipient != bytes32(0)); - vm.assume(_hook.length > 32); + vm.assume(_hookData.length > 0); TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( localMessageTransmitter, @@ -569,7 +544,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _destinationCaller, _maxFee, _minFinalityThreshold, - _hook + _hookData ); } @@ -581,12 +556,12 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _destinationCaller, uint256 _maxFee, uint32 _minFinalityThreshold, - bytes calldata _hook + bytes calldata _hookData ) public { vm.assume(_amount > 0); vm.assume(_maxFee < _amount); vm.assume(_mintRecipient != bytes32(0)); - vm.assume(_hook.length > 32); + vm.assume(_hookData.length > 0); TokenMessengerV2 _tokenMessenger = new TokenMessengerV2( localMessageTransmitter, @@ -607,7 +582,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _destinationCaller, _maxFee, _minFinalityThreshold, - _hook + _hookData ); } @@ -617,12 +592,12 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _destinationCaller, uint256 _maxFee, uint32 _minFinalityThreshold, - bytes calldata _hook + bytes calldata _hookData ) public { vm.assume(_amount > 0); vm.assume(_maxFee < _amount); vm.assume(_mintRecipient != bytes32(0)); - vm.assume(_hook.length > 32); + vm.assume(_hookData.length > 0); vm.mockCall( address(localToken), @@ -638,7 +613,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _destinationCaller, _maxFee, _minFinalityThreshold, - _hook + _hookData ); } @@ -649,12 +624,12 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _destinationCaller, uint256 _maxFee, uint32 _minFinalityThreshold, - bytes calldata _hook + bytes calldata _hookData ) public { vm.assume(_amount > 0); vm.assume(_maxFee < _amount); vm.assume(_mintRecipient != bytes32(0)); - vm.assume(_hook.length > 32); + vm.assume(_hookData.length > 0); // TransferFrom will revert, as localTokenMessenger has no allowance assertEq( @@ -671,7 +646,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _destinationCaller, _maxFee, _minFinalityThreshold, - _hook + _hookData ); } @@ -681,11 +656,11 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _destinationCaller, uint256 _maxFee, uint32 _minFinalityThreshold, - bytes calldata _hook, + bytes calldata _hookData, address _caller ) public { vm.assume(_mintRecipient != bytes32(0)); - vm.assume(_hook.length > 32); + vm.assume(_hookData.length > 0); vm.assume(_maxFee < _amount); vm.assume(_amount > 1); vm.assume(_caller != address(0)); @@ -702,7 +677,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _destinationCaller, _maxFee, _minFinalityThreshold, - _hook + _hookData ); } @@ -712,13 +687,13 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _mintRecipient, bytes32 _destinationCaller, uint32 _minFinalityThreshold, - bytes calldata _hook, + bytes calldata _hookData, address _caller ) public { vm.assume(_mintRecipient != bytes32(0)); vm.assume(_amount > 1); vm.assume(_amount < _burnLimit); - vm.assume(_hook.length > 32); + vm.assume(_hookData.length > 0); vm.assume(_caller != address(0)); uint256 _maxFee = _amount - 1; @@ -732,10 +707,239 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _amount, _maxFee, _minFinalityThreshold, - _hook + _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 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_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 + 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_revertsIfMintReverts( + 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 + ); + + // Mock a failing call to TokenMinter mint() + bytes memory _call = abi.encodeWithSelector( + TokenMinter.mint.selector, + remoteDomain, + _burnToken, + AddressUtils.bytes32ToAddress(_mintRecipient), + _amount + ); + vm.mockCallRevert( + address(localTokenMinter), + _call, + "Testing: token minter failed" + ); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Testing: token minter failed"); + localTokenMessenger.handleReceiveFinalizedMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody ); } + function testHandleReceiveFinalizedMessage_succeeds( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume(_finalityThresholdExecuted >= 2000); + + bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( + localTokenMessenger.messageBodyVersion(), + AddressUtils.addressToBytes32(remoteTokenAddr), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _hookData + ); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + _finalityThresholdExecuted, + _messageBody + ); + } + + // Test helpers + function _setupDepositForBurn( address _caller, uint256 _amount, @@ -760,17 +964,17 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint256 _amount, uint256 _maxFee, uint32 _minFinalityThreshold, - bytes calldata _hook + bytes calldata _hookData ) internal { bytes memory _expectedBurnMessage = BurnMessageV2 ._formatMessageForRelay( - messageBodyVersion, // version + localTokenMessenger.messageBodyVersion(), // version AddressUtils.addressToBytes32(address(localToken)), // burn token _mintRecipient, // mint recipient _amount, // amount AddressUtils.addressToBytes32(_caller), // sender _maxFee, // max fee - _hook + _hookData ); // expect burn() on localTokenMinter @@ -815,12 +1019,12 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _destinationCaller, _maxFee, _minFinalityThreshold, - _hook + _hookData ); vm.prank(_caller); - if (_hook.length == 0) { + if (_hookData.length == 0) { localTokenMessenger.depositForBurn( _amount, destinationDomain, @@ -839,8 +1043,75 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _destinationCaller, _maxFee, _minFinalityThreshold, - _hook + _hookData ); } } + + function _handleReceiveMessage( + uint32 _remoteDomain, + bytes32 _sender, + uint32 _finalityThresholdExecuted, + bytes memory _messageBody + ) internal { + bytes29 _msg = _messageBody.ref(0); + address _mintRecipient = AddressUtils.bytes32ToAddress( + _msg._getMintRecipient() + ); + uint256 _amount = _msg._getAmount(); + + // 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( + AddressUtils.bytes32ToAddress(_msg._getBurnToken()), + remoteTokenAddr + ); + + // Sanity check that the starting balance of mintRecipient is 0 + assertEq(localToken.balanceOf(_mintRecipient), 0); + + // Expect that mint() be called 1x on TokenMinter + bytes memory _encodedMintCall = abi.encodeWithSelector( + TokenMinter.mint.selector, + _remoteDomain, + _msg._getBurnToken(), + _mintRecipient, + _amount + ); + vm.expectCall(address(localTokenMinter), _encodedMintCall, 1); + + // Expect MintAndWithdraw to be emitted + vm.expectEmit(true, true, true, true); + emit MintAndWithdraw(_mintRecipient, _amount, address(localToken)); + + // Execute handleReceive() + vm.prank(localMessageTransmitter); + + bool _result; + if (_finalityThresholdExecuted >= 2000) { + _result = localTokenMessenger.handleReceiveFinalizedMessage( + _remoteDomain, + _sender, + _finalityThresholdExecuted, + _messageBody + ); + } else { + _result = localTokenMessenger.handleReceiveUnfinalizedMessage( + _remoteDomain, + _sender, + _finalityThresholdExecuted, + _messageBody + ); + } + + assertTrue(_result); + + // Check balance after + assertEq(_msg._getAmount(), localToken.balanceOf(_mintRecipient)); + } } From abb8c94419f34fd2963cd92221f7f482b0313f21 Mon Sep 17 00:00:00 2001 From: walkerq Date: Mon, 23 Sep 2024 19:01:45 -0400 Subject: [PATCH 12/40] typo fix (#11) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b5b526a..84239d7 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ 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). +- To install a specific version, see [here](https://github.com/foundry-rs/foundry/blob/3f13a986e69c18ea19ce634fea00f4df6b3666b0/foundryup/README.md#usage). ## Testing From eeea48f3911532480636fee2ddcea17e83872714 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:16:18 -0400 Subject: [PATCH 13/40] STABLE-7292: Implement handleReceiveUnfinalizedMessage (#25) --- .github/workflows/ci.yml | 5 + src/interfaces/v2/ITokenMinterV2.sol | 47 ++ src/v2/BaseTokenMessenger.sol | 79 ++- src/v2/TokenMessengerV2.sol | 138 +++++- src/v2/TokenMinterV2.sol | 84 ++++ test/TokenMinter.t.sol | 22 +- test/v2/BaseTokenMessenger.t.sol | 47 +- test/v2/TokenMessengerV2.t.sol | 692 ++++++++++++++++++++++++++- test/v2/TokenMinterV2.t.sol | 330 +++++++++++++ 9 files changed, 1353 insertions(+), 91 deletions(-) create mode 100644 src/interfaces/v2/ITokenMinterV2.sol create mode 100644 src/v2/TokenMinterV2.sol create mode 100644 test/v2/TokenMinterV2.t.sol diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9dfec98..65dee9b 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 diff --git a/src/interfaces/v2/ITokenMinterV2.sol b/src/interfaces/v2/ITokenMinterV2.sol new file mode 100644 index 0000000..a667905 --- /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 minter of tokens that are mintable, burnable, and interchangeable + * across domains. + */ +interface ITokenMinterV2 is ITokenMinter { + /** + * @notice Mints to multiple recipients amounts of local 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 token minted. + */ + function mint( + uint32 sourceDomain, + bytes32 burnToken, + address recipientOne, + address recipientTwo, + uint256 amountOne, + uint256 amountTwo + ) external returns (address); +} diff --git a/src/v2/BaseTokenMessenger.sol b/src/v2/BaseTokenMessenger.sol index d518c96..365523c 100644 --- a/src/v2/BaseTokenMessenger.sol +++ b/src/v2/BaseTokenMessenger.sol @@ -18,7 +18,7 @@ pragma solidity 0.7.6; import {Ownable2Step} from "../roles/Ownable2Step.sol"; -import {ITokenMinter} from "../interfaces/ITokenMinter.sol"; +import {ITokenMinterV2} from "../interfaces/v2/ITokenMinterV2.sol"; import {Rescuable} from "../roles/Rescuable.sol"; import {IMintBurnToken} from "../interfaces/IMintBurnToken.sol"; @@ -46,27 +46,33 @@ abstract contract BaseTokenMessenger is Rescuable { /** * @notice Emitted when the local minter is added * @param localMinter address of local minter - * @notice Emitted when the local minter is added */ event LocalMinterAdded(address localMinter); /** * @notice Emitted when the local minter is removed * @param localMinter address of local minter - * @notice Emitted when the local minter is removed */ 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 + * @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 + address indexed mintToken, + uint256 feeCollected ); // ============ State Variables ============ @@ -77,11 +83,14 @@ abstract contract BaseTokenMessenger is Rescuable { uint32 public immutable messageBodyVersion; // Minter responsible for minting and burning tokens on the local domain - ITokenMinter public localMinter; + 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 @@ -171,7 +180,7 @@ abstract contract BaseTokenMessenger is Rescuable { "Local minter is already set." ); - localMinter = ITokenMinter(newLocalMinter); + localMinter = ITokenMinterV2(newLocalMinter); emit LocalMinterAdded(newLocalMinter); } @@ -188,6 +197,18 @@ abstract contract BaseTokenMessenger is Rescuable { 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 { + require(_feeRecipient != address(0), "Zero address not allowed"); + feeRecipient = _feeRecipient; + emit FeeRecipientSet(_feeRecipient); + } + // ============ Internal Utils ============ /** * @notice return the remote TokenMessenger for the given `_domain` if one exists, else revert. @@ -206,7 +227,7 @@ abstract contract BaseTokenMessenger is Rescuable { * @notice return the local minter address if it is set, else revert. * @return local minter as ITokenMinter. */ - function _getLocalMinter() internal view returns (ITokenMinter) { + function _getLocalMinter() internal view returns (ITokenMinterV2) { require(address(localMinter) != address(0), "Local minter is not set"); return localMinter; } @@ -249,7 +270,7 @@ abstract contract BaseTokenMessenger is Rescuable { address _from, uint256 _amount ) internal { - ITokenMinter _localMinter = _getLocalMinter(); + ITokenMinterV2 _localMinter = _getLocalMinter(); IMintBurnToken _mintBurnToken = IMintBurnToken(_burnToken); require( _mintBurnToken.transferFrom(_from, address(_localMinter), _amount), @@ -259,28 +280,42 @@ abstract contract BaseTokenMessenger is Rescuable { } /** - * @notice Mints tokens to a recipient - * @param _tokenMinter address of TokenMinter contract + * @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 minted tokens + * @param _amount amount of tokens to mint to `_mintRecipient` + * @param _fee fee collected for mint */ function _mintAndWithdraw( - address _tokenMinter, uint32 _remoteDomain, bytes32 _burnToken, address _mintRecipient, - uint256 _amount + uint256 _amount, + uint256 _fee ) internal { - ITokenMinter _minter = ITokenMinter(_tokenMinter); - address _mintToken = _minter.mint( - _remoteDomain, - _burnToken, - _mintRecipient, - _amount - ); + 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); + emit MintAndWithdraw(_mintRecipient, _amount, _mintToken, _fee); } } diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol index abdb77a..11ab3a2 100644 --- a/src/v2/TokenMessengerV2.sol +++ b/src/v2/TokenMessengerV2.sol @@ -67,6 +67,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { using BurnMessageV2 for bytes29; // ============ State Variables ============ + uint32 public immutable MINIMUM_SUPPORTED_FINALITY_THRESHOLD = 1000; // ============ Modifiers ============ @@ -192,39 +193,70 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { onlyRemoteTokenMessenger(remoteDomain, sender) returns (bool) { + // Validate finalized message bytes29 _msg = messageBody.ref(0); - _msg._validateBurnMessageFormat(); + ( + address _mintRecipient, + bytes32 _burnToken, + uint256 _amount + ) = _validateFinalizedMessage(_msg); + + _mintAndWithdraw(remoteDomain, _burnToken, _mintRecipient, _amount, 0); + + return true; + } + + /** + * @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 1000. + * @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 handleReceiveUnfinalizedMessage( + uint32 remoteDomain, + bytes32 sender, + uint32 finalityThresholdExecuted, + bytes calldata messageBody + ) + external + override + onlyLocalMessageTransmitter + onlyRemoteTokenMessenger(remoteDomain, sender) + returns (bool) + { + // Require minimum supported finality threshold require( - _msg._getVersion() == messageBodyVersion, - "Invalid message body version" + finalityThresholdExecuted >= MINIMUM_SUPPORTED_FINALITY_THRESHOLD, + "Unsupported finality threshold" ); - bytes32 _mintRecipient = _msg._getMintRecipient(); - bytes32 _burnToken = _msg._getBurnToken(); - uint256 _amount = _msg._getAmount(); - - ITokenMinter _localMinter = _getLocalMinter(); + // Validate message + bytes29 _msg = messageBody.ref(0); + ( + address _mintRecipient, + bytes32 _burnToken, + uint256 _amount, + uint256 _fee + ) = _validateUnfinalizedMessage(_msg); _mintAndWithdraw( - address(_localMinter), remoteDomain, _burnToken, - AddressUtils.bytes32ToAddress(_mintRecipient), - _amount + _mintRecipient, + _amount - _fee, + _fee ); return true; } - function handleReceiveUnfinalizedMessage( - uint32 sourceDomain, - bytes32 sender, - uint32 finalityThresholdExecuted, - bytes calldata messageBody - ) external override returns (bool) { - // TODO: STABLE-7292 - } - // ============ Internal Utils ============ /** * @notice Deposits and burns tokens from sender to be minted on destination domain. @@ -291,4 +323,70 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { _hookData ); } + + /** + * @notice Validates a finalized BurnMessage and unpacks relevant message fields. + * @dev Reverts if the BurnMessage is malformed + * @dev Reverts if the BurnMessage version isn't supported + * @param _msg Finalized message + * @return _mintRecipient The recipient of the mint, as bytes32 + * @return _burnToken The token address burned on the source chain + * @return _amount The amount of burnToken burned + */ + function _validateFinalizedMessage( + bytes29 _msg + ) + internal + view + returns (address _mintRecipient, bytes32 _burnToken, uint256 _amount) + { + _msg._validateBurnMessageFormat(); + require( + _msg._getVersion() == messageBodyVersion, + "Invalid message body version" + ); + + return ( + AddressUtils.bytes32ToAddress(_msg._getMintRecipient()), + _msg._getBurnToken(), + _msg._getAmount() + ); + } + + /** + * @notice Validates a finalized BurnMessage and unpacks relevant message fields. + * @dev Reverts if the BurnMessage is malformed + * @dev Reverts if the BurnMessage version isn't supported + * @dev Reverts if the message is expired + * @dev Reverts if the fee executed exceeds the amount + * @param _msg Finalized message + * @return _mintRecipient The recipient of the mint, as bytes32 + * @return _burnToken The token address burned on the source chain + * @return _amount The amount of burnToken burned + */ + function _validateUnfinalizedMessage( + bytes29 _msg + ) + internal + view + returns ( + address _mintRecipient, + bytes32 _burnToken, + uint256 _amount, + uint256 _fee + ) + { + (_mintRecipient, _burnToken, _amount) = _validateFinalizedMessage(_msg); + + // Enforce message expiration + uint256 _expirationBlock = _msg._getExpirationBlock(); + require( + _expirationBlock == 0 || _expirationBlock > block.number, + "Message expired and must be re-signed" + ); + + // Validate fee + _fee = _msg._getFeeExecuted(); + require(_fee == 0 || _fee < _amount, "Fee equals or exceeds amount"); + } } 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/TokenMinter.t.sol b/test/TokenMinter.t.sol index ad71c7f..6f24554 100644 --- a/test/TokenMinter.t.sol +++ b/test/TokenMinter.t.sol @@ -77,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(); @@ -86,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); } @@ -116,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); @@ -150,7 +153,6 @@ contract TokenMinterTest is Test, TestUtils { function testBurn_succeeds( uint256 _amount, - address _localToken, uint256 _allowedBurnAmount ) public { vm.assume(_amount > 0); @@ -162,7 +164,7 @@ contract TokenMinterTest is Test, TestUtils { _allowedBurnAmount ); - _mintAndBurn(_amount, _localToken); + _mintAndBurn(_amount); } function testBurn_revertsOnUnsupportedBurnToken(uint256 _amount) public { @@ -199,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( @@ -304,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 @@ -482,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/v2/BaseTokenMessenger.t.sol b/test/v2/BaseTokenMessenger.t.sol index cdc8206..d1c3e59 100644 --- a/test/v2/BaseTokenMessenger.t.sol +++ b/test/v2/BaseTokenMessenger.t.sol @@ -24,33 +24,12 @@ import {TestUtils} from "../TestUtils.sol"; abstract contract BaseTokenMessengerTest is Test, TestUtils { // 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 RemoteTokenMessengerAdded(uint32 domain, bytes32 tokenMessenger); event RemoteTokenMessengerRemoved(uint32 domain, bytes32 tokenMessenger); - - /** - * @notice Emitted when the local minter is added - * @param localMinter address of local minter - * @notice Emitted when the local minter is added - */ event LocalMinterAdded(address localMinter); - - /** - * @notice Emitted when the local minter is removed - * @param localMinter address of local minter - * @notice Emitted when the local minter is removed - */ event LocalMinterRemoved(address localMinter); + event FeeRecipientSet(address feeRecipient); BaseTokenMessenger baseTokenMessenger; @@ -264,6 +243,28 @@ abstract contract BaseTokenMessengerTest is Test, TestUtils { 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( diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index a562e5e..e6a60f9 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -22,6 +22,7 @@ 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"; @@ -45,7 +46,8 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { event MintAndWithdraw( address indexed mintRecipient, uint256 amount, - address indexed mintToken + address indexed mintToken, + uint256 feeCollected ); // Libraries @@ -67,8 +69,12 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { address remoteTokenAddr = address(40); + address feeRecipient = address(50); + MockMintBurnToken localToken = new MockMintBurnToken(); - TokenMinter localTokenMinter = new TokenMinter(tokenController); + TokenMinterV2 localTokenMinter = new TokenMinterV2(tokenController); + + uint32 immutable CONFIRMED_FINALITY_THRESHOLD = 1000; function setUp() public override { // TokenMessenger under test @@ -78,6 +84,8 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); // Add a local minter localTokenMessenger.addLocalMinter(address(localTokenMinter)); + // Add fee recipient + localTokenMessenger.setFeeRecipient(feeRecipient); remoteTokenMessengerAddr = AddressUtils.addressToBytes32( remoteTokenMessageger @@ -938,8 +946,627 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); } + 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 < CONFIRMED_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 >= CONFIRMED_FINALITY_THRESHOLD); + + vm.prank(localMessageTransmitter); + vm.expectRevert("Invalid 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 >= CONFIRMED_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, + CONFIRMED_FINALITY_THRESHOLD, + _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, + CONFIRMED_FINALITY_THRESHOLD, + _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 >= CONFIRMED_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 >= CONFIRMED_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_revertsIfNoLocalMinterIsSet( + bytes32 _burnToken, + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _feeExecuted, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume(_finalityThresholdExecuted >= CONFIRMED_FINALITY_THRESHOLD); + vm.assume(_feeExecuted < _amount); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + _burnToken, + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + 0, + _hookData + ); + + assertTrue(address(localTokenMessenger.localMinter()) != address(0)); + + // Remove local minter + 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 >= CONFIRMED_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, + AddressUtils.bytes32ToAddress(_mintRecipient), + _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 _maxFee, + uint256 _feeExecuted, + bytes calldata _hookData, + uint32 _finalityThresholdExecuted + ) public { + vm.assume(_finalityThresholdExecuted >= CONFIRMED_FINALITY_THRESHOLD); + vm.assume(_feeExecuted < _amount); + vm.assume(_feeExecuted > 0); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + _burnToken, + _mintRecipient, + _amount, + _burnMessageSender, + _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, + AddressUtils.bytes32ToAddress(_mintRecipient), + 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); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + AddressUtils.addressToBytes32(remoteTokenAddr), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + 0, // expiration + _hookData + ); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + CONFIRMED_FINALITY_THRESHOLD, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_succeedsForNonZeroExpirationBlock( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + 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(), + AddressUtils.addressToBytes32(remoteTokenAddr), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + _expirationBlock, + _hookData + ); + + // Jump to a block height lower than the expiration block + vm.roll(_expirationBlock - 1); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + CONFIRMED_FINALITY_THRESHOLD, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_succeedsForZeroFee( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _expirationBlock, + bytes calldata _hookData + ) public { + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + AddressUtils.addressToBytes32(remoteTokenAddr), + _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, + CONFIRMED_FINALITY_THRESHOLD, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_succeedsForNonZeroFee( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + 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(), + AddressUtils.addressToBytes32(remoteTokenAddr), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + _expirationBlock, + _hookData + ); + + // Jump to a block height lower than the expiration block + vm.roll(_expirationBlock - 1); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + CONFIRMED_FINALITY_THRESHOLD, + _messageBody + ); + } + + function testHandleReceiveUnfinalizedMessage_succeeds( + bytes32 _mintRecipient, + uint256 _amount, + bytes32 _burnMessageSender, + uint256 _maxFee, + uint256 _expirationBlock, + uint256 _feeExecuted, + bytes calldata _hookData + ) public { + vm.assume(_feeExecuted < _amount); + vm.assume(_expirationBlock > 0); + + bytes memory _messageBody = _formatBurnMessageForReceive( + localTokenMessenger.messageBodyVersion(), + AddressUtils.addressToBytes32(remoteTokenAddr), + _mintRecipient, + _amount, + _burnMessageSender, + _maxFee, + _feeExecuted, + _expirationBlock, + _hookData + ); + + // Jump to a block height lower than the expiration block + vm.roll(_expirationBlock - 1); + + _handleReceiveMessage( + remoteDomain, + remoteTokenMessengerAddr, + CONFIRMED_FINALITY_THRESHOLD, + _messageBody + ); + } + // Test helpers + 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, @@ -1059,6 +1686,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _msg._getMintRecipient() ); uint256 _amount = _msg._getAmount(); + uint256 _fee = _msg._getFeeExecuted(); // Sanity checks to ensure this is being called with appropriate inputs assertEq( @@ -1071,23 +1699,46 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { AddressUtils.bytes32ToAddress(_msg._getBurnToken()), remoteTokenAddr ); + assertTrue(_fee == 0 || _amount > _fee); + assertTrue(feeRecipient != address(0)); + vm.assume(_mintRecipient != feeRecipient); - // Sanity check that the starting balance of mintRecipient is 0 + // 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); + } - // Expect that mint() be called 1x on TokenMinter - bytes memory _encodedMintCall = abi.encodeWithSelector( - TokenMinter.mint.selector, - _remoteDomain, - _msg._getBurnToken(), + vm.expectEmit(true, true, true, true); // Expect MintAndWithdraw to be emitted + emit MintAndWithdraw( _mintRecipient, - _amount + _amount - _fee, + address(localToken), + _fee ); - vm.expectCall(address(localTokenMinter), _encodedMintCall, 1); - - // Expect MintAndWithdraw to be emitted - vm.expectEmit(true, true, true, true); - emit MintAndWithdraw(_mintRecipient, _amount, address(localToken)); // Execute handleReceive() vm.prank(localMessageTransmitter); @@ -1111,7 +1762,16 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { assertTrue(_result); - // Check balance after - assertEq(_msg._getAmount(), localToken.balanceOf(_mintRecipient)); + // 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/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)); + } +} From bafdb8dfdb8ba50fe67a59074a7f0b844aaca253 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:37:41 -0400 Subject: [PATCH 14/40] STABLE-6894: Part 1, add proxy contract (#29) \ --- src/v2/AdminUpgradeableProxy.sol | 148 ++++++++ .../v2/MockPayableProxyImplementation.sol | 22 ++ test/mocks/v2/MockProxyImplementation.sol | 45 +++ test/v2/AdminUpgradeableProxy.t.sol | 332 ++++++++++++++++++ 4 files changed, 547 insertions(+) create mode 100644 src/v2/AdminUpgradeableProxy.sol create mode 100644 test/mocks/v2/MockPayableProxyImplementation.sol create mode 100644 test/mocks/v2/MockProxyImplementation.sol create mode 100644 test/v2/AdminUpgradeableProxy.t.sol diff --git a/src/v2/AdminUpgradeableProxy.sol b/src/v2/AdminUpgradeableProxy.sol new file mode 100644 index 0000000..df892f8 --- /dev/null +++ b/src/v2/AdminUpgradeableProxy.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/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/v2/AdminUpgradeableProxy.t.sol b/test/v2/AdminUpgradeableProxy.t.sol new file mode 100644 index 0000000..836ece8 --- /dev/null +++ b/test/v2/AdminUpgradeableProxy.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/v2/AdminUpgradeableProxy.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 AdminUpgradeableProxyTest 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") + ); + } +} From 1aa77d731bad9308db851e61366b845f99ec5038 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Fri, 11 Oct 2024 14:15:16 -0400 Subject: [PATCH 15/40] STABLE-7589: Add Denylist to TokenMessenger (#31) ### Summary Adds a Denylist to TokenMessenger. Updated the ABI to align with the Paymaster PR: https://github.com/circlefin/smart-wallet-contracts-private/pull/155, though the implementations are slightly difference. The requirements are to check the denylist on `depositForBurn` and `depositForBurnWithHook` for both the `msg.sender` and `tx.origin` addresses. This is pretty straightforward, but some callouts are down below. ### Testing Added dedicated tests using a mock Denylistable, and unit tests for TokenMessengerV2 and BaseTokenMessenger. --- src/roles/v2/Denylistable.sol | 154 +++++++++++++++++ src/v2/BaseTokenMessenger.sol | 3 +- src/v2/TokenMessengerV2.sol | 6 +- test/TestUtils.sol | 49 ++++++ test/mocks/v2/MockDenylistable.sol | 31 ++++ test/roles/v2/Denylistable.t.sol | 258 +++++++++++++++++++++++++++++ test/v2/BaseTokenMessenger.t.sol | 15 ++ test/v2/TokenMessengerV2.t.sol | 205 ++++++++++++++++++++++- 8 files changed, 715 insertions(+), 6 deletions(-) create mode 100644 src/roles/v2/Denylistable.sol create mode 100644 test/mocks/v2/MockDenylistable.sol create mode 100644 test/roles/v2/Denylistable.t.sol diff --git a/src/roles/v2/Denylistable.sol b/src/roles/v2/Denylistable.sol new file mode 100644 index 0000000..87685a5 --- /dev/null +++ b/src/roles/v2/Denylistable.sol @@ -0,0 +1,154 @@ +/* + * 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 Base contract that allows management of a denylist + */ +abstract contract Denylistable is Ownable2Step { + // ============ Events ============ + /** + * @notice Emitted when the Denylister is updated + * @param newDenylister address of new Denylister + */ + event DenylisterChanged(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 private _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) private _denylist; + + // ============ 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" + ); + _denylister = _newDenylister; + emit DenylisterChanged(_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/BaseTokenMessenger.sol b/src/v2/BaseTokenMessenger.sol index 365523c..2733871 100644 --- a/src/v2/BaseTokenMessenger.sol +++ b/src/v2/BaseTokenMessenger.sol @@ -20,6 +20,7 @@ pragma solidity 0.7.6; import {Ownable2Step} from "../roles/Ownable2Step.sol"; 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"; /** @@ -27,7 +28,7 @@ import {IMintBurnToken} from "../interfaces/IMintBurnToken.sol"; * @notice Base administrative functionality for TokenMessenger implementations, * including managing remote token messengers and the local token minter. */ -abstract contract BaseTokenMessenger is Rescuable { +abstract contract BaseTokenMessenger is Rescuable, Denylistable { // ============ Events ============ /** * @notice Emitted when a remote TokenMessenger is added diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol index 11ab3a2..2e368a6 100644 --- a/src/v2/TokenMessengerV2.sol +++ b/src/v2/TokenMessengerV2.sol @@ -69,8 +69,6 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { // ============ State Variables ============ uint32 public immutable MINIMUM_SUPPORTED_FINALITY_THRESHOLD = 1000; - // ============ Modifiers ============ - // ============ Constructor ============ /** * @param _messageTransmitter Message transmitter address @@ -110,7 +108,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold - ) external { + ) external notDenylistedCallers { bytes calldata _emptyHookData = msg.data[0:0]; _depositForBurn( amount, @@ -155,7 +153,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { uint256 maxFee, uint32 minFinalityThreshold, bytes calldata hookData - ) external { + ) external notDenylistedCallers { require(hookData.length > 0, "Hook data is empty"); _depositForBurn( diff --git a/test/TestUtils.sol b/test/TestUtils.sol index 2a0948a..8fd497a 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -19,6 +19,7 @@ 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 { /** @@ -283,6 +284,54 @@ 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, 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/roles/v2/Denylistable.t.sol b/test/roles/v2/Denylistable.t.sol new file mode 100644 index 0000000..e480831 --- /dev/null +++ b/test/roles/v2/Denylistable.t.sol @@ -0,0 +1,258 @@ +/* + * 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 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(_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/v2/BaseTokenMessenger.t.sol b/test/v2/BaseTokenMessenger.t.sol index d1c3e59..777883c 100644 --- a/test/v2/BaseTokenMessenger.t.sol +++ b/test/v2/BaseTokenMessenger.t.sol @@ -329,6 +329,21 @@ abstract contract BaseTokenMessengerTest is Test, TestUtils { ); } + // Denylistable Tests + + function testDenylistable( + address _randomAddress, + address _newDenylister, + address _nonOwner + ) public { + assertContractIsDenylistable( + address(baseTokenMessenger), + _randomAddress, + _newDenylister, + _nonOwner + ); + } + // Test utils function _addLocalMinter( diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index e6a60f9..c29e7a2 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -70,6 +70,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { address remoteTokenAddr = address(40); address feeRecipient = address(50); + address denylister = address(60); MockMintBurnToken localToken = new MockMintBurnToken(); TokenMinterV2 localTokenMinter = new TokenMinterV2(tokenController); @@ -84,9 +85,13 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); // Add a local minter localTokenMessenger.addLocalMinter(address(localTokenMinter)); - // Add fee recipient + + // Update fee recipient localTokenMessenger.setFeeRecipient(feeRecipient); + // Update denylister + localTokenMessenger.updateDenylister(denylister); + remoteTokenMessengerAddr = AddressUtils.addressToBytes32( remoteTokenMessageger ); @@ -132,6 +137,102 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { // Tests + 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, @@ -399,6 +500,108 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); } + 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, From 40a2e35a518cad1664ebc94bb951c5b43f4f13c7 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:04:56 -0400 Subject: [PATCH 16/40] STABLE-6894: Part 2, complete proxy implementation (#30) ### Summary This adds an `Initializable` type and updates `TokenMessengerV2` and `MessageTransmitterV2` to use it. It also adds initializers to both contracts. Some implementation notes: - The `Initializable` type is forked from OZ, and then modified for Solidity 0.7.6 (no custom errors) and stripped down of features that we're not using. - Please scrutinize the fork changes to `Initializable`. I'm not wed to using this type for our purposes. - Had to create internal setters for Pausable, Rescuable, and Attestable to allow the initializers to set those values. Some further callouts down below ### Testing Added tests for Initializable + the initializer function additions to the TokenMessenger and MessageTransmitter, and also simulated an upgrade to mock v3 versions. --- src/roles/Attestable.sol | 38 +- src/roles/Pausable.sol | 8 + src/roles/Rescuable.sol | 9 + ...ableProxy.sol => AdminUpgradableProxy.sol} | 0 src/v2/BaseTokenMessenger.sol | 26 +- src/v2/Initializable.sol | 200 ++++++++ src/v2/MessageTransmitterV2.sol | 77 +++- src/v2/TokenMessengerV2.sol | 70 ++- test/TestUtils.sol | 5 + .../mocks/MockInitializableImplementation.sol | 14 +- test/mocks/v2/MockMessageTransmitterV3.sol | 37 ++ test/mocks/v2/MockTokenMessengerV3.sol | 31 ++ ...Proxy.t.sol => AdminUpgradableProxy.t.sol} | 4 +- test/v2/Initializable.t.sol | 109 +++++ test/v2/MessageTransmitterV2.t.sol | 290 +++++++++++- test/v2/TokenMessengerV2.t.sol | 426 +++++++++++++++++- 16 files changed, 1283 insertions(+), 61 deletions(-) rename src/v2/{AdminUpgradeableProxy.sol => AdminUpgradableProxy.sol} (100%) create mode 100644 src/v2/Initializable.sol create mode 100644 test/mocks/v2/MockMessageTransmitterV3.sol create mode 100644 test/mocks/v2/MockTokenMessengerV3.sol rename test/v2/{AdminUpgradeableProxy.t.sol => AdminUpgradableProxy.t.sol} (98%) create mode 100644 test/v2/Initializable.t.sol diff --git a/src/roles/Attestable.sol b/src/roles/Attestable.sol index a88cf7e..42fed44 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,10 +122,9 @@ 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 - { + function updateAttesterManager( + address newAttesterManager + ) external onlyOwner { require( newAttesterManager != address(0), "Invalid attester manager address" @@ -167,10 +164,9 @@ contract Attestable is Ownable2Step { * of enabled attesters. * @param newSignatureThreshold new signature threshold */ - function setSignatureThreshold(uint256 newSignatureThreshold) - external - onlyAttesterManager - { + function setSignatureThreshold( + uint256 newSignatureThreshold + ) external onlyAttesterManager { require(newSignatureThreshold != 0, "Invalid signature threshold"); // New signature threshold cannot exceed the number of enabled attesters @@ -218,6 +214,17 @@ contract Attestable is Ownable2Step { _attesterManager = _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); + } + /** * @notice reverts if the attestation, which is comprised of one or more concatenated 65-byte signatures, is invalid. * @dev Rules for valid attestation: @@ -277,11 +284,10 @@ 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)); } } 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/v2/AdminUpgradeableProxy.sol b/src/v2/AdminUpgradableProxy.sol similarity index 100% rename from src/v2/AdminUpgradeableProxy.sol rename to src/v2/AdminUpgradableProxy.sol diff --git a/src/v2/BaseTokenMessenger.sol b/src/v2/BaseTokenMessenger.sol index 2733871..1f053b3 100644 --- a/src/v2/BaseTokenMessenger.sol +++ b/src/v2/BaseTokenMessenger.sol @@ -17,18 +17,18 @@ */ pragma solidity 0.7.6; -import {Ownable2Step} from "../roles/Ownable2Step.sol"; 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 "./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 { +abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable { // ============ Events ============ /** * @notice Emitted when a remote TokenMessenger is added @@ -205,9 +205,14 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable { * @param _feeRecipient Address of fee recipient */ function setFeeRecipient(address _feeRecipient) external onlyOwner { - require(_feeRecipient != address(0), "Zero address not allowed"); - feeRecipient = _feeRecipient; - emit FeeRecipientSet(_feeRecipient); + _setFeeRecipient(_feeRecipient); + } + + /** + * @dev Returns the current initialized version + */ + function initializedVersion() public view returns (uint64) { + return _getInitializedVersion(); } // ============ Internal Utils ============ @@ -319,4 +324,15 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable { 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); + } } diff --git a/src/v2/Initializable.sol b/src/v2/Initializable.sol new file mode 100644 index 0000000..81b440d --- /dev/null +++ b/src/v2/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/v2/MessageTransmitterV2.sol b/src/v2/MessageTransmitterV2.sol index ebd49a4..f59b9c9 100644 --- a/src/v2/MessageTransmitterV2.sol +++ b/src/v2/MessageTransmitterV2.sol @@ -25,15 +25,15 @@ 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 {Initializable} from "./Initializable.sol"; /** * @title MessageTransmitterV2 * @notice Contract responsible for sending and receiving messages across chains. */ -// TODO STABLE-6894 & STABLE-STABLE-7293: refactor inheritance -// as-needed to work with Proxy pattern. contract MessageTransmitterV2 is IMessageTransmitterV2, + Initializable, Pausable, Rescuable, Attestable @@ -92,17 +92,65 @@ contract MessageTransmitterV2 is mapping(bytes32 => uint256) public usedNonces; // ============ Constructor ============ - // TODO STABLE-6894 & STABLE-STABLE-7293: refactor constructor - // as-needed to work with Proxy pattern. - constructor( - uint32 _localDomain, - address _attester, - uint32 _maxMessageBodySize, - uint32 _version - ) Attestable(_attester) { + /** + * @param _localDomain Domain of chain on which the contract is deployed + * @param _version Message Format version + */ + constructor(uint32 _localDomain, uint32 _version) Attestable(msg.sender) { localDomain = _localDomain; version = _version; - maxMessageBodySize = _maxMessageBodySize; + + _disableInitializers(); + } + + // ============ Initializers ============ + /** + * @notice Initializes the contract + * @dev Addresses must be non-zero + * @dev Signature threshold must be greater than zero + * @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_ > 0, "Signature threshold is zero"); + require( + signatureThreshold_ <= attesters_.length, + "Signature threshold exceeds attesters" + ); + require(maxMessageBodySize_ > 0, "MaxMessageBodySize is zero"); + + // Roles + _transferOwnership(owner_); + _updateRescuer(rescuer_); + _updatePauser(pauser_); + _setAttesterManager(attesterManager_); + + // Settings + signatureThreshold = signatureThreshold_; + maxMessageBodySize = maxMessageBodySize_; + + // Attester configuration + for (uint256 i; i < attesters_.length; i++) { + _enableAttester(attesters_[i]); + } } // ============ External Functions ============ @@ -274,5 +322,10 @@ contract MessageTransmitterV2 is emit MaxMessageBodySizeUpdated(maxMessageBodySize); } - // ============ Internal Utils ============ + /** + * @dev Returns the current initialized version + */ + function initializedVersion() public view returns (uint64) { + return _getInitializedVersion(); + } } diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol index 2e368a6..4e451db 100644 --- a/src/v2/TokenMessengerV2.sol +++ b/src/v2/TokenMessengerV2.sol @@ -18,14 +18,13 @@ pragma solidity 0.7.6; import {BaseTokenMessenger} from "./BaseTokenMessenger.sol"; -import {ITokenMinter} from "../interfaces/ITokenMinter.sol"; -import {IMintBurnToken} from "../interfaces/IMintBurnToken.sol"; -import {BurnMessageV2} from "../messages/v2/BurnMessageV2.sol"; +import {ITokenMinterV2} from "../interfaces/v2/ITokenMinterV2.sol"; import {AddressUtils} from "../messages/v2/AddressUtils.sol"; -import {MessageTransmitterV2} from "./MessageTransmitterV2.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 {Initializable} from "./Initializable.sol"; /** * @title TokenMessengerV2 @@ -77,7 +76,66 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { constructor( address _messageTransmitter, uint32 _messageBodyVersion - ) BaseTokenMessenger(_messageTransmitter, _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_` is zero-length + * @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. + * @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(tokenMinter_ != address(0), "TokenMinter is the zero address"); + require( + remoteDomains_.length == remoteTokenMessengers_.length && + remoteDomains_.length > 0, + "Invalid remote domain configuration" + ); + + // Roles + _transferOwnership(owner_); + _updateRescuer(rescuer_); + _updateDenylister(denylister_); + _setFeeRecipient(feeRecipient_); + + localMinter = ITokenMinterV2(tokenMinter_); + + // Remote messenger configuration + for (uint256 i; i < remoteDomains_.length; i++) { + require( + remoteTokenMessengers_[i] != bytes32(0), + "Invalid TokenMessenger" + ); + uint32 _remoteDomain = remoteDomains_[i]; + bytes32 _remoteTokenMessenger = remoteTokenMessengers_[i]; + remoteTokenMessengers[_remoteDomain] = _remoteTokenMessenger; + } + } // ============ External Functions ============ /** @@ -300,7 +358,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { ); // Send message - MessageTransmitterV2(localMessageTransmitter).sendMessage( + IRelayerV2(localMessageTransmitter).sendMessage( _destinationDomain, _destinationTokenMessenger, _destinationCaller, diff --git a/test/TestUtils.sol b/test/TestUtils.sol index 8fd497a..0d618a1 100644 --- a/test/TestUtils.sol +++ b/test/TestUtils.sol @@ -188,6 +188,7 @@ contract TestUtils is Test { // Test updating the rescuer // (Updating rescuer to zero-address is not permitted) + vm.prank(_rescuableContract.owner()); vm.expectRevert("Rescuable: new rescuer is the zero address"); _rescuableContract.updateRescuer(address(0)); @@ -196,6 +197,7 @@ contract TestUtils is Test { // 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 @@ -423,6 +425,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); @@ -460,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); @@ -468,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 diff --git a/test/mocks/MockInitializableImplementation.sol b/test/mocks/MockInitializableImplementation.sol index d85b435..3115868 100644 --- a/test/mocks/MockInitializableImplementation.sol +++ b/test/mocks/MockInitializableImplementation.sol @@ -17,7 +17,7 @@ */ pragma solidity 0.7.6; -import {Initializable} from "@openzeppelin/contracts/proxy/Initializable.sol"; +import {Initializable} from "../../src/v2/Initializable.sol"; contract MockInitializableImplementation is Initializable { address public addr; @@ -27,4 +27,16 @@ contract MockInitializableImplementation is Initializable { addr = _addr; num = _num; } + + function initializeV2() external reinitializer(2) {} + + function initializeV3() external reinitializer(3) {} + + function disableInitializers() external { + _disableInitializers(); + } + + function initializedVersion() external view returns (uint64) { + return _getInitializedVersion(); + } } 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/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/v2/AdminUpgradeableProxy.t.sol b/test/v2/AdminUpgradableProxy.t.sol similarity index 98% rename from test/v2/AdminUpgradeableProxy.t.sol rename to test/v2/AdminUpgradableProxy.t.sol index 836ece8..5867e2a 100644 --- a/test/v2/AdminUpgradeableProxy.t.sol +++ b/test/v2/AdminUpgradableProxy.t.sol @@ -18,14 +18,14 @@ pragma solidity 0.7.6; pragma abicoder v2; -import {AdminUpgradableProxy} from "../../src/v2/AdminUpgradeableProxy.sol"; +import {AdminUpgradableProxy} from "../../src/v2/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 AdminUpgradeableProxyTest is Test { +contract AdminUpgradableProxyTest is Test { // Events event AdminChanged(address previousAdmin, address newAdmin); diff --git a/test/v2/Initializable.t.sol b/test/v2/Initializable.t.sol new file mode 100644 index 0000000..654192d --- /dev/null +++ b/test/v2/Initializable.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 {Initializable} from "../../src/v2/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); + + 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 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); + } +} diff --git a/test/v2/MessageTransmitterV2.t.sol b/test/v2/MessageTransmitterV2.t.sol index b41199f..1d1c7ab 100644 --- a/test/v2/MessageTransmitterV2.t.sol +++ b/test/v2/MessageTransmitterV2.t.sol @@ -25,6 +25,8 @@ 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/v2/AdminUpgradableProxy.sol"; +import {MockMessageTransmitterV3} from "../mocks/v2/MockMessageTransmitterV3.sol"; contract MessageTransmitterV2Test is TestUtils { event MessageSent(bytes message); @@ -40,6 +42,8 @@ contract MessageTransmitterV2Test is TestUtils { event MaxMessageBodySizeUpdated(uint256 newMaxMessageBodySize); + event Upgraded(address indexed implementation); + // ============ Libraries ============ using TypedMemView for bytes; using TypedMemView for bytes29; @@ -51,16 +55,286 @@ contract MessageTransmitterV2Test is TestUtils { 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 { - // message transmitter on source domain - messageTransmitter = new MessageTransmitterV2( + 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 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("Signature threshold is zero"); + 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 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, - attester, - maxMessageBodySize, 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 testSendMessage_revertsWhenPaused( @@ -76,6 +350,7 @@ contract MessageTransmitterV2Test is TestUtils { vm.assume(_pauser != address(0)); vm.assume(_destinationDomain != localDomain); + vm.prank(owner); messageTransmitter.updatePauser(_pauser); vm.prank(_pauser); @@ -181,6 +456,7 @@ contract MessageTransmitterV2Test is TestUtils { vm.assume(_pauser != address(0)); // Pause + vm.prank(owner); messageTransmitter.updatePauser(_pauser); vm.prank(_pauser); messageTransmitter.pause(); @@ -901,6 +1177,7 @@ contract MessageTransmitterV2Test is TestUtils { // Set new max size vm.expectEmit(true, true, true, true); emit MaxMessageBodySizeUpdated(_newMaxMessageBodySize); + vm.prank(owner); messageTransmitter.setMaxMessageBodySize(_newMaxMessageBodySize); assertEq( messageTransmitter.maxMessageBodySize(), @@ -929,6 +1206,7 @@ contract MessageTransmitterV2Test is TestUtils { address _nonOwner ) public { vm.assume(_currentPauser != address(0)); + vm.prank(owner); messageTransmitter.updatePauser(_currentPauser); assertContractIsPausable( @@ -1079,15 +1357,19 @@ contract MessageTransmitterV2Test is TestUtils { // 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( diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index c29e7a2..1ba9e6a 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -27,6 +27,8 @@ 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/v2/AdminUpgradableProxy.sol"; +import {MockTokenMessengerV3} from "../mocks/v2/MockTokenMessengerV3.sol"; contract TokenMessengerV2Test is BaseTokenMessengerTest { // Events @@ -50,6 +52,8 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint256 feeCollected ); + event Upgraded(address indexed implementation); + // Libraries using TypedMemView for bytes; using TypedMemView for bytes29; @@ -63,14 +67,18 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { address remoteMessageTransmitter = address(20); TokenMessengerV2 localTokenMessenger; + TokenMessengerV2 tokenMessengerImpl; - address remoteTokenMessageger = address(30); + 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); @@ -78,29 +86,38 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint32 immutable CONFIRMED_FINALITY_THRESHOLD = 1000; function setUp() public override { - // TokenMessenger under test - localTokenMessenger = new TokenMessengerV2( + // Deploy implementation + tokenMessengerImpl = new TokenMessengerV2( localMessageTransmitter, messageBodyVersion ); - // Add a local minter - localTokenMessenger.addLocalMinter(address(localTokenMinter)); - - // Update fee recipient - localTokenMessenger.setFeeRecipient(feeRecipient); - // Update denylister - localTokenMessenger.updateDenylister(denylister); + // Deploy and initialize proxy + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + bytes("") + ); remoteTokenMessengerAddr = AddressUtils.addressToBytes32( - remoteTokenMessageger + remoteTokenMessenger ); - // Register remote token messenger - localTokenMessenger.addRemoteTokenMessenger( - remoteDomain, - remoteTokenMessengerAddr + ( + 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, @@ -137,6 +154,368 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { // Tests + 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("TokenMinter is the zero address"); + 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("Invalid TokenMessenger"); + TokenMessengerV2(address(_proxy)).initialize( + owner, + rescuer, + feeRecipient, + denylister, + address(localTokenMinter), + _remoteDomains, + _remoteTokenMessengers + ); + } + + function testInitialize_revertsIfRemoteDomainsIsEmpty() public { + AdminUpgradableProxy _proxy = new AdminUpgradableProxy( + address(tokenMessengerImpl), + proxyAdmin, + bytes("") + ); + + uint32[] memory _remoteDomains = new uint32[](0); + bytes32[] memory _remoteTokenMessengers = new bytes32[](0); + + vm.expectRevert("Invalid remote domain configuration"); + 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 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, @@ -1064,6 +1443,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { assertTrue(address(localTokenMessenger.localMinter()) != address(0)); // Remove local minter + vm.prank(owner); localTokenMessenger.removeLocalMinter(); assertEq(address(localTokenMessenger.localMinter()), address(0)); @@ -1461,6 +1841,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { assertTrue(address(localTokenMessenger.localMinter()) != address(0)); // Remove local minter + vm.prank(owner); localTokenMessenger.removeLocalMinter(); assertEq(address(localTokenMessenger.localMinter()), address(0)); @@ -1745,6 +2126,21 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { // 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, From 7ff0deead6ad2f59d7d8c2633218c0d847d70d3a Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Fri, 18 Oct 2024 09:39:01 -0400 Subject: [PATCH 17/40] [No ticket] Add missing Initializable.sol tests (#32) ### Summary I ran `forge coverage` and noticed 2 tiny gaps. This PR fixes one of them. The other one is due to[ this guard statement](https://github.com/circlefin/evm-cctp-contracts-private/blob/40a2e35a518cad1664ebc94bb951c5b43f4f13c7/src/v2/AdminUpgradableProxy.sol#L71) never being hit; however, it's not actually possible to test that. However, all other files used in v2 have 100% coverage across all branches. The `Ownable` coverage is sorta misleading, since we don't use that contract directly, and instead via `Ownable2Step`. After this PR: `Ran 21 test suites in 650.11s (2864.13s CPU time): 415 tests passed, 0 failed, 0 skipped (415 total tests)` | File | % Lines | % Statements | % Branches | % Funcs | |------------------------------------------------|-------------------|--------------------|------------------|------------------| | scripts/v2/1_deploy.s.sol | 100.00% (53/53) | 100.00% (58/58) | 100.00% (0/0) | 100.00% (8/8) | | scripts/v2/2_setupSecondAttester.s.sol | 100.00% (12/12) | 100.00% (12/12) | 100.00% (0/0) | 66.67% (2/3) | | scripts/v2/3_setupRemoteResources.s.sol | 100.00% (20/20) | 100.00% (22/22) | 100.00% (0/0) | 100.00% (4/4) | | scripts/v2/4_rotateKeys.s.sol | 100.00% (27/27) | 100.00% (27/27) | 100.00% (0/0) | 40.00% (2/5) | | src/MessageTransmitter.sol | 100.00% (51/51) | 100.00% (70/70) | 100.00% (21/21) | 100.00% (9/9) | | src/TokenMessenger.sol | 100.00% (77/77) | 100.00% (95/95) | 100.00% (38/38) | 100.00% (18/18) | | src/TokenMinter.sol | 100.00% (22/22) | 100.00% (25/25) | 100.00% (12/12) | 100.00% (9/9) | | src/messages/BurnMessage.sol | 100.00% (9/9) | 100.00% (13/13) | 100.00% (4/4) | 100.00% (7/7) | | src/messages/Message.sol | 100.00% (15/15) | 100.00% (18/18) | 100.00% (4/4) | 100.00% (12/12) | | src/messages/v2/AddressUtils.sol | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (2/2) | | src/messages/v2/BurnMessageV2.sol | 100.00% (12/12) | 100.00% (20/20) | 100.00% (4/4) | 100.00% (10/10) | | src/messages/v2/MessageV2.sol | 100.00% (15/15) | 100.00% (19/19) | 100.00% (4/4) | 100.00% (12/12) | | src/roles/Attestable.sol | 100.00% (38/38) | 100.00% (46/46) | 100.00% (26/26) | 100.00% (14/14) | | src/roles/Ownable.sol | 77.78% (7/9) | 77.78% (7/9) | 50.00% (2/4) | 83.33% (5/6) | | src/roles/Ownable2Step.sol | 100.00% (8/8) | 100.00% (9/9) | 100.00% (2/2) | 100.00% (4/4) | | src/roles/Pausable.sol | 100.00% (11/11) | 100.00% (11/11) | 100.00% (6/6) | 100.00% (7/7) | | src/roles/Rescuable.sol | 100.00% (7/7) | 100.00% (7/7) | 100.00% (4/4) | 100.00% (5/5) | | src/roles/TokenController.sol | 100.00% (21/21) | 100.00% (25/25) | 100.00% (12/12) | 100.00% (9/9) | | src/roles/v2/Denylistable.sol | 100.00% (15/15) | 100.00% (16/16) | 100.00% (7/7) | 100.00% (9/9) | | src/v2/AdminUpgradableProxy.sol | 100.00% (16/16) | 100.00% (17/17) | 83.33% (5/6) | 100.00% (9/9) | | src/v2/BaseTokenMessenger.sol | 100.00% (47/47) | 100.00% (52/52) | 100.00% (28/28) | 100.00% (16/16) | | src/v2/Create2Factory.sol | 100.00% (4/4) | 100.00% (4/4) | 100.00% (0/0) | 100.00% (3/3) | | src/v2/Initializable.sol | 100.00% (29/29) | 100.00% (35/35) | 100.00% (11/11) | 100.00% (8/8) | | src/v2/MessageTransmitterV2.sol | 100.00% (45/45) | 100.00% (58/58) | 100.00% (31/31) | 100.00% (6/6) | | src/v2/TokenMessengerV2.sol | 100.00% (45/45) | 100.00% (52/52) | 100.00% (24/24) | 100.00% (9/9) | | src/v2/TokenMinterV2.sol | 100.00% (6/6) | 100.00% (7/7) | 100.00% (6/6) | 100.00% (2/2) | | test/TestUtils.sol | 100.00% (194/194) | 100.00% (211/211) | 100.00% (0/0) | 11.76% (2/17) | | test/mocks/MockInitializableImplementation.sol | 100.00% (5/5) | 100.00% (7/7) | 100.00% (0/0) | 100.00% (7/7) | | test/mocks/MockMintBurnToken.sol | 100.00% (31/31) | 100.00% (31/31) | 66.67% (8/12) | 100.00% (11/11) | | test/mocks/MockReentrantCaller.sol | 100.00% (12/12) | 100.00% (17/17) | 75.00% (3/4) | 100.00% (5/5) | | test/mocks/MockTokenMessenger.sol | 100.00% (4/4) | 100.00% (6/6) | 100.00% (3/3) | 100.00% (2/2) | | test/mocks/v2/MockDenylistable.sol | 100.00% (1/1) | 100.00% (1/1) | 100.00% (0/0) | 100.00% (1/1) | | test/mocks/v2/MockMessageTransmitterV3.sol | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (3/3) | | test/mocks/v2/MockProxyImplementation.sol | 75.00% (3/4) | 75.00% (3/4) | 100.00% (0/0) | 75.00% (3/4) | | test/mocks/v2/MockReentrantCallerV2.sol | 100.00% (2/2) | 100.00% (4/4) | 100.00% (0/0) | 100.00% (2/2) | | test/mocks/v2/MockTokenMessengerV3.sol | 100.00% (1/1) | 100.00% (1/1) | 100.00% (0/0) | 100.00% (2/2) | | test/scripts/ScriptV2TestUtils.sol | 100.00% (56/56) | 100.00% (60/60) | 100.00% (0/0) | 100.00% (4/4) | | Total | 99.68% (925/928) | 99.72% (1070/1073) | 97.07% (265/273) | 92.05% (243/264) | --- .../mocks/MockInitializableImplementation.sol | 6 ++ test/mocks/MockReentrantCaller.sol | 1 - test/v2/Initializable.t.sol | 60 +++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/test/mocks/MockInitializableImplementation.sol b/test/mocks/MockInitializableImplementation.sol index 3115868..ce0ee24 100644 --- a/test/mocks/MockInitializableImplementation.sol +++ b/test/mocks/MockInitializableImplementation.sol @@ -32,6 +32,8 @@ contract MockInitializableImplementation is Initializable { function initializeV3() external reinitializer(3) {} + function supportingInitializer() public view onlyInitializing {} + function disableInitializers() external { _disableInitializers(); } @@ -39,4 +41,8 @@ contract MockInitializableImplementation is Initializable { 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 6342ff8..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 { diff --git a/test/v2/Initializable.t.sol b/test/v2/Initializable.t.sol index 654192d..c1c2a07 100644 --- a/test/v2/Initializable.t.sol +++ b/test/v2/Initializable.t.sol @@ -27,6 +27,14 @@ contract InitializableTest is Test { event Initialized(uint64 version); + struct InitializableStorage { + uint64 _initialized; + bool _initializing; + } + + bytes32 private constant INITIALIZABLE_STORAGE = + 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + function setUp() public { impl = new MockInitializableImplementation(); } @@ -96,6 +104,17 @@ contract InitializableTest is Test { 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); @@ -106,4 +125,45 @@ contract InitializableTest is Test { 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) + ); + } } From 940b15c9f0079e93ef37926154144cfc5c3843d3 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Fri, 18 Oct 2024 09:40:34 -0400 Subject: [PATCH 18/40] [No ticket] Update DenylisterUpdated event to take additional argument (#33) ### Summary See discussion here: https://circlefin.slack.com/archives/C07L385TX7B/p1729099569014349 We'd like the ABIs across the projects to be identical. --- src/roles/v2/Denylistable.sol | 9 +++++++-- test/roles/v2/Denylistable.t.sol | 7 +++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/roles/v2/Denylistable.sol b/src/roles/v2/Denylistable.sol index 87685a5..c9d3c68 100644 --- a/src/roles/v2/Denylistable.sol +++ b/src/roles/v2/Denylistable.sol @@ -27,9 +27,13 @@ abstract contract Denylistable is Ownable2Step { // ============ Events ============ /** * @notice Emitted when the Denylister is updated + * @param oldDenylister address of the previous Denylister * @param newDenylister address of new Denylister */ - event DenylisterChanged(address indexed newDenylister); + event DenylisterChanged( + address indexed oldDenylister, + address indexed newDenylister + ); /** * @notice Emitted when `account` is added to the denylist @@ -137,8 +141,9 @@ abstract contract Denylistable is Ownable2Step { _newDenylister != address(0), "Denylistable: new denylister is the zero address" ); + address _oldDenylister = _denylister; _denylister = _newDenylister; - emit DenylisterChanged(_newDenylister); + emit DenylisterChanged(_oldDenylister, _newDenylister); } /** diff --git a/test/roles/v2/Denylistable.t.sol b/test/roles/v2/Denylistable.t.sol index e480831..bdd0d03 100644 --- a/test/roles/v2/Denylistable.t.sol +++ b/test/roles/v2/Denylistable.t.sol @@ -23,7 +23,10 @@ import {Test} from "forge-std/Test.sol"; contract DenylistableTest is Test { // Test events - event DenylisterChanged(address indexed newDenylister); + event DenylisterChanged( + address indexed oldDenylister, + address indexed newDenylister + ); event Denylisted(address indexed account); event UnDenylisted(address indexed account); @@ -68,7 +71,7 @@ contract DenylistableTest is Test { vm.assume(_newDenylister != address(0)); vm.expectEmit(true, true, true, true); - emit DenylisterChanged(_newDenylister); + emit DenylisterChanged(denylister, _newDenylister); vm.prank(owner); denylistable.updateDenylister(_newDenylister); From 4b918dbebe9b2868658df870725f4e0c3cbafc19 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Sun, 20 Oct 2024 20:31:27 -0400 Subject: [PATCH 19/40] [No ticket] Allow passing no remote token messengers on initialization (#35) --- src/v2/TokenMessengerV2.sol | 3 +-- test/v2/TokenMessengerV2.t.sol | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol index 4e451db..4fc5373 100644 --- a/src/v2/TokenMessengerV2.sol +++ b/src/v2/TokenMessengerV2.sol @@ -112,8 +112,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { require(owner_ != address(0), "Owner is the zero address"); require(tokenMinter_ != address(0), "TokenMinter is the zero address"); require( - remoteDomains_.length == remoteTokenMessengers_.length && - remoteDomains_.length > 0, + remoteDomains_.length == remoteTokenMessengers_.length, "Invalid remote domain configuration" ); diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index 1ba9e6a..ee346c0 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -334,7 +334,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); } - function testInitialize_revertsIfRemoteDomainsIsEmpty() public { + function testInitialize_succeedsIfRemoteDomainsIsEmpty() public { AdminUpgradableProxy _proxy = new AdminUpgradableProxy( address(tokenMessengerImpl), proxyAdmin, @@ -344,7 +344,6 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint32[] memory _remoteDomains = new uint32[](0); bytes32[] memory _remoteTokenMessengers = new bytes32[](0); - vm.expectRevert("Invalid remote domain configuration"); TokenMessengerV2(address(_proxy)).initialize( owner, rescuer, From c0ac86b7af3fb7d0b7659a806ab9845f87a7a4a8 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Thu, 24 Oct 2024 10:34:26 -0400 Subject: [PATCH 20/40] STABLE-7553: Add e2e cross-chain transfer test for v2 (#36) --- .github/workflows/ci.yml | 3 + Makefile | 6 +- anvil/crosschainTransferITV2.py | 355 +++++++++++++++++++++++++++++++ requirements.txt | 2 +- src/messages/v2/AddressUtils.sol | 4 +- 5 files changed, 366 insertions(+), 4 deletions(-) create mode 100644 anvil/crosschainTransferITV2.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65dee9b..c243576 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,9 @@ jobs: - name: Run Integration Tests run: make anvil-test + - name: Run v2 Integration Tests + run: make anvil-test-v2 + - name: Run Slither uses: crytic/slither-action@v0.3.0 with: diff --git a/Makefile b/Makefile index 79e01b0..a146992 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,11 @@ anvil: 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 diff --git a/anvil/crosschainTransferITV2.py b/anvil/crosschainTransferITV2.py new file mode 100644 index 0000000..963d57f --- /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/v2/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/v2/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/v2/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/v2/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/requirements.txt b/requirements.txt index 5d92b31..e65806c 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 diff --git a/src/messages/v2/AddressUtils.sol b/src/messages/v2/AddressUtils.sol index 70bed18..afa5a1c 100644 --- a/src/messages/v2/AddressUtils.sol +++ b/src/messages/v2/AddressUtils.sol @@ -26,7 +26,7 @@ library AddressUtils { * @notice converts address to bytes32 (alignment preserving cast.) * @param addr the address to convert to bytes32 */ - function addressToBytes32(address addr) external pure returns (bytes32) { + function addressToBytes32(address addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(addr))); } @@ -36,7 +36,7 @@ library AddressUtils { * 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) public pure returns (address) { + function bytes32ToAddress(bytes32 _buf) internal pure returns (address) { return address(uint160(uint256(_buf))); } } From 5de4cdfdd74701d91afd74f3691e55bdf48819ca Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:20:48 -0400 Subject: [PATCH 21/40] [No ticket] [Looking for feedback] Small cleanups to the repo (#34) --- src/{v2 => proxy}/AdminUpgradableProxy.sol | 0 src/{v2 => proxy}/Initializable.sol | 0 src/v2/BaseMessageTransmitter.sol | 91 +++++++++ src/v2/BaseTokenMessenger.sol | 2 +- src/v2/Constants.sol | 22 +++ src/v2/MessageTransmitterV2.sol | 173 ++++++++---------- src/v2/TokenMessengerV2.sol | 7 +- .../mocks/MockInitializableImplementation.sol | 2 +- test/v2/AdminUpgradableProxy.t.sol | 2 +- test/v2/Initializable.t.sol | 2 +- test/v2/MessageTransmitterV2.t.sol | 36 +--- test/v2/TokenMessengerV2.t.sol | 59 ++++-- 12 files changed, 249 insertions(+), 147 deletions(-) rename src/{v2 => proxy}/AdminUpgradableProxy.sol (100%) rename src/{v2 => proxy}/Initializable.sol (100%) create mode 100644 src/v2/BaseMessageTransmitter.sol create mode 100644 src/v2/Constants.sol diff --git a/src/v2/AdminUpgradableProxy.sol b/src/proxy/AdminUpgradableProxy.sol similarity index 100% rename from src/v2/AdminUpgradableProxy.sol rename to src/proxy/AdminUpgradableProxy.sol diff --git a/src/v2/Initializable.sol b/src/proxy/Initializable.sol similarity index 100% rename from src/v2/Initializable.sol rename to src/proxy/Initializable.sol diff --git a/src/v2/BaseMessageTransmitter.sol b/src/v2/BaseMessageTransmitter.sol new file mode 100644 index 0000000..3fde443 --- /dev/null +++ b/src/v2/BaseMessageTransmitter.sol @@ -0,0 +1,91 @@ +/* + * 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 {Attestable} from "../roles/Attestable.sol"; +import {Pausable} from "../roles/Pausable.sol"; +import {Rescuable} from "../roles/Rescuable.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 {Initializable} from "../proxy/Initializable.sol"; + +/** + * @title BaseMessageTransmitter + * @notice Base MessageTransmitter implementation, focused on administrative actions. + */ +contract BaseMessageTransmitter is + Initializable, + Pausable, + Rescuable, + Attestable +{ + // ============ Events ============ + /** + * @notice Emitted when max message body size is updated + * @param newMaxMessageBodySize new maximum message body size, in bytes + */ + event MaxMessageBodySizeUpdated(uint256 newMaxMessageBodySize); + + // ============ 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) Attestable(msg.sender) { + 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 { + maxMessageBodySize = newMaxMessageBodySize; + emit MaxMessageBodySizeUpdated(maxMessageBodySize); + } + + /** + * @dev Returns the current initialized version + */ + function initializedVersion() public view returns (uint64) { + return _getInitializedVersion(); + } +} diff --git a/src/v2/BaseTokenMessenger.sol b/src/v2/BaseTokenMessenger.sol index 1f053b3..e2a3e75 100644 --- a/src/v2/BaseTokenMessenger.sol +++ b/src/v2/BaseTokenMessenger.sol @@ -21,7 +21,7 @@ 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 "./Initializable.sol"; +import {Initializable} from "../proxy/Initializable.sol"; /** * @title BaseTokenMessenger diff --git a/src/v2/Constants.sol b/src/v2/Constants.sol new file mode 100644 index 0000000..fc833aa --- /dev/null +++ b/src/v2/Constants.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; + +// Finality Thresholds + +uint32 constant FINALITY_THRESHOLD_FINALIZED = 2000; diff --git a/src/v2/MessageTransmitterV2.sol b/src/v2/MessageTransmitterV2.sol index f59b9c9..3be9bca 100644 --- a/src/v2/MessageTransmitterV2.sol +++ b/src/v2/MessageTransmitterV2.sol @@ -18,26 +18,18 @@ pragma solidity 0.7.6; import {IMessageTransmitterV2} from "../interfaces/v2/IMessageTransmitterV2.sol"; -import {Attestable} from "../roles/Attestable.sol"; -import {Pausable} from "../roles/Pausable.sol"; -import {Rescuable} from "../roles/Rescuable.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 {Initializable} from "./Initializable.sol"; +import {FINALITY_THRESHOLD_FINALIZED} from "./Constants.sol"; /** * @title MessageTransmitterV2 * @notice Contract responsible for sending and receiving messages across chains. */ -contract MessageTransmitterV2 is - IMessageTransmitterV2, - Initializable, - Pausable, - Rescuable, - Attestable -{ +contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { // ============ Events ============ /** * @notice Emitted when a new message is dispatched @@ -63,43 +55,20 @@ contract MessageTransmitterV2 is bytes messageBody ); - /** - * @notice Emitted when max message body size is updated - * @param newMaxMessageBodySize new maximum message body size, in bytes - */ - event MaxMessageBodySizeUpdated(uint256 newMaxMessageBodySize); - // ============ Libraries ============ using TypedMemView for bytes; using TypedMemView for bytes29; using MessageV2 for bytes29; - // ============ State Variables ============ - // Domain of chain on which the contract is deployed - uint32 public immutable localDomain; - - // Message Format version - uint32 public immutable version; - - // The threshold at which messages are considered finalized - uint32 public immutable finalizedMessageThreshold = 2000; - - // 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) Attestable(msg.sender) { - localDomain = _localDomain; - version = _version; - + constructor( + uint32 _localDomain, + uint32 _version + ) BaseMessageTransmitter(_localDomain, _version) { _disableInitializers(); } @@ -231,50 +200,21 @@ contract MessageTransmitterV2 is bytes calldata message, bytes calldata attestation ) external override whenNotPaused returns (bool success) { - // 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() == - AddressUtils.addressToBytes32(msg.sender), - "Invalid caller for message" - ); - } - - // Validate version - require(_msg._getVersion() == version, "Invalid message version"); - - // Validate nonce is available - bytes32 _nonce = _msg._getNonce(); - require(usedNonces[_nonce] == 0, "Nonce already used"); - // Mark nonce used + // Validate message + ( + bytes32 _nonce, + uint32 _sourceDomain, + bytes32 _sender, + address _recipient, + uint32 _finalityThresholdExecuted, + bytes memory _messageBody + ) = _validateReceivedMessage(message, attestation); + + // Mark nonce as used usedNonces[_nonce] = 1; - // Unpack remaining values - uint32 _sourceDomain = _msg._getSourceDomain(); - bytes32 _sender = _msg._getSender(); - address _recipient = AddressUtils.bytes32ToAddress( - _msg._getRecipient() - ); - uint32 _finalityThresholdExecuted = _msg - ._getFinalityThresholdExecuted(); - bytes memory _messageBody = _msg._getMessageBody().clone(); - // Handle receive message - if (_finalityThresholdExecuted < finalizedMessageThreshold) { + if (_finalityThresholdExecuted < FINALITY_THRESHOLD_FINALIZED) { require( IMessageHandlerV2(_recipient).handleReceiveUnfinalizedMessage( _sourceDomain, @@ -310,22 +250,67 @@ contract MessageTransmitterV2 is } /** - * @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 + * @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 messagebody */ - function setMaxMessageBodySize( - uint256 newMaxMessageBodySize - ) external onlyOwner { - maxMessageBodySize = newMaxMessageBodySize; - emit MaxMessageBodySizeUpdated(maxMessageBodySize); - } + 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); - /** - * @dev Returns the current initialized version - */ - function initializedVersion() public view returns (uint64) { - return _getInitializedVersion(); + 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() == + AddressUtils.addressToBytes32(msg.sender), + "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 = AddressUtils.bytes32ToAddress(_msg._getRecipient()); + _finalityThresholdExecuted = _msg._getFinalityThresholdExecuted(); + _messageBody = _msg._getMessageBody().clone(); } } diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol index 4fc5373..6257f87 100644 --- a/src/v2/TokenMessengerV2.sol +++ b/src/v2/TokenMessengerV2.sol @@ -24,7 +24,7 @@ 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 {Initializable} from "./Initializable.sol"; +import {Initializable} from "../proxy/Initializable.sol"; /** * @title TokenMessengerV2 @@ -66,7 +66,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { using BurnMessageV2 for bytes29; // ============ State Variables ============ - uint32 public immutable MINIMUM_SUPPORTED_FINALITY_THRESHOLD = 1000; + uint32 public constant minFinalityThresholdSupported = 500; // ============ Constructor ============ /** @@ -286,9 +286,8 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { onlyRemoteTokenMessenger(remoteDomain, sender) returns (bool) { - // Require minimum supported finality threshold require( - finalityThresholdExecuted >= MINIMUM_SUPPORTED_FINALITY_THRESHOLD, + finalityThresholdExecuted >= minFinalityThresholdSupported, "Unsupported finality threshold" ); diff --git a/test/mocks/MockInitializableImplementation.sol b/test/mocks/MockInitializableImplementation.sol index ce0ee24..750c333 100644 --- a/test/mocks/MockInitializableImplementation.sol +++ b/test/mocks/MockInitializableImplementation.sol @@ -17,7 +17,7 @@ */ pragma solidity 0.7.6; -import {Initializable} from "../../src/v2/Initializable.sol"; +import {Initializable} from "../../src/proxy/Initializable.sol"; contract MockInitializableImplementation is Initializable { address public addr; diff --git a/test/v2/AdminUpgradableProxy.t.sol b/test/v2/AdminUpgradableProxy.t.sol index 5867e2a..348ae8e 100644 --- a/test/v2/AdminUpgradableProxy.t.sol +++ b/test/v2/AdminUpgradableProxy.t.sol @@ -18,7 +18,7 @@ pragma solidity 0.7.6; pragma abicoder v2; -import {AdminUpgradableProxy} from "../../src/v2/AdminUpgradableProxy.sol"; +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"; diff --git a/test/v2/Initializable.t.sol b/test/v2/Initializable.t.sol index c1c2a07..a202f9f 100644 --- a/test/v2/Initializable.t.sol +++ b/test/v2/Initializable.t.sol @@ -18,7 +18,7 @@ pragma solidity 0.7.6; pragma abicoder v2; -import {Initializable} from "../../src/v2/Initializable.sol"; +import {Initializable} from "../../src/proxy/Initializable.sol"; import {MockInitializableImplementation} from "../mocks/MockInitializableImplementation.sol"; import {Test} from "forge-std/Test.sol"; diff --git a/test/v2/MessageTransmitterV2.t.sol b/test/v2/MessageTransmitterV2.t.sol index 1d1c7ab..a3f6fe5 100644 --- a/test/v2/MessageTransmitterV2.t.sol +++ b/test/v2/MessageTransmitterV2.t.sol @@ -25,8 +25,9 @@ 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/v2/AdminUpgradableProxy.sol"; +import {AdminUpgradableProxy} from "../../src/proxy/AdminUpgradableProxy.sol"; import {MockMessageTransmitterV3} from "../mocks/v2/MockMessageTransmitterV3.sol"; +import {FINALITY_THRESHOLD_FINALIZED} from "../../src/v2/Constants.sol"; contract MessageTransmitterV2Test is TestUtils { event MessageSent(bytes message); @@ -724,10 +725,7 @@ contract MessageTransmitterV2Test is TestUtils { uint32 _finalityThresholdExecuted, bytes calldata _messageBody ) public { - vm.assume( - _finalityThresholdExecuted >= - messageTransmitter.finalizedMessageThreshold() - ); + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); bytes memory _message = _formatMessageForReceive( version, @@ -768,10 +766,7 @@ contract MessageTransmitterV2Test is TestUtils { uint32 _finalityThresholdExecuted, bytes calldata _messageBody ) public { - vm.assume( - _finalityThresholdExecuted < - messageTransmitter.finalizedMessageThreshold() - ); + vm.assume(_finalityThresholdExecuted < FINALITY_THRESHOLD_FINALIZED); bytes memory _message = _formatMessageForReceive( version, @@ -812,10 +807,7 @@ contract MessageTransmitterV2Test is TestUtils { uint32 _finalityThresholdExecuted, bytes calldata _messageBody ) public { - vm.assume( - _finalityThresholdExecuted >= - messageTransmitter.finalizedMessageThreshold() - ); + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); vm.assume(_recipient != foundryCheatCodeAddr); bytes memory _message = _formatMessageForReceive( @@ -858,10 +850,7 @@ contract MessageTransmitterV2Test is TestUtils { uint32 _finalityThresholdExecuted, bytes calldata _messageBody ) public { - vm.assume( - _finalityThresholdExecuted < - messageTransmitter.finalizedMessageThreshold() - ); + vm.assume(_finalityThresholdExecuted < FINALITY_THRESHOLD_FINALIZED); vm.assume(_recipient != foundryCheatCodeAddr); bytes memory _message = _formatMessageForReceive( @@ -1055,10 +1044,7 @@ contract MessageTransmitterV2Test is TestUtils { bytes calldata _messageBody, address _randomCaller ) public { - vm.assume( - _finalityThresholdExecuted >= - messageTransmitter.finalizedMessageThreshold() - ); + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); bytes memory _message = _formatMessageForReceive( version, _sourceDomain, @@ -1086,10 +1072,7 @@ contract MessageTransmitterV2Test is TestUtils { bytes calldata _messageBody, address _randomCaller ) public { - vm.assume( - _finalityThresholdExecuted < - messageTransmitter.finalizedMessageThreshold() - ); + vm.assume(_finalityThresholdExecuted < FINALITY_THRESHOLD_FINALIZED); bytes memory _message = _formatMessageForReceive( version, _sourceDomain, @@ -1311,8 +1294,7 @@ contract MessageTransmitterV2Test is TestUtils { // Mock a successful response from IMessageHandlerV2 to message.recipient, // and expect it to be called once. bytes memory _encodedMessageHandlerCall = abi.encodeWithSelector( - _msg._getFinalityThresholdExecuted() >= - messageTransmitter.finalizedMessageThreshold() + _msg._getFinalityThresholdExecuted() >= FINALITY_THRESHOLD_FINALIZED ? IMessageHandlerV2.handleReceiveFinalizedMessage.selector : IMessageHandlerV2.handleReceiveUnfinalizedMessage.selector, _msg._getSourceDomain(), diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index ee346c0..a80b526 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -27,7 +27,7 @@ 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/v2/AdminUpgradableProxy.sol"; +import {AdminUpgradableProxy} from "../../src/proxy/AdminUpgradableProxy.sol"; import {MockTokenMessengerV3} from "../mocks/v2/MockTokenMessengerV3.sol"; contract TokenMessengerV2Test is BaseTokenMessengerTest { @@ -62,6 +62,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { // Constants uint32 remoteDomain = 1; uint32 messageBodyVersion = 2; + uint32 confirmedFinalityThreshold = 1000; address localMessageTransmitter = address(10); address remoteMessageTransmitter = address(20); @@ -83,8 +84,6 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { MockMintBurnToken localToken = new MockMintBurnToken(); TokenMinterV2 localTokenMinter = new TokenMinterV2(tokenController); - uint32 immutable CONFIRMED_FINALITY_THRESHOLD = 1000; - function setUp() public override { // Deploy implementation tokenMessengerImpl = new TokenMessengerV2( @@ -1603,7 +1602,10 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint32 _finalityThresholdExecuted, bytes calldata _messageBody ) public { - vm.assume(_finalityThresholdExecuted < CONFIRMED_FINALITY_THRESHOLD); + vm.assume( + _finalityThresholdExecuted < + localTokenMessenger.minFinalityThresholdSupported() + ); vm.prank(localMessageTransmitter); vm.expectRevert("Unsupported finality threshold"); @@ -1621,7 +1623,10 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ) public { // See: BurnMessageV2#HOOK_DATA_INDEX vm.assume(_messageBody.length < 228); - vm.assume(_finalityThresholdExecuted >= CONFIRMED_FINALITY_THRESHOLD); + vm.assume( + _finalityThresholdExecuted >= + localTokenMessenger.minFinalityThresholdSupported() + ); vm.prank(localMessageTransmitter); vm.expectRevert("Invalid message: too short"); @@ -1644,7 +1649,10 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint32 _finalityThresholdExecuted ) public { vm.assume(_version != localTokenMessenger.messageBodyVersion()); - vm.assume(_finalityThresholdExecuted >= CONFIRMED_FINALITY_THRESHOLD); + vm.assume( + _finalityThresholdExecuted >= + localTokenMessenger.minFinalityThresholdSupported() + ); bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( _version, @@ -1700,7 +1708,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { localTokenMessenger.handleReceiveUnfinalizedMessage( remoteDomain, remoteTokenMessengerAddr, - CONFIRMED_FINALITY_THRESHOLD, + confirmedFinalityThreshold, _messageBody ); } @@ -1738,7 +1746,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { localTokenMessenger.handleReceiveUnfinalizedMessage( remoteDomain, remoteTokenMessengerAddr, - CONFIRMED_FINALITY_THRESHOLD, + confirmedFinalityThreshold, _messageBody ); } @@ -1753,7 +1761,10 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes calldata _hookData, uint32 _finalityThresholdExecuted ) public { - vm.assume(_finalityThresholdExecuted >= CONFIRMED_FINALITY_THRESHOLD); + vm.assume( + _finalityThresholdExecuted >= + localTokenMessenger.minFinalityThresholdSupported() + ); vm.assume(_feeExecuted > _amount); bytes memory _messageBody = _formatBurnMessageForReceive( @@ -1787,7 +1798,10 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes calldata _hookData, uint32 _finalityThresholdExecuted ) public { - vm.assume(_finalityThresholdExecuted >= CONFIRMED_FINALITY_THRESHOLD); + vm.assume( + _finalityThresholdExecuted >= + localTokenMessenger.minFinalityThresholdSupported() + ); vm.assume(_amount > 0); bytes memory _messageBody = _formatBurnMessageForReceive( @@ -1822,7 +1836,10 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes calldata _hookData, uint32 _finalityThresholdExecuted ) public { - vm.assume(_finalityThresholdExecuted >= CONFIRMED_FINALITY_THRESHOLD); + vm.assume( + _finalityThresholdExecuted >= + localTokenMessenger.minFinalityThresholdSupported() + ); vm.assume(_feeExecuted < _amount); bytes memory _messageBody = _formatBurnMessageForReceive( @@ -1864,7 +1881,10 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes calldata _hookData, uint32 _finalityThresholdExecuted ) public { - vm.assume(_finalityThresholdExecuted >= CONFIRMED_FINALITY_THRESHOLD); + vm.assume( + _finalityThresholdExecuted >= + localTokenMessenger.minFinalityThresholdSupported() + ); bytes memory _messageBody = _formatBurnMessageForReceive( localTokenMessenger.messageBodyVersion(), @@ -1912,7 +1932,10 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes calldata _hookData, uint32 _finalityThresholdExecuted ) public { - vm.assume(_finalityThresholdExecuted >= CONFIRMED_FINALITY_THRESHOLD); + vm.assume( + _finalityThresholdExecuted >= + localTokenMessenger.minFinalityThresholdSupported() + ); vm.assume(_feeExecuted < _amount); vm.assume(_feeExecuted > 0); @@ -1980,7 +2003,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _handleReceiveMessage( remoteDomain, remoteTokenMessengerAddr, - CONFIRMED_FINALITY_THRESHOLD, + confirmedFinalityThreshold, _messageBody ); } @@ -2016,7 +2039,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _handleReceiveMessage( remoteDomain, remoteTokenMessengerAddr, - CONFIRMED_FINALITY_THRESHOLD, + confirmedFinalityThreshold, _messageBody ); } @@ -2047,7 +2070,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _handleReceiveMessage( remoteDomain, remoteTokenMessengerAddr, - CONFIRMED_FINALITY_THRESHOLD, + confirmedFinalityThreshold, _messageBody ); } @@ -2083,7 +2106,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _handleReceiveMessage( remoteDomain, remoteTokenMessengerAddr, - CONFIRMED_FINALITY_THRESHOLD, + confirmedFinalityThreshold, _messageBody ); } @@ -2118,7 +2141,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _handleReceiveMessage( remoteDomain, remoteTokenMessengerAddr, - CONFIRMED_FINALITY_THRESHOLD, + confirmedFinalityThreshold, _messageBody ); } From 813d9bc1778fda1456baf4312ed436007a0d52fc Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:24:34 -0400 Subject: [PATCH 22/40] [No ticket] Fix AdminUpgradability file path in v2 integration test (#39) --- anvil/crosschainTransferITV2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/anvil/crosschainTransferITV2.py b/anvil/crosschainTransferITV2.py index 963d57f..ac79b57 100644 --- a/anvil/crosschainTransferITV2.py +++ b/anvil/crosschainTransferITV2.py @@ -196,7 +196,7 @@ def setUp(self): # 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/v2/AdminUpgradableProxy.sol', 'AdminUpgradableProxy', + 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, @@ -219,7 +219,7 @@ def setUp(self): # 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/v2/AdminUpgradableProxy.sol', 'AdminUpgradableProxy', + 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, @@ -238,7 +238,7 @@ def setUp(self): # 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/v2/AdminUpgradableProxy.sol', 'AdminUpgradableProxy', + 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, @@ -257,7 +257,7 @@ def setUp(self): 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/v2/AdminUpgradableProxy.sol', 'AdminUpgradableProxy', + 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, From d4a40231b448b3478f9e2bff964ed152ed0e6f21 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:50:38 -0400 Subject: [PATCH 23/40] [No ticket] Add AddressUtilsExternal library with external functions (#40) --- src/messages/v2/AddressUtilsExternal.sol | 42 +++++++++++++++++++++ test/messages/v2/AddressUtilsExternal.t.sol | 41 ++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/messages/v2/AddressUtilsExternal.sol create mode 100644 test/messages/v2/AddressUtilsExternal.t.sol 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/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); + } + } +} From 0790159cf8c80cb27c071b817431a6d8ee6b0ea8 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Thu, 24 Oct 2024 19:57:51 -0400 Subject: [PATCH 24/40] [STABLE-7280] Natspec updates; refactored the finality thresholds to a dedicated file (#41) --- src/interfaces/v2/IMessageHandlerV2.sol | 26 +++++----- src/interfaces/v2/IMessageTransmitterV2.sol | 2 +- src/interfaces/v2/IReceiverV2.sol | 3 +- src/interfaces/v2/IRelayerV2.sol | 8 ++-- src/interfaces/v2/ITokenMinterV2.sol | 6 +-- src/messages/v2/AddressUtils.sol | 6 +-- src/messages/v2/BurnMessageV2.sol | 22 ++++----- src/roles/v2/Denylistable.sol | 12 ++--- src/v2/BaseMessageTransmitter.sol | 4 +- src/v2/BaseTokenMessenger.sol | 2 +- .../{Constants.sol => FinalityThresholds.sol} | 9 +++- src/v2/MessageTransmitterV2.sol | 17 ++++--- src/v2/TokenMessengerV2.sol | 47 +++++++++---------- test/v2/MessageTransmitterV2.t.sol | 2 +- test/v2/TokenMessengerV2.t.sol | 44 +++++++---------- 15 files changed, 102 insertions(+), 108 deletions(-) rename src/v2/{Constants.sol => FinalityThresholds.sol} (69%) diff --git a/src/interfaces/v2/IMessageHandlerV2.sol b/src/interfaces/v2/IMessageHandlerV2.sol index 33e3838..64f9c54 100644 --- a/src/interfaces/v2/IMessageHandlerV2.sol +++ b/src/interfaces/v2/IMessageHandlerV2.sol @@ -19,18 +19,18 @@ pragma solidity 0.7.6; /** * @title IMessageHandlerV2 - * @notice Handles messages on destination domain forwarded from - * an IReceiverV2 + * @notice Handles messages on the destination domain, forwarded from + * an IReceiverV2. */ interface IMessageHandlerV2 { /** - * @notice handles an incoming finalized message from a Receiver + * @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 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 message raw bytes - * @return success bool, true if successful + * @param messageBody The raw bytes of the message body + * @return success True, if successful; false, if not. */ function handleReceiveFinalizedMessage( uint32 sourceDomain, @@ -40,13 +40,13 @@ interface IMessageHandlerV2 { ) external returns (bool); /** - * @notice handles an incoming unfinalized message from a Receiver + * @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 (sub)finality threshold at which the message was attested to - * @param messageBody The message raw bytes - * @return success bool, true if successful + * @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, diff --git a/src/interfaces/v2/IMessageTransmitterV2.sol b/src/interfaces/v2/IMessageTransmitterV2.sol index 4c6143c..36b1bbc 100644 --- a/src/interfaces/v2/IMessageTransmitterV2.sol +++ b/src/interfaces/v2/IMessageTransmitterV2.sol @@ -22,7 +22,7 @@ import {IRelayerV2} from "./IRelayerV2.sol"; /** * @title IMessageTransmitterV2 - * @notice Interface for message transmitters, which both relay and receive messages. + * @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 index b9a95bd..d9c8295 100644 --- a/src/interfaces/v2/IReceiverV2.sol +++ b/src/interfaces/v2/IReceiverV2.sol @@ -21,7 +21,8 @@ import {IReceiver} from "../IReceiver.sol"; /** * @title IReceiverV2 - * @notice Receives messages on destination chain and forwards them to IMessageDestinationHandler + * @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 index 5cb0add..a498786 100644 --- a/src/interfaces/v2/IRelayerV2.sol +++ b/src/interfaces/v2/IRelayerV2.sol @@ -19,7 +19,7 @@ pragma solidity 0.7.6; /** * @title IRelayerV2 - * @notice Sends messages from source domain to destination domain + * @notice Sends messages from the source domain to the destination domain */ interface IRelayerV2 { /** @@ -30,9 +30,9 @@ interface IRelayerV2 { * 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 Caller on destination domain. - * @param minFinalityThreshold Minimum finality threshold requested. - * @param messageBody Raw bytes content of message + * @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, diff --git a/src/interfaces/v2/ITokenMinterV2.sol b/src/interfaces/v2/ITokenMinterV2.sol index a667905..0e3164a 100644 --- a/src/interfaces/v2/ITokenMinterV2.sol +++ b/src/interfaces/v2/ITokenMinterV2.sol @@ -21,12 +21,12 @@ import {ITokenMinter} from "../ITokenMinter.sol"; /** * @title ITokenMinterV2 - * @notice interface for minter of tokens that are mintable, burnable, and interchangeable + * @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 local tokens corresponding to the + * @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. @@ -34,7 +34,7 @@ interface ITokenMinterV2 is ITokenMinter { * @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. + * @return mintToken Address of the token that was minted, corresponding to the (`sourceDomain`, `burnToken`) pair */ function mint( uint32 sourceDomain, diff --git a/src/messages/v2/AddressUtils.sol b/src/messages/v2/AddressUtils.sol index afa5a1c..1956573 100644 --- a/src/messages/v2/AddressUtils.sol +++ b/src/messages/v2/AddressUtils.sol @@ -23,15 +23,15 @@ pragma solidity 0.7.6; **/ library AddressUtils { /** - * @notice converts address to bytes32 (alignment preserving cast.) - * @param addr the address to convert to bytes32 + * @notice Converts an address to bytes32 by left-padding with zeros (alignment preserving cast.) + * @param addr The address to convert to bytes32 */ function addressToBytes32(address addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(addr))); } /** - * @notice converts bytes32 to address (alignment preserving cast.) + * @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 diff --git a/src/messages/v2/BurnMessageV2.sol b/src/messages/v2/BurnMessageV2.sol index 46139a7..de4bd1f 100644 --- a/src/messages/v2/BurnMessageV2.sol +++ b/src/messages/v2/BurnMessageV2.sol @@ -22,7 +22,7 @@ import {BurnMessage} from "../BurnMessage.sol"; /** * @title BurnMessageV2 Library - * @notice Library for formatted BurnMessages used by TokenMessengerV2. + * @notice Library for formatted V2 BurnMessages used by TokenMessengerV2. * @dev BurnMessageV2 format: * Field Bytes Type Index * version 4 uint32 0 @@ -34,11 +34,11 @@ import {BurnMessage} from "../BurnMessage.sol"; * feeExecuted 32 uint256 164 * expirationBlock 32 uint256 196 * hookData dynamic bytes 228 - * @dev Differences from v1: - * - maxFee is added - * - feeExecuted is added - * - expirationBlock is added - * - hookData is added + * @dev Additions from v1: + * - maxFee + * - feeExecuted + * - expirationBlock + * - hookData **/ library BurnMessageV2 { using TypedMemView for bytes; @@ -55,15 +55,15 @@ library BurnMessageV2 { uint256 private constant EMPTY_EXPIRATION_BLOCK = 0; /** - * @notice Formats a burn message + * @notice Formats a V2 burn message * @param _version The message body version - * @param _burnToken The burn token address on source domain as bytes32 + * @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 to execute on destination domain - * @return Formatted message. + * @param _hookData Optional hook data for processing on the destination domain + * @return Formatted message bytes. */ function _formatMessageForRelay( uint32 _version, @@ -93,7 +93,7 @@ library BurnMessageV2 { return _message._getVersion(); } - // @notice Returns _message's burn token field + // @notice Returns _message's burnToken field function _getBurnToken(bytes29 _message) internal pure returns (bytes32) { return _message._getBurnToken(); } diff --git a/src/roles/v2/Denylistable.sol b/src/roles/v2/Denylistable.sol index c9d3c68..c2f6f90 100644 --- a/src/roles/v2/Denylistable.sol +++ b/src/roles/v2/Denylistable.sol @@ -21,14 +21,14 @@ import {Ownable2Step} from "../Ownable2Step.sol"; /** * @title Denylistable - * @notice Base contract that allows management of a denylist + * @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 new Denylister + * @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, @@ -56,11 +56,11 @@ abstract contract Denylistable is Ownable2Step { // ============ State Variables ============ // The currently set denylister - address private _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) private _denylist; + mapping(address => uint256) internal _denylist; // ============ Modifiers ============ /** diff --git a/src/v2/BaseMessageTransmitter.sol b/src/v2/BaseMessageTransmitter.sol index 3fde443..f95d576 100644 --- a/src/v2/BaseMessageTransmitter.sol +++ b/src/v2/BaseMessageTransmitter.sol @@ -29,7 +29,7 @@ import {Initializable} from "../proxy/Initializable.sol"; /** * @title BaseMessageTransmitter - * @notice Base MessageTransmitter implementation, focused on administrative actions. + * @notice A base type containing administrative and configuration functionality for message transmitters. */ contract BaseMessageTransmitter is Initializable, @@ -83,7 +83,7 @@ contract BaseMessageTransmitter is } /** - * @dev Returns the current initialized version + * @notice Returns the current initialized version */ function initializedVersion() public view returns (uint64) { return _getInitializedVersion(); diff --git a/src/v2/BaseTokenMessenger.sol b/src/v2/BaseTokenMessenger.sol index e2a3e75..ddfd3be 100644 --- a/src/v2/BaseTokenMessenger.sol +++ b/src/v2/BaseTokenMessenger.sol @@ -209,7 +209,7 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable { } /** - * @dev Returns the current initialized version + * @notice Returns the current initialized version */ function initializedVersion() public view returns (uint64) { return _getInitializedVersion(); diff --git a/src/v2/Constants.sol b/src/v2/FinalityThresholds.sol similarity index 69% rename from src/v2/Constants.sol rename to src/v2/FinalityThresholds.sol index fc833aa..dd1fdce 100644 --- a/src/v2/Constants.sol +++ b/src/v2/FinalityThresholds.sol @@ -17,6 +17,11 @@ */ pragma solidity 0.7.6; -// Finality Thresholds - +// 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 finalized. +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 index 3be9bca..d5d51a2 100644 --- a/src/v2/MessageTransmitterV2.sol +++ b/src/v2/MessageTransmitterV2.sol @@ -23,7 +23,7 @@ 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 "./Constants.sol"; +import {FINALITY_THRESHOLD_FINALIZED} from "./FinalityThresholds.sol"; /** * @title MessageTransmitterV2 @@ -125,12 +125,12 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { // ============ External Functions ============ /** * @notice Send the message to the destination domain and recipient - * @dev Formats the message, and emit `MessageSent` event with message information. + * @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 raw bytes content of message + * @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, @@ -167,8 +167,7 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { /** * @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. + * 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 @@ -194,7 +193,7 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { * @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 bool, true if successful + * @return success True, if successful; false, if not */ function receiveMessage( bytes calldata message, @@ -259,7 +258,7 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { * @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 messagebody + * @return _messageBody The message body bytes */ function _validateReceivedMessage( bytes calldata _message, diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol index 6257f87..5be4b0b 100644 --- a/src/v2/TokenMessengerV2.sol +++ b/src/v2/TokenMessengerV2.sol @@ -25,11 +25,12 @@ import {IMessageHandlerV2} from "../interfaces/v2/IMessageHandlerV2.sol"; import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol"; import {BurnMessageV2} from "../messages/v2/BurnMessageV2.sol"; import {Initializable} from "../proxy/Initializable.sol"; +import {TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD} from "./FinalityThresholds.sol"; /** * @title TokenMessengerV2 - * @notice Sends messages and receives messages to/from MessageTransmitters - * and to/from TokenMinters + * @notice Sends and receives messages to/from MessageTransmitters + * and to/from TokenMinters. */ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { // ============ Events ============ @@ -65,9 +66,6 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { using TypedMemView for bytes29; using BurnMessageV2 for bytes29; - // ============ State Variables ============ - uint32 public constant minFinalityThresholdSupported = 500; - // ============ Constructor ============ /** * @param _messageTransmitter Message transmitter address @@ -88,7 +86,6 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { * @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_` is zero-length * @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. @@ -146,12 +143,12 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { * - 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 the amount. - * - MessageTransmitter#sendMessage reverts. + * - maxFee is greater than or equal to `amount`. + * - MessageTransmitterV2#sendMessage reverts. * @param amount amount of tokens to burn - * @param destinationDomain destination domain + * @param destinationDomain destination domain to receive message on * @param mintRecipient address of mint recipient on destination domain - * @param burnToken address of contract to burn deposited tokens, on local 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 @@ -183,22 +180,21 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { * @notice Deposits and burns tokens from sender to be minted on destination domain. * Emits a `DepositForBurn` event. * @dev reverts if: - * - hookData is zero-length - * - given burnToken is not supported - * - given destinationDomain has no TokenMessenger registered + * - `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. - * - fee is greater than or equal to the amount. - * - MessageTransmitter#sendMessage reverts. + * - maxFee is greater than or equal to `amount`. + * - MessageTransmitterV2#sendMessage reverts. * @param amount amount of tokens to burn - * @param destinationDomain destination domain - * @param mintRecipient address of mint recipient on destination domain - * @param burnToken address of contract to burn deposited tokens, on local domain + * @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 minFinalityThreshold the minimum finality at which a burn message will be attested to. * @param hookData hook data to append to burn message for interpretation on destination domain */ function depositForBurnWithHook( @@ -268,9 +264,10 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { * 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 1000. + * @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. */ @@ -287,7 +284,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { returns (bool) { require( - finalityThresholdExecuted >= minFinalityThresholdSupported, + finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD, "Unsupported finality threshold" ); @@ -318,10 +315,10 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { * @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 contract to burn deposited tokens, on local 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 execution on destination chain + * @param _hookData optional hook data for interpretation on destination chain */ function _depositForBurn( uint256 _amount, @@ -384,7 +381,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { * @dev Reverts if the BurnMessage version isn't supported * @param _msg Finalized message * @return _mintRecipient The recipient of the mint, as bytes32 - * @return _burnToken The token address burned on the source chain + * @return _burnToken The address of the token burned on the source chain * @return _amount The amount of burnToken burned */ function _validateFinalizedMessage( @@ -415,7 +412,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { * @dev Reverts if the fee executed exceeds the amount * @param _msg Finalized message * @return _mintRecipient The recipient of the mint, as bytes32 - * @return _burnToken The token address burned on the source chain + * @return _burnToken The address of the token burned on the source chain * @return _amount The amount of burnToken burned */ function _validateUnfinalizedMessage( diff --git a/test/v2/MessageTransmitterV2.t.sol b/test/v2/MessageTransmitterV2.t.sol index a3f6fe5..5da974c 100644 --- a/test/v2/MessageTransmitterV2.t.sol +++ b/test/v2/MessageTransmitterV2.t.sol @@ -27,7 +27,7 @@ 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/Constants.sol"; +import {FINALITY_THRESHOLD_FINALIZED} from "../../src/v2/FinalityThresholds.sol"; contract MessageTransmitterV2Test is TestUtils { event MessageSent(bytes message); diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index a80b526..c107d76 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -29,6 +29,7 @@ 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 @@ -62,7 +63,6 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { // Constants uint32 remoteDomain = 1; uint32 messageBodyVersion = 2; - uint32 confirmedFinalityThreshold = 1000; address localMessageTransmitter = address(10); address remoteMessageTransmitter = address(20); @@ -1507,7 +1507,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes calldata _hookData, uint32 _finalityThresholdExecuted ) public { - vm.assume(_finalityThresholdExecuted >= 2000); + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( localTokenMessenger.messageBodyVersion(), @@ -1603,8 +1603,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes calldata _messageBody ) public { vm.assume( - _finalityThresholdExecuted < - localTokenMessenger.minFinalityThresholdSupported() + _finalityThresholdExecuted < TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD ); vm.prank(localMessageTransmitter); @@ -1624,8 +1623,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { // See: BurnMessageV2#HOOK_DATA_INDEX vm.assume(_messageBody.length < 228); vm.assume( - _finalityThresholdExecuted >= - localTokenMessenger.minFinalityThresholdSupported() + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD ); vm.prank(localMessageTransmitter); @@ -1650,8 +1648,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ) public { vm.assume(_version != localTokenMessenger.messageBodyVersion()); vm.assume( - _finalityThresholdExecuted >= - localTokenMessenger.minFinalityThresholdSupported() + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD ); bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( @@ -1708,7 +1705,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { localTokenMessenger.handleReceiveUnfinalizedMessage( remoteDomain, remoteTokenMessengerAddr, - confirmedFinalityThreshold, + FINALITY_THRESHOLD_CONFIRMED, _messageBody ); } @@ -1746,7 +1743,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { localTokenMessenger.handleReceiveUnfinalizedMessage( remoteDomain, remoteTokenMessengerAddr, - confirmedFinalityThreshold, + FINALITY_THRESHOLD_CONFIRMED, _messageBody ); } @@ -1762,8 +1759,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint32 _finalityThresholdExecuted ) public { vm.assume( - _finalityThresholdExecuted >= - localTokenMessenger.minFinalityThresholdSupported() + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD ); vm.assume(_feeExecuted > _amount); @@ -1799,8 +1795,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint32 _finalityThresholdExecuted ) public { vm.assume( - _finalityThresholdExecuted >= - localTokenMessenger.minFinalityThresholdSupported() + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD ); vm.assume(_amount > 0); @@ -1837,8 +1832,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint32 _finalityThresholdExecuted ) public { vm.assume( - _finalityThresholdExecuted >= - localTokenMessenger.minFinalityThresholdSupported() + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD ); vm.assume(_feeExecuted < _amount); @@ -1882,8 +1876,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint32 _finalityThresholdExecuted ) public { vm.assume( - _finalityThresholdExecuted >= - localTokenMessenger.minFinalityThresholdSupported() + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD ); bytes memory _messageBody = _formatBurnMessageForReceive( @@ -1933,8 +1926,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { uint32 _finalityThresholdExecuted ) public { vm.assume( - _finalityThresholdExecuted >= - localTokenMessenger.minFinalityThresholdSupported() + _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD ); vm.assume(_feeExecuted < _amount); vm.assume(_feeExecuted > 0); @@ -2003,7 +1995,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _handleReceiveMessage( remoteDomain, remoteTokenMessengerAddr, - confirmedFinalityThreshold, + FINALITY_THRESHOLD_CONFIRMED, _messageBody ); } @@ -2039,7 +2031,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _handleReceiveMessage( remoteDomain, remoteTokenMessengerAddr, - confirmedFinalityThreshold, + FINALITY_THRESHOLD_CONFIRMED, _messageBody ); } @@ -2070,7 +2062,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _handleReceiveMessage( remoteDomain, remoteTokenMessengerAddr, - confirmedFinalityThreshold, + FINALITY_THRESHOLD_CONFIRMED, _messageBody ); } @@ -2106,7 +2098,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _handleReceiveMessage( remoteDomain, remoteTokenMessengerAddr, - confirmedFinalityThreshold, + FINALITY_THRESHOLD_CONFIRMED, _messageBody ); } @@ -2141,7 +2133,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _handleReceiveMessage( remoteDomain, remoteTokenMessengerAddr, - confirmedFinalityThreshold, + FINALITY_THRESHOLD_CONFIRMED, _messageBody ); } @@ -2365,7 +2357,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { vm.prank(localMessageTransmitter); bool _result; - if (_finalityThresholdExecuted >= 2000) { + if (_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED) { _result = localTokenMessenger.handleReceiveFinalizedMessage( _remoteDomain, _sender, From 0d3fa118d97cba18801baa752f31365058200918 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:25:23 -0400 Subject: [PATCH 25/40] STABLE-7553 (Part 2): Add basic integration test for TokenMessengerV2 (#38) --- test/v2/TokenMessengerV2IT.t.sol | 360 +++++++++++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 test/v2/TokenMessengerV2IT.t.sol diff --git a/test/v2/TokenMessengerV2IT.t.sol b/test/v2/TokenMessengerV2IT.t.sol new file mode 100644 index 0000000..fbf139c --- /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; + + // 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] = AddressUtils.addressToBytes32( + address(localTokenMessenger) + ); + + 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, + AddressUtils.addressToBytes32(address(remoteTokenMessenger)) + ); + vm.stopPrank(); + + // Link token pair on local domain + linkTokenPair( + localTokenMinter, + address(localToken), + remoteDomain, + AddressUtils.addressToBytes32(address(remoteToken)) + ); + + // Link token pair on remote domain + linkTokenPair( + remoteTokenMinter, + address(remoteToken), + localDomain, + AddressUtils.addressToBytes32(address(localToken)) + ); + + // 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, + AddressUtils.addressToBytes32(remoteMintRecipient), + address(localToken), + AddressUtils.addressToBytes32(remoteMintRecipient), + 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), + AddressUtils.addressToBytes32(address(localTokenMessenger)) + ); + assertEq( + MessageV2._getRecipient(_msg), + AddressUtils.addressToBytes32(address(remoteTokenMessenger)) + ); + assertEq( + MessageV2._getDestinationCaller(_msg), + AddressUtils.addressToBytes32(remoteMintRecipient) + ); + 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), + AddressUtils.addressToBytes32(address(localToken)) + ); + assertEq( + BurnMessageV2._getMintRecipient(_burnMessageV2), + AddressUtils.addressToBytes32(remoteMintRecipient) + ); + assertEq(BurnMessageV2._getAmount(_burnMessageV2), localDepositAmount); + assertEq( + BurnMessageV2._getMessageSender(_burnMessageV2), + AddressUtils.addressToBytes32(localDepositor) + ); + assertEq(BurnMessageV2._getMaxFee(_burnMessageV2), localFeeExecuted); + assertEq( + BurnMessageV2._getFeeExecuted(_burnMessageV2), + localFeeExecuted + ); + assertEq(BurnMessageV2._getExpirationBlock(_burnMessageV2), 0); + } +} From 2e28bff77d87278018102c44e07eef92cf451bcf Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:15:06 -0500 Subject: [PATCH 26/40] [STABLE-7170] Add sample CCTP v2 wrapper contract (#42) --- src/examples/CCTPHookWrapper.sol | 190 ++++++++++++++ src/messages/v2/BurnMessageV2.sol | 10 + test/examples/CCTPHookWrapper.t.sol | 364 +++++++++++++++++++++++++++ test/messages/v2/BurnMessageV2.t.sol | 3 +- test/mocks/v2/MockHookTarget.sol | 35 +++ 5 files changed, 601 insertions(+), 1 deletion(-) create mode 100644 src/examples/CCTPHookWrapper.sol create mode 100644 test/examples/CCTPHookWrapper.t.sol create mode 100644 test/mocks/v2/MockHookTarget.sol diff --git a/src/examples/CCTPHookWrapper.sol b/src/examples/CCTPHookWrapper.sol new file mode 100644 index 0000000..27174b1 --- /dev/null +++ b/src/examples/CCTPHookWrapper.sol @@ -0,0 +1,190 @@ +/* + * 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"; + +/** + * @title CCTPHookWrapper + * @notice A sample wrapper around CCTP v2 that relays a message and + * optionally executes the hook contained in the Burn Message. + * @dev This is intended to only work with CCTP v2 message formats and interfaces. + */ +contract CCTPHookWrapper { + // ============ State Variables ============ + // Address of the local message transmitter + IReceiverV2 public immutable messageTransmitter; + + // The supported Message Format version + uint32 public immutable supportedMessageVersion; + + // The supported Message Body version + uint32 public immutable supportedMessageBodyVersion; + + // Byte-length of an address + uint256 internal constant ADDRESS_BYTE_LENGTH = 20; + + // ============ Libraries ============ + using TypedMemView for bytes; + using TypedMemView for bytes29; + + // ============ Modifiers ============ + /** + * @notice A modifier to enable access control + * @dev Can be overridden to customize the behavior + */ + modifier onlyAllowed() virtual { + _; + } + + // ============ Constructor ============ + /** + * @param _messageTransmitter The address of the local message transmitter + * @param _messageVersion The required CCTP message version. For CCTP v2, this is 1. + * @param _messageBodyVersion The required message body (Burn Message) version. For CCTP v2, this is 1. + */ + constructor( + address _messageTransmitter, + uint32 _messageVersion, + uint32 _messageBodyVersion + ) { + require( + _messageTransmitter != address(0), + "Message transmitter is the zero address" + ); + + messageTransmitter = IReceiverV2(_messageTransmitter); + supportedMessageVersion = _messageVersion; + supportedMessageBodyVersion = _messageBodyVersion; + } + + // ============ 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. + * + * WARNING: this implementation does NOT enforce atomicity in the hook call. If atomicity is + * required, a new wrapper contract can be created, possibly by overriding this behavior in `_handleHook`, + * or by introducing a different format for the hook data that includes more information about + * the desired handling. + * + * WARNING: in a permissionless context, it is important not to view this wrapper implementation as a trusted + * caller of a hook, as others can craft messages containing hooks that look identical, that are + * similarly executed from this wrapper, either by setting this contract as the destination caller, + * or by setting the destination caller to be bytes32(0). Alternate implementations may extract more information + * from the burn message, such as the mintRecipient or the amount, to include in the hook call to allow recipients + * to further filter their receiving actions. + * + * WARNING: re-entrant behavior is allowed in this implementation. Relay() can be overridden to disable this. + * + * @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 + onlyAllowed + returns ( + bool relaySuccess, + bool hookSuccess, + bytes memory hookReturnData + ) + { + bytes29 _msg = message.ref(0); + bytes29 _msgBody = MessageV2._getMessageBody(_msg); + + // Perform message validation + _validateMessage(_msg, _msgBody); + + // Relay message + require( + messageTransmitter.receiveMessage(message, attestation), + "Receive message failed" + ); + + relaySuccess = true; + + // Handle hook + bytes29 _hookData = BurnMessageV2._getHookData(_msgBody); + (hookSuccess, hookReturnData) = _handleHook(_hookData); + } + + // ============ Internal Functions ============ + /** + * @notice Validates a message and its message body + * @dev Can be overridden to customize the validation + * @dev Reverts if the message format version or message body version + * do not match the supported versions. + */ + function _validateMessage( + bytes29 _message, + bytes29 _messageBody + ) internal virtual { + require( + MessageV2._getVersion(_message) == supportedMessageVersion, + "Invalid message version" + ); + require( + BurnMessageV2._getVersion(_messageBody) == + supportedMessageBodyVersion, + "Invalid message body version" + ); + } + + /** + * @notice Handles hook data by executing a call to a target address + * @dev Can be overridden to customize the execution behavior + * @param _hookData The hook data contained in the Burn Message + * @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 _handleHook( + bytes29 _hookData + ) internal virtual returns (bool _success, bytes memory _returnData) { + 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(); + + (_success, _returnData) = address(_target).call(_hookCalldata); + } + } +} diff --git a/src/messages/v2/BurnMessageV2.sol b/src/messages/v2/BurnMessageV2.sol index de4bd1f..e9bc81a 100644 --- a/src/messages/v2/BurnMessageV2.sol +++ b/src/messages/v2/BurnMessageV2.sol @@ -134,6 +134,16 @@ library BurnMessageV2 { 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 diff --git a/test/examples/CCTPHookWrapper.t.sol b/test/examples/CCTPHookWrapper.t.sol new file mode 100644 index 0000000..86749d6 --- /dev/null +++ b/test/examples/CCTPHookWrapper.t.sol @@ -0,0 +1,364 @@ +/* + * 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"; + +contract CCTPHookWrapperTest is Test { + // Test events + event HookReceived(uint256 paramOne, uint256 paramTwo); + + // Test constants + + uint32 messageVersion = 1; + uint32 messageBodyVersion = 1; + + address localMessageTransmitter = address(10); + MockHookTarget hookTarget; + CCTPHookWrapper wrapper; + + function setUp() public { + wrapper = new CCTPHookWrapper( + localMessageTransmitter, + messageVersion, + messageBodyVersion + ); + hookTarget = new MockHookTarget(); + } + + // Tests + + function testInitialization__revertsIfMessageTransmitterIsZero() public { + vm.expectRevert("Message transmitter is the zero address"); + new CCTPHookWrapper(address(0), messageVersion, messageBodyVersion); + } + + function testInitialization__setsTheMessageTransmitter( + address _messageTransmitter + ) public { + vm.assume(_messageTransmitter != address(0)); + CCTPHookWrapper _wrapper = new CCTPHookWrapper( + _messageTransmitter, + messageVersion, + messageBodyVersion + ); + assertEq(address(_wrapper.messageTransmitter()), _messageTransmitter); + } + + function testInitialization__setsTheMessageVersion( + uint32 _messageVersion + ) public { + CCTPHookWrapper _wrapper = new CCTPHookWrapper( + localMessageTransmitter, + _messageVersion, + messageBodyVersion + ); + assertEq( + uint256(address(_wrapper.supportedMessageVersion())), + uint256(_messageVersion) + ); + } + + function testInitialization__setsTheMessageBodyVersion( + uint32 _messageBodyVersion + ) public { + CCTPHookWrapper _wrapper = new CCTPHookWrapper( + localMessageTransmitter, + messageVersion, + _messageBodyVersion + ); + assertEq( + uint256(address(_wrapper.supportedMessageBodyVersion())), + uint256(_messageBodyVersion) + ); + } + + function testRelay__revertsIfMessageFormatVersionIsInvalid( + uint32 _messageVersion + ) public { + vm.assume(_messageVersion != messageVersion); + + vm.expectRevert("Invalid message version"); + bytes memory _message = _createMessage( + _messageVersion, + messageBodyVersion, + bytes("") + ); + wrapper.relay(_message, bytes("")); + } + + function testRelay__revertsIfMessageBodyVersionIsInvalid( + uint32 _messageBodyVersion + ) public { + vm.assume(_messageBodyVersion != messageBodyVersion); + + vm.expectRevert("Invalid message body version"); + bytes memory _message = _createMessage( + messageVersion, + _messageBodyVersion, + bytes("") + ); + wrapper.relay(_message, bytes("")); + } + + function testRelay__revertsIfMessageTransmitterCallReverts() public { + bytes memory _message = _createMessage( + messageVersion, + messageBodyVersion, + bytes("") + ); + + // Mock a reverting call to message transmitter + vm.mockCallRevert( + localMessageTransmitter, + abi.encodeWithSelector( + IReceiver.receiveMessage.selector, + _message, + bytes("") + ), + "Testing: token minter failed" + ); + + vm.expectRevert(); + wrapper.relay(_message, bytes("")); + } + + function testRelay__revertsIfMessageTransmitterReturnsFalse() public { + bytes memory _message = _createMessage( + messageVersion, + messageBodyVersion, + 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"); + wrapper.relay(_message, bytes("")); + } + + function testRelay__succeedsWithNoHook() public { + bytes memory _message = _createMessage( + messageVersion, + messageBodyVersion, + bytes("") + ); + + vm.mockCall( + localMessageTransmitter, + abi.encodeWithSelector( + IReceiver.receiveMessage.selector, + _message, + bytes("") + ), + abi.encode(true) + ); + + ( + 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( + messageVersion, + messageBodyVersion, + 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 + ( + bool _relaySuccess, + bool _hookSuccess, + bytes memory _returnData + ) = wrapper.relay(_message, bytes("")); + + assertTrue(_relaySuccess); + assertFalse(_hookSuccess); + assertEq(_getRevertMsg(_returnData), "Hook failure"); + } + + function testRelay__succeedsWithCallToEOAHookTarget( + bytes calldata _hookCalldata + ) public { + bytes memory _message = _createMessage( + messageVersion, + messageBodyVersion, + 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 + ( + 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 fail + uint256 _expectedReturnData = 12; + bytes memory _succeedingHookCallData = abi.encodeWithSelector( + MockHookTarget.succeedingHook.selector, + 5, + 7 + ); + bytes memory _message = _createMessage( + messageVersion, + messageBodyVersion, + 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 + ( + 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 + MessageV2._formatMessageForRelay( + _messageVersion, + 0, + 0, + bytes32(0), + bytes32(0), + bytes32(0), + 0, + _createBurnMessage(_messageBodyVersion, _hookData) + ); + } + + function _createBurnMessage( + uint32 _burnMessageVersion, + bytes memory _hookData + ) internal pure returns (bytes memory) { + return + abi.encodePacked( + _burnMessageVersion, + bytes32(0), + bytes32(0), + uint256(0), + bytes32(0), + uint256(0), + uint256(0), + uint256(0), + _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/v2/BurnMessageV2.t.sol b/test/messages/v2/BurnMessageV2.t.sol index d8e3ff4..85559f4 100644 --- a/test/messages/v2/BurnMessageV2.t.sol +++ b/test/messages/v2/BurnMessageV2.t.sol @@ -35,7 +35,7 @@ contract BurnMessageV2Test is Test { bytes32 _messageSender, uint256 _maxFee, bytes calldata _hookData - ) public pure { + ) public view { bytes memory _expectedMessageBody = abi.encodePacked( _version, _burnToken, @@ -67,6 +67,7 @@ contract BurnMessageV2Test is Test { assertEq(_m._getMaxFee(), _maxFee); assertEq(_m._getFeeExecuted(), 0); assertEq(_m._getExpirationBlock(), 0); + assertEq(_m._getHookData().clone(), _hookData); _m._validateBurnMessageFormat(); 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"); + } +} From 47dcbca306092c51a15dfbaab06fe88e4679a288 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:10:13 -0500 Subject: [PATCH 27/40] STABLE-7554: Internal audit changes (#44) --- src/MessageTransmitter.sol | 26 ++-- src/messages/v2/AddressUtils.sol | 4 +- src/messages/v2/MessageV2.sol | 2 +- src/v2/BaseMessageTransmitter.sol | 7 +- src/v2/BaseTokenMessenger.sol | 6 +- src/v2/FinalityThresholds.sol | 2 +- src/v2/MessageTransmitterV2.sol | 18 ++- src/v2/TokenMessengerV2.sol | 16 ++- test/examples/CCTPHookWrapper.t.sol | 16 +-- test/messages/v2/AddressUtils.t.sol | 6 +- test/messages/v2/MessageV2.t.sol | 4 +- test/v2/MessageTransmitterV2.t.sol | 176 ++++++++++++++++++++++------ test/v2/TokenMessengerV2.t.sol | 95 +++++++++------ test/v2/TokenMessengerV2IT.t.sol | 28 ++--- 14 files changed, 273 insertions(+), 133 deletions(-) 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/messages/v2/AddressUtils.sol b/src/messages/v2/AddressUtils.sol index 1956573..32784d3 100644 --- a/src/messages/v2/AddressUtils.sol +++ b/src/messages/v2/AddressUtils.sol @@ -26,7 +26,7 @@ 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 addressToBytes32(address addr) internal pure returns (bytes32) { + function toBytes32(address addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(addr))); } @@ -36,7 +36,7 @@ library AddressUtils { * 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) internal pure returns (address) { + function toAddress(bytes32 _buf) internal pure returns (address) { return address(uint160(uint256(_buf))); } } diff --git a/src/messages/v2/MessageV2.sol b/src/messages/v2/MessageV2.sol index 7b444c0..c5dbd91 100644 --- a/src/messages/v2/MessageV2.sol +++ b/src/messages/v2/MessageV2.sol @@ -83,7 +83,7 @@ library MessageV2 { bytes32 _recipient, bytes32 _destinationCaller, uint32 _minFinalityThreshold, - bytes memory _messageBody + bytes calldata _messageBody ) internal pure returns (bytes memory) { return abi.encodePacked( diff --git a/src/v2/BaseMessageTransmitter.sol b/src/v2/BaseMessageTransmitter.sol index f95d576..b84c009 100644 --- a/src/v2/BaseMessageTransmitter.sol +++ b/src/v2/BaseMessageTransmitter.sol @@ -17,14 +17,9 @@ */ pragma solidity 0.7.6; -import {IMessageTransmitterV2} from "../interfaces/v2/IMessageTransmitterV2.sol"; import {Attestable} from "../roles/Attestable.sol"; import {Pausable} from "../roles/Pausable.sol"; import {Rescuable} from "../roles/Rescuable.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 {Initializable} from "../proxy/Initializable.sol"; /** @@ -85,7 +80,7 @@ contract BaseMessageTransmitter is /** * @notice Returns the current initialized version */ - function initializedVersion() public view returns (uint64) { + function initializedVersion() external view returns (uint64) { return _getInitializedVersion(); } } diff --git a/src/v2/BaseTokenMessenger.sol b/src/v2/BaseTokenMessenger.sol index ddfd3be..a94e4f5 100644 --- a/src/v2/BaseTokenMessenger.sol +++ b/src/v2/BaseTokenMessenger.sol @@ -211,7 +211,7 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable { /** * @notice Returns the current initialized version */ - function initializedVersion() public view returns (uint64) { + function initializedVersion() external view returns (uint64) { return _getInitializedVersion(); } @@ -260,9 +260,7 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable { * @return true if message sender is the registered local message transmitter */ function _isLocalMessageTransmitter() internal view returns (bool) { - return - address(localMessageTransmitter) != address(0) && - msg.sender == address(localMessageTransmitter); + return msg.sender == address(localMessageTransmitter); } /** diff --git a/src/v2/FinalityThresholds.sol b/src/v2/FinalityThresholds.sol index dd1fdce..7534415 100644 --- a/src/v2/FinalityThresholds.sol +++ b/src/v2/FinalityThresholds.sol @@ -20,7 +20,7 @@ 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 finalized. +// 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 diff --git a/src/v2/MessageTransmitterV2.sol b/src/v2/MessageTransmitterV2.sol index d5d51a2..d9955da 100644 --- a/src/v2/MessageTransmitterV2.sol +++ b/src/v2/MessageTransmitterV2.sol @@ -56,9 +56,12 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { ); // ============ 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; - using MessageV2 for bytes29; // ============ Constructor ============ /** @@ -117,9 +120,13 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { maxMessageBodySize = maxMessageBodySize_; // Attester configuration - for (uint256 i; i < attesters_.length; i++) { + uint256 _attestersLength = attesters_.length; + for (uint256 i; i < _attestersLength; ++i) { _enableAttester(attesters_[i]); } + + // Claim 0-nonce + usedNonces[bytes32(0)] = 1; } // ============ External Functions ============ @@ -147,7 +154,7 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { ); require(recipient != bytes32(0), "Recipient must be nonzero"); - bytes32 _messageSender = AddressUtils.addressToBytes32(msg.sender); + bytes32 _messageSender = msg.sender.toBytes32(); // serialize message bytes memory _message = MessageV2._formatMessageForRelay( @@ -292,8 +299,7 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { // Validate destination caller if (_msg._getDestinationCaller() != bytes32(0)) { require( - _msg._getDestinationCaller() == - AddressUtils.addressToBytes32(msg.sender), + _msg._getDestinationCaller() == msg.sender.toBytes32(), "Invalid caller for message" ); } @@ -308,7 +314,7 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { // Unpack remaining values _sourceDomain = _msg._getSourceDomain(); _sender = _msg._getSender(); - _recipient = AddressUtils.bytes32ToAddress(_msg._getRecipient()); + _recipient = _msg._getRecipient().toAddress(); _finalityThresholdExecuted = _msg._getFinalityThresholdExecuted(); _messageBody = _msg._getMessageBody().clone(); } diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol index 5be4b0b..6bcccd1 100644 --- a/src/v2/TokenMessengerV2.sol +++ b/src/v2/TokenMessengerV2.sol @@ -24,7 +24,6 @@ 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 {Initializable} from "../proxy/Initializable.sol"; import {TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD} from "./FinalityThresholds.sol"; /** @@ -62,9 +61,12 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { ); // ============ 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; - using BurnMessageV2 for bytes29; // ============ Constructor ============ /** @@ -122,7 +124,8 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { localMinter = ITokenMinterV2(tokenMinter_); // Remote messenger configuration - for (uint256 i; i < remoteDomains_.length; i++) { + uint256 _remoteDomainsLength = remoteDomains_.length; + for (uint256 i; i < _remoteDomainsLength; ++i) { require( remoteTokenMessengers_[i] != bytes32(0), "Invalid TokenMessenger" @@ -344,10 +347,10 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { // Format message body bytes memory _burnMessage = BurnMessageV2._formatMessageForRelay( messageBodyVersion, - AddressUtils.addressToBytes32(_burnToken), + _burnToken.toBytes32(), _mintRecipient, _amount, - AddressUtils.addressToBytes32(msg.sender), + msg.sender.toBytes32(), _maxFee, _hookData ); @@ -398,7 +401,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { ); return ( - AddressUtils.bytes32ToAddress(_msg._getMintRecipient()), + _msg._getMintRecipient().toAddress(), _msg._getBurnToken(), _msg._getAmount() ); @@ -439,5 +442,6 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { // Validate fee _fee = _msg._getFeeExecuted(); require(_fee == 0 || _fee < _amount, "Fee equals or exceeds amount"); + require(_fee <= _msg._getMaxFee(), "Fee exceeds max fee"); } } diff --git a/test/examples/CCTPHookWrapper.t.sol b/test/examples/CCTPHookWrapper.t.sol index 86749d6..dd46d15 100644 --- a/test/examples/CCTPHookWrapper.t.sol +++ b/test/examples/CCTPHookWrapper.t.sol @@ -318,14 +318,16 @@ contract CCTPHookWrapperTest is Test { bytes memory _hookData ) internal pure returns (bytes memory) { return - MessageV2._formatMessageForRelay( + abi.encodePacked( _messageVersion, - 0, - 0, - bytes32(0), - bytes32(0), - bytes32(0), - 0, + 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) ); } diff --git a/test/messages/v2/AddressUtils.t.sol b/test/messages/v2/AddressUtils.t.sol index 90b57dd..12283bc 100644 --- a/test/messages/v2/AddressUtils.t.sol +++ b/test/messages/v2/AddressUtils.t.sol @@ -23,13 +23,13 @@ import {AddressUtils} from "../../../src/messages/v2/AddressUtils.sol"; contract AddressUtilsTest is Test { function testAddressToBytes32Conversion(address _addr) public pure { - bytes32 _addrAsBytes32 = AddressUtils.addressToBytes32(_addr); - address _recoveredAddr = AddressUtils.bytes32ToAddress(_addrAsBytes32); + bytes32 _addrAsBytes32 = AddressUtils.toBytes32(_addr); + address _recoveredAddr = AddressUtils.toAddress(_addrAsBytes32); assertEq(_recoveredAddr, _addr); } function testAddressToBytes32LeftPads(address _addr) public pure { - bytes32 _addrAsBytes32 = AddressUtils.addressToBytes32(_addr); + 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++) { diff --git a/test/messages/v2/MessageV2.t.sol b/test/messages/v2/MessageV2.t.sol index 8a81daf..0e719f0 100644 --- a/test/messages/v2/MessageV2.t.sol +++ b/test/messages/v2/MessageV2.t.sol @@ -35,7 +35,7 @@ contract MessageV2Test is Test { bytes32 _recipient, bytes32 _destinationCaller, uint32 _minFinalityThreshold, - bytes memory _messageBody + bytes calldata _messageBody ) public view { bytes memory _message = MessageV2._formatMessageForRelay( _version, @@ -82,7 +82,7 @@ contract MessageV2Test is Test { bytes32 _recipient, bytes32 _destinationCaller, uint32 _minFinalityThreshold, - bytes memory _messageBody + bytes calldata _messageBody ) public { bytes memory _message = MessageV2._formatMessageForRelay( _version, diff --git a/test/v2/MessageTransmitterV2.t.sol b/test/v2/MessageTransmitterV2.t.sol index 5da974c..60622f2 100644 --- a/test/v2/MessageTransmitterV2.t.sol +++ b/test/v2/MessageTransmitterV2.t.sol @@ -49,6 +49,8 @@ contract MessageTransmitterV2Test is TestUtils { 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; @@ -338,6 +340,76 @@ contract MessageTransmitterV2Test is TestUtils { 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)), 1); + } + function testSendMessage_revertsWhenPaused( uint32 _destinationDomain, bytes32 _recipient, @@ -700,7 +772,7 @@ contract MessageTransmitterV2Test is TestUtils { _nonce, _sender, _recipient, - AddressUtils.addressToBytes32(_destinationCaller), + _destinationCaller.toBytes32(), _minFinalityThreshold, _finalityThresholdExecuted, _messageBody @@ -733,8 +805,8 @@ contract MessageTransmitterV2Test is TestUtils { localDomain, _nonce, _sender, - AddressUtils.addressToBytes32(_recipient), - AddressUtils.addressToBytes32(_destinationCaller), + _recipient.toBytes32(), + _destinationCaller.toBytes32(), _minFinalityThreshold, _finalityThresholdExecuted, _messageBody @@ -774,8 +846,8 @@ contract MessageTransmitterV2Test is TestUtils { localDomain, _nonce, _sender, - AddressUtils.addressToBytes32(_recipient), - AddressUtils.addressToBytes32(_destinationCaller), + _recipient.toBytes32(), + _destinationCaller.toBytes32(), _minFinalityThreshold, _finalityThresholdExecuted, _messageBody @@ -809,6 +881,7 @@ contract MessageTransmitterV2Test is TestUtils { ) public { vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); vm.assume(_recipient != foundryCheatCodeAddr); + vm.assume(_nonce != bytes32(0)); bytes memory _message = _formatMessageForReceive( version, @@ -816,8 +889,8 @@ contract MessageTransmitterV2Test is TestUtils { localDomain, _nonce, _sender, - AddressUtils.addressToBytes32(_recipient), - AddressUtils.addressToBytes32(_destinationCaller), + _recipient.toBytes32(), + _destinationCaller.toBytes32(), _minFinalityThreshold, _finalityThresholdExecuted, _messageBody @@ -850,6 +923,7 @@ contract MessageTransmitterV2Test is TestUtils { uint32 _finalityThresholdExecuted, bytes calldata _messageBody ) public { + vm.assume(_nonce != bytes32(0)); vm.assume(_finalityThresholdExecuted < FINALITY_THRESHOLD_FINALIZED); vm.assume(_recipient != foundryCheatCodeAddr); @@ -859,8 +933,8 @@ contract MessageTransmitterV2Test is TestUtils { localDomain, _nonce, _sender, - AddressUtils.addressToBytes32(_recipient), - AddressUtils.addressToBytes32(_destinationCaller), + _recipient.toBytes32(), + _destinationCaller.toBytes32(), _minFinalityThreshold, _finalityThresholdExecuted, _messageBody @@ -883,6 +957,37 @@ contract MessageTransmitterV2Test is TestUtils { 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, @@ -894,14 +999,15 @@ contract MessageTransmitterV2Test is TestUtils { bytes calldata _messageBody, address _randomCaller ) public { + vm.assume(_nonce != bytes32(0)); bytes memory _message = _formatMessageForReceive( version, _sourceDomain, localDomain, _nonce, _sender, - AddressUtils.addressToBytes32(_recipient), - AddressUtils.addressToBytes32(_destinationCaller), + _recipient.toBytes32(), + _destinationCaller.toBytes32(), _minFinalityThreshold, _finalityThresholdExecuted, _messageBody @@ -922,6 +1028,7 @@ contract MessageTransmitterV2Test is TestUtils { uint32 _minFinalityThreshold, uint32 _finalityThresholdExecuted ) public { + vm.assume(_nonce != bytes32(0)); MockReentrantCallerV2 _mockReentrantCaller = new MockReentrantCallerV2(); // Encode mockReentrantCaller as recipient @@ -931,7 +1038,7 @@ contract MessageTransmitterV2Test is TestUtils { localDomain, _nonce, _sender, - AddressUtils.addressToBytes32(address(_mockReentrantCaller)), + address(_mockReentrantCaller).toBytes32(), bytes32(0), _minFinalityThreshold, _finalityThresholdExecuted, @@ -959,14 +1066,15 @@ contract MessageTransmitterV2Test is TestUtils { bytes calldata _messageBody, address _randomCaller ) public { + vm.assume(_nonce != bytes32(0)); bytes memory _message = _formatMessageForReceive( version, _sourceDomain, localDomain, _nonce, _sender, - AddressUtils.addressToBytes32(_recipient), - AddressUtils.addressToBytes32(_destinationCaller), + _recipient.toBytes32(), + _destinationCaller.toBytes32(), _minFinalityThreshold, _finalityThresholdExecuted, _messageBody @@ -986,6 +1094,7 @@ contract MessageTransmitterV2Test is TestUtils { bytes calldata _messageBody, address _randomCaller ) public { + vm.assume(_nonce != bytes32(0)); _setup2of2Multisig(); bytes memory _message = _formatMessageForReceive( @@ -994,8 +1103,8 @@ contract MessageTransmitterV2Test is TestUtils { localDomain, _nonce, _sender, - AddressUtils.addressToBytes32(_recipient), - AddressUtils.addressToBytes32(_destinationCaller), + _recipient.toBytes32(), + _destinationCaller.toBytes32(), _minFinalityThreshold, _finalityThresholdExecuted, _messageBody @@ -1015,6 +1124,7 @@ contract MessageTransmitterV2Test is TestUtils { bytes calldata _messageBody, address _randomCaller ) public { + vm.assume(_nonce != bytes32(0)); _setup2of3Multisig(); bytes memory _message = _formatMessageForReceive( @@ -1023,8 +1133,8 @@ contract MessageTransmitterV2Test is TestUtils { localDomain, _nonce, _sender, - AddressUtils.addressToBytes32(_recipient), - AddressUtils.addressToBytes32(_destinationCaller), + _recipient.toBytes32(), + _destinationCaller.toBytes32(), _minFinalityThreshold, _finalityThresholdExecuted, _messageBody @@ -1044,6 +1154,7 @@ contract MessageTransmitterV2Test is TestUtils { bytes calldata _messageBody, address _randomCaller ) public { + vm.assume(_nonce != bytes32(0)); vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); bytes memory _message = _formatMessageForReceive( version, @@ -1051,8 +1162,8 @@ contract MessageTransmitterV2Test is TestUtils { localDomain, _nonce, _sender, - AddressUtils.addressToBytes32(_recipient), - AddressUtils.addressToBytes32(_destinationCaller), + _recipient.toBytes32(), + _destinationCaller.toBytes32(), _minFinalityThreshold, _finalityThresholdExecuted, _messageBody @@ -1072,6 +1183,7 @@ contract MessageTransmitterV2Test is TestUtils { bytes calldata _messageBody, address _randomCaller ) public { + vm.assume(_nonce != bytes32(0)); vm.assume(_finalityThresholdExecuted < FINALITY_THRESHOLD_FINALIZED); bytes memory _message = _formatMessageForReceive( version, @@ -1079,8 +1191,8 @@ contract MessageTransmitterV2Test is TestUtils { localDomain, _nonce, _sender, - AddressUtils.addressToBytes32(_recipient), - AddressUtils.addressToBytes32(_destinationCaller), + _recipient.toBytes32(), + _destinationCaller.toBytes32(), _minFinalityThreshold, _finalityThresholdExecuted, _messageBody @@ -1099,6 +1211,7 @@ contract MessageTransmitterV2Test is TestUtils { uint32 _finalityThresholdExecuted, bytes calldata _messageBody ) public { + vm.assume(_nonce != bytes32(0)); vm.assume(_destinationCaller != address(0)); bytes memory _message = _formatMessageForReceive( version, @@ -1106,8 +1219,8 @@ contract MessageTransmitterV2Test is TestUtils { localDomain, _nonce, _sender, - AddressUtils.addressToBytes32(_recipient), - AddressUtils.addressToBytes32(_destinationCaller), + _recipient.toBytes32(), + _destinationCaller.toBytes32(), _minFinalityThreshold, _finalityThresholdExecuted, _messageBody @@ -1126,6 +1239,7 @@ contract MessageTransmitterV2Test is TestUtils { bytes calldata _messageBody, address _randomCaller ) public { + vm.assume(_nonce != bytes32(0)); vm.assume(_randomCaller != address(0)); bytes memory _message = _formatMessageForReceive( version, @@ -1133,7 +1247,7 @@ contract MessageTransmitterV2Test is TestUtils { localDomain, _nonce, _sender, - AddressUtils.addressToBytes32(_recipient), + _recipient.toBytes32(), bytes32(0), // destinationCaller _minFinalityThreshold, _finalityThresholdExecuted, @@ -1248,14 +1362,14 @@ contract MessageTransmitterV2Test is TestUtils { bytes32 _recipient, bytes32 _destinationCaller, uint32 _minFinalityThreshold, - bytes memory _messageBody, + bytes calldata _messageBody, address _sender ) internal { bytes memory _expectedMessage = MessageV2._formatMessageForRelay( version, localDomain, _destinationDomain, - AddressUtils.addressToBytes32(_sender), + _sender.toBytes32(), _recipient, _destinationCaller, _minFinalityThreshold, @@ -1286,9 +1400,7 @@ contract MessageTransmitterV2Test is TestUtils { address _randomCaller ) internal { bytes29 _msg = _message.ref(0); - address _recipient = AddressUtils.bytes32ToAddress( - _msg._getRecipient() - ); + address _recipient = _msg._getRecipient().toAddress(); vm.assume(_recipient != foundryCheatCodeAddr); // Mock a successful response from IMessageHandlerV2 to message.recipient, @@ -1312,9 +1424,7 @@ contract MessageTransmitterV2Test is TestUtils { vm.assume(_randomCaller != address(0)); _caller = _randomCaller; } else { - _caller = AddressUtils.bytes32ToAddress( - _msg._getDestinationCaller() - ); + _caller = _msg._getDestinationCaller().toAddress(); } // assert that a MessageReceive event was logged with expected message bytes diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index c107d76..21d1601 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -59,6 +59,8 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { using TypedMemView for bytes; using TypedMemView for bytes29; using BurnMessageV2 for bytes29; + using AddressUtils for address; + using AddressUtils for bytes32; // Constants uint32 remoteDomain = 1; @@ -98,9 +100,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes("") ); - remoteTokenMessengerAddr = AddressUtils.addressToBytes32( - remoteTokenMessenger - ); + remoteTokenMessengerAddr = remoteTokenMessenger.toBytes32(); ( uint32[] memory _remoteDomains, @@ -122,7 +122,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { localTokenMinter, address(localToken), remoteDomain, - AddressUtils.addressToBytes32(remoteTokenAddr) + remoteTokenAddr.toBytes32() ); localTokenMinter.addLocalTokenMessenger(address(localTokenMessenger)); @@ -1480,7 +1480,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { TokenMinter.mint.selector, remoteDomain, _burnToken, - AddressUtils.bytes32ToAddress(_mintRecipient), + _mintRecipient.toAddress(), _amount ); vm.mockCallRevert( @@ -1511,7 +1511,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( localTokenMessenger.messageBodyVersion(), - AddressUtils.addressToBytes32(remoteTokenAddr), + remoteTokenAddr.toBytes32(), _mintRecipient, _amount, _burnMessageSender, @@ -1821,12 +1821,48 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); } + 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 _maxFee, uint256 _feeExecuted, bytes calldata _hookData, uint32 _finalityThresholdExecuted @@ -1842,7 +1878,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _mintRecipient, _amount, _burnMessageSender, - _maxFee, + _feeExecuted, // maxFee _feeExecuted, 0, _hookData @@ -1896,7 +1932,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { TokenMinter.mint.selector, remoteDomain, _burnToken, - AddressUtils.bytes32ToAddress(_mintRecipient), + _mintRecipient.toAddress(), _amount ); vm.mockCallRevert( @@ -1920,7 +1956,6 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _mintRecipient, uint256 _amount, bytes32 _burnMessageSender, - uint256 _maxFee, uint256 _feeExecuted, bytes calldata _hookData, uint32 _finalityThresholdExecuted @@ -1928,8 +1963,9 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { vm.assume( _finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD ); - vm.assume(_feeExecuted < _amount); + vm.assume(_amount > 0); vm.assume(_feeExecuted > 0); + vm.assume(_feeExecuted < _amount); bytes memory _messageBody = _formatBurnMessageForReceive( localTokenMessenger.messageBodyVersion(), @@ -1937,7 +1973,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _mintRecipient, _amount, _burnMessageSender, - _maxFee, + _feeExecuted, // maxFee _feeExecuted, // non-zero fee, meaning we'll try to mint() on TokenMinterV2, passing in multiple recipients 0, _hookData @@ -1948,7 +1984,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { TokenMinterV2.mint.selector, remoteDomain, _burnToken, - AddressUtils.bytes32ToAddress(_mintRecipient), + _mintRecipient.toAddress(), feeRecipient, _amount - _feeExecuted, _feeExecuted @@ -1979,10 +2015,11 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ) public { vm.assume(_amount > 0); vm.assume(_feeExecuted < _amount); + vm.assume(_maxFee >= _feeExecuted); bytes memory _messageBody = _formatBurnMessageForReceive( localTokenMessenger.messageBodyVersion(), - AddressUtils.addressToBytes32(remoteTokenAddr), + remoteTokenAddr.toBytes32(), _mintRecipient, _amount, _burnMessageSender, @@ -2004,7 +2041,6 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _mintRecipient, uint256 _amount, bytes32 _burnMessageSender, - uint256 _maxFee, uint256 _feeExecuted, uint256 _expirationBlock, bytes calldata _hookData @@ -2015,11 +2051,11 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes memory _messageBody = _formatBurnMessageForReceive( localTokenMessenger.messageBodyVersion(), - AddressUtils.addressToBytes32(remoteTokenAddr), + remoteTokenAddr.toBytes32(), _mintRecipient, _amount, _burnMessageSender, - _maxFee, + _feeExecuted, // maxFee _feeExecuted, _expirationBlock, _hookData @@ -2046,7 +2082,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ) public { bytes memory _messageBody = _formatBurnMessageForReceive( localTokenMessenger.messageBodyVersion(), - AddressUtils.addressToBytes32(remoteTokenAddr), + remoteTokenAddr.toBytes32(), _mintRecipient, _amount, _burnMessageSender, @@ -2071,7 +2107,6 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _mintRecipient, uint256 _amount, bytes32 _burnMessageSender, - uint256 _maxFee, uint256 _expirationBlock, uint256 _feeExecuted, bytes calldata _hookData @@ -2082,11 +2117,11 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes memory _messageBody = _formatBurnMessageForReceive( localTokenMessenger.messageBodyVersion(), - AddressUtils.addressToBytes32(remoteTokenAddr), + remoteTokenAddr.toBytes32(), _mintRecipient, _amount, _burnMessageSender, - _maxFee, + _feeExecuted, // maxFee _feeExecuted, _expirationBlock, _hookData @@ -2107,7 +2142,6 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32 _mintRecipient, uint256 _amount, bytes32 _burnMessageSender, - uint256 _maxFee, uint256 _expirationBlock, uint256 _feeExecuted, bytes calldata _hookData @@ -2117,11 +2151,11 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes memory _messageBody = _formatBurnMessageForReceive( localTokenMessenger.messageBodyVersion(), - AddressUtils.addressToBytes32(remoteTokenAddr), + remoteTokenAddr.toBytes32(), _mintRecipient, _amount, _burnMessageSender, - _maxFee, + _feeExecuted, // maxFee _feeExecuted, _expirationBlock, _hookData @@ -2209,10 +2243,10 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes memory _expectedBurnMessage = BurnMessageV2 ._formatMessageForRelay( localTokenMessenger.messageBodyVersion(), // version - AddressUtils.addressToBytes32(address(localToken)), // burn token + address(localToken).toBytes32(), // burn token _mintRecipient, // mint recipient _amount, // amount - AddressUtils.addressToBytes32(_caller), // sender + _caller.toBytes32(), // sender _maxFee, // max fee _hookData ); @@ -2295,9 +2329,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes memory _messageBody ) internal { bytes29 _msg = _messageBody.ref(0); - address _mintRecipient = AddressUtils.bytes32ToAddress( - _msg._getMintRecipient() - ); + address _mintRecipient = _msg._getMintRecipient().toAddress(); uint256 _amount = _msg._getAmount(); uint256 _fee = _msg._getFeeExecuted(); @@ -2308,10 +2340,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); assertEq(uint256(_remoteDomain), uint256(remoteDomain)); assertEq(_sender, remoteTokenMessengerAddr); - assertEq( - AddressUtils.bytes32ToAddress(_msg._getBurnToken()), - remoteTokenAddr - ); + assertEq(_msg._getBurnToken().toAddress(), remoteTokenAddr); assertTrue(_fee == 0 || _amount > _fee); assertTrue(feeRecipient != address(0)); vm.assume(_mintRecipient != feeRecipient); diff --git a/test/v2/TokenMessengerV2IT.t.sol b/test/v2/TokenMessengerV2IT.t.sol index fbf139c..882bc41 100644 --- a/test/v2/TokenMessengerV2IT.t.sol +++ b/test/v2/TokenMessengerV2IT.t.sol @@ -39,6 +39,7 @@ contract TokenMessengerV2IntegrationTest is TestUtils { using TypedMemView for bytes29; using BurnMessageV2 for bytes29; using MessageV2 for bytes29; + using AddressUtils for address; // Constants uint32 localDomain = 0; @@ -178,9 +179,8 @@ contract TokenMessengerV2IntegrationTest is TestUtils { bytes32[] memory _remoteTokenMessengerAddresses = new bytes32[](1); _remoteDomains[0] = 0; // configure localDomain, on remoteDomain - _remoteTokenMessengerAddresses[0] = AddressUtils.addressToBytes32( - address(localTokenMessenger) - ); + _remoteTokenMessengerAddresses[0] = address(localTokenMessenger) + .toBytes32(); proxy = new AdminUpgradableProxy( address(remoteTokenMessengerImpl), @@ -203,7 +203,7 @@ contract TokenMessengerV2IntegrationTest is TestUtils { vm.startPrank(localDeployer); localTokenMessenger.addRemoteTokenMessenger( remoteDomain, - AddressUtils.addressToBytes32(address(remoteTokenMessenger)) + address(remoteTokenMessenger).toBytes32() ); vm.stopPrank(); @@ -212,7 +212,7 @@ contract TokenMessengerV2IntegrationTest is TestUtils { localTokenMinter, address(localToken), remoteDomain, - AddressUtils.addressToBytes32(address(remoteToken)) + address(remoteToken).toBytes32() ); // Link token pair on remote domain @@ -220,7 +220,7 @@ contract TokenMessengerV2IntegrationTest is TestUtils { remoteTokenMinter, address(remoteToken), localDomain, - AddressUtils.addressToBytes32(address(localToken)) + address(localToken).toBytes32() ); // Set maxBurnAmountPerMessage @@ -256,9 +256,9 @@ contract TokenMessengerV2IntegrationTest is TestUtils { localTokenMessenger.depositForBurn( localDepositAmount, remoteDomain, - AddressUtils.addressToBytes32(remoteMintRecipient), + remoteMintRecipient.toBytes32(), address(localToken), - AddressUtils.addressToBytes32(remoteMintRecipient), + remoteMintRecipient.toBytes32(), localFeeExecuted, 1000 ); @@ -312,15 +312,15 @@ contract TokenMessengerV2IntegrationTest is TestUtils { assertTrue(MessageV2._getNonce(_msg) > 0); assertEq( MessageV2._getSender(_msg), - AddressUtils.addressToBytes32(address(localTokenMessenger)) + address(localTokenMessenger).toBytes32() ); assertEq( MessageV2._getRecipient(_msg), - AddressUtils.addressToBytes32(address(remoteTokenMessenger)) + address(remoteTokenMessenger).toBytes32() ); assertEq( MessageV2._getDestinationCaller(_msg), - AddressUtils.addressToBytes32(remoteMintRecipient) + remoteMintRecipient.toBytes32() ); assertEq( uint256(MessageV2._getMinFinalityThreshold(_msg)), @@ -339,16 +339,16 @@ contract TokenMessengerV2IntegrationTest is TestUtils { ); assertEq( BurnMessageV2._getBurnToken(_burnMessageV2), - AddressUtils.addressToBytes32(address(localToken)) + address(localToken).toBytes32() ); assertEq( BurnMessageV2._getMintRecipient(_burnMessageV2), - AddressUtils.addressToBytes32(remoteMintRecipient) + remoteMintRecipient.toBytes32() ); assertEq(BurnMessageV2._getAmount(_burnMessageV2), localDepositAmount); assertEq( BurnMessageV2._getMessageSender(_burnMessageV2), - AddressUtils.addressToBytes32(localDepositor) + localDepositor.toBytes32() ); assertEq(BurnMessageV2._getMaxFee(_burnMessageV2), localFeeExecuted); assertEq( From e5e3b5a116602bccb4718ec808fbfa1643cb8c29 Mon Sep 17 00:00:00 2001 From: epoon-circle Date: Wed, 6 Nov 2024 05:31:40 -0700 Subject: [PATCH 28/40] Update Create2Factory deployAndMultiCall (#45) Update `deployAndCall` to `deployAndMultiCall` to allow for atomic bundling of all proxy deployment steps in a singular transaction. - Arrayified `data` param. - Updated tests. V2 deployment process doc: https://docs.google.com/document/d/1ayFWGNVmaO6jlLwgZsa0Yk-490CTBdiFASuY4jJzkE0/edit?pli=1&tab=t.0#heading=h.r8mn3sdqq2kb --- src/v2/Create2Factory.sol | 10 +++++++--- test/v2/Create2Factory.t.sol | 21 ++++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/v2/Create2Factory.sol b/src/v2/Create2Factory.sol index 3b9fdb8..0e5941a 100644 --- a/src/v2/Create2Factory.sol +++ b/src/v2/Create2Factory.sol @@ -14,6 +14,7 @@ * 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"; @@ -48,16 +49,19 @@ contract Create2Factory is Ownable { * @param data The data to call the implementation with * @return addr The deployed address */ - function deployAndCall( + function deployAndMultiCall( uint256 amount, bytes32 salt, bytes calldata bytecode, - bytes calldata data + bytes[] calldata data ) external payable onlyOwner returns (address addr) { // Deploy deterministically addr = Create2.deploy(amount, salt, bytecode); - Address.functionCall(addr, data); + uint256 dataLength = data.length; + for (uint256 i = 0; i < dataLength; ++i) { + Address.functionCall(addr, data[i]); + } } /** diff --git a/test/v2/Create2Factory.t.sol b/test/v2/Create2Factory.t.sol index 0f98707..51f5c30 100644 --- a/test/v2/Create2Factory.t.sol +++ b/test/v2/Create2Factory.t.sol @@ -65,18 +65,24 @@ contract Create2FactoryTest is Test { assertEq(MockInitializableImplementation(proxyAddr).num(), num); } - function testDeployAndCall( + function testDeployAndMultiCall( address addr, uint256 num, uint256 amount, bytes32 salt ) public { - // Construct initializer - bytes memory initializer = abi.encodeWithSelector( + // 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, @@ -88,11 +94,16 @@ contract Create2FactoryTest is Test { keccak256(bytecode) ); vm.deal(address(this), amount); - address proxyAddr = create2Factory.deployAndCall{value: amount}( + + // Expect calls + vm.expectCall(expectedAddr, initializer1); + vm.expectCall(expectedAddr, initializer2); + + address proxyAddr = create2Factory.deployAndMultiCall{value: amount}( amount, salt, bytecode, - initializer + data ); // Verify deterministic From 5ee9bbd27dbdc479c3d5dd2b024b87ecb1b167f3 Mon Sep 17 00:00:00 2001 From: ams9198 <111915188+ams9198@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:10:58 -0500 Subject: [PATCH 29/40] STABLE-7659 (follow-up): Add storage gaps (#46) --- src/roles/v2/AttestableV2.sol | 41 ++++++++++++++++++++++++++++++ src/roles/v2/Denylistable.sol | 7 +++++ src/v2/BaseMessageTransmitter.sol | 6 ++--- test/v2/MessageTransmitterV2.t.sol | 19 ++++++++++++++ test/v2/TokenMessengerV2.t.sol | 19 ++++++++++++++ 5 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 src/roles/v2/AttestableV2.sol 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 index c2f6f90..3f69d46 100644 --- a/src/roles/v2/Denylistable.sol +++ b/src/roles/v2/Denylistable.sol @@ -62,6 +62,13 @@ abstract contract Denylistable is Ownable2Step { // 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. diff --git a/src/v2/BaseMessageTransmitter.sol b/src/v2/BaseMessageTransmitter.sol index b84c009..7261d13 100644 --- a/src/v2/BaseMessageTransmitter.sol +++ b/src/v2/BaseMessageTransmitter.sol @@ -17,7 +17,7 @@ */ pragma solidity 0.7.6; -import {Attestable} from "../roles/Attestable.sol"; +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"; @@ -30,7 +30,7 @@ contract BaseMessageTransmitter is Initializable, Pausable, Rescuable, - Attestable + AttestableV2 { // ============ Events ============ /** @@ -58,7 +58,7 @@ contract BaseMessageTransmitter is * @param _localDomain Domain of chain on which the contract is deployed * @param _version Message Format version */ - constructor(uint32 _localDomain, uint32 _version) Attestable(msg.sender) { + constructor(uint32 _localDomain, uint32 _version) AttestableV2() { localDomain = _localDomain; version = _version; } diff --git a/test/v2/MessageTransmitterV2.t.sol b/test/v2/MessageTransmitterV2.t.sol index 60622f2..5998047 100644 --- a/test/v2/MessageTransmitterV2.t.sol +++ b/test/v2/MessageTransmitterV2.t.sol @@ -97,6 +97,25 @@ contract MessageTransmitterV2Test is TestUtils { 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), diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index 21d1601..b1a461f 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -153,6 +153,25 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { // 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), From 897f7f8b3e5f80ffcff8b19fee83742a7ca95771 Mon Sep 17 00:00:00 2001 From: epoon-circle Date: Wed, 13 Nov 2024 13:32:41 -0700 Subject: [PATCH 30/40] Stable 7722 v2 deployment scripts (#43) # Overview Updated deployment scripts and process documentation for V2 contracts as [documented](https://docs.google.com/document/d/1ayFWGNVmaO6jlLwgZsa0Yk-490CTBdiFASuY4jJzkE0/edit?usp=sharing). # Changes - Added new scripts for deploying and configuring the V2 contracts. - Added tests for the new scripts. - Updated the readme and Makefile for V2 deployment process. # Tests - Added new script tests. - Manually followed the deployment process locally. # Review - Help take a look to make sure all the env vars make sense. (Especially regarding all the owners/pks.) - Optionally, run through the process locally. --- Makefile | 46 ++- README.md | 175 +++++---- scripts/v2/1_deploy.s.sol | 274 ------------- scripts/v2/2_setupSecondAttester.s.sol | 63 --- scripts/v2/3_setupRemoteResources.s.sol | 108 ------ scripts/v2/4_rotateKeys.s.sol | 114 ------ scripts/v2/DeployAddressUtilsExternal.s.sol | 53 +++ scripts/v2/DeployImplementationsV2.s.sol | 108 ++++++ scripts/v2/DeployProxiesV2.s.sol | 364 ++++++++++++++++++ scripts/v2/RotateKeysV2.s.sol | 133 +++++++ scripts/v2/SetupRemoteResourcesV2.s.sol | 123 ++++++ test/scripts/1_deploy.t.sol | 105 ----- test/scripts/2_setupSecondAttester.t.sol | 37 -- test/scripts/ScriptV2TestUtils.sol | 171 -------- test/scripts/v2/DeployImplementationsV2.t.sol | 51 +++ test/scripts/v2/DeployProxiesV2.t.sol | 149 +++++++ .../RotateKeysV2.t.sol} | 19 +- test/scripts/v2/ScriptV2TestUtils.sol | 317 +++++++++++++++ .../SetupRemoteResourcesV2.t.sol} | 11 +- 19 files changed, 1433 insertions(+), 988 deletions(-) delete mode 100644 scripts/v2/1_deploy.s.sol delete mode 100644 scripts/v2/2_setupSecondAttester.s.sol delete mode 100644 scripts/v2/3_setupRemoteResources.s.sol delete mode 100644 scripts/v2/4_rotateKeys.s.sol create mode 100644 scripts/v2/DeployAddressUtilsExternal.s.sol create mode 100644 scripts/v2/DeployImplementationsV2.s.sol create mode 100644 scripts/v2/DeployProxiesV2.s.sol create mode 100644 scripts/v2/RotateKeysV2.s.sol create mode 100644 scripts/v2/SetupRemoteResourcesV2.s.sol delete mode 100644 test/scripts/1_deploy.t.sol delete mode 100644 test/scripts/2_setupSecondAttester.t.sol delete mode 100644 test/scripts/ScriptV2TestUtils.sol create mode 100644 test/scripts/v2/DeployImplementationsV2.t.sol create mode 100644 test/scripts/v2/DeployProxiesV2.t.sol rename test/scripts/{4_rotateKeys.t.sol => v2/RotateKeysV2.t.sol} (62%) create mode 100644 test/scripts/v2/ScriptV2TestUtils.sol rename test/scripts/{3_setupRemoteResources.t.sol => v2/SetupRemoteResourcesV2.t.sol} (79%) diff --git a/Makefile b/Makefile index a146992..cba6c8b 100644 --- a/Makefile +++ b/Makefile @@ -15,35 +15,41 @@ simulate-deploy: deploy: forge script scripts/v1/deploy.s.sol:DeployScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast -simulate-deploy-proxy-factory: - forge script scripts/DeployProxyFactory.s.sol:DeployProxyFactoryScript --rpc-url ${RPC_URL} --sender ${SENDER} +simulate-deploy-implementations-v2: + forge script scripts/v2/DeployImplementationsV2.s.sol:DeployImplementationsV2Script --rpc-url ${RPC_URL} --sender ${SENDER} -deploy-proxy-factory: - forge script scripts/DeployProxyFactory.s.sol:DeployProxyFactoryScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast +deploy-implementations-v2: + forge script scripts/v2/DeployImplementationsV2.s.sol:DeployImplementationsV2Script --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast -simulate-deployv2: - forge script scripts/v2/1_deploy.s.sol:DeployV2Script --rpc-url ${RPC_URL} --sender ${SENDER} +simulate-deploy-create2-factory: + forge script scripts/DeployCreate2Factory.s.sol:DeployCreate2FactoryScript --rpc-url ${RPC_URL} --sender ${SENDER} -deployv2: - forge script scripts/v2/1_deploy.s.sol:DeployV2Script --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast +deploy-create2-factory: + forge script scripts/DeployCreate2Factory.s.sol:DeployCreate2FactoryScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast -simulate-setup-second-attester: - forge script scripts/v2/2_setupSecondAttester.s.sol:SetupSecondAttesterScript --rpc-url ${RPC_URL} --sender ${SENDER} +simulate-deploy-proxies-v2: + forge script scripts/v2/DeployProxiesV2.s.sol:DeployProxiesV2Script --rpc-url ${RPC_URL} --sender ${SENDER} -setup-second-attester: - forge script scripts/v2/2_setupSecondAttester.s.sol:SetupSecondAttesterScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast +deploy-proxies-v2: + forge script scripts/v2/DeployProxiesV2.s.sol:DeployProxiesV2Script --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast -simulate-setup-remote-resources: - forge script scripts/v2/3_setupRemoteResources.s.sol:SetupRemoteResourcesScript --rpc-url ${RPC_URL} --sender ${SENDER} +simulate-setup-remote-resources-v2: + forge script scripts/v2/SetupRemoteResourcesV2.s.sol:SetupRemoteResourcesV2Script --rpc-url ${RPC_URL} --sender ${SENDER} -setup-remote-resources: - forge script scripts/v2/3_setupRemoteResources.s.sol:SetupRemoteResourcesScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast +setup-remote-resources-v2: + forge script scripts/v2/SetupRemoteResourcesV2.s.sol:SetupRemoteResourcesV2Script --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast -simulate-rotate-keys: - forge script scripts/v2/4_rotateKeys.s.sol:RotateKeysScript --rpc-url ${RPC_URL} --sender ${SENDER} +simulate-rotate-keys-v2: + forge script scripts/v2/RotateKeysV2.s.sol:RotateKeysV2Script --rpc-url ${RPC_URL} --sender ${SENDER} -rotate-keys: - forge script scripts/v2/4_rotateKeys.s.sol:RotateKeysScript --rpc-url ${RPC_URL} --sender ${SENDER} --broadcast +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 diff --git a/README.md b/README.md index 84239d7..be7f29a 100644 --- a/README.md +++ b/README.md @@ -96,117 +96,128 @@ The contracts are deployed using [Forge Scripts](https://book.getfoundry.sh/tuto ### V2 +#### Create2Factory + Deploy Create2Factory first if not yet deployed. -
    -
  1. - - Add the below environment variable to your [env](.env) file: - - `CREATE2_FACTORY_DEPLOYER_KEY`
  2. -
  3. - - Run `make simulate-deploy-create2-factory RPC_URL= SENDER=` to perform a dry run.
  4. -
  5. - - Run +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.
  6. -
+ to deploy the Create2Factory. -The contracts are deployed via `CREATE2` through Create2Factory. Follow the below steps to deploy the contracts: +#### V2 Implementation Contracts -1. Replace the environment variables in your [env](.env) file with the following: +Deploy the implementation contracts. - - `MESSAGE_TRANSMITTER_DEPLOYER_KEY` - - `TOKEN_MESSENGER_DEPLOYER_KEY` - - `TOKEN_MINTER_DEPLOYER_KEY` - - `TOKEN_CONTROLLER_DEPLOYER_KEY` - - `ATTESTER_ADDRESS` - - `USDC_CONTRACT_ADDRESS` - - `REMOTE_USDC_CONTRACT_ADDRESS` - - `MESSAGE_TRANSMITTER_PAUSER_ADDRESS` - - `TOKEN_MINTER_PAUSER_ADDRESS` - - `MESSAGE_TRANSMITTER_RESCUER_ADDRESS` - - `TOKEN_MESSENGER_RESCUER_ADDRESS` - - `TOKEN_MINTER_RESCUER_ADDRESS` - - `TOKEN_CONTROLLER_ADDRESS` - - `DOMAIN` - - `REMOTE_DOMAIN` - - `BURN_LIMIT_PER_MESSAGE` - - `CREATE2_FACTORY_ADDRESS` +1. Add the following [env](.env) variables + + - `CREATE2_FACTORY_CONTRACT_ADDRESS` + - `TOKEN_CONTROLLER_ADDRESS` + - `DOMAIN` + - `MESSAGE_BODY_VERSION` + - `VERSION` + - `IMPLEMENTATION_DEPLOYER_PRIVATE_KEY` + +2. Run `make simulate-deploy-implementations-v2 RPC_URL= SENDER=` to perform a dry run. - In addition, to link the remote bridge, one of two steps needs to be followed: +3. Run + ```make deploy-implementations-v2 RPC_URL= SENDER=``` + to deploy MessageTransmitterV2, TokenMinterV2, and TokenMessengerV2. - - 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. +#### V2 Proxies -2. Run `make simulate-deployv2 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 deployv2 RPC_URL= SENDER=` to deploy the contracts +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` -4. Replace the environment variables in your [env](.env) file with: + - `CREATE2_FACTORY_OWNER_KEY` + - `TOKEN_MINTER_V2_DEPLOYER_KEY` + - `TOKEN_CONTROLLER_KEY` - - `MESSAGE_TRANSMITTER_CONTRACT_ADDRESS` - - `MESSAGE_TRANSMITTER_DEPLOYER_KEY` - - `NEW_ATTESTER_MANAGER_ADDRESS` - - `SECOND_ATTESTER_ADDRESS` +2. Run `make simulate-deploy-proxies-v2 RPC_URL= SENDER=` to perform a dry run. -5. Run `make simulate-setup-second-attester RPC_URL= SENDER=` to perform a dry run of setting up the second attester. +3. Run `make deploy-proxies-v2 RPC_URL= SENDER=` to deploy the contracts -6. Run `make setup-second-attester RPC_URL= SENDER=` to setup the second attester. +4. ONLY perform steps 5-7 for additional remote resources NOT already configured above. -7. 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: +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_DEPLOYER_KEY` + - `TOKEN_MESSENGER_V2_OWNER_KEY` - `TOKEN_CONTROLLER_KEY` - - `REMOTE_TOKEN_MESSENGER_ADDRESS` - - `TOKEN_MINTER_CONTRACT_ADDRESS` - - `TOKEN_MESSENGER_CONTRACT_ADDRESS` - - `REMOTE_USDC_CONTRACT_ADDRESS` + - `TOKEN_MESSENGER_V2_CONTRACT_ADDRESS` + - `TOKEN_MINTER_V2_CONTRACT_ADDRESS` - `USDC_CONTRACT_ADDRESS` + - `REMOTE_USDC_CONTRACT_ADDRESS` - `REMOTE_DOMAIN` -8. Run `make simulate-setup-remote-resources RPC_URL= SENDER=` to perform a dry run of adding remote resources. +6. Run `make simulate-setup-remote-resources-v2 RPC_URL= SENDER=` to perform a dry run of adding remote resources. -9. Run `make setup-remote-resources RPC_URL= SENDER=` to setup the remote resources. +7. Run `make setup-remote-resources-v2 RPC_URL= SENDER=` to setup the remote resources. -10. Repeat steps 7-9 for all remote resources. This needs to be done for all existing remote chains at - contract setup except for the initial remote chain used in `1_deploy.s.sol`. +**[Remaining steps are only for mainnet]** -**[Only execute the following if replacing remote resources for an existing chain]** +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` -11. Replace the environment variables in your [env](.env) file with the following. We'll replace one set of remote resources for a given chain (e.g. changing the remote token messenger and remote usdc contract addresses) at a time so it will need to be repeated for each applicable chain. - - `TOKEN_MESSENGER_DEPLOYER_KEY` - - `TOKEN_CONTROLLER_KEY` - - `REMOTE_TOKEN_MESSENGER_ADDRESS` - - `REMOTE_TOKEN_MESSENGER_ADDRESS_DEPRECATED` - - `TOKEN_MINTER_CONTRACT_ADDRESS` - - `TOKEN_MESSENGER_CONTRACT_ADDRESS` - - `REMOTE_USDC_CONTRACT_ADDRESS` - - `REMOTE_USDC_CONTRACT_ADDRESS_DEPRECATED` - - `USDC_CONTRACT_ADDRESS` - - `REMOTE_DOMAIN` -12. Run `make simulate-replace-remote-resources RPC_URL= SENDER=` to perform a dry run of replacing remote resources. +9. Run `make simulate-rotate-keys-v2 RPC_URL= SENDER=` to perform a dry run of rotating the keys. -13. Run `make replace-remote-resources RPC_URL= SENDER=` to replace the remote resources. +10. Run `make rotate-keys-v2 RPC_URL= SENDER=` to rotate keys. -**[Remaining steps are only for mainnet]** +#### AddressUtilsExternal -14. Replace the environment variables in your [env](.env) file with: +Use Create2Factory to deploy the helper library to a deterministic address for easy integration. - - `MESSAGE_TRANSMITTER_CONTRACT_ADDRESS` - - `TOKEN_MESSENGER_CONTRACT_ADDRESS` - - `TOKEN_MINTER_CONTRACT_ADDRESS` - - `MESSAGE_TRANSMITTER_DEPLOYER_KEY` - - `TOKEN_MESSENGER_DEPLOYER_KEY` - - `TOKEN_MINTER_DEPLOYER_KEY` - - `MESSAGE_TRANSMITTER_NEW_OWNER_ADDRESS` - - `TOKEN_MESSENGER_NEW_OWNER_ADDRESS` - - `TOKEN_MINTER_NEW_OWNER_ADDRESS` - - `NEW_TOKEN_CONTROLLER_ADDRESS` +1. Set the following [env](.env) variables: + + - `CREATE2_FACTORY_CONTRACT_ADDRESS` + - `CREATE2_FACTORY_OWNER_KEY` -15. Run `make simulate-rotate-keys RPC_URL= SENDER=` to perform a dry run of rotating the keys. +2. Run `make simulate-deploy-address-utils-external RPC_URL= SENDER=` to perform a dry run. -16. Run `make rotate-keys RPC_URL= SENDER=` to rotate keys. +3. Run `make deploy-address-utils-external RPC_URL= SENDER=` to deploy. ## License diff --git a/scripts/v2/1_deploy.s.sol b/scripts/v2/1_deploy.s.sol deleted file mode 100644 index 0bf07bd..0000000 --- a/scripts/v2/1_deploy.s.sol +++ /dev/null @@ -1,274 +0,0 @@ -pragma solidity 0.7.6; - -import "forge-std/Script.sol"; -import "../../src/TokenMessenger.sol"; -import "../../src/TokenMinter.sol"; -import "../../src/MessageTransmitter.sol"; -import "../../src/messages/Message.sol"; - -contract DeployV2Script is Script { - // Expose for tests - MessageTransmitter public messageTransmitter; - TokenMessenger public tokenMessenger; - TokenMinter public tokenMinter; - - address private attesterAddress; - address private usdcContractAddress; - address private usdcRemoteContractAddress; - address private remoteTokenMessengerAddress; - address private tokenControllerAddress; - address private messageTransmitterPauserAddress; - address private tokenMinterPauserAddress; - address private messageTransmitterRescuerAddress; - address private tokenMessengerRescuerAddress; - address private tokenMinterRescuerAddress; - - uint32 private messageBodyVersion = 0; - uint32 private version = 0; - uint32 private domain; - uint32 private remoteDomain; - uint32 private maxMessageBodySize = 8192; - uint256 private burnLimitPerMessage; - - uint256 private messageTransmitterDeployerPrivateKey; - uint256 private tokenMessengerDeployerPrivateKey; - uint256 private tokenMinterDeployerPrivateKey; - uint256 private tokenControllerPrivateKey; - - /** - * @notice deploys Message Transmitter - * @param privateKey Private Key for signing the transactions - * @return MessageTransmitter instance - */ - function deployMessageTransmitter( - uint256 privateKey - ) private returns (MessageTransmitter) { - // Start recording transactions - vm.startBroadcast(privateKey); - - // Deploy MessageTransmitter - MessageTransmitter _messageTransmitter = new MessageTransmitter( - domain, - attesterAddress, - maxMessageBodySize, - version - ); - - // Add Pauser - _messageTransmitter.updatePauser(messageTransmitterPauserAddress); - - // Add Rescuer - _messageTransmitter.updateRescuer(messageTransmitterRescuerAddress); - - // Stop recording transactions - vm.stopBroadcast(); - return _messageTransmitter; - } - - /** - * @notice deploys TokenMessenger - * @param privateKey Private Key for signing the transactions - * @param messageTransmitterAddress Message Transmitter Contract address - * @return TokenMessenger instance - */ - function deployTokenMessenger( - uint256 privateKey, - address messageTransmitterAddress - ) private returns (TokenMessenger) { - // Start recording transations - vm.startBroadcast(privateKey); - - // Deploy TokenMessenger - TokenMessenger _tokenMessenger = new TokenMessenger( - messageTransmitterAddress, - messageBodyVersion - ); - - // Add Rescuer - _tokenMessenger.updateRescuer(tokenMessengerRescuerAddress); - - // Stop recording transations - vm.stopBroadcast(); - - return _tokenMessenger; - } - - /** - * @notice deploys TokenMinter - * @param privateKey Private Key for signing the transactions - * @param tokenMessengerAddress TokenMessenger Contract address - * @return TokenMinter instance - */ - function deployTokenMinter( - uint256 privateKey, - address tokenMessengerAddress - ) private returns (TokenMinter) { - // Start recording transations - vm.startBroadcast(privateKey); - - // Deploy TokenMinter - TokenMinter _tokenMinter = new TokenMinter(tokenControllerAddress); - - // Add Local TokenMessenger - _tokenMinter.addLocalTokenMessenger(tokenMessengerAddress); - - // Add Pauser - _tokenMinter.updatePauser(tokenMinterPauserAddress); - - // Add Rescuer - _tokenMinter.updateRescuer(tokenMinterRescuerAddress); - - // Stop recording transations - vm.stopBroadcast(); - - return _tokenMinter; - } - - /** - * @notice add local minter to the TokenMessenger - */ - function addMinterAddressToTokenMessenger( - TokenMessenger _tokenMessenger, - uint256 privateKey, - address minterAddress - ) private { - // Start recording transations - vm.startBroadcast(privateKey); - - _tokenMessenger.addLocalMinter(minterAddress); - - // Stop recording transations - vm.stopBroadcast(); - } - - /** - * @notice link current chain and remote chain tokens - */ - function linkTokenPair( - TokenMinter _tokenMinter, - uint256 privateKey - ) private { - // Start recording transations - vm.startBroadcast(privateKey); - - bytes32 remoteUsdcContractAddressInBytes32 = Message.addressToBytes32( - usdcRemoteContractAddress - ); - - _tokenMinter.setMaxBurnAmountPerMessage( - usdcContractAddress, - burnLimitPerMessage - ); - - _tokenMinter.linkTokenPair( - usdcContractAddress, - remoteDomain, - remoteUsdcContractAddressInBytes32 - ); - - // Stop recording transations - vm.stopBroadcast(); - } - - /** - * @notice add address of TokenMessenger deployed on another chain - */ - function addRemoteTokenMessenger( - TokenMessenger _tokenMessenger, - uint256 privateKey - ) private { - // Start recording transations - vm.startBroadcast(privateKey); - bytes32 remoteTokenMessengerAddressInBytes32 = Message.addressToBytes32( - remoteTokenMessengerAddress - ); - _tokenMessenger.addRemoteTokenMessenger( - remoteDomain, - remoteTokenMessengerAddressInBytes32 - ); - - // Stop recording transations - vm.stopBroadcast(); - } - - /** - * @notice initialize variables from environment - */ - function setUp() public { - messageTransmitterDeployerPrivateKey = vm.envUint( - "MESSAGE_TRANSMITTER_DEPLOYER_KEY" - ); - tokenMessengerDeployerPrivateKey = vm.envUint( - "TOKEN_MESSENGER_DEPLOYER_KEY" - ); - tokenMinterDeployerPrivateKey = vm.envUint("TOKEN_MINTER_DEPLOYER_KEY"); - tokenControllerPrivateKey = vm.envUint("TOKEN_CONTROLLER_DEPLOYER_KEY"); - - attesterAddress = vm.envAddress("ATTESTER_ADDRESS"); - usdcContractAddress = vm.envAddress("USDC_CONTRACT_ADDRESS"); - tokenControllerAddress = vm.envAddress("TOKEN_CONTROLLER_ADDRESS"); - burnLimitPerMessage = vm.envUint("BURN_LIMIT_PER_MESSAGE"); - - usdcRemoteContractAddress = vm.envAddress( - "REMOTE_USDC_CONTRACT_ADDRESS" - ); - - remoteTokenMessengerAddress = vm.envAddress( - "REMOTE_TOKEN_MESSENGER_ADDRESS" - ); - - domain = uint32(vm.envUint("DOMAIN")); - remoteDomain = uint32(vm.envUint("REMOTE_DOMAIN")); - - messageTransmitterPauserAddress = vm.envAddress( - "MESSAGE_TRANSMITTER_PAUSER_ADDRESS" - ); - tokenMinterPauserAddress = vm.envAddress("TOKEN_MINTER_PAUSER_ADDRESS"); - - messageTransmitterRescuerAddress = vm.envAddress( - "MESSAGE_TRANSMITTER_RESCUER_ADDRESS" - ); - tokenMessengerRescuerAddress = vm.envAddress( - "TOKEN_MESSENGER_RESCUER_ADDRESS" - ); - tokenMinterRescuerAddress = vm.envAddress( - "TOKEN_MINTER_RESCUER_ADDRESS" - ); - } - - /** - * @notice main function that will be run by forge - */ - function run() public { - // Deploy MessageTransmitter - messageTransmitter = deployMessageTransmitter( - messageTransmitterDeployerPrivateKey - ); - - // Deploy TokenMessenger - tokenMessenger = deployTokenMessenger( - tokenMessengerDeployerPrivateKey, - address(messageTransmitter) - ); - - // Deploy TokenMinter - tokenMinter = deployTokenMinter( - tokenMinterDeployerPrivateKey, - address(tokenMessenger) - ); - - // Add Local Minter - addMinterAddressToTokenMessenger( - tokenMessenger, - tokenMessengerDeployerPrivateKey, - address(tokenMinter) - ); - - // Link token pair and add remote token messenger - linkTokenPair(tokenMinter, tokenControllerPrivateKey); - addRemoteTokenMessenger( - tokenMessenger, - tokenMessengerDeployerPrivateKey - ); - } -} diff --git a/scripts/v2/2_setupSecondAttester.s.sol b/scripts/v2/2_setupSecondAttester.s.sol deleted file mode 100644 index 65fbc86..0000000 --- a/scripts/v2/2_setupSecondAttester.s.sol +++ /dev/null @@ -1,63 +0,0 @@ -pragma solidity 0.7.6; - -import "forge-std/Script.sol"; -import "../../src/MessageTransmitter.sol"; - -contract SetupSecondAttesterScript is Script { - address private secondAttesterAddress; - address private newAttesterManagerAddress; - address private messageTransmitterContractAddress; - - uint256 private attesterManagerPrivateKey; - - function configureSecondAttesterThenRotateAttesterManager( - uint256 privateKey - ) public { - MessageTransmitter messageTransmitter = MessageTransmitter( - messageTransmitterContractAddress - ); - - vm.startBroadcast(privateKey); - - // enable second attester - messageTransmitter.enableAttester(secondAttesterAddress); - - // setSignatureThreshold to 2 - messageTransmitter.setSignatureThreshold(2); - - // updateAttesterManager - messageTransmitter.updateAttesterManager(newAttesterManagerAddress); - - vm.stopBroadcast(); - } - - /** - * @notice initialize variables from environment - */ - function setUp() public { - messageTransmitterContractAddress = vm.envAddress( - "MESSAGE_TRANSMITTER_CONTRACT_ADDRESS" - ); - - attesterManagerPrivateKey = vm.envUint( - "MESSAGE_TRANSMITTER_DEPLOYER_KEY" - ); - - newAttesterManagerAddress = vm.envAddress( - "NEW_ATTESTER_MANAGER_ADDRESS" - ); - - secondAttesterAddress = vm.envAddress("SECOND_ATTESTER_ADDRESS"); - } - - /** - * @notice main function that will be run by forge - */ - function run() public { - setUp(); - - configureSecondAttesterThenRotateAttesterManager( - attesterManagerPrivateKey - ); - } -} diff --git a/scripts/v2/3_setupRemoteResources.s.sol b/scripts/v2/3_setupRemoteResources.s.sol deleted file mode 100644 index 3645278..0000000 --- a/scripts/v2/3_setupRemoteResources.s.sol +++ /dev/null @@ -1,108 +0,0 @@ -pragma solidity 0.7.6; - -import "forge-std/Script.sol"; -import "../../src/TokenMessenger.sol"; -import "../../src/TokenMinter.sol"; -import "../../src/messages/Message.sol"; - -contract SetupRemoteResourcesScript is Script { - address private usdcRemoteContractAddress; - address private usdcContractAddress; - address private remoteTokenMessengerAddress; - address private tokenMessengerContractAddress; - address private tokenMinterContractAddress; - - uint32 private remoteDomain; - - uint256 private tokenMessengerDeployerPrivateKey; - uint256 private tokenControllerPrivateKey; - - /** - * @notice link current chain and remote chain tokens - */ - function linkTokenPair( - TokenMinter tokenMinter, - uint256 privateKey - ) private { - // Start recording transactions - vm.startBroadcast(privateKey); - - bytes32 remoteUsdcContractAddressInBytes32 = Message.addressToBytes32( - usdcRemoteContractAddress - ); - - tokenMinter.linkTokenPair( - usdcContractAddress, - remoteDomain, - remoteUsdcContractAddressInBytes32 - ); - - // Stop recording transactions - vm.stopBroadcast(); - } - - /** - * @notice add address of TokenMessenger deployed on another chain - */ - function addRemoteTokenMessenger( - TokenMessenger tokenMessenger, - uint256 privateKey - ) private { - // Start recording transactions - vm.startBroadcast(privateKey); - bytes32 remoteTokenMessengerAddressInBytes32 = Message.addressToBytes32( - remoteTokenMessengerAddress - ); - tokenMessenger.addRemoteTokenMessenger( - remoteDomain, - remoteTokenMessengerAddressInBytes32 - ); - - // Stop recording transactions - vm.stopBroadcast(); - } - - /** - * @notice initialize variables from environment - */ - function setUp() public { - tokenMessengerDeployerPrivateKey = vm.envUint( - "TOKEN_MESSENGER_DEPLOYER_KEY" - ); - tokenControllerPrivateKey = vm.envUint("TOKEN_CONTROLLER_KEY"); - - tokenMessengerContractAddress = vm.envAddress( - "TOKEN_MESSENGER_CONTRACT_ADDRESS" - ); - tokenMinterContractAddress = vm.envAddress( - "TOKEN_MINTER_CONTRACT_ADDRESS" - ); - usdcContractAddress = vm.envAddress("USDC_CONTRACT_ADDRESS"); - usdcRemoteContractAddress = vm.envAddress( - "REMOTE_USDC_CONTRACT_ADDRESS" - ); - remoteTokenMessengerAddress = vm.envAddress( - "REMOTE_TOKEN_MESSENGER_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 { - TokenMessenger tokenMessenger = TokenMessenger( - tokenMessengerContractAddress - ); - TokenMinter tokenMinter = TokenMinter(tokenMinterContractAddress); - - // Link token pair and add remote token messenger - linkTokenPair(tokenMinter, tokenControllerPrivateKey); - addRemoteTokenMessenger( - tokenMessenger, - tokenMessengerDeployerPrivateKey - ); - } -} diff --git a/scripts/v2/4_rotateKeys.s.sol b/scripts/v2/4_rotateKeys.s.sol deleted file mode 100644 index 591f4a2..0000000 --- a/scripts/v2/4_rotateKeys.s.sol +++ /dev/null @@ -1,114 +0,0 @@ -pragma solidity 0.7.6; - -import "forge-std/Script.sol"; -import "../../src/TokenMessenger.sol"; -import "../../src/TokenMinter.sol"; -import "../../src/MessageTransmitter.sol"; - -contract RotateKeysScript is Script { - address private messageTransmitterContractAddress; - address private tokenMessengerContractAddress; - address private tokenMinterContractAddress; - address private newTokenControllerAddress; - - uint256 private messageTransmitterDeployerPrivateKey; - uint256 private tokenMessengerDeployerPrivateKey; - uint256 private tokenMinterDeployerPrivateKey; - - address private messageTransmitterNewOwnerAddress; - address private tokenMessengerNewOwnerAddress; - address private tokenMinterNewOwnerAddress; - - function rotateMessageTransmitterOwner(uint256 privateKey) public { - // load messageTransmitter - MessageTransmitter messageTransmitter = MessageTransmitter( - messageTransmitterContractAddress - ); - - vm.startBroadcast(privateKey); - - messageTransmitter.transferOwnership(messageTransmitterNewOwnerAddress); - - vm.stopBroadcast(); - } - - function rotateTokenMessengerOwner(uint256 privateKey) public { - TokenMessenger tokenMessenger = TokenMessenger( - tokenMessengerContractAddress - ); - - vm.startBroadcast(privateKey); - - tokenMessenger.transferOwnership(tokenMessengerNewOwnerAddress); - - vm.stopBroadcast(); - } - - function rotateTokenControllerThenTokenMinterOwner( - uint256 privateKey - ) public { - TokenMinter tokenMinter = TokenMinter(tokenMinterContractAddress); - - vm.startBroadcast(privateKey); - - tokenMinter.setTokenController(newTokenControllerAddress); - - tokenMinter.transferOwnership(tokenMinterNewOwnerAddress); - - vm.stopBroadcast(); - } - - /** - * @notice initialize variables from environment - */ - function setUp() public { - messageTransmitterContractAddress = vm.envAddress( - "MESSAGE_TRANSMITTER_CONTRACT_ADDRESS" - ); - - tokenMessengerContractAddress = vm.envAddress( - "TOKEN_MESSENGER_CONTRACT_ADDRESS" - ); - - tokenMinterContractAddress = vm.envAddress( - "TOKEN_MINTER_CONTRACT_ADDRESS" - ); - - messageTransmitterDeployerPrivateKey = vm.envUint( - "MESSAGE_TRANSMITTER_DEPLOYER_KEY" - ); - tokenMessengerDeployerPrivateKey = vm.envUint( - "TOKEN_MESSENGER_DEPLOYER_KEY" - ); - tokenMinterDeployerPrivateKey = vm.envUint("TOKEN_MINTER_DEPLOYER_KEY"); - - messageTransmitterNewOwnerAddress = vm.envAddress( - "MESSAGE_TRANSMITTER_NEW_OWNER_ADDRESS" - ); - - tokenMessengerNewOwnerAddress = vm.envAddress( - "TOKEN_MESSENGER_NEW_OWNER_ADDRESS" - ); - - tokenMinterNewOwnerAddress = vm.envAddress( - "TOKEN_MINTER_NEW_OWNER_ADDRESS" - ); - - newTokenControllerAddress = vm.envAddress( - "NEW_TOKEN_CONTROLLER_ADDRESS" - ); - } - - /** - * @notice main function that will be run by forge - */ - function run() public { - setUp(); - - rotateMessageTransmitterOwner(messageTransmitterDeployerPrivateKey); - rotateTokenMessengerOwner(tokenMessengerDeployerPrivateKey); - rotateTokenControllerThenTokenMinterOwner( - tokenMinterDeployerPrivateKey - ); - } -} diff --git a/scripts/v2/DeployAddressUtilsExternal.s.sol b/scripts/v2/DeployAddressUtilsExternal.s.sol new file mode 100644 index 0000000..06b5bd9 --- /dev/null +++ b/scripts/v2/DeployAddressUtilsExternal.s.sol @@ -0,0 +1,53 @@ +/* + * 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"; + +contract DeployAddressUtilsExternalScript is Script { + Create2Factory private create2Factory; + uint256 private create2FactoryOwnerKey; + + function deployAddressUtilsExternalScript() + private + returns (AddressUtilsExternal _addressUtilsExternal) + { + vm.startBroadcast(create2FactoryOwnerKey); + _addressUtilsExternal = AddressUtilsExternal( + create2Factory.deploy( + 0, + bytes32(0), + 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..4e9cb98 --- /dev/null +++ b/scripts/v2/DeployImplementationsV2.s.sol @@ -0,0 +1,108 @@ +/* + * 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 {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"; + +contract DeployImplementationsV2Script is Script { + // Expose for tests + MessageTransmitterV2 public messageTransmitterV2; + TokenMessengerV2 public tokenMessengerV2; + TokenMinterV2 public tokenMinterV2; + address public expectedMessageTransmitterV2ProxyAddress; + + address private factoryAddress; + address private tokenControllerAddress; + uint32 private messageBodyVersion; + uint32 private version; + uint32 private domain; + uint256 private implementationDeployerPrivateKey; + + function deployImplementationsV2( + uint256 privateKey + ) private returns (MessageTransmitterV2, TokenMinterV2, TokenMessengerV2) { + // Calculate MessageTransmitterV2 proxy address + expectedMessageTransmitterV2ProxyAddress = vm.computeCreate2Address( + keccak256(type(MessageTransmitterV2).creationCode), + keccak256( + abi.encodePacked( + type(AdminUpgradableProxy).creationCode, + abi.encode(factoryAddress, factoryAddress, "") + ) + ), + factoryAddress + ); + + // Start recording transactions + vm.startBroadcast(privateKey); + + // Deploy MessageTransmitterV2 implementation + MessageTransmitterV2 messageTransmitterV2Implementation = new MessageTransmitterV2( + domain, + version + ); + + // Deploy TokenMinter + TokenMinterV2 tokenMinterV2Implementation = new TokenMinterV2( + tokenControllerAddress + ); + + // Deploy TokenMessengerV2 + TokenMessengerV2 tokenMessengerV2Implementation = new TokenMessengerV2( + expectedMessageTransmitterV2ProxyAddress, + messageBodyVersion + ); + + // Stop recording transactions + vm.stopBroadcast(); + return ( + messageTransmitterV2Implementation, + tokenMinterV2Implementation, + tokenMessengerV2Implementation + ); + } + + /** + * @notice initialize variables from environment + */ + function setUp() public { + factoryAddress = vm.envAddress("CREATE2_FACTORY_CONTRACT_ADDRESS"); + tokenControllerAddress = vm.envAddress("TOKEN_CONTROLLER_ADDRESS"); + domain = uint32(vm.envUint("DOMAIN")); + messageBodyVersion = uint32(vm.envUint("MESSAGE_BODY_VERSION")); + version = uint32(vm.envUint("VERSION")); + implementationDeployerPrivateKey = vm.envUint( + "IMPLEMENTATION_DEPLOYER_PRIVATE_KEY" + ); + } + + /** + * @notice main function that will be run by forge + */ + function run() public { + ( + messageTransmitterV2, + tokenMinterV2, + tokenMessengerV2 + ) = deployImplementationsV2(implementationDeployerPrivateKey); + } +} diff --git a/scripts/v2/DeployProxiesV2.s.sol b/scripts/v2/DeployProxiesV2.s.sol new file mode 100644 index 0000000..19b9be4 --- /dev/null +++ b/scripts/v2/DeployProxiesV2.s.sol @@ -0,0 +1,364 @@ +/* + * 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"; + +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 tokenMinterV2DeployerPrivateKey; + 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, + keccak256(type(MessageTransmitterV2).creationCode), // TODO: Verify salt + 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, + "" + ); + + // Construct initializer + bytes32[] memory remoteTokenMessengerAddresses = new bytes32[]( + remoteDomains.length + ); + uint256 remoteDomainsLength = remoteDomains.length; + for (uint256 i = 0; i < remoteDomainsLength; ++i) { + remoteTokenMessengerAddresses[i] = remoteTokenMessengerV2Addresses[ + i + ]; + } + 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, + keccak256(type(TokenMessengerV2).creationCode), // TODO: Verify salt + 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"); + tokenMinterV2DeployerPrivateKey = vm.envUint( + "TOKEN_MINTER_V2_DEPLOYER_KEY" + ); + tokenControllerPrivateKey = vm.envUint("TOKEN_CONTROLLER_KEY"); + + remoteTokenMessengerV2Addresses = vm.envBytes32( + "REMOTE_TOKEN_MESSENGER_V2_ADDRESSES", + "," + ); + } + + /** + * @notice main function that will be run by forge + */ + function run() public { + messageTransmitterV2 = deployMessageTransmitterV2( + create2Factory, + create2FactoryOwnerPrivateKey + ); + + tokenMessengerV2 = deployTokenMessengerV2( + create2Factory, + create2FactoryOwnerPrivateKey, + address(tokenMinterV2) + ); + + addMessengerPauserRescuerToTokenMinterV2( + tokenMinterV2DeployerPrivateKey, + 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/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/test/scripts/1_deploy.t.sol b/test/scripts/1_deploy.t.sol deleted file mode 100644 index a9747ff..0000000 --- a/test/scripts/1_deploy.t.sol +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 DeployTest is ScriptV2TestUtils { - function setUp() public { - _deploy(); - } - - function testDeployMessageTransmitter() public { - // domain - assertEq(messageTransmitter.localDomain(), uint256(sourceDomain)); - - // attester - assertEq(messageTransmitter.attesterManager(), deployer); - assertTrue(messageTransmitter.isEnabledAttester(deployer)); - - // maxMessageBodySize - assertEq(messageTransmitter.maxMessageBodySize(), maxMessageBodySize); - - // version - assertEq(messageTransmitter.version(), uint256(version)); - - // pauser - assertEq(messageTransmitter.pauser(), pauser); - - // rescuer - assertEq(messageTransmitter.rescuer(), rescuer); - } - - function testDeployTokenMessenger() public { - // message transmitter - assertEq( - address(tokenMessenger.localMessageTransmitter()), - address(messageTransmitter) - ); - - // message body version - assertEq( - tokenMessenger.messageBodyVersion(), - uint256(_messageBodyVersion) - ); - - // rescuer - assertEq(tokenMessenger.rescuer(), rescuer); - } - - function testDeployTokenMinter() public { - // token controller - assertEq(tokenMinter.tokenController(), deployer); - - // token messenger - assertEq(tokenMinter.localTokenMessenger(), address(tokenMessenger)); - - // pauser - assertEq(tokenMinter.pauser(), pauser); - - // rescuer - assertEq(tokenMinter.rescuer(), rescuer); - } - - function testAddMinterAddressToTokenMessenger() public { - assertEq(address(tokenMessenger.localMinter()), address(tokenMinter)); - } - - function testLinkTokenPair() public { - // max burn per msg - assertEq( - tokenMinter.burnLimitsPerMessage(token), - maxBurnAmountPerMessage - ); - - // linked token pair - bytes32 remoteKey = keccak256( - abi.encodePacked( - destinationDomain, - bytes32(uint256(uint160(remoteToken))) - ) - ); - assertEq(tokenMinter.remoteTokensToLocalTokens(remoteKey), token); - } - - function testAddRemoteTokenMessenger() public { - assertEq( - tokenMessenger.remoteTokenMessengers(destinationDomain), - bytes32(uint256(uint160(remoteTokenMessengerAddress))) - ); - } -} diff --git a/test/scripts/2_setupSecondAttester.t.sol b/test/scripts/2_setupSecondAttester.t.sol deleted file mode 100644 index 7310ffb..0000000 --- a/test/scripts/2_setupSecondAttester.t.sol +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 SetupSecondAttesterTest is ScriptV2TestUtils { - function setUp() public { - _deploy(); - _setupSecondAttester(); - } - - function testConfigureSecondAttester() public { - // second attester enabled - assertTrue(messageTransmitter.isEnabledAttester(secondAttester)); - - // sig threshold - assertEq(messageTransmitter.signatureThreshold(), 2); - - // attester manager, didn't change - assertEq(messageTransmitter.attesterManager(), deployer); - } -} diff --git a/test/scripts/ScriptV2TestUtils.sol b/test/scripts/ScriptV2TestUtils.sol deleted file mode 100644 index 167dc6d..0000000 --- a/test/scripts/ScriptV2TestUtils.sol +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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 {DeployV2Script} from "../../scripts/v2/1_deploy.s.sol"; -import {SetupSecondAttesterScript} from "../../scripts/v2/2_setupSecondAttester.s.sol"; -import {SetupRemoteResourcesScript} from "../../scripts/v2/3_setupRemoteResources.s.sol"; -import {RotateKeysScript} from "../../scripts/v2/4_rotateKeys.s.sol"; -import {MessageTransmitter} from "../../src/MessageTransmitter.sol"; -import {TokenMessenger} from "../../src/TokenMessenger.sol"; -import {TokenMinter} from "../../src/TokenMinter.sol"; -import {MockMintBurnToken} from "../mocks/MockMintBurnToken.sol"; -import {TestUtils} from "../TestUtils.sol"; - -contract ScriptV2TestUtils is TestUtils { - uint32 _messageBodyVersion = 0; - address token; - address remoteToken; - address remoteTokenMessengerAddress; - uint256 deployerPK; - address deployer; - address pauser; - address rescuer; - MessageTransmitter messageTransmitter; - TokenMessenger tokenMessenger; - TokenMinter tokenMinter; - - uint32 anotherRemoteDomain = 2; - address anotherRemoteToken; - address anotherRemoteTokenMessengerAddress; - - uint256 newOwnerPK; - address newOwner; - - function _deploy() internal { - token = address(new MockMintBurnToken()); - remoteToken = address(new MockMintBurnToken()); - - deployerPK = uint256(keccak256("DEPLOYTEST_DEPLOYER_PK")); - deployer = vm.addr(deployerPK); - pauser = vm.addr(uint256(keccak256("DEPLOYTEST_PAUSER_PK"))); - rescuer = vm.addr(uint256(keccak256("DEPLOYTEST_RESCUER_PK"))); - - // Override env vars - vm.setEnv("MESSAGE_TRANSMITTER_DEPLOYER_KEY", vm.toString(deployerPK)); - vm.setEnv("TOKEN_MESSENGER_DEPLOYER_KEY", vm.toString(deployerPK)); - vm.setEnv("TOKEN_MINTER_DEPLOYER_KEY", vm.toString(deployerPK)); - vm.setEnv("TOKEN_CONTROLLER_DEPLOYER_KEY", vm.toString(deployerPK)); - - vm.setEnv("ATTESTER_ADDRESS", vm.toString(deployer)); - vm.setEnv("USDC_CONTRACT_ADDRESS", vm.toString(token)); - vm.setEnv("TOKEN_CONTROLLER_ADDRESS", vm.toString(deployer)); - vm.setEnv( - "BURN_LIMIT_PER_MESSAGE", - vm.toString(maxBurnAmountPerMessage) - ); - - vm.setEnv("REMOTE_USDC_CONTRACT_ADDRESS", vm.toString(remoteToken)); - - remoteTokenMessengerAddress = vm.addr( - uint256(keccak256("REMOTE_TOKEN_MESSENGER_ADDRESS")) - ); - vm.setEnv( - "REMOTE_TOKEN_MESSENGER_ADDRESS", - vm.toString(remoteTokenMessengerAddress) - ); - - vm.setEnv("DOMAIN", vm.toString(uint256(sourceDomain))); - vm.setEnv("REMOTE_DOMAIN", vm.toString(uint256(destinationDomain))); - - vm.setEnv("MESSAGE_TRANSMITTER_PAUSER_ADDRESS", vm.toString(pauser)); - vm.setEnv("TOKEN_MINTER_PAUSER_ADDRESS", vm.toString(pauser)); - - vm.setEnv("MESSAGE_TRANSMITTER_RESCUER_ADDRESS", vm.toString(rescuer)); - vm.setEnv("TOKEN_MESSENGER_RESCUER_ADDRESS", vm.toString(rescuer)); - vm.setEnv("TOKEN_MINTER_RESCUER_ADDRESS", vm.toString(rescuer)); - - DeployV2Script deployScript = new DeployV2Script(); - deployScript.setUp(); - deployScript.run(); - - messageTransmitter = deployScript.messageTransmitter(); - tokenMessenger = deployScript.tokenMessenger(); - tokenMinter = deployScript.tokenMinter(); - } - - function _setupSecondAttester() internal { - vm.setEnv( - "MESSAGE_TRANSMITTER_CONTRACT_ADDRESS", - vm.toString(address(messageTransmitter)) - ); - // [SKIP] Use same MESSAGE_TRANSMITTER_DEPLOYER_KEY - // Use same attester manager, deployer - vm.setEnv("NEW_ATTESTER_MANAGER_ADDRESS", vm.toString(deployer)); - vm.setEnv("SECOND_ATTESTER_ADDRESS", vm.toString(secondAttester)); - - SetupSecondAttesterScript setupSecondAttesterScript = new SetupSecondAttesterScript(); - setupSecondAttesterScript.setUp(); - setupSecondAttesterScript.run(); - } - - function _setupRemoteResources() internal { - // [SKIP] Use same TOKEN_MESSENGER_DEPLOYER_KEY - // Use same TOKEN_CONTROLLER_DEPLOYER_KEY as TOKEN_CONTROLLER_KEY - vm.setEnv("TOKEN_CONTROLLER_KEY", vm.toString(deployerPK)); - vm.setEnv( - "TOKEN_MESSENGER_CONTRACT_ADDRESS", - vm.toString(address(tokenMessenger)) - ); - vm.setEnv( - "TOKEN_MINTER_CONTRACT_ADDRESS", - vm.toString(address(tokenMinter)) - ); - vm.setEnv("USDC_CONTRACT_ADDRESS", vm.toString(token)); - vm.setEnv( - "REMOTE_USDC_CONTRACT_ADDRESS", - vm.toString(anotherRemoteToken) - ); - - anotherRemoteTokenMessengerAddress = vm.addr( - uint256(keccak256("ANOTHER_REMOTE_TOKEN_MESSENGER_ADDRESS")) - ); - vm.setEnv( - "REMOTE_TOKEN_MESSENGER_ADDRESS", - vm.toString(anotherRemoteTokenMessengerAddress) - ); - vm.setEnv("REMOTE_DOMAIN", vm.toString(uint256(anotherRemoteDomain))); - - SetupRemoteResourcesScript setupRemoteResourcesScript = new SetupRemoteResourcesScript(); - setupRemoteResourcesScript.setUp(); - setupRemoteResourcesScript.run(); - } - - function _rotateKeys() internal { - // [SKIP] Use same MESSAGE_TRANSMITTER_CONTRACT_ADDRESS - // [SKIP] Use same TOKEN_MESSENGER_CONTRACT_ADDRESS - // [SKIP] Use same TOKEN_MINTER_CONTRACT_ADDRESS - // [SKIP] Use same MESSAGE_TRANSMITTER_DEPLOYER_KEY - // [SKIP] Use same TOKEN_MESSENGER_DEPLOYER_KEY - // [SKIP] Use same TOKEN_MINTER_DEPLOYER_KEY - - newOwnerPK = uint256(keccak256("ROTATEKEYSTEST_NEW_OWNER")); - newOwner = vm.addr(newOwnerPK); - - vm.setEnv( - "MESSAGE_TRANSMITTER_NEW_OWNER_ADDRESS", - vm.toString(newOwner) - ); - vm.setEnv("TOKEN_MESSENGER_NEW_OWNER_ADDRESS", vm.toString(newOwner)); - vm.setEnv("TOKEN_MINTER_NEW_OWNER_ADDRESS", vm.toString(newOwner)); - vm.setEnv("NEW_TOKEN_CONTROLLER_ADDRESS", vm.toString(newOwner)); - - RotateKeysScript rotateKeysScript = new RotateKeysScript(); - rotateKeysScript.setUp(); - rotateKeysScript.run(); - } -} diff --git a/test/scripts/v2/DeployImplementationsV2.t.sol b/test/scripts/v2/DeployImplementationsV2.t.sol new file mode 100644 index 0000000..29a79d6 --- /dev/null +++ b/test/scripts/v2/DeployImplementationsV2.t.sol @@ -0,0 +1,51 @@ +/* + * 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); + + // 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..8ea2395 --- /dev/null +++ b/test/scripts/v2/DeployProxiesV2.t.sol @@ -0,0 +1,149 @@ +/* + * 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"; + +contract DeployProxiesV2Test is ScriptV2TestUtils { + DeployProxiesV2Script deployProxiesV2Script; + + function setUp() public { + _deployCreate2Factory(); + _deployImplementations(); + _deployProxies(); + deployProxiesV2Script = new DeployProxiesV2Script(); + } + + function testDeployMessageTransmitterV2() public { + // create2 address + address predicted = create2Factory.computeAddress( + keccak256(type(MessageTransmitterV2).creationCode), + 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( + keccak256(type(TokenMessengerV2).creationCode), + 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]; + assertEq( + tokenMessengerV2.remoteTokenMessengers(remoteDomain), + bytes32(uint256(uint160(address(remoteTokenMessengerV2s[i])))) + ); + } + // 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/4_rotateKeys.t.sol b/test/scripts/v2/RotateKeysV2.t.sol similarity index 62% rename from test/scripts/4_rotateKeys.t.sol rename to test/scripts/v2/RotateKeysV2.t.sol index 64fdc1b..edfe9c6 100644 --- a/test/scripts/4_rotateKeys.t.sol +++ b/test/scripts/v2/RotateKeysV2.t.sol @@ -20,22 +20,23 @@ import {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; contract RotateKeysTest is ScriptV2TestUtils { function setUp() public { - _deploy(); - _setupSecondAttester(); + _deployCreate2Factory(); + _deployImplementations(); + _deployProxies(); _setupRemoteResources(); _rotateKeys(); } - function testRotateMessageTransmitterOwner() public { - assertEq(messageTransmitter.pendingOwner(), newOwner); + function testRotateMessageTransmitterV2Owner() public { + assertEq(messageTransmitterV2.pendingOwner(), newOwner); } - function testRotateTokenMessengerOwner() public { - assertEq(tokenMessenger.pendingOwner(), newOwner); + function testRotateTokenMessengerV2Owner() public { + assertEq(tokenMessengerV2.pendingOwner(), newOwner); } - function testRotateTokenControllerThenTokenMinterOwner() public { - assertEq(tokenMinter.tokenController(), newOwner); - assertEq(tokenMinter.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..fac4ca1 --- /dev/null +++ b/test/scripts/v2/ScriptV2TestUtils.sol @@ -0,0 +1,317 @@ +/* + * 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 implDeployerPK; + 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; + 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 { + implDeployerPK = uint256(keccak256("DEPLOYTEST_IMPL_DEPLOYER_PK")); + + vm.setEnv( + "CREATE2_FACTORY_CONTRACT_ADDRESS", + vm.toString(address(create2Factory)) + ); + 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))); + vm.setEnv( + "IMPLEMENTATION_DEPLOYER_PRIVATE_KEY", + vm.toString(implDeployerPK) + ); + + 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])) + ) + ) + ); + 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_DEPLOYER_KEY", vm.toString(implDeployerPK)); + 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)); + + // Use same TOKEN_CONTROLLER_DEPLOYER_KEY as TOKEN_CONTROLLER_KEY + vm.setEnv("TOKEN_CONTROLLER_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_MESSENGER_V2_OWNER_KEY", vm.toString(deployerPK)); + vm.setEnv("TOKEN_MINTER_V2_OWNER_KEY", vm.toString(implDeployerPK)); + + 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/3_setupRemoteResources.t.sol b/test/scripts/v2/SetupRemoteResourcesV2.t.sol similarity index 79% rename from test/scripts/3_setupRemoteResources.t.sol rename to test/scripts/v2/SetupRemoteResourcesV2.t.sol index a5bfc3d..b6b7c2c 100644 --- a/test/scripts/3_setupRemoteResources.t.sol +++ b/test/scripts/v2/SetupRemoteResourcesV2.t.sol @@ -20,8 +20,9 @@ import {ScriptV2TestUtils} from "./ScriptV2TestUtils.sol"; contract SetupRemoteResourcesTest is ScriptV2TestUtils { function setUp() public { - _deploy(); - _setupSecondAttester(); + _deployCreate2Factory(); + _deployImplementations(); + _deployProxies(); _setupRemoteResources(); } @@ -32,13 +33,13 @@ contract SetupRemoteResourcesTest is ScriptV2TestUtils { bytes32(uint256(uint160(anotherRemoteToken))) ) ); - assertEq(tokenMinter.remoteTokensToLocalTokens(remoteKey), token); + assertEq(tokenMinterV2.remoteTokensToLocalTokens(remoteKey), token); } function testAddRemoteTokenMessenger() public { assertEq( - tokenMessenger.remoteTokenMessengers(anotherRemoteDomain), - bytes32(uint256(uint160(anotherRemoteTokenMessengerAddress))) + tokenMessengerV2.remoteTokenMessengers(anotherRemoteDomain), + bytes32(uint256(uint160(address(tokenMessengerV2)))) ); } } From 2ae9be392547158592181169edb8ac657c0b3334 Mon Sep 17 00:00:00 2001 From: tongshi Date: Thu, 21 Nov 2024 13:14:08 -0800 Subject: [PATCH 31/40] [STABLE-7559]: Migrate from Slither to Mythril for static analysis (#47) ### Summary Migrate from Slither to Mythril for static analysis ### Detail - update Makefile command and update CI - remove Slither relevant configs and add Mythril config - update Readme -- *story*: https://circlepay.atlassian.net/browse/STABLE-7559 --- .github/workflows/ci.yml | 51 ++++++++++++++++++++++++++++++++++++---- Makefile | 21 +++++++++++++---- README.md | 2 +- mythril.config.json | 8 +++++++ requirements.txt | 1 - slither.config.json | 9 ------- 6 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 mythril.config.json delete mode 100644 slither.config.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c243576..da2e374 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.10' - name: Install Node uses: actions/setup-node@v4 @@ -38,10 +38,53 @@ jobs: - name: Run v2 Integration Tests run: make anvil-test-v2 - - name: Run Slither - uses: crytic/slither-action@v0.3.0 + 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/Makefile b/Makefile index cba6c8b..81d4d62 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ deploy-address-utils-external: 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 @@ -71,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 be7f29a..b5c663e 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ 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 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 e65806c..68b3142 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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/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 From 45dc74700618e892cf5c297448ae6a34674ee930 Mon Sep 17 00:00:00 2001 From: pthakur-circle Date: Thu, 5 Dec 2024 13:01:44 -0500 Subject: [PATCH 32/40] feat(ci): olympix scan integration (#49) Summary: Introduces a new Github Action workflow to integrate the Olympix Static Analysis tool for Solidity code scanning. - Adds .github/workflows/ci-olympix.yml that uses a reusable workflow from [security-seceng-templates](https://github.com/circlefin/security-seceng-templates/blob/master/.github/workflows/olympix_scan.yml) - Workflow is triggered on every pull request as well as scheduled on a weekly basis.(Monday) --- .github/workflows/ci-olympix.yml | 13 +++++++++++++ .vscode/settings.json | 22 ++++++++++++---------- README.md | 6 ++++++ 3 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/ci-olympix.yml 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/.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/README.md b/README.md index b5c663e..55f4a82 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,12 @@ Run `make analyze-{message-transmitter | message-transmitter-v2 | token-messenge 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 From cc83ccbfc477e61d2da1d2cf75d355d161a324fd Mon Sep 17 00:00:00 2001 From: ams9198 Date: Fri, 6 Dec 2024 13:19:07 -0800 Subject: [PATCH 33/40] [No ticket] Allow fee collection on finalized message codepath (#48) ### Summary ~Note: Leaving as a draft for now for early feedback; will flip it to a regular PR later.~ This adds fee collection support to the finalized message handling codepath in TokenMessengerV2. This is helpful to collect fees on re-signed messages that have expired, but originally consumed an allowance. Changes: - Unpack and validate the `feeExecuted` parameter on both finalized and unfinalized message handling codepaths - Unpack and validate the `expirationBlock` parameter on both finalized and unfinalized message handling codepaths. Note that for re-signed messages, `expirationBlock` should be set to 0 at launch. ### Testing The first commit adds failing tests; the 2nd commit updates the implementation such that they pass. The latter commits refactor slightly and add an additional test. To test, run the unit and integration tests. --- src/v2/TokenMessengerV2.sol | 100 ++++---- test/v2/TokenMessengerV2.t.sol | 436 ++++++++++++++++++++++++++++++++- 2 files changed, 470 insertions(+), 66 deletions(-) diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol index 6bcccd1..b9e2ff6 100644 --- a/src/v2/TokenMessengerV2.sol +++ b/src/v2/TokenMessengerV2.sol @@ -247,17 +247,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { onlyRemoteTokenMessenger(remoteDomain, sender) returns (bool) { - // Validate finalized message - bytes29 _msg = messageBody.ref(0); - ( - address _mintRecipient, - bytes32 _burnToken, - uint256 _amount - ) = _validateFinalizedMessage(_msg); - - _mintAndWithdraw(remoteDomain, _burnToken, _mintRecipient, _amount, 0); - - return true; + return _handleReceiveMessage(messageBody.ref(0), remoteDomain); } /** @@ -291,24 +281,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { "Unsupported finality threshold" ); - // Validate message - bytes29 _msg = messageBody.ref(0); - ( - address _mintRecipient, - bytes32 _burnToken, - uint256 _amount, - uint256 _fee - ) = _validateUnfinalizedMessage(_msg); - - _mintAndWithdraw( - remoteDomain, - _burnToken, - _mintRecipient, - _amount - _fee, - _fee - ); - - return true; + return _handleReceiveMessage(messageBody.ref(0), remoteDomain); } // ============ Internal Utils ============ @@ -379,46 +352,51 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { } /** - * @notice Validates a finalized BurnMessage and unpacks relevant message fields. - * @dev Reverts if the BurnMessage is malformed - * @dev Reverts if the BurnMessage version isn't supported - * @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 + * @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 _validateFinalizedMessage( - bytes29 _msg - ) - internal - view - returns (address _mintRecipient, bytes32 _burnToken, uint256 _amount) - { - _msg._validateBurnMessageFormat(); - require( - _msg._getVersion() == messageBodyVersion, - "Invalid message body version" - ); + function _handleReceiveMessage( + bytes29 _msg, + uint32 _remoteDomain + ) internal returns (bool) { + // Validate message and unpack fields + ( + address _mintRecipient, + bytes32 _burnToken, + uint256 _amount, + uint256 _fee + ) = _validatedReceivedMessage(_msg); - return ( - _msg._getMintRecipient().toAddress(), - _msg._getBurnToken(), - _msg._getAmount() + // Mint tokens + _mintAndWithdraw( + _remoteDomain, + _burnToken, + _mintRecipient, + _amount - _fee, + _fee ); + + return true; } /** - * @notice Validates a finalized BurnMessage and unpacks relevant message fields. + * @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 message is expired - * @dev Reverts if the fee executed exceeds the amount + * @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 _validateUnfinalizedMessage( + function _validatedReceivedMessage( bytes29 _msg ) internal @@ -430,7 +408,11 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { uint256 _fee ) { - (_mintRecipient, _burnToken, _amount) = _validateFinalizedMessage(_msg); + _msg._validateBurnMessageFormat(); + require( + _msg._getVersion() == messageBodyVersion, + "Invalid message body version" + ); // Enforce message expiration uint256 _expirationBlock = _msg._getExpirationBlock(); @@ -440,8 +422,12 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { ); // 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/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index b1a461f..e830ce5 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -1438,6 +1438,187 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); } + 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, @@ -1475,8 +1656,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); } - function testHandleReceiveFinalizedMessage_revertsIfMintReverts( - bytes32 _burnToken, + function testHandleReceiveFinalizedMessage_revertsIfMintRevertsWithZeroFees( bytes32 _mintRecipient, uint256 _amount, bytes32 _burnMessageSender, @@ -1484,32 +1664,36 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes calldata _hookData, uint32 _finalityThresholdExecuted ) public { - bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( + vm.assume(_finalityThresholdExecuted >= FINALITY_THRESHOLD_FINALIZED); + + bytes memory _messageBody = _formatBurnMessageForReceive( localTokenMessenger.messageBodyVersion(), - _burnToken, + 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() + // Mock a failing call to TokenMinter mint() for amount bytes memory _call = abi.encodeWithSelector( TokenMinter.mint.selector, remoteDomain, - _burnToken, + remoteTokenAddr.toBytes32(), _mintRecipient.toAddress(), _amount ); vm.mockCallRevert( address(localTokenMinter), _call, - "Testing: token minter failed" + "Testing: mint() failed" ); vm.prank(localMessageTransmitter); - vm.expectRevert("Testing: token minter failed"); + vm.expectRevert("Testing: mint() failed"); localTokenMessenger.handleReceiveFinalizedMessage( remoteDomain, remoteTokenMessengerAddr, @@ -1518,23 +1702,218 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); } + 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); - bytes memory _messageBody = BurnMessageV2._formatMessageForRelay( + // 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 ); @@ -2191,6 +2570,45 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); } + // 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() From c0d6707efc81672f575380aca38122d3cdcc9a94 Mon Sep 17 00:00:00 2001 From: Jeffrey Hilaire Date: Fri, 20 Dec 2024 18:04:08 -0500 Subject: [PATCH 34/40] Allow users of this repository as a submodule to override the foundry version to use (#53) ## Summary Allow users of this repository as a submodule to override the foundry version to use ## Detail This repository is used as a submodule elsewhere to build evm contracts, at the same time these other repositories install said contracts using a locally managed foundry version. when the versions don't align the builds can sometimes fail causing the need to cascade update the foundry version everywhere. Allowing the Dockefile to use a build argument means other downstream repositories can update the version at their own pace without having to update this repository first. ## Testing covered by existing tests ## Documentation **Story:** [UNTRACKED](https://circlepay.atlassian.net/browse/) --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c77c2a1..1f9bcc4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ +ARG FOUNDRY_VERSION=nightly-3fa02706ca732c994715ba42d923605692062375 # Use fixed foundry image -FROM ghcr.io/foundry-rs/foundry:nightly-3fa02706ca732c994715ba42d923605692062375 +FROM ghcr.io/foundry-rs/foundry:${FOUNDRY_VERSION} # Copy our source code into the container WORKDIR /app From 6c405f1f18c42216010452a20899e407c26ed217 Mon Sep 17 00:00:00 2001 From: ams9198 Date: Mon, 23 Dec 2024 14:51:30 -0500 Subject: [PATCH 35/40] STABLE-7871: (Part 1) Audit fixes (#51) --- src/roles/Attestable.sol | 63 ++++++++++++++---------- src/roles/TokenController.sol | 20 ++++---- src/v2/BaseMessageTransmitter.sol | 17 ++++++- src/v2/BaseTokenMessenger.sol | 62 ++++++++++++++++-------- src/v2/MessageTransmitterV2.sol | 17 +++---- src/v2/TokenMessengerV2.sol | 16 +++---- test/v2/MessageTransmitterV2.t.sol | 77 ++++++++++++++++++++++++++++-- test/v2/TokenMessengerV2.t.sol | 52 +++++++++++++++++++- 8 files changed, 244 insertions(+), 80 deletions(-) diff --git a/src/roles/Attestable.sol b/src/roles/Attestable.sol index 42fed44..2889d91 100644 --- a/src/roles/Attestable.sol +++ b/src/roles/Attestable.sol @@ -125,13 +125,7 @@ contract Attestable is Ownable2Step { function updateAttesterManager( address newAttesterManager ) external onlyOwner { - require( - newAttesterManager != address(0), - "Invalid attester manager address" - ); - address _oldAttesterManager = _attesterManager; _setAttesterManager(newAttesterManager); - emit AttesterManagerUpdated(_oldAttesterManager, newAttesterManager); } /** @@ -167,25 +161,7 @@ contract Attestable is Ownable2Step { 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 - ); + _setSignatureThreshold(newSignatureThreshold); } /** @@ -208,10 +184,18 @@ 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); } /** @@ -290,4 +274,33 @@ contract Attestable is Ownable2Step { ) 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/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/v2/BaseMessageTransmitter.sol b/src/v2/BaseMessageTransmitter.sol index 7261d13..7b0e380 100644 --- a/src/v2/BaseMessageTransmitter.sol +++ b/src/v2/BaseMessageTransmitter.sol @@ -39,6 +39,10 @@ contract BaseMessageTransmitter is */ 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; @@ -73,8 +77,7 @@ contract BaseMessageTransmitter is function setMaxMessageBodySize( uint256 newMaxMessageBodySize ) external onlyOwner { - maxMessageBodySize = newMaxMessageBodySize; - emit MaxMessageBodySizeUpdated(maxMessageBodySize); + _setMaxMessageBodySize(newMaxMessageBodySize); } /** @@ -83,4 +86,14 @@ contract BaseMessageTransmitter is 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 index a94e4f5..6d5e2a7 100644 --- a/src/v2/BaseTokenMessenger.sol +++ b/src/v2/BaseTokenMessenger.sol @@ -140,15 +140,7 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable { uint32 domain, bytes32 tokenMessenger ) external onlyOwner { - require(tokenMessenger != bytes32(0), "bytes32(0) not allowed"); - - require( - remoteTokenMessengers[domain] == bytes32(0), - "TokenMessenger already set" - ); - - remoteTokenMessengers[domain] = tokenMessenger; - emit RemoteTokenMessengerAdded(domain, tokenMessenger); + _addRemoteTokenMessenger(domain, tokenMessenger); } /** @@ -174,16 +166,7 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable { * @param newLocalMinter The address of the minter on the local domain. */ function addLocalMinter(address newLocalMinter) external onlyOwner { - require(newLocalMinter != address(0), "Zero address not allowed"); - - require( - address(localMinter) == address(0), - "Local minter is already set." - ); - - localMinter = ITokenMinterV2(newLocalMinter); - - emit LocalMinterAdded(newLocalMinter); + _setLocalMinter(newLocalMinter); } /** @@ -260,7 +243,7 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable { * @return true if message sender is the registered local message transmitter */ function _isLocalMessageTransmitter() internal view returns (bool) { - return msg.sender == address(localMessageTransmitter); + return msg.sender == localMessageTransmitter; } /** @@ -333,4 +316,43 @@ abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable { 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/MessageTransmitterV2.sol b/src/v2/MessageTransmitterV2.sol index d9955da..bc669d4 100644 --- a/src/v2/MessageTransmitterV2.sol +++ b/src/v2/MessageTransmitterV2.sol @@ -78,8 +78,8 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { // ============ Initializers ============ /** * @notice Initializes the contract - * @dev Addresses must be non-zero - * @dev Signature threshold must be greater than zero + * @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 @@ -102,7 +102,6 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { attesterManager_ != address(0), "AttesterManager is the zero address" ); - require(signatureThreshold_ > 0, "Signature threshold is zero"); require( signatureThreshold_ <= attesters_.length, "Signature threshold exceeds attesters" @@ -115,9 +114,8 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { _updatePauser(pauser_); _setAttesterManager(attesterManager_); - // Settings - signatureThreshold = signatureThreshold_; - maxMessageBodySize = maxMessageBodySize_; + // Max message body size + _setMaxMessageBodySize(maxMessageBodySize_); // Attester configuration uint256 _attestersLength = attesters_.length; @@ -125,8 +123,11 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { _enableAttester(attesters_[i]); } + // Signature threshold + _setSignatureThreshold(signatureThreshold_); + // Claim 0-nonce - usedNonces[bytes32(0)] = 1; + usedNonces[bytes32(0)] = NONCE_USED; } // ============ External Functions ============ @@ -217,7 +218,7 @@ contract MessageTransmitterV2 is IMessageTransmitterV2, BaseMessageTransmitter { ) = _validateReceivedMessage(message, attestation); // Mark nonce as used - usedNonces[_nonce] = 1; + usedNonces[_nonce] = NONCE_USED; // Handle receive message if (_finalityThresholdExecuted < FINALITY_THRESHOLD_FINALIZED) { diff --git a/src/v2/TokenMessengerV2.sol b/src/v2/TokenMessengerV2.sol index b9e2ff6..cf8abb2 100644 --- a/src/v2/TokenMessengerV2.sol +++ b/src/v2/TokenMessengerV2.sol @@ -91,6 +91,7 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { * @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 @@ -109,7 +110,6 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { bytes32[] calldata remoteTokenMessengers_ ) external initializer { require(owner_ != address(0), "Owner is the zero address"); - require(tokenMinter_ != address(0), "TokenMinter is the zero address"); require( remoteDomains_.length == remoteTokenMessengers_.length, "Invalid remote domain configuration" @@ -121,18 +121,16 @@ contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger { _updateDenylister(denylister_); _setFeeRecipient(feeRecipient_); - localMinter = ITokenMinterV2(tokenMinter_); + // Local minter configuration + _setLocalMinter(tokenMinter_); - // Remote messenger configuration + // Remote token messenger configuration uint256 _remoteDomainsLength = remoteDomains_.length; for (uint256 i; i < _remoteDomainsLength; ++i) { - require( - remoteTokenMessengers_[i] != bytes32(0), - "Invalid TokenMessenger" + _addRemoteTokenMessenger( + remoteDomains_[i], + remoteTokenMessengers_[i] ); - uint32 _remoteDomain = remoteDomains_[i]; - bytes32 _remoteTokenMessenger = remoteTokenMessengers_[i]; - remoteTokenMessengers[_remoteDomain] = _remoteTokenMessenger; } } diff --git a/test/v2/MessageTransmitterV2.t.sol b/test/v2/MessageTransmitterV2.t.sol index 5998047..646e503 100644 --- a/test/v2/MessageTransmitterV2.t.sol +++ b/test/v2/MessageTransmitterV2.t.sol @@ -45,6 +45,18 @@ contract MessageTransmitterV2Test is TestUtils { 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; @@ -208,7 +220,7 @@ contract MessageTransmitterV2Test is TestUtils { address[] memory _attesters = new address[](1); _attesters[0] = attester; - vm.expectRevert("Signature threshold is zero"); + vm.expectRevert("Invalid signature threshold"); MessageTransmitterV2(address(_proxy)).initialize( owner, pauser, @@ -299,6 +311,59 @@ contract MessageTransmitterV2Test is TestUtils { 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); @@ -426,7 +491,10 @@ contract MessageTransmitterV2Test is TestUtils { } function testInitialize_setsZeroNonceAsUsed() public view { - assertEq(messageTransmitter.usedNonces(bytes32(0)), 1); + assertEq( + messageTransmitter.usedNonces(bytes32(0)), + messageTransmitter.NONCE_USED() + ); } function testSendMessage_revertsWhenPaused( @@ -1463,7 +1531,10 @@ contract MessageTransmitterV2Test is TestUtils { vm.stopPrank(); // Check that the nonce is now used - assertEq(messageTransmitter.usedNonces(_msg._getNonce()), 1); + assertEq( + messageTransmitter.usedNonces(_msg._getNonce()), + messageTransmitter.NONCE_USED() + ); } // setup second and third attester (first set in setUp()); set sig threshold at 2 diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index e830ce5..c9e0997 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -55,6 +55,11 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { event Upgraded(address indexed implementation); + event DenylisterChanged( + address indexed oldDenylister, + address indexed newDenylister + ); + // Libraries using TypedMemView for bytes; using TypedMemView for bytes29; @@ -280,7 +285,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { bytes32[] memory _remoteTokenMessengers ) = _defaultRemoteTokenMessengers(); - vm.expectRevert("TokenMinter is the zero address"); + vm.expectRevert("Zero address not allowed"); TokenMessengerV2(address(_proxy)).initialize( owner, rescuer, @@ -340,7 +345,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { _remoteDomains[0] = 1; _remoteDomains[1] = 2; - vm.expectRevert("Invalid TokenMessenger"); + vm.expectRevert("bytes32(0) not allowed"); TokenMessengerV2(address(_proxy)).initialize( owner, rescuer, @@ -502,6 +507,49 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); } + 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)) From 88c699f4080667d5ffc91c3136a22ca6f540cb57 Mon Sep 17 00:00:00 2001 From: ams9198 Date: Tue, 7 Jan 2025 07:53:58 -0500 Subject: [PATCH 36/40] STABLE-7871 (Part 2): Update CCTPHookWrapper (#52) --- src/examples/CCTPHookWrapper.sol | 132 +++++++---------- src/messages/v2/BurnMessageV2.sol | 2 +- test/examples/CCTPHookWrapper.t.sol | 207 +++++++++++++++++++-------- test/messages/v2/BurnMessageV2.t.sol | 2 +- test/v2/TokenMessengerV2.t.sol | 4 +- 5 files changed, 201 insertions(+), 146 deletions(-) diff --git a/src/examples/CCTPHookWrapper.sol b/src/examples/CCTPHookWrapper.sol index 27174b1..82e926a 100644 --- a/src/examples/CCTPHookWrapper.sol +++ b/src/examples/CCTPHookWrapper.sol @@ -21,23 +21,24 @@ 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 This is intended to only work with CCTP v2 message formats and interfaces. + * @dev Intended to only work with CCTP v2 message formats and interfaces. */ -contract CCTPHookWrapper { - // ============ State Variables ============ +contract CCTPHookWrapper is Ownable2Step { + // ============ Constants ============ // Address of the local message transmitter IReceiverV2 public immutable messageTransmitter; // The supported Message Format version - uint32 public immutable supportedMessageVersion; + uint32 public constant supportedMessageVersion = 1; // The supported Message Body version - uint32 public immutable supportedMessageBodyVersion; + uint32 public constant supportedMessageBodyVersion = 1; // Byte-length of an address uint256 internal constant ADDRESS_BYTE_LENGTH = 20; @@ -46,34 +47,17 @@ contract CCTPHookWrapper { using TypedMemView for bytes; using TypedMemView for bytes29; - // ============ Modifiers ============ - /** - * @notice A modifier to enable access control - * @dev Can be overridden to customize the behavior - */ - modifier onlyAllowed() virtual { - _; - } - // ============ Constructor ============ /** * @param _messageTransmitter The address of the local message transmitter - * @param _messageVersion The required CCTP message version. For CCTP v2, this is 1. - * @param _messageBodyVersion The required message body (Burn Message) version. For CCTP v2, this is 1. */ - constructor( - address _messageTransmitter, - uint32 _messageVersion, - uint32 _messageBodyVersion - ) { + constructor(address _messageTransmitter) Ownable2Step() { require( _messageTransmitter != address(0), "Message transmitter is the zero address" ); messageTransmitter = IReceiverV2(_messageTransmitter); - supportedMessageVersion = _messageVersion; - supportedMessageBodyVersion = _messageBodyVersion; } // ============ External Functions ============ @@ -89,19 +73,12 @@ contract CCTPHookWrapper { * 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. * - * WARNING: this implementation does NOT enforce atomicity in the hook call. If atomicity is - * required, a new wrapper contract can be created, possibly by overriding this behavior in `_handleHook`, - * or by introducing a different format for the hook data that includes more information about - * the desired handling. + * @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: in a permissionless context, it is important not to view this wrapper implementation as a trusted - * caller of a hook, as others can craft messages containing hooks that look identical, that are - * similarly executed from this wrapper, either by setting this contract as the destination caller, - * or by setting the destination caller to be bytes32(0). Alternate implementations may extract more information - * from the burn message, such as the mintRecipient or the amount, to include in the hook call to allow recipients - * to further filter their receiving actions. - * - * WARNING: re-entrant behavior is allowed in this implementation. Relay() can be overridden to disable this. + * 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 @@ -118,73 +95,66 @@ contract CCTPHookWrapper { ) external virtual - onlyAllowed returns ( bool relaySuccess, bool hookSuccess, bytes memory hookReturnData ) { - bytes29 _msg = message.ref(0); - bytes29 _msgBody = MessageV2._getMessageBody(_msg); + _checkOwner(); - // Perform message validation - _validateMessage(_msg, _msgBody); + // Validate message + bytes29 _msg = message.ref(0); + MessageV2._validateMessageFormat(_msg); + require( + MessageV2._getVersion(_msg) == supportedMessageVersion, + "Invalid message version" + ); - // Relay message + // Validate burn message + bytes29 _msgBody = MessageV2._getMessageBody(_msg); + BurnMessageV2._validateBurnMessageFormat(_msgBody); require( - messageTransmitter.receiveMessage(message, attestation), - "Receive message failed" + BurnMessageV2._getVersion(_msgBody) == supportedMessageBodyVersion, + "Invalid message body version" ); - relaySuccess = true; + // Relay message + relaySuccess = messageTransmitter.receiveMessage(message, attestation); + require(relaySuccess, "Receive message failed"); - // Handle hook + // Handle hook if present bytes29 _hookData = BurnMessageV2._getHookData(_msgBody); - (hookSuccess, hookReturnData) = _handleHook(_hookData); + 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 Validates a message and its message body - * @dev Can be overridden to customize the validation - * @dev Reverts if the message format version or message body version - * do not match the supported versions. - */ - function _validateMessage( - bytes29 _message, - bytes29 _messageBody - ) internal virtual { - require( - MessageV2._getVersion(_message) == supportedMessageVersion, - "Invalid message version" - ); - require( - BurnMessageV2._getVersion(_messageBody) == - supportedMessageBodyVersion, - "Invalid message body version" - ); - } - /** * @notice Handles hook data by executing a call to a target address - * @dev Can be overridden to customize the execution behavior - * @param _hookData The hook data contained in the Burn Message + * @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 _handleHook( - bytes29 _hookData + function _executeHook( + address _hookTarget, + bytes memory _hookCalldata ) internal virtual returns (bool _success, bytes memory _returnData) { - 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(); - - (_success, _returnData) = address(_target).call(_hookCalldata); - } + (_success, _returnData) = address(_hookTarget).call(_hookCalldata); } } diff --git a/src/messages/v2/BurnMessageV2.sol b/src/messages/v2/BurnMessageV2.sol index e9bc81a..703dc06 100644 --- a/src/messages/v2/BurnMessageV2.sol +++ b/src/messages/v2/BurnMessageV2.sol @@ -152,7 +152,7 @@ library BurnMessageV2 { require(_message.isValid(), "Malformed message"); require( _message.len() >= HOOK_DATA_INDEX, - "Invalid message: too short" + "Invalid burn message: too short" ); } } diff --git a/test/examples/CCTPHookWrapper.t.sol b/test/examples/CCTPHookWrapper.t.sol index dd46d15..78aac7b 100644 --- a/test/examples/CCTPHookWrapper.t.sol +++ b/test/examples/CCTPHookWrapper.t.sol @@ -24,26 +24,32 @@ 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 messageVersion = 1; - uint32 messageBodyVersion = 1; + uint32 v2MessageVersion = 1; + uint32 v2MessageBodyVersion = 1; + + address wrapperOwner = address(123); address localMessageTransmitter = address(10); MockHookTarget hookTarget; CCTPHookWrapper wrapper; function setUp() public { - wrapper = new CCTPHookWrapper( - localMessageTransmitter, - messageVersion, - messageBodyVersion - ); + vm.prank(wrapperOwner); + wrapper = new CCTPHookWrapper(localMessageTransmitter); + hookTarget = new MockHookTarget(); } @@ -51,81 +57,116 @@ contract CCTPHookWrapperTest is Test { function testInitialization__revertsIfMessageTransmitterIsZero() public { vm.expectRevert("Message transmitter is the zero address"); - new CCTPHookWrapper(address(0), messageVersion, messageBodyVersion); + new CCTPHookWrapper(address(0)); } function testInitialization__setsTheMessageTransmitter( address _messageTransmitter ) public { vm.assume(_messageTransmitter != address(0)); - CCTPHookWrapper _wrapper = new CCTPHookWrapper( - _messageTransmitter, - messageVersion, - messageBodyVersion - ); + CCTPHookWrapper _wrapper = new CCTPHookWrapper(_messageTransmitter); assertEq(address(_wrapper.messageTransmitter()), _messageTransmitter); } - function testInitialization__setsTheMessageVersion( - uint32 _messageVersion - ) public { - CCTPHookWrapper _wrapper = new CCTPHookWrapper( - localMessageTransmitter, - _messageVersion, - messageBodyVersion - ); + function testInitialization__usesTheV2MessageVersion() public view { assertEq( - uint256(address(_wrapper.supportedMessageVersion())), - uint256(_messageVersion) + uint256(address(wrapper.supportedMessageVersion())), + uint256(v2MessageVersion) ); } - function testInitialization__setsTheMessageBodyVersion( - uint32 _messageBodyVersion - ) public { - CCTPHookWrapper _wrapper = new CCTPHookWrapper( - localMessageTransmitter, - messageVersion, - _messageBodyVersion - ); + function testInitialization__usesTheV2MessageBodyVersion() public view { assertEq( - uint256(address(_wrapper.supportedMessageBodyVersion())), - uint256(_messageBodyVersion) + 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 != messageVersion); + vm.assume(_messageVersion != v2MessageVersion); vm.expectRevert("Invalid message version"); bytes memory _message = _createMessage( _messageVersion, - messageBodyVersion, + v2MessageBodyVersion, bytes("") ); + + vm.prank(wrapperOwner); wrapper.relay(_message, bytes("")); } function testRelay__revertsIfMessageBodyVersionIsInvalid( uint32 _messageBodyVersion ) public { - vm.assume(_messageBodyVersion != messageBodyVersion); + vm.assume(_messageBodyVersion != v2MessageBodyVersion); vm.expectRevert("Invalid message body version"); bytes memory _message = _createMessage( - messageVersion, + 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( - messageVersion, - messageBodyVersion, + v2MessageVersion, + v2MessageBodyVersion, bytes("") ); @@ -137,17 +178,19 @@ contract CCTPHookWrapperTest is Test { _message, bytes("") ), - "Testing: token minter failed" + "Testing: message transmitter failed" ); vm.expectRevert(); + + vm.prank(wrapperOwner); wrapper.relay(_message, bytes("")); } function testRelay__revertsIfMessageTransmitterReturnsFalse() public { bytes memory _message = _createMessage( - messageVersion, - messageBodyVersion, + v2MessageVersion, + v2MessageBodyVersion, bytes("") ); @@ -173,13 +216,15 @@ contract CCTPHookWrapperTest is Test { ); vm.expectRevert("Receive message failed"); + + vm.prank(wrapperOwner); wrapper.relay(_message, bytes("")); } function testRelay__succeedsWithNoHook() public { bytes memory _message = _createMessage( - messageVersion, - messageBodyVersion, + v2MessageVersion, + v2MessageBodyVersion, bytes("") ); @@ -193,6 +238,7 @@ contract CCTPHookWrapperTest is Test { abi.encode(true) ); + vm.prank(wrapperOwner); ( bool _relaySuccess, bool _hookSuccess, @@ -210,8 +256,8 @@ contract CCTPHookWrapperTest is Test { MockHookTarget.failingHook.selector ); bytes memory _message = _createMessage( - messageVersion, - messageBodyVersion, + v2MessageVersion, + v2MessageBodyVersion, abi.encodePacked(address(hookTarget), _failingHookCalldata) ); @@ -227,6 +273,7 @@ contract CCTPHookWrapperTest is Test { ); // Call wrapper + vm.prank(wrapperOwner); ( bool _relaySuccess, bool _hookSuccess, @@ -238,12 +285,48 @@ contract CCTPHookWrapperTest is Test { 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( - messageVersion, - messageBodyVersion, + v2MessageVersion, + v2MessageBodyVersion, abi.encodePacked(address(12345), _hookCalldata) ); @@ -259,6 +342,7 @@ contract CCTPHookWrapperTest is Test { ); // Call wrapper + vm.prank(wrapperOwner); ( bool _relaySuccess, bool _hookSuccess, @@ -271,7 +355,7 @@ contract CCTPHookWrapperTest is Test { } function testRelay__succeedsWithSucceedingHook() public { - // Prepare a message with hookCalldata that will fail + // Prepare a message with hookCalldata that will succeed uint256 _expectedReturnData = 12; bytes memory _succeedingHookCallData = abi.encodeWithSelector( MockHookTarget.succeedingHook.selector, @@ -279,8 +363,8 @@ contract CCTPHookWrapperTest is Test { 7 ); bytes memory _message = _createMessage( - messageVersion, - messageBodyVersion, + v2MessageVersion, + v2MessageBodyVersion, abi.encodePacked(address(hookTarget), _succeedingHookCallData) ); @@ -299,6 +383,7 @@ contract CCTPHookWrapperTest is Test { emit HookReceived(5, 7); // Call wrapper + vm.prank(wrapperOwner); ( bool _relaySuccess, bool _hookSuccess, @@ -319,7 +404,7 @@ contract CCTPHookWrapperTest is Test { ) internal pure returns (bytes memory) { return abi.encodePacked( - _messageVersion, + _messageVersion, // messageVersion uint32(0), // sourceDomain uint32(0), // destinationDomain bytes32(0), // nonce @@ -338,15 +423,15 @@ contract CCTPHookWrapperTest is Test { ) internal pure returns (bytes memory) { return abi.encodePacked( - _burnMessageVersion, - bytes32(0), - bytes32(0), - uint256(0), - bytes32(0), - uint256(0), - uint256(0), - uint256(0), - _hookData + _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 ); } diff --git a/test/messages/v2/BurnMessageV2.t.sol b/test/messages/v2/BurnMessageV2.t.sol index 85559f4..5561322 100644 --- a/test/messages/v2/BurnMessageV2.t.sol +++ b/test/messages/v2/BurnMessageV2.t.sol @@ -97,7 +97,7 @@ contract BurnMessageV2Test is Test { // Lop off the hookData bytes, and then one more _m = _m.slice(0, _m.len() - _hookData.length - 1, 0); - vm.expectRevert("Invalid message: too short"); + vm.expectRevert("Invalid burn message: too short"); _m._validateBurnMessageFormat(); } diff --git a/test/v2/TokenMessengerV2.t.sol b/test/v2/TokenMessengerV2.t.sol index c9e0997..fa8552d 100644 --- a/test/v2/TokenMessengerV2.t.sol +++ b/test/v2/TokenMessengerV2.t.sol @@ -1445,7 +1445,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { vm.assume(_messageBody.length < 228); vm.prank(localMessageTransmitter); - vm.expectRevert("Invalid message: too short"); + vm.expectRevert("Invalid burn message: too short"); localTokenMessenger.handleReceiveFinalizedMessage( remoteDomain, remoteTokenMessengerAddr, @@ -2073,7 +2073,7 @@ contract TokenMessengerV2Test is BaseTokenMessengerTest { ); vm.prank(localMessageTransmitter); - vm.expectRevert("Invalid message: too short"); + vm.expectRevert("Invalid burn message: too short"); localTokenMessenger.handleReceiveUnfinalizedMessage( remoteDomain, remoteTokenMessengerAddr, From 7d703109a2cfcb3f76375fef5f1a97f03c447b94 Mon Sep 17 00:00:00 2001 From: ams9198 Date: Tue, 7 Jan 2025 11:01:40 -0500 Subject: [PATCH 37/40] STABLE-7178 (cont.): Bump TypedMemView to v2.1.2 (#54) --- lib/memview-sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 8a25c1ae2d7c6940b4b093ebc672fad41d8157cf Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 15 Jan 2025 10:08:59 -0800 Subject: [PATCH 38/40] [STABLE-8131] Calculate tokenMessengerV2 proxy address instead of parsing from env (#55) - Add flag in DeployProxiesV2 script to determine whether to read `remoteTokenMessengerAddresses` from env variable. - Add logic to use deterministic Create2 address calculation if not reading from env. - Update UT to test the false case. True case will most likely not get used. - TODO: Figure out a way to set flag to true based on test selector before `setUp()` is run to test the true case --- scripts/v2/DeployProxiesV2.s.sol | 27 ++++++++++++++++----- test/scripts/v2/DeployProxiesV2.t.sol | 6 ++++- test/scripts/v2/ScriptV2TestUtils.sol | 35 +++++++++++++++------------ 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/scripts/v2/DeployProxiesV2.s.sol b/scripts/v2/DeployProxiesV2.s.sol index 19b9be4..c32e08a 100644 --- a/scripts/v2/DeployProxiesV2.s.sol +++ b/scripts/v2/DeployProxiesV2.s.sol @@ -24,6 +24,7 @@ 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"; contract DeployProxiesV2Script is Script { // Expose for tests @@ -150,15 +151,28 @@ contract DeployProxiesV2Script is Script { "" ); + // Calculate TokenMessengerV2 proxy address + address expectedTokenMessengerV2ProxyAddress = vm.computeCreate2Address( + keccak256(type(TokenMessengerV2).creationCode), + 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) { - remoteTokenMessengerAddresses[i] = remoteTokenMessengerV2Addresses[ - i - ]; + if (remoteTokenMessengerV2FromEnv) { + remoteTokenMessengerAddresses[i] = remoteTokenMessengerV2Addresses[i]; + } else { + remoteTokenMessengerAddresses[i] = AddressUtils.toBytes32(expectedTokenMessengerV2ProxyAddress); + } } bytes memory initializer = abi.encodeWithSelector( TokenMessengerV2.initialize.selector, @@ -199,7 +213,6 @@ contract DeployProxiesV2Script is Script { proxyCreateCode, multiCallData ); - // Stop recording transations vm.stopBroadcast(); @@ -333,9 +346,11 @@ contract DeployProxiesV2Script is Script { ); tokenControllerPrivateKey = vm.envUint("TOKEN_CONTROLLER_KEY"); - remoteTokenMessengerV2Addresses = vm.envBytes32( + bytes32[] memory emptyRemoteTokenMessengerV2Addresses = new bytes32[](0); + remoteTokenMessengerV2Addresses = vm.envOr( "REMOTE_TOKEN_MESSENGER_V2_ADDRESSES", - "," + ",", + emptyRemoteTokenMessengerV2Addresses ); } diff --git a/test/scripts/v2/DeployProxiesV2.t.sol b/test/scripts/v2/DeployProxiesV2.t.sol index 8ea2395..883f470 100644 --- a/test/scripts/v2/DeployProxiesV2.t.sol +++ b/test/scripts/v2/DeployProxiesV2.t.sol @@ -105,9 +105,13 @@ contract DeployProxiesV2Test is ScriptV2TestUtils { // 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), - bytes32(uint256(uint160(address(remoteTokenMessengerV2s[i])))) + remoteTokenMessengerAddress ); } // admin diff --git a/test/scripts/v2/ScriptV2TestUtils.sol b/test/scripts/v2/ScriptV2TestUtils.sol index fac4ca1..47ffc66 100644 --- a/test/scripts/v2/ScriptV2TestUtils.sol +++ b/test/scripts/v2/ScriptV2TestUtils.sol @@ -54,6 +54,7 @@ contract ScriptV2TestUtils is TestUtils { address[] remoteTokens; uint32[] remoteDomains; address[] remoteTokenMessengerV2s; + bool remoteTokenMessengerV2FromEnv = false; uint32 anotherRemoteDomain = 5; address anotherRemoteToken; @@ -166,24 +167,26 @@ contract ScriptV2TestUtils is TestUtils { ) ) ); - 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]) + 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", From 8cf36730033099b02d2ab06a27faaf4e8c130a74 Mon Sep 17 00:00:00 2001 From: ams9198 Date: Fri, 17 Jan 2025 10:48:41 -0500 Subject: [PATCH 39/40] STABLE-8092: Update deployment scripts to use CREATE2 for implementation contracts (#57) --- README.md | 6 +- scripts/v2/DeployAddressUtilsExternal.s.sol | 3 +- scripts/v2/DeployImplementationsV2.s.sol | 97 ++++++++++++++----- scripts/v2/DeployProxiesV2.s.sol | 34 ++++--- scripts/v2/Salts.sol | 29 ++++++ test/scripts/v2/DeployImplementationsV2.t.sol | 1 + test/scripts/v2/DeployProxiesV2.t.sol | 13 ++- test/scripts/v2/ScriptV2TestUtils.sol | 21 ++-- 8 files changed, 142 insertions(+), 62 deletions(-) create mode 100644 scripts/v2/Salts.sol diff --git a/README.md b/README.md index 55f4a82..3b07cb1 100644 --- a/README.md +++ b/README.md @@ -119,11 +119,13 @@ 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` - - `IMPLEMENTATION_DEPLOYER_PRIVATE_KEY` 2. Run `make simulate-deploy-implementations-v2 RPC_URL= SENDER=` to perform a dry run. @@ -170,8 +172,8 @@ The proxies are deployed via `CREATE2` through Create2Factory. The scripts assum - `BURN_LIMIT_PER_MESSAGE` - `CREATE2_FACTORY_OWNER_KEY` - - `TOKEN_MINTER_V2_DEPLOYER_KEY` - `TOKEN_CONTROLLER_KEY` + - `TOKEN_MINTER_V2_OWNER_KEY` 2. Run `make simulate-deploy-proxies-v2 RPC_URL= SENDER=` to perform a dry run. diff --git a/scripts/v2/DeployAddressUtilsExternal.s.sol b/scripts/v2/DeployAddressUtilsExternal.s.sol index 06b5bd9..6258006 100644 --- a/scripts/v2/DeployAddressUtilsExternal.s.sol +++ b/scripts/v2/DeployAddressUtilsExternal.s.sol @@ -20,6 +20,7 @@ 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; @@ -33,7 +34,7 @@ contract DeployAddressUtilsExternalScript is Script { _addressUtilsExternal = AddressUtilsExternal( create2Factory.deploy( 0, - bytes32(0), + SALT_ADDRESS_UTILS_EXTERNAL, type(AddressUtilsExternal).creationCode ) ); diff --git a/scripts/v2/DeployImplementationsV2.s.sol b/scripts/v2/DeployImplementationsV2.s.sol index 4e9cb98..a1f3cc1 100644 --- a/scripts/v2/DeployImplementationsV2.s.sol +++ b/scripts/v2/DeployImplementationsV2.s.sol @@ -16,12 +16,16 @@ * 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 @@ -31,18 +35,21 @@ contract DeployImplementationsV2Script is Script { 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 implementationDeployerPrivateKey; + uint256 private create2FactoryOwnerPrivateKey; - function deployImplementationsV2( - uint256 privateKey - ) private returns (MessageTransmitterV2, TokenMinterV2, TokenMessengerV2) { + function deployImplementationsV2() + private + returns (MessageTransmitterV2, TokenMinterV2, TokenMessengerV2) + { // Calculate MessageTransmitterV2 proxy address expectedMessageTransmitterV2ProxyAddress = vm.computeCreate2Address( - keccak256(type(MessageTransmitterV2).creationCode), + SALT_MESSAGE_TRANSMITTER, keccak256( abi.encodePacked( type(AdminUpgradableProxy).creationCode, @@ -52,33 +59,69 @@ contract DeployImplementationsV2Script is Script { factoryAddress ); + Create2Factory factory = Create2Factory(factoryAddress); + // Start recording transactions - vm.startBroadcast(privateKey); + vm.startBroadcast(create2FactoryOwnerPrivateKey); // Deploy MessageTransmitterV2 implementation - MessageTransmitterV2 messageTransmitterV2Implementation = new MessageTransmitterV2( - domain, - version - ); + messageTransmitterV2 = MessageTransmitterV2( + factory.deploy( + 0, + SALT_MESSAGE_TRANSMITTER, + abi.encodePacked( + type(MessageTransmitterV2).creationCode, + abi.encode(domain, version) + ) + ) + ); - // Deploy TokenMinter - TokenMinterV2 tokenMinterV2Implementation = new TokenMinterV2( - tokenControllerAddress + // Deploy TokenMessengerV2 implementation + tokenMessengerV2 = TokenMessengerV2( + factory.deploy( + 0, + SALT_TOKEN_MESSENGER, + abi.encodePacked( + type(TokenMessengerV2).creationCode, + abi.encode( + expectedMessageTransmitterV2ProxyAddress, + messageBodyVersion + ) + ) + ) ); - // Deploy TokenMessengerV2 - TokenMessengerV2 tokenMessengerV2Implementation = new TokenMessengerV2( - 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(); - return ( - messageTransmitterV2Implementation, - tokenMinterV2Implementation, - tokenMessengerV2Implementation - ); + + // Accept the TokenMinter 2-step ownership + vm.startBroadcast(tokenMinterOwnerKey); + tokenMinterV2.acceptOwnership(); + vm.stopBroadcast(); + + return (messageTransmitterV2, tokenMinterV2, tokenMessengerV2); } /** @@ -86,13 +129,15 @@ contract DeployImplementationsV2Script is Script { */ 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")); - implementationDeployerPrivateKey = vm.envUint( - "IMPLEMENTATION_DEPLOYER_PRIVATE_KEY" - ); } /** @@ -103,6 +148,6 @@ contract DeployImplementationsV2Script is Script { messageTransmitterV2, tokenMinterV2, tokenMessengerV2 - ) = deployImplementationsV2(implementationDeployerPrivateKey); + ) = deployImplementationsV2(); } } diff --git a/scripts/v2/DeployProxiesV2.s.sol b/scripts/v2/DeployProxiesV2.s.sol index c32e08a..d4245d8 100644 --- a/scripts/v2/DeployProxiesV2.s.sol +++ b/scripts/v2/DeployProxiesV2.s.sol @@ -25,6 +25,7 @@ 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 @@ -64,7 +65,7 @@ contract DeployProxiesV2Script is Script { uint256 private burnLimitPerMessage; uint256 private create2FactoryOwnerPrivateKey; - uint256 private tokenMinterV2DeployerPrivateKey; + uint256 private tokenMinterV2OwnerPrivateKey; uint256 private tokenControllerPrivateKey; function getProxyCreationCode( @@ -129,7 +130,7 @@ contract DeployProxiesV2Script is Script { address messageTransmitterV2ProxyAddress = Create2Factory(factory) .deployAndMultiCall( 0, - keccak256(type(MessageTransmitterV2).creationCode), // TODO: Verify salt + SALT_MESSAGE_TRANSMITTER, proxyCreateCode, multiCallData ); @@ -153,14 +154,13 @@ contract DeployProxiesV2Script is Script { // Calculate TokenMessengerV2 proxy address address expectedTokenMessengerV2ProxyAddress = vm.computeCreate2Address( - keccak256(type(TokenMessengerV2).creationCode), - keccak256( - proxyCreateCode - ), + SALT_TOKEN_MESSENGER, + keccak256(proxyCreateCode), factory ); - bool remoteTokenMessengerV2FromEnv = remoteTokenMessengerV2Addresses.length > 0; + bool remoteTokenMessengerV2FromEnv = remoteTokenMessengerV2Addresses + .length > 0; // Construct initializer bytes32[] memory remoteTokenMessengerAddresses = new bytes32[]( @@ -169,9 +169,13 @@ contract DeployProxiesV2Script is Script { uint256 remoteDomainsLength = remoteDomains.length; for (uint256 i = 0; i < remoteDomainsLength; ++i) { if (remoteTokenMessengerV2FromEnv) { - remoteTokenMessengerAddresses[i] = remoteTokenMessengerV2Addresses[i]; + remoteTokenMessengerAddresses[ + i + ] = remoteTokenMessengerV2Addresses[i]; } else { - remoteTokenMessengerAddresses[i] = AddressUtils.toBytes32(expectedTokenMessengerV2ProxyAddress); + remoteTokenMessengerAddresses[i] = AddressUtils.toBytes32( + expectedTokenMessengerV2ProxyAddress + ); } } bytes memory initializer = abi.encodeWithSelector( @@ -209,7 +213,7 @@ contract DeployProxiesV2Script is Script { address tokenMessengerV2ProxyAddress = Create2Factory(factory) .deployAndMultiCall( 0, - keccak256(type(TokenMessengerV2).creationCode), // TODO: Verify salt + SALT_TOKEN_MESSENGER, proxyCreateCode, multiCallData ); @@ -341,12 +345,12 @@ contract DeployProxiesV2Script is Script { burnLimitPerMessage = vm.envUint("BURN_LIMIT_PER_MESSAGE"); create2FactoryOwnerPrivateKey = vm.envUint("CREATE2_FACTORY_OWNER_KEY"); - tokenMinterV2DeployerPrivateKey = vm.envUint( - "TOKEN_MINTER_V2_DEPLOYER_KEY" - ); + tokenMinterV2OwnerPrivateKey = vm.envUint("TOKEN_MINTER_V2_OWNER_KEY"); tokenControllerPrivateKey = vm.envUint("TOKEN_CONTROLLER_KEY"); - bytes32[] memory emptyRemoteTokenMessengerV2Addresses = new bytes32[](0); + bytes32[] memory emptyRemoteTokenMessengerV2Addresses = new bytes32[]( + 0 + ); remoteTokenMessengerV2Addresses = vm.envOr( "REMOTE_TOKEN_MESSENGER_V2_ADDRESSES", ",", @@ -370,7 +374,7 @@ contract DeployProxiesV2Script is Script { ); addMessengerPauserRescuerToTokenMinterV2( - tokenMinterV2DeployerPrivateKey, + tokenMinterV2OwnerPrivateKey, tokenControllerPrivateKey, tokenMinterV2, address(tokenMessengerV2) 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/test/scripts/v2/DeployImplementationsV2.t.sol b/test/scripts/v2/DeployImplementationsV2.t.sol index 29a79d6..90f69b0 100644 --- a/test/scripts/v2/DeployImplementationsV2.t.sol +++ b/test/scripts/v2/DeployImplementationsV2.t.sol @@ -37,6 +37,7 @@ contract DeployImplementationsV2Test is ScriptV2TestUtils { // TokenMinterV2 assertEq(tokenMinterV2.tokenController(), deployer); + assertEq(tokenMinterV2.owner(), deployer); // TokenMessengerV2 assertEq( diff --git a/test/scripts/v2/DeployProxiesV2.t.sol b/test/scripts/v2/DeployProxiesV2.t.sol index 883f470..4f621bc 100644 --- a/test/scripts/v2/DeployProxiesV2.t.sol +++ b/test/scripts/v2/DeployProxiesV2.t.sol @@ -22,6 +22,7 @@ import {DeployImplementationsV2Script} from "../../../scripts/v2/DeployImplement 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; @@ -36,7 +37,7 @@ contract DeployProxiesV2Test is ScriptV2TestUtils { function testDeployMessageTransmitterV2() public { // create2 address address predicted = create2Factory.computeAddress( - keccak256(type(MessageTransmitterV2).creationCode), + SALT_MESSAGE_TRANSMITTER, keccak256( deployProxiesV2Script.getProxyCreationCode( address(create2Factory), @@ -74,7 +75,7 @@ contract DeployProxiesV2Test is ScriptV2TestUtils { function testDeployTokenMessengerV2() public { // create2 address address predicted = create2Factory.computeAddress( - keccak256(type(TokenMessengerV2).creationCode), + SALT_TOKEN_MESSENGER, keccak256( deployProxiesV2Script.getProxyCreationCode( address(create2Factory), @@ -105,9 +106,13 @@ contract DeployProxiesV2Test is ScriptV2TestUtils { // remote token messengers for (uint256 i = 0; i < remoteDomains.length; i++) { uint32 remoteDomain = remoteDomains[i]; - bytes32 remoteTokenMessengerAddress = bytes32(uint256(uint160(address(tokenMessengerV2)))); + bytes32 remoteTokenMessengerAddress = bytes32( + uint256(uint160(address(tokenMessengerV2))) + ); if (remoteTokenMessengerV2FromEnv) { - remoteTokenMessengerAddress = bytes32(uint256(uint160(address(remoteTokenMessengerV2s[i])))); + remoteTokenMessengerAddress = bytes32( + uint256(uint160(address(remoteTokenMessengerV2s[i]))) + ); } assertEq( tokenMessengerV2.remoteTokenMessengers(remoteDomain), diff --git a/test/scripts/v2/ScriptV2TestUtils.sol b/test/scripts/v2/ScriptV2TestUtils.sol index 47ffc66..5f79885 100644 --- a/test/scripts/v2/ScriptV2TestUtils.sol +++ b/test/scripts/v2/ScriptV2TestUtils.sol @@ -32,7 +32,6 @@ contract ScriptV2TestUtils is TestUtils { uint32 _messageBodyVersion = 1; uint32 _version = 1; address token; - uint256 implDeployerPK; uint256 deployerPK; address deployer; address attester1; @@ -72,12 +71,13 @@ contract ScriptV2TestUtils is TestUtils { } function _deployImplementations() internal { - implDeployerPK = uint256(keccak256("DEPLOYTEST_IMPL_DEPLOYER_PK")); - 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( @@ -85,10 +85,6 @@ contract ScriptV2TestUtils is TestUtils { vm.toString(uint256(_messageBodyVersion)) ); vm.setEnv("VERSION", vm.toString(uint256(_version))); - vm.setEnv( - "IMPLEMENTATION_DEPLOYER_PRIVATE_KEY", - vm.toString(implDeployerPK) - ); DeployImplementationsV2Script deployImplScript = new DeployImplementationsV2Script(); deployImplScript.setUp(); @@ -167,7 +163,8 @@ contract ScriptV2TestUtils is TestUtils { ) ) ); - if (remoteTokenMessengerV2FromEnv) { // TODO: Figure out if there is a way to dynamically set this before setUp() + if (remoteTokenMessengerV2FromEnv) { + // TODO: Figure out if there is a way to dynamically set this before setUp() vm.setEnv( "REMOTE_TOKEN_MESSENGER_V2_ADDRESSES", string( @@ -251,7 +248,7 @@ contract ScriptV2TestUtils is TestUtils { ); vm.setEnv("CREATE2_FACTORY_OWNER_KEY", vm.toString(deployerPK)); - vm.setEnv("TOKEN_MINTER_V2_DEPLOYER_KEY", vm.toString(implDeployerPK)); + vm.setEnv("TOKEN_MINTER_V2_OWNER_KEY", vm.toString(deployerPK)); vm.setEnv("TOKEN_CONTROLLER_KEY", vm.toString(deployerPK)); DeployProxiesV2Script deployProxiesV2Script = new DeployProxiesV2Script(); @@ -264,9 +261,6 @@ contract ScriptV2TestUtils is TestUtils { function _setupRemoteResources() internal { vm.setEnv("TOKEN_MESSENGER_V2_OWNER_KEY", vm.toString(deployerPK)); - - // Use same TOKEN_CONTROLLER_DEPLOYER_KEY as TOKEN_CONTROLLER_KEY - vm.setEnv("TOKEN_CONTROLLER_KEY", vm.toString(deployerPK)); vm.setEnv( "TOKEN_MESSENGER_V2_CONTRACT_ADDRESS", vm.toString(address(tokenMessengerV2)) @@ -296,8 +290,7 @@ contract ScriptV2TestUtils is TestUtils { // [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_MESSENGER_V2_OWNER_KEY", vm.toString(deployerPK)); - vm.setEnv("TOKEN_MINTER_V2_OWNER_KEY", vm.toString(implDeployerPK)); + vm.setEnv("TOKEN_MINTER_V2_OWNER_KEY", vm.toString(deployerPK)); newOwnerPK = uint256(keccak256("ROTATEKEYSTEST_NEW_OWNER")); newOwner = vm.addr(newOwnerPK); From c0b9ea7dee06b48a7356025f219b6bb84b63affd Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Thu, 23 Jan 2025 14:51:40 -0500 Subject: [PATCH 40/40] [Untracked] Copyright and security file (#59) --- SECURITY.md | 4 ++++ anvil/crosschainTransferIT.py | 18 ++++++++++++++++++ scripts/v1/deploy.s.sol | 17 +++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 SECURITY.md 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/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/scripts/v1/deploy.s.sol b/scripts/v1/deploy.s.sol index 470489f..ce6bcaf 100644 --- a/scripts/v1/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";