diff --git a/evm/Makefile b/evm/Makefile index be411c6..e246b77 100644 --- a/evm/Makefile +++ b/evm/Makefile @@ -33,7 +33,7 @@ forge-test: dependencies .PHONY: gas-report gas-report: dependencies - forge test --fork-url ${TESTING_FORK_RPC} --match-path forge/tests/gas/* --fuzz-runs 256 --gas-report + forge test --fork-url ${TESTING_FORK_RPC} --match-path forge/tests/gas/* --fuzz-runs 512 --gas-report .PHONY: gas-snapshot gas-snapshot: dependencies diff --git a/evm/forge/tests/CircleIntegration.t.sol b/evm/forge/tests/CircleIntegration.t.sol index a3bdaed..a0bd5dc 100644 --- a/evm/forge/tests/CircleIntegration.t.sol +++ b/evm/forge/tests/CircleIntegration.t.sol @@ -11,7 +11,7 @@ import {IWormhole} from "src/interfaces/IWormhole.sol"; import {ICircleIntegration} from "src/interfaces/ICircleIntegration.sol"; import {Utils} from "src/libraries/Utils.sol"; -import {Deposit, WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; +import {WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; import {Setup} from "src/contracts/CircleIntegration/Setup.sol"; import {Implementation} from "src/contracts/CircleIntegration/Implementation.sol"; @@ -239,17 +239,16 @@ contract CircleIntegrationTest is Test { assertEq( keccak256(fetchedPayloads[i]), keccak256( - Deposit({ - token: USDC_ADDRESS.toUniversalAddress(), - amount: amount, - sourceCctpDomain: circleIntegration.circleBridge().localMessageTransmitter() - .localDomain(), - targetCctpDomain: targetDomain, - cctpNonce: circleIntegration.circleBridge().localMessageTransmitter() + USDC_ADDRESS.encodeDeposit( + amount, + circleIntegration.circleBridge().localMessageTransmitter().localDomain(), + targetDomain, + circleIntegration.circleBridge().localMessageTransmitter() .nextAvailableNonce() - 2 + uint64(i), - burnSource: address(this).toUniversalAddress(), - mintRecipient: mintRecipient - }).encodeWithPayload(payloads[i]) + address(this).toUniversalAddress(), + mintRecipient, + payloads[i] + ) ) ); unchecked { diff --git a/evm/forge/tests/InheritingWormholeCctp.t.sol b/evm/forge/tests/InheritingWormholeCctp.t.sol index 0bb08d2..5a15edf 100644 --- a/evm/forge/tests/InheritingWormholeCctp.t.sol +++ b/evm/forge/tests/InheritingWormholeCctp.t.sol @@ -11,7 +11,7 @@ import {ICircleIntegration} from "src/interfaces/ICircleIntegration.sol"; import {IWormhole} from "src/interfaces/IWormhole.sol"; import {Utils} from "src/libraries/Utils.sol"; -import {Deposit, WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; +import {WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; import {Setup} from "src/contracts/CircleIntegration/Setup.sol"; import {Implementation} from "src/contracts/CircleIntegration/Implementation.sol"; @@ -115,16 +115,16 @@ contract InheritingWormholeCctpTest is Test { assertEq( keccak256(fetchedPayloads[0]), keccak256( - Deposit({ - token: USDC_ADDRESS.toUniversalAddress(), - amount: amount, - sourceCctpDomain: 0, - targetCctpDomain: inheritedContract.myBffDomain(), - cctpNonce: circleIntegration.circleBridge().localMessageTransmitter() - .nextAvailableNonce() - 1, - burnSource: address(this).toUniversalAddress(), - mintRecipient: mintRecipient - }).encodeWithPayload(payload) + USDC_ADDRESS.encodeDeposit( + amount, + 0, // sourceCctpDomain + inheritedContract.myBffDomain(), // targetCctpDomain + circleIntegration.circleBridge().localMessageTransmitter().nextAvailableNonce() + - 1, + address(this).toUniversalAddress(), + mintRecipient, + payload + ) ) ); diff --git a/evm/forge/tests/WormholeCctpMessages.t.sol b/evm/forge/tests/WormholeCctpMessages.t.sol index 10af794..c0042d9 100644 --- a/evm/forge/tests/WormholeCctpMessages.t.sol +++ b/evm/forge/tests/WormholeCctpMessages.t.sol @@ -5,7 +5,9 @@ pragma solidity ^0.8.19; import "forge-std/Test.sol"; import "forge-std/console.sol"; -import {Deposit, WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; +import {IWormhole} from "src/interfaces/IWormhole.sol"; + +import {WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; contract MessagesTest is Test { using WormholeCctpMessages for *; @@ -24,24 +26,47 @@ contract MessagesTest is Test { vm.assume(payload.length > 0); vm.assume(payload.length < type(uint16).max); - Deposit memory deposit = Deposit({ - token: token, - amount: amount, - sourceCctpDomain: sourceCctpDomain, - targetCctpDomain: targetCctpDomain, - cctpNonce: cctpNonce, - burnSource: burnSource, - mintRecipient: mintRecipient - }); - - bytes memory encoded = deposit.encodeWithPayload(payload); - assertEq(encoded.length, 147 + payload.length); + IWormhole.VM memory fakeVaa; + fakeVaa.payload = token.encodeDeposit( + amount, + sourceCctpDomain, + targetCctpDomain, + cctpNonce, + burnSource, + mintRecipient, + payload + ); + assertEq(fakeVaa.payload.length, 147 + payload.length); - uint8 payloadId = uint8(bytes1(encoded)); + uint8 payloadId = uint8(bytes1(fakeVaa.payload)); assertEq(payloadId, 1); - (Deposit memory decoded, bytes memory takenPayload) = encoded.decodeDepositWithPayload(); - assertEq(keccak256(abi.encode(decoded)), keccak256(abi.encode(deposit))); + bytes32 decodedToken; + uint256 decodedAmount; + uint32 decodedSourceCctpDomain; + uint32 decodedTargetCctpDomain; + uint64 decodedCctpNonce; + bytes32 decodedBurnSource; + bytes32 decodedMintRecipient; + bytes memory takenPayload; + ( + decodedToken, + decodedAmount, + decodedSourceCctpDomain, + decodedTargetCctpDomain, + decodedCctpNonce, + decodedBurnSource, + decodedMintRecipient, + takenPayload + ) = fakeVaa.decodeDeposit(); + + assertEq(decodedToken, token); + assertEq(decodedAmount, amount); + assertEq(decodedSourceCctpDomain, sourceCctpDomain); + assertEq(decodedTargetCctpDomain, targetCctpDomain); + assertEq(decodedCctpNonce, cctpNonce); + assertEq(decodedBurnSource, burnSource); + assertEq(decodedMintRecipient, mintRecipient); assertEq(keccak256(abi.encode(takenPayload)), keccak256(abi.encode(payload))); } } diff --git a/evm/forge/tests/gas/CircleIntegrationComparison.t.sol b/evm/forge/tests/gas/CircleIntegrationComparison.t.sol index 68c483c..a199c7b 100644 --- a/evm/forge/tests/gas/CircleIntegrationComparison.t.sol +++ b/evm/forge/tests/gas/CircleIntegrationComparison.t.sol @@ -11,7 +11,7 @@ import {IWormhole} from "src/interfaces/IWormhole.sol"; import {ICircleIntegration} from "src/interfaces/ICircleIntegration.sol"; import {Utils} from "src/libraries/Utils.sol"; -import {Deposit, WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; +import {WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; import {Setup} from "src/contracts/CircleIntegration/Setup.sol"; import {Implementation} from "src/contracts/CircleIntegration/Implementation.sol"; diff --git a/evm/forge/tests/helpers/libraries/CircleIntegrationOverride.sol b/evm/forge/tests/helpers/libraries/CircleIntegrationOverride.sol index 42160f7..a5bb178 100644 --- a/evm/forge/tests/helpers/libraries/CircleIntegrationOverride.sol +++ b/evm/forge/tests/helpers/libraries/CircleIntegrationOverride.sol @@ -7,7 +7,7 @@ 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 {WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; import "forge-std/Test.sol"; import "forge-std/console.sol"; @@ -332,15 +332,15 @@ library CircleIntegrationOverride { vaaParams.emitterChain, burnMsg.messageSender, vaaParams.sequence, - Deposit({ - token: burnMsg.burnToken, - amount: burnMsg.amount, - sourceCctpDomain: burnMsg.header.sourceDomain, - targetCctpDomain: burnMsg.header.destinationDomain, - cctpNonce: burnMsg.header.nonce, - burnSource: burnSource, - mintRecipient: burnMsg.mintRecipient - }).encodeWithPayload(payload) + burnMsg.burnToken.encodeDeposit( + burnMsg.amount, + burnMsg.header.sourceDomain, + burnMsg.header.destinationDomain, + burnMsg.header.nonce, + burnSource, + burnMsg.mintRecipient, + payload + ) ); } diff --git a/evm/src/contracts/CircleIntegration/Logic.sol b/evm/src/contracts/CircleIntegration/Logic.sol index b13da78..c0e8b85 100644 --- a/evm/src/contracts/CircleIntegration/Logic.sol +++ b/evm/src/contracts/CircleIntegration/Logic.sol @@ -9,7 +9,7 @@ import {ITokenMinter} from "src/interfaces/ITokenMinter.sol"; import {ICircleIntegration} from "src/interfaces/ICircleIntegration.sol"; import {Utils} from "src/libraries/Utils.sol"; -import {Deposit, WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; +import {WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; import {Governance} from "./Governance.sol"; import { @@ -63,15 +63,25 @@ abstract contract Logic is ICircleIntegration, Governance { { require(evmChain() == block.chainid, "invalid evm chain"); - uint16 chain; - bytes32 emitter; - bytes32 vaaHash; - Deposit memory depositHeader; - (chain, emitter, vaaHash, depositHeader, deposit.payload) = - verifyVaaAndMintLegacy(params.cctpMessage, params.cctpAttestation, params.encodedVaa); + IWormhole.VM memory vaa; + ( + vaa, + deposit.token, + deposit.amount, + deposit.sourceDomain, + deposit.targetDomain, + deposit.nonce, + deposit.fromAddress, + deposit.mintRecipient, + 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"); + require( + vaa.emitterAddress != 0 + && vaa.emitterAddress == getRegisteredEmitters()[vaa.emitterChainId], + "unknown emitter" + ); mapping(bytes32 => bool) storage consumedVaas = getConsumedVaas(); @@ -79,19 +89,13 @@ abstract contract Logic is ICircleIntegration, Governance { // attacks, but it may not be necessary because the CCTP Message Transmitter already keeps // track of used nonces. // NOTE: Reverting with Error(string) comes from the old implementation, so we preserve it. - require(!consumedVaas[vaaHash], "message already consumed"); + require(!consumedVaas[vaa.hash], "message already consumed"); // Mark as consumed. - consumedVaas[vaaHash] = true; + consumedVaas[vaa.hash] = true; - // Set remaining deposit vars. - deposit.token = depositHeader.token; - deposit.amount = depositHeader.amount; - deposit.sourceDomain = depositHeader.sourceCctpDomain; - deposit.targetDomain = depositHeader.targetCctpDomain; - deposit.nonce = depositHeader.cctpNonce; - deposit.fromAddress = depositHeader.burnSource; - deposit.mintRecipient = depositHeader.mintRecipient; + // Emit Redeemed event. + emit Redeemed(vaa.emitterChainId, vaa.emitterAddress, vaa.sequence); } // getters @@ -116,18 +120,22 @@ abstract contract Logic is ICircleIntegration, Governance { pure returns (DepositWithPayload memory deposit) { - Deposit memory depositHeader; - (depositHeader, deposit.payload) = encoded.decodeDepositWithPayload( - false // revertCustomErrors - ); - - deposit.token = depositHeader.token; - deposit.amount = depositHeader.amount; - deposit.sourceDomain = depositHeader.sourceCctpDomain; - deposit.targetDomain = depositHeader.targetCctpDomain; - deposit.nonce = depositHeader.cctpNonce; - deposit.fromAddress = depositHeader.burnSource; - deposit.mintRecipient = depositHeader.mintRecipient; + // This is a hack to get around using the decodeDeposit method. This is not a real VM + // obviously. + // + // Plus, this getter should never be used in practice. + IWormhole.VM memory fakeVaa; + fakeVaa.payload = encoded; + ( + deposit.token, + deposit.amount, + deposit.sourceDomain, + deposit.targetDomain, + deposit.nonce, + deposit.fromAddress, + deposit.mintRecipient, + deposit.payload + ) = fakeVaa.decodeDeposit(); } /// @inheritdoc ICircleIntegration @@ -136,15 +144,15 @@ abstract contract Logic is ICircleIntegration, Governance { pure returns (bytes memory encoded) { - encoded = Deposit({ - token: message.token, - amount: message.amount, - sourceCctpDomain: message.sourceDomain, - targetCctpDomain: message.targetDomain, - cctpNonce: message.nonce, - burnSource: message.fromAddress, - mintRecipient: message.mintRecipient - }).encodeWithPayload(message.payload); + encoded = message.token.encodeDeposit( + message.amount, + message.sourceDomain, + message.targetDomain, + message.nonce, + message.fromAddress, + message.mintRecipient, + message.payload + ); } /// @inheritdoc ICircleIntegration diff --git a/evm/src/contracts/WormholeCctp.sol b/evm/src/contracts/WormholeCctp.sol index e1b36ed..e7a07b0 100644 --- a/evm/src/contracts/WormholeCctp.sol +++ b/evm/src/contracts/WormholeCctp.sol @@ -11,7 +11,7 @@ import {ITokenMessenger} from "src/interfaces/ITokenMessenger.sol"; import {ITokenMinter} from "src/interfaces/ITokenMinter.sol"; import {Utils} from "src/libraries/Utils.sol"; -import {Deposit, WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; +import {WormholeCctpMessages} from "src/libraries/WormholeCctpMessages.sol"; abstract contract WormholeCctp { using Utils for address; @@ -22,28 +22,6 @@ abstract contract WormholeCctp { error CallerNotMintRecipient(bytes32, bytes32); 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 - * @param emitterChainId Wormhole chain ID of emitter contract on source chain - * @param emitterAddress Address (bytes32 zero-left-padded) of emitter on source chain - * @param sequence Sequence of Wormhole message used to mint tokens - */ - event Redeemed( - uint16 indexed emitterChainId, bytes32 indexed emitterAddress, uint64 indexed sequence - ); - IWormhole immutable _wormhole; uint16 immutable _chainId; uint8 immutable _messageFinality; @@ -93,15 +71,15 @@ abstract contract WormholeCctp { // Publish DepositWithPayload via Wormhole Core Bridge. wormholeSequence = _wormhole.publishMessage{value: msg.value}( wormholeNonce, - Deposit({ - token: token.toUniversalAddress(), - amount: amount, - sourceCctpDomain: _localCctpDomain, - targetCctpDomain: targetCctpDomain, - cctpNonce: cctpNonce, - burnSource: msg.sender.toUniversalAddress(), - mintRecipient: mintRecipient - }).encodeWithPayload(payload), + token.encodeDeposit( + amount, + _localCctpDomain, // sourceCctpDomain + targetCctpDomain, + cctpNonce, + msg.sender.toUniversalAddress(), // burnSource + mintRecipient, + payload + ), _messageFinality ); } @@ -110,28 +88,46 @@ abstract contract WormholeCctp { bytes calldata encodedCctpMessage, bytes calldata cctpAttestation, bytes calldata encodedVaa - ) internal returns (RedeemedDeposit memory redeemed, bytes memory payload) { - Deposit memory deposit; + ) + internal + returns ( + IWormhole.VM memory vaa, + bytes32 token, + uint256 amount, + bytes32 burnSource, + bytes memory payload + ) + { + vaa = _parseAndVerifyVaa( + encodedVaa, + true // revertCustomErrors + ); + + uint32 sourceCctpDomain; + uint32 targetCctpDomain; + uint64 cctpNonce; + bytes32 mintRecipient; ( - redeemed.vaaTimestamp, - redeemed.vaaNonce, - redeemed.emitterChain, - redeemed.emitterAddress, - redeemed.vaaSequence, - redeemed.vaaHash, - deposit, + token, + amount, + sourceCctpDomain, + targetCctpDomain, + cctpNonce, + burnSource, + mintRecipient, payload - ) = _verifyVaaAndMint( + ) = vaa.decodeDeposit(); + + token = _matchMessagesAndMint( encodedCctpMessage, cctpAttestation, - encodedVaa, + mintRecipient, + sourceCctpDomain, + targetCctpDomain, + cctpNonce, + token, true // revertCustomErrors ); - - // Set remaining redeemed deposit vars. - redeemed.token = deposit.token; - redeemed.amount = deposit.amount; - redeemed.burnSource = deposit.burnSource; } function verifyVaaAndMintLegacy( @@ -141,17 +137,41 @@ abstract contract WormholeCctp { ) internal returns ( - uint16 emitterChain, - bytes32 emitterAddress, - bytes32 vaaHash, - Deposit memory deposit, + IWormhole.VM memory vaa, + bytes32 token, + uint256 amount, + uint32 sourceCctpDomain, + uint32 targetCctpDomain, + uint64 cctpNonce, + bytes32 burnSource, + bytes32 mintRecipient, bytes memory payload ) { - (,, emitterChain, emitterAddress,, vaaHash, deposit, payload) = _verifyVaaAndMint( + vaa = _parseAndVerifyVaa( + encodedVaa, + false // revertCustomErrors + ); + + ( + token, + amount, + sourceCctpDomain, + targetCctpDomain, + cctpNonce, + burnSource, + mintRecipient, + payload + ) = vaa.decodeDeposit(); + + token = _matchMessagesAndMint( encodedCctpMessage, cctpAttestation, - encodedVaa, + mintRecipient, + sourceCctpDomain, + targetCctpDomain, + cctpNonce, + token, false // revertCustomErrors ); } @@ -168,49 +188,40 @@ abstract contract WormholeCctp { // private - function _verifyVaaAndMint( - bytes calldata encodedCctpMessage, - bytes calldata cctpAttestation, - bytes calldata encodedVaa, - bool revertCustomErrors - ) + function _parseAndVerifyVaa(bytes calldata encodedVaa, bool revertCustomErrors) private - returns ( - uint32 vaaTimestamp, - uint32 vaaNonce, - uint16 emitterChain, - bytes32 emitterAddress, - uint64 vaaSequence, - bytes32 vaaHash, - Deposit memory deposit, - bytes memory payload - ) + view + returns (IWormhole.VM memory vaa) { - // Parse and Verify VAA. - (IWormhole.VM memory vaa, bool valid, string memory reason) = - _wormhole.parseAndVerifyVM(encodedVaa); + bool valid; + string memory reason; + (vaa, valid, reason) = _wormhole.parseAndVerifyVM(encodedVaa); - // Confirm that the core layer verified the message. if (!valid) { if (revertCustomErrors) { revert InvalidVaa(); } else { - _revertBuiltIn(reason); + Utils.revertBuiltIn(reason); } } + } - // Decode the message payload into the DepositWithPayload struct. Call the TokenMinter - // contract to determine the address of the encoded token on this chain. - (deposit, payload) = vaa.payload.decodeDepositWithPayload(revertCustomErrors); - + function _matchMessagesAndMint( + bytes calldata encodedCctpMessage, + bytes calldata cctpAttestation, + bytes32 mintRecipient, + uint32 vaaSourceCctpDomain, + uint32 vaaTargetCctpDomain, + uint64 vaaCctpNonce, + bytes32 burnToken, + bool revertCustomErrors + ) private returns (bytes32 mintToken) { // Confirm that the caller is the `mintRecipient` to ensure atomic execution. - if (msg.sender.toUniversalAddress() != deposit.mintRecipient) { + if (msg.sender.toUniversalAddress() != mintRecipient) { if (revertCustomErrors) { - revert CallerNotMintRecipient( - msg.sender.toUniversalAddress(), deposit.mintRecipient - ); + revert CallerNotMintRecipient(msg.sender.toUniversalAddress(), mintRecipient); } else { - _revertBuiltIn("caller must be mintRecipient"); + Utils.revertBuiltIn("caller must be mintRecipient"); } } @@ -236,13 +247,13 @@ abstract contract WormholeCctp { } if ( - deposit.sourceCctpDomain != sourceDomain || deposit.targetCctpDomain != targetDomain - || deposit.cctpNonce != nonce + vaaSourceCctpDomain != sourceDomain || vaaTargetCctpDomain != targetDomain + || vaaCctpNonce != nonce ) { if (revertCustomErrors) { revert CctpVaaMismatch(sourceDomain, targetDomain, nonce); } else { - _revertBuiltIn("invalid message pair"); + Utils.revertBuiltIn("invalid message pair"); } } } @@ -250,22 +261,8 @@ abstract contract WormholeCctp { // Call the circle bridge to mint tokens to the recipient. _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 - // already minted the valid token for the mint recipient. - deposit.token = fetchLocalToken(deposit.sourceCctpDomain, deposit.token); - - // Set remaining stack vars. - vaaTimestamp = vaa.timestamp; - vaaNonce = vaa.nonce; - emitterChain = vaa.emitterChainId; - emitterAddress = vaa.emitterAddress; - vaaSequence = vaa.sequence; - vaaHash = vaa.hash; - } - - function _revertBuiltIn(string memory reason) private pure { - // NOTE: Using require is the easy way to revert with the built-in Error type. - require(false, reason); + // We should trust that this getter will not return the zero address because the TokenMinter + // will have already minted the valid token for the mint recipient. + mintToken = fetchLocalToken(vaaSourceCctpDomain, burnToken); } } diff --git a/evm/src/interfaces/ICircleIntegration.sol b/evm/src/interfaces/ICircleIntegration.sol index ff858fc..7a103b1 100644 --- a/evm/src/interfaces/ICircleIntegration.sol +++ b/evm/src/interfaces/ICircleIntegration.sol @@ -51,6 +51,16 @@ interface ICircleIntegration is IGovernance { bytes cctpAttestation; } + /** + * @notice Emitted when Circle-supported assets have been minted to the mintRecipient + * @param emitterChainId Wormhole chain ID of emitter contract on source chain + * @param emitterAddress Address (bytes32 zero-left-padded) of emitter on source chain + * @param sequence Sequence of Wormhole message used to mint tokens + */ + event Redeemed( + uint16 indexed emitterChainId, bytes32 indexed emitterAddress, uint64 indexed sequence + ); + /** * @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/Utils.sol b/evm/src/libraries/Utils.sol index b579812..a00227f 100644 --- a/evm/src/libraries/Utils.sol +++ b/evm/src/libraries/Utils.sol @@ -24,4 +24,9 @@ library Utils { converted := universalAddr } } + + function revertBuiltIn(string memory reason) internal pure { + // NOTE: Using require is the easy way to revert with the built-in Error type. + require(false, reason); + } } diff --git a/evm/src/libraries/WormholeCctpMessages.sol b/evm/src/libraries/WormholeCctpMessages.sol index 253b8b9..24ec34f 100644 --- a/evm/src/libraries/WormholeCctpMessages.sol +++ b/evm/src/libraries/WormholeCctpMessages.sol @@ -5,18 +5,10 @@ pragma solidity ^0.8.19; import {IWormhole} from "src/interfaces/IWormhole.sol"; import {BytesParsing} from "src/libraries/BytesParsing.sol"; - -struct Deposit { - bytes32 token; - uint256 amount; - uint32 sourceCctpDomain; - uint32 targetCctpDomain; - uint64 cctpNonce; - bytes32 burnSource; - bytes32 mintRecipient; -} +import {Utils} from "src/libraries/Utils.sol"; library WormholeCctpMessages { + using Utils for address; using BytesParsing for bytes; // Payload IDs. @@ -24,60 +16,65 @@ library WormholeCctpMessages { uint256 public constant PAYLOAD_OFFSET = 149; + error MissingPayload(); error PayloadTooLarge(uint256); error InvalidMessage(); error UnexpectedMessageLength(uint256, uint256); - function encodeWithPayload(Deposit memory deposit, bytes memory payload) - internal - pure - returns (bytes memory) - { - if (payload.length > type(uint16).max) { - revert PayloadTooLarge(payload.length); - } - - return abi.encodePacked( - DEPOSIT, - deposit.token, - deposit.amount, - deposit.sourceCctpDomain, - deposit.targetCctpDomain, - deposit.cctpNonce, - deposit.burnSource, - deposit.mintRecipient, - _encodeBytes(payload) + function encodeDeposit( + address token, + uint256 amount, + uint32 sourceCctpDomain, + uint32 targetCctpDomain, + uint64 cctpNonce, + bytes32 burnSource, + bytes32 mintRecipient, + bytes memory payload + ) internal pure returns (bytes memory encoded) { + encoded = encodeDeposit( + token.toUniversalAddress(), + amount, + sourceCctpDomain, + targetCctpDomain, + cctpNonce, + burnSource, + mintRecipient, + payload ); } - function decodeDepositWithPayload(bytes memory encoded) - internal - pure - returns (Deposit memory deposit, bytes memory payload) - { - (deposit, payload) = decodeDepositWithPayload(encoded, true); - } - - function decodeDepositWithPayload(bytes memory encoded, bool revertCustomErrors) - internal - pure - returns (Deposit memory deposit, bytes memory payload) - { - uint256 offset = _checkPayloadId(encoded, 0, DEPOSIT, revertCustomErrors); - - (deposit.token, offset) = encoded.asBytes32Unchecked(offset); - (deposit.amount, offset) = encoded.asUint256Unchecked(offset); - (deposit.sourceCctpDomain, offset) = encoded.asUint32Unchecked(offset); - (deposit.targetCctpDomain, offset) = encoded.asUint32Unchecked(offset); - (deposit.cctpNonce, offset) = encoded.asUint64Unchecked(offset); - (deposit.burnSource, offset) = encoded.asBytes32Unchecked(offset); - (deposit.mintRecipient, offset) = encoded.asBytes32Unchecked(offset); - (payload, offset) = _decodeBytes(encoded, offset); + function encodeDeposit( + bytes32 universalTokenAddress, + uint256 amount, + uint32 sourceCctpDomain, + uint32 targetCctpDomain, + uint64 cctpNonce, + bytes32 burnSource, + bytes32 mintRecipient, + bytes memory payload + ) internal pure returns (bytes memory encoded) { + uint256 payloadLen = payload.length; + if (payloadLen == 0) { + revert MissingPayload(); + } else if (payloadLen > type(uint16).max) { + revert PayloadTooLarge(payloadLen); + } - _checkLength(encoded, offset, revertCustomErrors); + encoded = abi.encodePacked( + DEPOSIT, + universalTokenAddress, + amount, + sourceCctpDomain, + targetCctpDomain, + cctpNonce, + burnSource, + mintRecipient, + uint16(payloadLen), + payload + ); } - function readDeposit(IWormhole.VM memory vaa) + function decodeDeposit(IWormhole.VM memory vaa) internal pure returns ( @@ -91,111 +88,45 @@ library WormholeCctpMessages { bytes memory payload ) { - bytes memory vaaPayload = vaa.payload; - { - uint8 msgId; - assembly ("memory-safe") { - msgId := mload(add(vaaPayload, 1)) - } - if (msgId != DEPOSIT) { - revert InvalidMessage(); - } - } - - uint16 payloadLen; - assembly ("memory-safe") { - payloadLen := mload(add(vaaPayload, 147)) - } - _checkLength(vaaPayload, 147 + uint256(payloadLen), true); - - payload = new bytes(payloadLen); - assembly ("memory-safe") { - token := mload(add(vaaPayload, 33)) - amount := mload(add(vaaPayload, 65)) - sourceCctpDomain := mload(add(vaaPayload, 69)) - targetCctpDomain := mload(add(vaaPayload, 73)) - cctpNonce := mload(add(vaaPayload, 81)) - burnSource := mload(add(vaaPayload, 123)) - mintRecipient := mload(add(vaaPayload, 145)) - mstore(add(payload, 0x20), mload(add(vaaPayload, 149))) - } - } - - function readDepositTokenUnchecked(IWormhole.VM memory vaa) - internal - pure - returns (bytes32 token) - { - bytes memory payload = vaa.payload; - assembly ("memory-safe") { - token := mload(add(payload, 33)) - } - } - - function readDepositAmountUnchecked(IWormhole.VM memory vaa) - internal - pure - returns (uint256 amount) - { - bytes memory payload = vaa.payload; - assembly ("memory-safe") { - amount := mload(add(payload, 65)) - } + ( + token, + amount, + sourceCctpDomain, + targetCctpDomain, + cctpNonce, + burnSource, + mintRecipient, + payload + ) = decodeDeposit(vaa, true); } - function readDepositSourceCctpDomainUnchecked(IWormhole.VM memory vaa) + function decodeDeposit(IWormhole.VM memory vaa, bool revertCustomErrors) internal pure - returns (uint32 sourceCctpDomain) - { - bytes memory payload = vaa.payload; - assembly ("memory-safe") { - sourceCctpDomain := mload(add(payload, 69)) - } - } - - function readDepositTargetCctpDomainUnchecked(IWormhole.VM memory vaa) - internal - pure - returns (uint32 targetCctpDomain) - { - bytes memory payload = vaa.payload; - assembly ("memory-safe") { - targetCctpDomain := mload(add(payload, 73)) - } - } - - function readDepositCctpNonceUnchecked(IWormhole.VM memory vaa) - internal - pure - returns (uint64 cctpNonce) + returns ( + bytes32 token, + uint256 amount, + uint32 sourceCctpDomain, + uint32 targetCctpDomain, + uint64 cctpNonce, + bytes32 burnSource, + bytes32 mintRecipient, + bytes memory payload + ) { - bytes memory payload = vaa.payload; - assembly ("memory-safe") { - cctpNonce := mload(add(payload, 81)) - } - } + bytes memory encoded = vaa.payload; + uint256 offset = _checkPayloadId(encoded, 0, DEPOSIT, revertCustomErrors); - function readDepositBurnSourceUnchecked(IWormhole.VM memory vaa) - internal - pure - returns (bytes32 burnSource) - { - bytes memory payload = vaa.payload; - assembly ("memory-safe") { - burnSource := mload(add(payload, 123)) - } - } + (token, offset) = encoded.asBytes32Unchecked(offset); + (amount, offset) = encoded.asUint256Unchecked(offset); + (sourceCctpDomain, offset) = encoded.asUint32Unchecked(offset); + (targetCctpDomain, offset) = encoded.asUint32Unchecked(offset); + (cctpNonce, offset) = encoded.asUint64Unchecked(offset); + (burnSource, offset) = encoded.asBytes32Unchecked(offset); + (mintRecipient, offset) = encoded.asBytes32Unchecked(offset); + (payload, offset) = _decodeBytes(encoded, offset); - function readDepositMintRecipientUnchecked(IWormhole.VM memory vaa) - internal - pure - returns (bytes32 mintRecipient) - { - bytes memory payload = vaa.payload; - assembly ("memory-safe") { - mintRecipient := mload(add(payload, 145)) - } + _checkLength(encoded.length, offset, revertCustomErrors); } // ---------------------------------------- private ------------------------------------------- @@ -210,20 +141,12 @@ library WormholeCctpMessages { (payload, offset) = encoded.sliceUnchecked(offset, payloadLength); } - function _encodeBytes(bytes memory payload) private pure returns (bytes memory encoded) { - encoded = abi.encodePacked(uint16(payload.length), payload); - } - - function _checkLength(bytes memory encoded, uint256 expected, bool revertCustomErrors) - private - pure - { - if (encoded.length != expected) { + function _checkLength(uint256 actual, uint256 expected, bool revertCustomErrors) private pure { + if (actual != expected) { if (revertCustomErrors) { - revert UnexpectedMessageLength(encoded.length, expected); + revert UnexpectedMessageLength(actual, expected); } else { - // NOTE: Using require is the only way to revert with the built-in Error type. - require(encoded.length == expected, "invalid message length"); + Utils.revertBuiltIn("invalid message length"); } } } @@ -241,8 +164,7 @@ library WormholeCctpMessages { if (revertCustomErrors) { revert InvalidMessage(); } else { - // NOTE: Using require is the only way to revert with the built-in Error type. - require(false, "invalid message payloadId"); + Utils.revertBuiltIn("invalid message payloadId"); } } }