diff --git a/evm/forge/tests/CircleIntegration.t.sol b/evm/forge/tests/CircleIntegration.t.sol index a581936..a3bdaed 100644 --- a/evm/forge/tests/CircleIntegration.t.sol +++ b/evm/forge/tests/CircleIntegration.t.sol @@ -8,7 +8,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {IWormhole} from "src/interfaces/IWormhole.sol"; -import {ICircleIntegration, RedeemParameters} from "src/interfaces/ICircleIntegration.sol"; +import {ICircleIntegration} from "src/interfaces/ICircleIntegration.sol"; import {Utils} from "src/libraries/Utils.sol"; import {Deposit, WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; @@ -264,7 +264,8 @@ contract CircleIntegrationTest is Test { uint32 remoteDomain = 1; uint16 emitterChain = 6; - RedeemParameters memory redeemParams = circleIntegration.craftRedeemParameters( + ICircleIntegration.RedeemParameters memory redeemParams = circleIntegration + .craftRedeemParameters( CraftedCctpMessageParams({ remoteDomain: remoteDomain, nonce: 2 ** 64 - 1, @@ -292,7 +293,8 @@ contract CircleIntegrationTest is Test { (uint16 emitterChain, uint32 remoteDomain) = _registerEmitterAndDomain(); - RedeemParameters memory redeemParams = circleIntegration.craftRedeemParameters( + ICircleIntegration.RedeemParameters memory redeemParams = circleIntegration + .craftRedeemParameters( CraftedCctpMessageParams({ remoteDomain: remoteDomain, nonce: 2 ** 64 - 1, @@ -317,7 +319,8 @@ contract CircleIntegrationTest is Test { (uint16 emitterChain, uint32 remoteDomain) = _registerEmitterAndDomain(); - RedeemParameters memory redeemParams = circleIntegration.craftRedeemParameters( + ICircleIntegration.RedeemParameters memory redeemParams = circleIntegration + .craftRedeemParameters( CraftedCctpMessageParams({ remoteDomain: remoteDomain, nonce: 2 ** 64 - 1, @@ -340,7 +343,8 @@ contract CircleIntegrationTest is Test { function test_CannotRedeemTokensWithPayloadInvalidMessagePair() public { (uint16 emitterChain, uint32 remoteDomain) = _registerEmitterAndDomain(); - RedeemParameters memory redeemParams1 = circleIntegration.craftRedeemParameters( + ICircleIntegration.RedeemParameters memory redeemParams1 = circleIntegration + .craftRedeemParameters( CraftedCctpMessageParams({ remoteDomain: remoteDomain, nonce: 2 ** 64 - 2, @@ -353,7 +357,8 @@ contract CircleIntegrationTest is Test { abi.encodePacked("Somebody set up us the bomb") ); - RedeemParameters memory redeemParams2 = circleIntegration.craftRedeemParameters( + ICircleIntegration.RedeemParameters memory redeemParams2 = circleIntegration + .craftRedeemParameters( CraftedCctpMessageParams({ remoteDomain: remoteDomain, nonce: 2 ** 64 - 1, @@ -393,7 +398,8 @@ contract CircleIntegrationTest is Test { payload: abi.encodePacked("Somebody set up us the bomb") }); - RedeemParameters memory redeemParams = circleIntegration.craftRedeemParameters( + ICircleIntegration.RedeemParameters memory redeemParams = circleIntegration + .craftRedeemParameters( CraftedCctpMessageParams({ remoteDomain: expected.sourceDomain, nonce: expected.nonce, diff --git a/evm/forge/tests/gas/CircleIntegrationComparison.t.sol b/evm/forge/tests/gas/CircleIntegrationComparison.t.sol index e89caf8..68c483c 100644 --- a/evm/forge/tests/gas/CircleIntegrationComparison.t.sol +++ b/evm/forge/tests/gas/CircleIntegrationComparison.t.sol @@ -8,7 +8,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {IWormhole} from "src/interfaces/IWormhole.sol"; -import {ICircleIntegration, RedeemParameters} from "src/interfaces/ICircleIntegration.sol"; +import {ICircleIntegration} from "src/interfaces/ICircleIntegration.sol"; import {Utils} from "src/libraries/Utils.sol"; import {Deposit, WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; @@ -211,7 +211,8 @@ contract CircleIntegrationComparison is Test { (uint16 emitterChain, uint32 remoteDomain) = _registerEmitterAndDomain(circleIntegration); - RedeemParameters memory redeemParams = circleIntegration.craftRedeemParameters( + ICircleIntegration.RedeemParameters memory redeemParams = circleIntegration + .craftRedeemParameters( CraftedCctpMessageParams({ remoteDomain: remoteDomain, nonce: 2 ** 64 - 1, @@ -228,7 +229,9 @@ contract CircleIntegrationComparison is Test { address(inheritedContract).toUniversalAddress() // destinationCaller ); - inheritedContract.redeemUsdc(redeemParams); + inheritedContract.redeemUsdc( + redeemParams.cctpMessage, redeemParams.cctpAttestation, redeemParams.encodedVaa + ); } function test_Composed__RedeemUsdc(uint256 amount, bytes32 fromAddress, bytes32 data) public { @@ -238,7 +241,8 @@ contract CircleIntegrationComparison is Test { (uint16 emitterChain, uint32 remoteDomain) = _registerEmitterAndDomain(circleIntegration); - RedeemParameters memory redeemParams = circleIntegration.craftRedeemParameters( + ICircleIntegration.RedeemParameters memory redeemParams = circleIntegration + .craftRedeemParameters( CraftedCctpMessageParams({ remoteDomain: remoteDomain, nonce: 2 ** 64 - 1, @@ -263,7 +267,8 @@ contract CircleIntegrationComparison is Test { (uint16 emitterChain, uint32 remoteDomain) = _registerEmitterAndDomain(circleIntegration); - RedeemParameters memory redeemParams = circleIntegration.craftRedeemParameters( + ICircleIntegration.RedeemParameters memory redeemParams = circleIntegration + .craftRedeemParameters( CraftedCctpMessageParams({ remoteDomain: remoteDomain, nonce: 2 ** 64 - 1, @@ -289,7 +294,8 @@ contract CircleIntegrationComparison is Test { (uint16 emitterChain, uint32 remoteDomain) = _registerEmitterAndDomain(forkedCircleIntegration); - RedeemParameters memory redeemParams = forkedCircleIntegration.craftRedeemParameters( + ICircleIntegration.RedeemParameters memory redeemParams = forkedCircleIntegration + .craftRedeemParameters( CraftedCctpMessageParams({ remoteDomain: remoteDomain, nonce: 2 ** 64 - 1, @@ -317,7 +323,8 @@ contract CircleIntegrationComparison is Test { (uint16 emitterChain, uint32 remoteDomain) = _registerEmitterAndDomain(forkedCircleIntegration); - RedeemParameters memory redeemParams = forkedCircleIntegration.craftRedeemParameters( + ICircleIntegration.RedeemParameters memory redeemParams = forkedCircleIntegration + .craftRedeemParameters( CraftedCctpMessageParams({ remoteDomain: remoteDomain, nonce: 2 ** 64 - 1, diff --git a/evm/forge/tests/helpers/libraries/CircleIntegrationOverride.sol b/evm/forge/tests/helpers/libraries/CircleIntegrationOverride.sol index 96bbb57..42160f7 100644 --- a/evm/forge/tests/helpers/libraries/CircleIntegrationOverride.sol +++ b/evm/forge/tests/helpers/libraries/CircleIntegrationOverride.sol @@ -8,7 +8,6 @@ import {IWormhole} from "src/interfaces/IWormhole.sol"; import {BytesParsing} from "src/libraries/BytesParsing.sol"; import {Utils} from "src/libraries/Utils.sol"; import {Deposit, WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; -import {RedeemParameters} from "src/libraries/WormholeCctpStructs.sol"; import "forge-std/Test.sol"; import "forge-std/console.sol"; @@ -246,7 +245,7 @@ library CircleIntegrationOverride { bytes memory payload, bytes32 messageSender, bytes32 destinationCaller - ) internal view returns (RedeemParameters memory params) { + ) internal view returns (ICircleIntegration.RedeemParameters memory params) { params = _craftRedeemParameters( circleIntegration, cctpParams, @@ -265,7 +264,7 @@ library CircleIntegrationOverride { bytes32 fromAddress, bytes memory payload, bytes32 messageSender - ) internal view returns (RedeemParameters memory params) { + ) internal view returns (ICircleIntegration.RedeemParameters memory params) { params = _craftRedeemParameters( circleIntegration, cctpParams, @@ -283,7 +282,7 @@ library CircleIntegrationOverride { CraftedVaaParams memory vaaParams, bytes32 fromAddress, bytes memory payload - ) internal view returns (RedeemParameters memory params) { + ) internal view returns (ICircleIntegration.RedeemParameters memory params) { params = _craftRedeemParameters( circleIntegration, cctpParams, @@ -316,7 +315,7 @@ library CircleIntegrationOverride { bytes memory payload, bytes32 messageSender, bytes32 destinationCaller - ) private view returns (RedeemParameters memory params) { + ) private view returns (ICircleIntegration.RedeemParameters memory params) { CctpTokenBurnMessage memory burnMsg; (burnMsg, params.cctpMessage, params.cctpAttestation) = _craftCctpTokenBurnMessage( circleIntegration, diff --git a/evm/forge/tests/integrations/ComposingWithCircleIntegration.sol b/evm/forge/tests/integrations/ComposingWithCircleIntegration.sol index 4665aa1..b399f55 100644 --- a/evm/forge/tests/integrations/ComposingWithCircleIntegration.sol +++ b/evm/forge/tests/integrations/ComposingWithCircleIntegration.sol @@ -9,7 +9,6 @@ import {ICircleIntegration} from "src/interfaces/ICircleIntegration.sol"; import {IWormhole} from "src/interfaces/IWormhole.sol"; import {Utils} from "src/libraries/Utils.sol"; -import {RedeemParameters} from "src/libraries/WormholeCctpStructs.sol"; contract ComposingWithCircleIntegration { using SafeERC20 for IERC20; @@ -50,7 +49,7 @@ contract ComposingWithCircleIntegration { ); } - function redeemUsdc(RedeemParameters calldata params) public { + function redeemUsdc(ICircleIntegration.RedeemParameters calldata params) public { ICircleIntegration.DepositWithPayload memory deposit = _circleIntegration.redeemTokensWithPayload(params); diff --git a/evm/forge/tests/integrations/InheritingWormholeCctp.sol b/evm/forge/tests/integrations/InheritingWormholeCctp.sol index c075483..2f8e9c4 100644 --- a/evm/forge/tests/integrations/InheritingWormholeCctp.sol +++ b/evm/forge/tests/integrations/InheritingWormholeCctp.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.19; import {IWormhole} from "src/interfaces/IWormhole.sol"; import {Utils} from "src/libraries/Utils.sol"; -import {RedeemParameters} from "src/libraries/WormholeCctpStructs.sol"; import {WormholeCctp} from "src/contracts/WormholeCctp.sol"; @@ -44,8 +43,12 @@ contract InheritingWormholeCctp is WormholeCctp { ); } - function redeemUsdc(RedeemParameters calldata params) public { - verifyVaaAndMint(params); + function redeemUsdc( + bytes calldata encodedCctpMessage, + bytes calldata cctpAttestation, + bytes calldata encodedVaa + ) public { + verifyVaaAndMint(encodedCctpMessage, cctpAttestation, encodedVaa); } function myBffDomain() public pure returns (uint32 domain) { diff --git a/evm/src/contracts/CircleIntegration/Logic.sol b/evm/src/contracts/CircleIntegration/Logic.sol index d1e4638..b13da78 100644 --- a/evm/src/contracts/CircleIntegration/Logic.sol +++ b/evm/src/contracts/CircleIntegration/Logic.sol @@ -10,7 +10,6 @@ import {ICircleIntegration} from "src/interfaces/ICircleIntegration.sol"; import {Utils} from "src/libraries/Utils.sol"; import {Deposit, WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; -import {RedeemParameters} from "src/libraries/WormholeCctpStructs.sol"; import {Governance} from "./Governance.sol"; import { @@ -68,7 +67,8 @@ abstract contract Logic is ICircleIntegration, Governance { bytes32 emitter; bytes32 vaaHash; Deposit memory depositHeader; - (chain, emitter, vaaHash, depositHeader, deposit.payload) = verifyVaaAndMintLegacy(params); + (chain, emitter, vaaHash, depositHeader, deposit.payload) = + verifyVaaAndMintLegacy(params.cctpMessage, params.cctpAttestation, params.encodedVaa); // NOTE: Reverting with Error(string) comes from the old implementation, so we preserve it. require(emitter != 0 && emitter == getRegisteredEmitters()[chain], "unknown emitter"); diff --git a/evm/src/contracts/WormholeCctp.sol b/evm/src/contracts/WormholeCctp.sol index cc70e28..e1b36ed 100644 --- a/evm/src/contracts/WormholeCctp.sol +++ b/evm/src/contracts/WormholeCctp.sol @@ -12,7 +12,6 @@ import {ITokenMinter} from "src/interfaces/ITokenMinter.sol"; import {Utils} from "src/libraries/Utils.sol"; import {Deposit, WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; -import {RedeemParameters, RedeemedDeposit} from "src/libraries/WormholeCctpStructs.sol"; abstract contract WormholeCctp { using Utils for address; @@ -21,7 +20,19 @@ abstract contract WormholeCctp { error InvalidVaa(); error CallerNotMintRecipient(bytes32, bytes32); - error CctpVaaMismatch(); + error CctpVaaMismatch(uint32, uint32, uint64); + + struct RedeemedDeposit { + bytes32 token; + uint256 amount; + bytes32 burnSource; + uint32 vaaTimestamp; + uint32 vaaNonce; + uint16 emitterChain; + bytes32 emitterAddress; + uint64 vaaSequence; + bytes32 vaaHash; + } /** * @notice Emitted when Circle-supported assets have been minted to the mintRecipient @@ -95,10 +106,11 @@ abstract contract WormholeCctp { ); } - function verifyVaaAndMint(RedeemParameters calldata params) - internal - returns (RedeemedDeposit memory redeemed, bytes memory payload) - { + function verifyVaaAndMint( + bytes calldata encodedCctpMessage, + bytes calldata cctpAttestation, + bytes calldata encodedVaa + ) internal returns (RedeemedDeposit memory redeemed, bytes memory payload) { Deposit memory deposit; ( redeemed.vaaTimestamp, @@ -110,7 +122,9 @@ abstract contract WormholeCctp { deposit, payload ) = _verifyVaaAndMint( - params, + encodedCctpMessage, + cctpAttestation, + encodedVaa, true // revertCustomErrors ); @@ -120,7 +134,11 @@ abstract contract WormholeCctp { redeemed.burnSource = deposit.burnSource; } - function verifyVaaAndMintLegacy(RedeemParameters calldata params) + function verifyVaaAndMintLegacy( + bytes calldata encodedCctpMessage, + bytes calldata cctpAttestation, + bytes calldata encodedVaa + ) internal returns ( uint16 emitterChain, @@ -131,7 +149,9 @@ abstract contract WormholeCctp { ) { (,, emitterChain, emitterAddress,, vaaHash, deposit, payload) = _verifyVaaAndMint( - params, + encodedCctpMessage, + cctpAttestation, + encodedVaa, false // revertCustomErrors ); } @@ -148,7 +168,12 @@ abstract contract WormholeCctp { // private - function _verifyVaaAndMint(RedeemParameters calldata params, bool revertCustomErrors) + function _verifyVaaAndMint( + bytes calldata encodedCctpMessage, + bytes calldata cctpAttestation, + bytes calldata encodedVaa, + bool revertCustomErrors + ) private returns ( uint32 vaaTimestamp, @@ -163,7 +188,7 @@ abstract contract WormholeCctp { { // Parse and Verify VAA. (IWormhole.VM memory vaa, bool valid, string memory reason) = - _wormhole.parseAndVerifyVM(params.encodedVaa); + _wormhole.parseAndVerifyVM(encodedVaa); // Confirm that the core layer verified the message. if (!valid) { @@ -191,20 +216,23 @@ abstract contract WormholeCctp { // Confirm that the caller passed the correct message pair. { - bytes memory encodedCctpMessage = params.cctpMessage; - uint32 sourceDomain; uint32 targetDomain; uint64 nonce; - // NOTE: First four bytes is the CCTP message version. assembly ("memory-safe") { - // source domain is 4 bytes after the version - sourceDomain := mload(add(encodedCctpMessage, 8)) - // target domain is 4 bytes after the source domain - targetDomain := mload(add(encodedCctpMessage, 12)) - // nonce is 8 bytes after the target version - nonce := mload(add(encodedCctpMessage, 20)) + // NOTE: First four bytes is the CCTP message version. + let ptr := calldataload(encodedCctpMessage.offset) + + // NOTE: There is no need to mask here because the types defined outside of this + // YUL block will already perform big-endian masking. + + // Source domain is bytes 4..8, so shift 24 bytes to the right. + sourceDomain := shr(192, ptr) + // Target domain is bytes 8..12, so shift 20 bytes to the right. + targetDomain := shr(160, ptr) + // Nonce is bytes 12..20, so shift 12 bytes to the right. + nonce := shr(96, ptr) } if ( @@ -212,7 +240,7 @@ abstract contract WormholeCctp { || deposit.cctpNonce != nonce ) { if (revertCustomErrors) { - revert CctpVaaMismatch(); + revert CctpVaaMismatch(sourceDomain, targetDomain, nonce); } else { _revertBuiltIn("invalid message pair"); } @@ -220,7 +248,7 @@ abstract contract WormholeCctp { } // Call the circle bridge to mint tokens to the recipient. - _messageTransmitter.receiveMessage(params.cctpMessage, params.cctpAttestation); + _messageTransmitter.receiveMessage(encodedCctpMessage, cctpAttestation); // Overwrite the token address in the deposit with the local token address. We should trust // that this getter will not return the zero address because the Token Minter will have diff --git a/evm/src/interfaces/ICircleIntegration.sol b/evm/src/interfaces/ICircleIntegration.sol index 089611f..ff858fc 100644 --- a/evm/src/interfaces/ICircleIntegration.sol +++ b/evm/src/interfaces/ICircleIntegration.sol @@ -2,8 +2,6 @@ pragma solidity ^0.8.0; -import {RedeemParameters} from "src/libraries/WormholeCctpStructs.sol"; - import {IWormhole} from "./IWormhole.sol"; import {IMessageTransmitter} from "./IMessageTransmitter.sol"; import {ITokenMessenger} from "./ITokenMessenger.sol"; @@ -44,6 +42,15 @@ interface ICircleIntegration is IGovernance { bytes32 mintRecipient; } + /** + * @notice Parameters used for inbound transfers. + */ + struct RedeemParameters { + bytes encodedVaa; + bytes cctpMessage; + bytes cctpAttestation; + } + /** * @notice `transferTokensWithPayload` calls the Circle Bridge contract to burn Circle-supported * tokens. It emits a Wormhole message containing a user-specified payload with instructions for diff --git a/evm/src/libraries/WormholeCctpStructs.sol b/evm/src/libraries/WormholeCctpStructs.sol deleted file mode 100644 index 0016dd4..0000000 --- a/evm/src/libraries/WormholeCctpStructs.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: Apache 2 - -pragma solidity ^0.8.0; - -/** - * @notice Parameters used for inbound transfers. - */ -struct RedeemParameters { - bytes encodedVaa; - bytes cctpMessage; - bytes cctpAttestation; -} - -struct RedeemedDeposit { - bytes32 token; - uint256 amount; - bytes32 burnSource; - uint32 vaaTimestamp; - uint32 vaaNonce; - uint16 emitterChain; - bytes32 emitterAddress; - uint64 vaaSequence; - bytes32 vaaHash; -} diff --git a/evm/src/mock/MockIntegration.sol b/evm/src/mock/MockIntegration.sol deleted file mode 100644 index 367eb1e..0000000 --- a/evm/src/mock/MockIntegration.sol +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: Apache 2 -pragma solidity ^0.8.19; - -import {IWormhole} from "src/interfaces/IWormhole.sol"; -import {ICircleIntegration, RedeemParameters} from "src/interfaces/ICircleIntegration.sol"; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -contract MockIntegration { - // owner address - address public owner; - - // trusted sender address - address public trustedSender; - - // trusted Wormhole chainId - uint16 public trustedChainId; - - // redemption sequence - uint256 public redemptionSequence; - - // payload mapping - mapping(uint256 => bytes) payloadMap; - - // Wormhole's CircleIntegration instance - ICircleIntegration public circleIntegration; - - // save the deployer's address in the `owner` state variable - constructor() { - owner = msg.sender; - } - - function redeemTokensWithPayload( - RedeemParameters memory redeemParams, - address transferRecipient - ) public returns (uint256) { - // mint USDC to this contract - ICircleIntegration.DepositWithPayload memory deposit = - circleIntegration.redeemTokensWithPayload(redeemParams); - - // verify that the sender is the trustedSender - require( - msg.sender == trustedSender - && circleIntegration.getChainIdFromDomain(deposit.sourceDomain) == trustedChainId, - "invalid sender" - ); - - // uptick sequence - redemptionSequence += 1; - - // save the payload - payloadMap[redemptionSequence] = deposit.payload; - - // send the tokens to the transferRecipient address - SafeERC20.safeTransfer( - IERC20(address(uint160(uint256(deposit.token)))), transferRecipient, deposit.amount - ); - - return redemptionSequence; - } - - function getPayload(uint256 redemptionSequence_) public view returns (bytes memory) { - return payloadMap[redemptionSequence_]; - } - - function setup(address circleIntegrationAddress, address trustedSender_, uint16 trustedChainId_) - public - onlyOwner - { - // create contract interfaces and store `trustedSender` address - circleIntegration = ICircleIntegration(circleIntegrationAddress); - trustedSender = trustedSender_; - trustedChainId = trustedChainId_; - } - - modifier onlyOwner() { - require(owner == msg.sender, "caller not the owner"); - _; - } -}