diff --git a/docs/Testing.md b/docs/Testing.md index 6193a42..2da8311 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -42,10 +42,6 @@ The `WormholeCctpSimulator` contract can be deployed to simulate a virtual `Worm Forge's `deal` cheat code does not work for USDC. `UsdcDealer` is another override library that implements a `deal` function that allows minting of USDC. -### CctpMessages - -Library to parse CCTP messages composed/emitted by Circle's `TokenMessenger` and `MessageTransmitter` contracts. Used in `CctpOverride` and `WormholeCctpSimulator`. - ### ERC20Mock Copy of SolMate's ERC20 Mock token that uses the overrideable `IERC20` interface of this SDK to guarantee compatibility. diff --git a/src/WormholeCctpTokenMessenger.sol b/src/WormholeCctpTokenMessenger.sol index 04540d9..2905ee1 100644 --- a/src/WormholeCctpTokenMessenger.sol +++ b/src/WormholeCctpTokenMessenger.sol @@ -8,7 +8,9 @@ import {IMessageTransmitter} from "wormhole-sdk/interfaces/cctp/IMessageTransmit import {ITokenMessenger} from "wormhole-sdk/interfaces/cctp/ITokenMessenger.sol"; import {ITokenMinter} from "wormhole-sdk/interfaces/cctp/ITokenMinter.sol"; -import {toUniversalAddress} from "wormhole-sdk/Utils.sol"; +import {toUniversalAddress, + eagerAnd, + eagerOr} from "wormhole-sdk/Utils.sol"; import {WormholeCctpMessages} from "wormhole-sdk/libraries/WormholeCctpMessages.sol"; import {CONSISTENCY_LEVEL_FINALIZED} from "wormhole-sdk/constants/ConsistencyLevel.sol"; @@ -37,7 +39,7 @@ abstract contract WormholeCctpTokenMessenger { /// @dev The emitter of the VAA must match the expected emitter. error UnexpectedEmitter(bytes32, bytes32); - /// @dev Wormhole Core Bridge contract address. + /// @dev Wormhole Core Bridge contract address. IWormhole immutable _wormhole; /// @dev Wormhole Chain ID. NOTE: This is NOT the EVM chain ID. @@ -114,7 +116,7 @@ abstract contract WormholeCctpTokenMessenger { mintRecipient, payload ), - CONSISTENCY_LEVEL_FINALIZED + CONSISTENCY_LEVEL_FINALIZED ); } @@ -138,7 +140,7 @@ abstract contract WormholeCctpTokenMessenger { bytes memory payload ) { // First parse and verify VAA. - vaa = _parseAndVerifyVaa( encodedVaa, true /*revertCustomErrors*/); + vaa = _parseAndVerifyVaa( encodedVaa); // Decode the deposit message so we can match the Wormhole message with the CCTP message. uint32 sourceCctpDomain; @@ -162,8 +164,7 @@ abstract contract WormholeCctpTokenMessenger { sourceCctpDomain, destinationCctpDomain, cctpNonce, - token, - true // revertCustomErrors + token ); } @@ -191,7 +192,7 @@ abstract contract WormholeCctpTokenMessenger { bytes memory payload ) { // First parse and verify VAA. - vaa = _parseAndVerifyVaa(encodedVaa, false /*revertCustomErrors*/); + vaa = _parseAndVerifyVaa(encodedVaa); // Decode the deposit message so we can match the Wormhole message with the CCTP message. ( @@ -212,8 +213,7 @@ abstract contract WormholeCctpTokenMessenger { sourceCctpDomain, destinationCctpDomain, cctpNonce, - token, - false // revertCustomErrors + token ); } @@ -238,37 +238,20 @@ abstract contract WormholeCctpTokenMessenger { * NOTE: Reverts with `UnexpectedEmitter(bytes32, bytes32)`. */ function requireEmitter(IWormhole.VM memory vaa, bytes32 expectedEmitter) internal pure { - if (expectedEmitter != 0 && vaa.emitterAddress != expectedEmitter) + if (eagerAnd(expectedEmitter != 0, vaa.emitterAddress != expectedEmitter)) revert UnexpectedEmitter(vaa.emitterAddress, expectedEmitter); } - /** - * @dev We encourage an integrator to use this method to make sure the VAA is emitted from one - * that his contract trusts. Usually foreign emitters are stored in a mapping keyed off by - * Wormhole Chain ID (uint16). - * - * NOTE: Reverts with built-in Error(string). - */ - function requireEmitterLegacy(IWormhole.VM memory vaa, bytes32 expectedEmitter) internal pure { - require(expectedEmitter != 0 && vaa.emitterAddress == expectedEmitter, "unknown emitter"); - } - - // private + // ----- private methods ----- function _parseAndVerifyVaa( - bytes calldata encodedVaa, - bool revertCustomErrors + bytes calldata encodedVaa ) private view returns (IWormhole.VM memory vaa) { bool valid; - string memory reason; - (vaa, valid, reason) = _wormhole.parseAndVerifyVM(encodedVaa); - - if (!valid) { - if (revertCustomErrors) - revert InvalidVaa(); - else - require(false, reason); - } + (vaa, valid, ) = _wormhole.parseAndVerifyVM(encodedVaa); + + if (!valid) + revert InvalidVaa(); } function _matchMessagesAndMint( @@ -277,8 +260,7 @@ abstract contract WormholeCctpTokenMessenger { uint32 vaaSourceCctpDomain, uint32 vaaDestinationCctpDomain, uint64 vaaCctpNonce, - bytes32 burnToken, - bool revertCustomErrors + bytes32 burnToken ) private returns (bytes32 mintToken) { // Confirm that the caller passed the correct message pair. { @@ -301,16 +283,13 @@ abstract contract WormholeCctpTokenMessenger { nonce := shr(96, ptr) } - if ( - vaaSourceCctpDomain != sourceDomain || - vaaDestinationCctpDomain != destinationDomain|| + //avoid short circuiting (more gas and bytecode efficient) + if (eagerOr(eagerOr( + vaaSourceCctpDomain != sourceDomain, + vaaDestinationCctpDomain != destinationDomain, vaaCctpNonce != nonce - ) { - if (revertCustomErrors) - revert CctpVaaMismatch(sourceDomain, destinationDomain, nonce); - else - require(false, "invalid message pair"); - } + ))) + revert CctpVaaMismatch(sourceDomain, destinationDomain, nonce); } // Call the circle bridge to mint tokens to the recipient. diff --git a/src/libraries/BytesParsing.sol b/src/libraries/BytesParsing.sol index 86a0aac..9f886af 100644 --- a/src/libraries/BytesParsing.sol +++ b/src/libraries/BytesParsing.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache 2 pragma solidity ^0.8.4; -import "../constants/Common.sol"; +import "wormhole-sdk/constants/Common.sol"; //This file appears comically large, but all unused functions are removed by the compiler. library BytesParsing { diff --git a/src/libraries/CctpMessages.sol b/src/libraries/CctpMessages.sol new file mode 100644 index 0000000..84280d0 --- /dev/null +++ b/src/libraries/CctpMessages.sol @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.19; + +import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol"; +import {eagerAnd, eagerOr} from "wormhole-sdk/Utils.sol"; + +// ┌─────────────────────────────────────────────────────────────────────────────────────┐ +// │ Library for encoding and decoding CCTP MessageTransmitter & TokenMessenger messages │ +// └─────────────────────────────────────────────────────────────────────────────────────┘ + +//#Basic Analogy +// +// Circle's MessageTransmitter <> Wormhole CoreBridge +// Circle's TokenMessenger <> Wormhole TokenBridge +// +//Unlike the Wormhole CoreBridge which broadcasts, Circle Messages always have an intended +// destination and recipient. +//Another difference is that Cctp messages are "redeemed" by calling receiveMessage() on the +// Circle Message Transmitter which in turn invokes handleReceiveMessage() on the recipient of +// the message, see https://github.com/circlefin/evm-cctp-contracts/blob/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/src/MessageTransmitter.sol#L294-L295 +//So even messages that originate from the TokenMessenger are first sent to the MessageTransmitter +// whereas Wormhole TokenBridge messages must be redeemed with the TokenBridge, which internally +// verifies the veracity of the VAA with the CoreBridge. +//To provide a similar restriction like the TokenBridge's redeemWithPayload() function, which can +// only be called by the recipient of the TokenBridge transferWithPayload message, Circle provides +// an additional, optional field named destinationCaller which must be the caller of +// receiveMessage() when it has been specified (i.e. the field is != 0). + +//#Message Formats +// +//Header - https://github.com/circlefin/evm-cctp-contracts/blob/master/src/messages/Message.sol +// +// Type │ Name │ Description +// ─────────┼───────────────────┼────────────────────────────────────────────────────────────────── +// uint32 │ headerVersion │ fixed value: see MESSAGE_TRANSMITTER_HEADER_VERSION below +// uint32 │ sourceDomain │ +// uint32 │ destinationDomain │ +// uint64 │ nonce │ +// bytes32 │ sender │ for TokenMessenger messages this is the source TokenMessenger +// bytes32 │ recipient │ for TokenMessenger messages this is the destination TokenMessenger +// bytes32 │ destinationCaller │ zero means anyone can redeem the message +// +//All Messages +// Always a header, followed by a message body (akin to a VAA's payload). +// Just like a VAA the body has no length prefix but simply consumes the remainder of the message. +// +//TokenBurn body (~= TokenBridge Transfer (with optional destinationCaller restriction)) +// +// Type │ Name │ Description +// ─────────┼───────────────┼────────────────────────────────────────────────────────────────── +// uint32 │ bodyVersion │ fixed value: see TOKEN_MESSENGER_BODY_VERSION below +// bytes32 │ burnToken │ source token contract address whose tokens were burned (e.g. USDC) +// bytes32 │ mintRecipient │ address on the destination domain to mint the new tokens to +// uint256 │ amount │ the number of tokens burned/minted +// bytes32 │ messageSender │ address of caller of depositAndBurn on the source chain + +error InvalidCctpMessageHeaderVersion(); +error InvalidCctpMessageBodyVersion(); + +//Function Families: +//1. Message Encoding +// - encode(CctpHeader) +// - encode(CctpMessage) +// - encode(CctpTokenBurnMessage) +// +//2. Message Type Checking +// - isCctpTokenBurnMessageCd(encoded) +// - isCctpTokenBurnMessage(encoded) +// +//3. Decoding Functions +// TODO +library CctpMessages { + using BytesParsing for bytes; + + uint private constant _CCTP_HEADER_SIZE = 3*4 + 8 + 3*32; + uint private constant _CCTP_TOKEN_BURN_MESSAGE_SIZE = _CCTP_HEADER_SIZE + 4 + 4*32; + + //returned by MessageTransmitter.version() - see here: + //https://github.com/circlefin/evm-cctp-contracts/blob/1662356f9e60bb3f18cb6d09f95f628f0cc3637f/src/MessageTransmitter.sol#L238 + uint32 constant MESSAGE_TRANSMITTER_HEADER_VERSION = 0; + + //returned by TokenMessenger.messageBodyVersion() - see here: + //https://github.com/circlefin/evm-cctp-contracts/blob/1662356f9e60bb3f18cb6d09f95f628f0cc3637f/src/TokenMessenger.sol#L107 + uint32 constant TOKEN_MESSENGER_BODY_VERSION = 0; + + // ------------ Message Encoding Functions ------------ + function encode(CctpHeader memory header) internal pure returns (bytes memory) { + return abi.encodePacked( + MESSAGE_TRANSMITTER_HEADER_VERSION, + header.sourceDomain, + header.destinationDomain, + header.nonce, + header.sender, + header.recipient, + header.destinationCaller + ); + } + + function encode(CctpMessage memory message) internal pure returns (bytes memory) { + return abi.encodePacked( + encode(message.header), + message.messageBody + ); + } + + function encode(CctpTokenBurnMessage memory burnMsg) internal pure returns (bytes memory) { + return abi.encodePacked( + encode(burnMsg.header), + TOKEN_MESSENGER_BODY_VERSION, + burnMsg.burnToken, + burnMsg.mintRecipient, + burnMsg.amount, + burnMsg.messageSender + ); + } + + // ------------ Message Type Checking Functions ------------ + + function isCctpTokenBurnMessageCd(bytes calldata encoded) internal pure returns (bool) { + (uint headerVersion,) = encoded.asUint32CdUnchecked(0); + (uint bodyVersion, ) = encoded.asUint32CdUnchecked(_CCTP_HEADER_SIZE); + //avoid short-circuiting to save gas and code size + return eagerAnd(eagerAnd( + encoded.length == _CCTP_TOKEN_BURN_MESSAGE_SIZE, + headerVersion == MESSAGE_TRANSMITTER_HEADER_VERSION, + bodyVersion == TOKEN_MESSENGER_BODY_VERSION + )); + } + + function isCctpTokenBurnMessage(bytes memory encoded) internal pure returns (bool) { + (uint headerVersion,) = encoded.asUint32Unchecked(0); + (uint bodyVersion, ) = encoded.asUint32Unchecked(_CCTP_HEADER_SIZE); + return eagerAnd(eagerAnd( + encoded.length == _CCTP_TOKEN_BURN_MESSAGE_SIZE, + headerVersion == MESSAGE_TRANSMITTER_HEADER_VERSION, + bodyVersion == TOKEN_MESSENGER_BODY_VERSION + )); + } + + // ------------ Header Decoding Functions ------------ + + function decodeCctpHeaderCdUnchecked( + bytes calldata encoded, + uint offset + ) internal pure returns ( + uint32 sourceDomain, + uint32 destinationDomain, + uint64 nonce, + bytes32 sender, + bytes32 recipient, + bytes32 destinationCaller, + uint newOffset + ) { + uint32 version; + (version, offset) = encoded.asUint32CdUnchecked(offset); + if (version != MESSAGE_TRANSMITTER_HEADER_VERSION) + revert InvalidCctpMessageHeaderVersion(); + + (sourceDomain, offset) = encoded.asUint32CdUnchecked(offset); + (destinationDomain, offset) = encoded.asUint32CdUnchecked(offset); + (nonce, offset) = encoded.asUint64CdUnchecked(offset); + (sender, offset) = encoded.asBytes32CdUnchecked(offset); + (recipient, offset) = encoded.asBytes32CdUnchecked(offset); + (destinationCaller, offset) = encoded.asBytes32CdUnchecked(offset); + newOffset = offset; + } + + function decodeCctpHeaderUnchecked( + bytes memory encoded, + uint offset + ) internal pure returns ( + uint32 sourceDomain, + uint32 destinationDomain, + uint64 nonce, + bytes32 sender, + bytes32 recipient, + bytes32 destinationCaller, + uint newOffset + ) { + uint32 version; + (version, offset) = encoded.asUint32Unchecked(offset); + if (version != MESSAGE_TRANSMITTER_HEADER_VERSION) + revert InvalidCctpMessageHeaderVersion(); + + (sourceDomain, offset) = encoded.asUint32Unchecked(offset); + (destinationDomain, offset) = encoded.asUint32Unchecked(offset); + (nonce, offset) = encoded.asUint64Unchecked(offset); + (sender, offset) = encoded.asBytes32Unchecked(offset); + (recipient, offset) = encoded.asBytes32Unchecked(offset); + (destinationCaller, offset) = encoded.asBytes32Unchecked(offset); + newOffset = offset; + } + + // ------------ Message Decoding Functions ------------ + + function decodeCctpMessageCd(bytes calldata encoded) internal pure returns (CctpMessage memory) { + return decodeCctpMessageCd(encoded, 0); + } + + function decodeCctpMessageCd( + bytes calldata encoded, + uint offset + ) internal pure returns (CctpMessage memory ret) { unchecked { + (ret.header, offset) = decodeCctpHeaderCdUnchecked(encoded, offset); + + BytesParsing.checkBound(offset, encoded.length); + ret.messageBody = encoded.sliceCdUnchecked(offset, encoded.length - offset); + }} + + function decodeCctpMessage(bytes memory encoded) internal pure returns (CctpMessage memory) { + return decodeCctpMessageCd(encoded, 0); + } + + function decodeCctpMessage( + bytes memory encoded, + uint offset + ) internal pure returns (CctpMessage memory ret) { unchecked { + (ret.header, offset) = decodeCctpHeaderUnchecked(encoded, offset); + BytesParsing.checkBound(offset, encoded.length); + ret.messageBody = encoded.sliceUnchecked(offset, encoded.length - offset); + }} + + // ------------ Token Burn Message Decoding Functions ------------ + + function decodeCctpTokenBurnMessageCd( + bytes calldata encoded + ) internal pure returns (CctpTokenBurnMessage memory) { + return decodeCctpTokenBurnMessageCd(encoded, 0); + } + + function decodeCctpTokenBurnMessageCd( + bytes calldata encoded, + uint offset + ) internal pure returns (CctpTokenBurnMessage memory ret) { + (ret, offset) = decodeCctpTokenBurnMessageCdUnchecked(encoded, offset); + encoded.checkLengthCd(offset); + } + + function decodeCctpTokenBurnMessageCdUnchecked( + bytes calldata encoded, + uint offset + ) internal pure returns (CctpTokenBurnMessage memory ret, uint newOffset) { + (ret.header, offset) = decodeCctpHeaderCdUnchecked(encoded, offset); + uint32 version; + (version, offset) = encoded.asUint32CdUnchecked(offset); + if (version != TOKEN_MESSENGER_BODY_VERSION) + revert InvalidCctpMessageBodyVersion(); + + (ret.burnToken, offset) = encoded.asBytes32CdUnchecked(offset); + (ret.mintRecipient, offset) = encoded.asBytes32CdUnchecked(offset); + (ret.amount, offset) = encoded.asUint256CdUnchecked(offset); + (ret.messageSender, offset) = encoded.asBytes32CdUnchecked(offset); + newOffset = offset; + } + + function decodeCctpTokenBurnMessage( + bytes memory encoded + ) internal pure returns (CctpTokenBurnMessage memory) { + return decodeCctpTokenBurnMessage(encoded, 0); + } + + function decodeCctpTokenBurnMessage( + bytes memory encoded, + uint offset + ) internal pure returns (CctpTokenBurnMessage memory ret) { + (ret, offset) = decodeCctpTokenBurnMessageUnchecked(encoded, offset); + encoded.checkLength(offset); + } + + function decodeCctpTokenBurnMessageUnchecked( + bytes memory encoded, + uint offset + ) internal pure returns (CctpTokenBurnMessage memory ret, uint newOffset) { + (bytes memory encHeader, offset) = encoded.sliceUnchecked(offset, _CCTP_HEADER_SIZE); + ret.header = decodeCctpHeaderUnchecked(encHeader); + uint32 version; + (version, offset) = encoded.asUint32Unchecked(offset); + if (version != TOKEN_MESSENGER_BODY_VERSION) + revert InvalidCctpMessageBodyVersion(); + (ret.burnToken, offset) = encoded.asBytes32Unchecked(offset); + (ret.mintRecipient, offset) = encoded.asBytes32Unchecked(offset); + (ret.amount, offset) = encoded.asUint256Unchecked(offset); + (ret.messageSender, offset) = encoded.asBytes32Unchecked(offset); + newOffset = offset; + } +} \ No newline at end of file diff --git a/src/libraries/QueryResponse.sol b/src/libraries/QueryResponse.sol index 63c30ca..2091791 100644 --- a/src/libraries/QueryResponse.sol +++ b/src/libraries/QueryResponse.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.4; import {IWormhole} from "wormhole-sdk/interfaces/IWormhole.sol"; import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol"; +import {eagerAnd, eagerOr} from "wormhole-sdk/Utils.sol"; error UnsupportedQueryType(uint8 received); @@ -24,13 +25,14 @@ library QueryType { function checkValid(uint8 queryType) internal pure { //slightly more gas efficient than calling `isValid` - if (queryType == 0 || queryType > SOLANA_PDA) + if (eagerOr(queryType == 0, queryType > SOLANA_PDA)) revert UnsupportedQueryType(queryType); } function isValid(uint8 queryType) internal pure returns (bool) { - //see docs/optimizations.md why `< CONST + 1` rather than `<= CONST` - return (queryType > 0 && queryType < SOLANA_PDA + 1); + //see docs/Optimization.md why `< CONST + 1` rather than `<= CONST` + //see docs/Optimization.md for rationale behind `eagerAnd` + return eagerAnd(queryType > 0, queryType < SOLANA_PDA + 1); } } @@ -186,7 +188,7 @@ library QueryResponseLib { IWormhole wormhole_ = IWormhole(wormhole); uint32 guardianSetIndex = wormhole_.getCurrentGuardianSetIndex(); IWormhole.GuardianSet memory guardianSet = wormhole_.getGuardianSet(guardianSetIndex); - + while (true) { uint quorum = guardianSet.keys.length * 2 / 3 + 1; if (signatures.length >= quorum) { diff --git a/src/libraries/WormholeCctpMessages.sol b/src/libraries/WormholeCctpMessages.sol index 1445b9c..3ddc8aa 100644 --- a/src/libraries/WormholeCctpMessages.sol +++ b/src/libraries/WormholeCctpMessages.sol @@ -12,23 +12,19 @@ library WormholeCctpMessages { using { toUniversalAddress } for address; using BytesParsing for bytes; - // Payload IDs. - // - // NOTE: This library reserves payloads 1 through 10 for future use. When using this library, - // please consider starting your own Wormhole message payloads at 11. - uint8 private constant DEPOSIT = 1; - uint8 private constant RESERVED_2 = 2; - uint8 private constant RESERVED_3 = 3; - uint8 private constant RESERVED_4 = 4; - uint8 private constant RESERVED_5 = 5; - uint8 private constant RESERVED_6 = 6; - uint8 private constant RESERVED_7 = 7; - uint8 private constant RESERVED_8 = 8; - uint8 private constant RESERVED_9 = 9; - uint8 private constant RESERVED_10 = 10; + uint8 private constant _DEPOSIT_ID = 1; + + uint private constant _DEPOSIT_META_SIZE = + 32 /*universalTokenAddress*/ + + 32 /*amount*/ + + 4 /*sourceCctpDomain*/ + + 4 /*targetCctpDomain*/ + + 8 /*cctpNonce*/ + + 32 /*burnSource*/ + + 32 /*mintRecipient*/; error PayloadTooLarge(uint256); - error InvalidMessage(); + error InvalidPayloadId(uint8); function encodeDeposit( bytes32 universalTokenAddress, @@ -39,13 +35,13 @@ library WormholeCctpMessages { bytes32 burnSource, bytes32 mintRecipient, bytes memory payload - ) internal pure returns (bytes memory encoded) { + ) internal pure returns (bytes memory) { uint payloadLen = payload.length; if (payloadLen > type(uint16).max) revert PayloadTooLarge(payloadLen); - encoded = abi.encodePacked( - DEPOSIT, + return abi.encodePacked( + _DEPOSIT_ID, universalTokenAddress, amount, sourceCctpDomain, @@ -57,9 +53,47 @@ library WormholeCctpMessages { payload ); } - - function asDepositUnchecked( - bytes memory encoded, + + // calldata variant + + function decodeDepositMetaCd(bytes calldata vaaPayload) internal pure returns ( + bytes32 token, + uint256 amount, + uint32 sourceCctpDomain, + uint32 targetCctpDomain, + uint64 cctpNonce, + bytes32 burnSource, + bytes32 mintRecipient + ) { + return decodeDepositMetaCd(vaaPayload, 0); + } + + function decodeDepositMetaCd(bytes calldata vaaPayload, uint offset) internal pure returns ( + bytes32 token, + uint256 amount, + uint32 sourceCctpDomain, + uint32 targetCctpDomain, + uint64 cctpNonce, + bytes32 burnSource, + bytes32 mintRecipient + ) { + ( + token, + amount, + sourceCctpDomain, + targetCctpDomain, + cctpNonce, + burnSource, + mintRecipient, + payload, + offset + ) = decodeDepositCdUnchecked(vaaPayload, offset); + + vaaPayload.checkLengthCd(offset); + } + + function decodeDepositMetaCdUnchecked( + bytes calldata vaaPayload, uint offset ) internal pure returns ( bytes32 token, @@ -69,66 +103,66 @@ library WormholeCctpMessages { uint64 cctpNonce, bytes32 burnSource, bytes32 mintRecipient, - bytes memory payload, uint newOffset ) { uint8 payloadId; - (payloadId, offset) = encoded.asUint8Unchecked(offset); - if (payloadId != DEPOSIT) - revert InvalidMessage(); - - (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) = encoded.sliceUint16PrefixedUnchecked(offset); + (payloadId, offset) = vaaPayload.asUint8CdUnchecked(offset); + if (payloadId != _DEPOSIT_ID) + revert InvalidPayloadId(payloadId); + + (token, offset) = vaaPayload.asBytes32CdUnchecked(offset); + (amount, offset) = vaaPayload.asUint256CdUnchecked(offset); + (sourceCctpDomain, offset) = vaaPayload.asUint32CdUnchecked(offset); + (targetCctpDomain, offset) = vaaPayload.asUint32CdUnchecked(offset); + (cctpNonce, offset) = vaaPayload.asUint64CdUnchecked(offset); + (burnSource, offset) = vaaPayload.asBytes32CdUnchecked(offset); + (mintRecipient, offset) = vaaPayload.asBytes32CdUnchecked(offset); newOffset = offset; } - function asDepositCdUnchecked( - bytes calldata encoded, + function decodeDepositPayloadCd(bytes calldata vaaPayload) internal pure returns (bytes memory) { + return decodeDepositPayloadCd(vaaPayload, _DEPOSIT_META_SIZE); + } + + function decodeDepositPayloadCd( + bytes calldata vaaPayload, uint offset - ) internal pure returns ( + ) internal pure returns (bytes memory payload) { + (payload, offset) = decodeDepositPayloadCdUnchecked(vaaPayload, offset); + vaaPayload.checkLengthCd(offset); + } + + function decodeDepositPayloadCdUnchecked( + bytes calldata vaaPayload, + uint offset + ) internal pure returns (bytes memory payload, uint newOffset) { + (payload, offset) = vaaPayload.sliceUint16PrefixedCdUnchecked(offset); + newOffset = offset; + } + + // memory variant + + function decodeDepositMeta(bytes memory vaaPayload) internal pure returns ( bytes32 token, uint256 amount, uint32 sourceCctpDomain, uint32 targetCctpDomain, uint64 cctpNonce, bytes32 burnSource, - bytes32 mintRecipient, - bytes memory payload, - uint newOffset + bytes32 mintRecipient ) { - uint8 payloadId; - (payloadId, offset) = encoded.asUint8CdUnchecked(offset); - if (payloadId != DEPOSIT) - revert InvalidMessage(); - - (token, offset) = encoded.asBytes32CdUnchecked(offset); - (amount, offset) = encoded.asUint256CdUnchecked(offset); - (sourceCctpDomain, offset) = encoded.asUint32CdUnchecked(offset); - (targetCctpDomain, offset) = encoded.asUint32CdUnchecked(offset); - (cctpNonce, offset) = encoded.asUint64CdUnchecked(offset); - (burnSource, offset) = encoded.asBytes32CdUnchecked(offset); - (mintRecipient, offset) = encoded.asBytes32CdUnchecked(offset); - (payload, offset) = encoded.sliceUint16PrefixedCdUnchecked(offset); - newOffset = offset; + return decodeDepositMeta(vaaPayload, 0); } - function decodeDeposit(bytes memory encoded) internal pure returns ( + function decodeDepositMeta(bytes memory vaaPayload, uint offset) internal pure returns ( bytes32 token, uint256 amount, uint32 sourceCctpDomain, uint32 targetCctpDomain, uint64 cctpNonce, bytes32 burnSource, - bytes32 mintRecipient, - bytes memory payload + bytes32 mintRecipient ) { - uint offset = 0; ( token, amount, @@ -139,8 +173,56 @@ library WormholeCctpMessages { mintRecipient, payload, offset - ) = asDepositUnchecked(encoded, offset); + ) = decodeDepositUnchecked(vaaPayload, offset); - encoded.checkLength(offset); + vaaPayload.checkLength(offset); + } + + function decodeDepositMetaUnchecked( + bytes memory vaaPayload, + uint offset + ) internal pure returns ( + bytes32 token, + uint256 amount, + uint32 sourceCctpDomain, + uint32 targetCctpDomain, + uint64 cctpNonce, + bytes32 burnSource, + bytes32 mintRecipient, + uint newOffset + ) { + uint8 payloadId; + (payloadId, offset) = vaaPayload.asUint8Unchecked(offset); + if (payloadId != _DEPOSIT_ID) + revert InvalidPayloadId(payloadId); + + (token, offset) = vaaPayload.asBytes32Unchecked(offset); + (amount, offset) = vaaPayload.asUint256Unchecked(offset); + (sourceCctpDomain, offset) = vaaPayload.asUint32Unchecked(offset); + (targetCctpDomain, offset) = vaaPayload.asUint32Unchecked(offset); + (cctpNonce, offset) = vaaPayload.asUint64Unchecked(offset); + (burnSource, offset) = vaaPayload.asBytes32Unchecked(offset); + (mintRecipient, offset) = vaaPayload.asBytes32Unchecked(offset); + newOffset = offset; + } + + function decodeDepositPayload(bytes memory vaaPayload) internal pure returns (bytes memory) { + return decodeDepositPayload(vaaPayload, _DEPOSIT_META_SIZE); + } + + function decodeDepositPayload( + bytes memory vaaPayload, + uint offset + ) internal pure returns (bytes memory payload) { + (payload, offset) = decodeDepositPayloadUnchecked(vaaPayload, offset); + vaaPayload.checkLength(offset); + } + + function decodeDepositPayloadUnchecked( + bytes memory vaaPayload, + uint offset + ) internal pure returns (bytes memory payload, uint newOffset) { + (payload, offset) = vaaPayload.sliceUint16PrefixedUnchecked(offset); + newOffset = offset; } } diff --git a/src/libraries/WormholeMessages.sol b/src/libraries/WormholeMessages.sol new file mode 100644 index 0000000..39290f8 --- /dev/null +++ b/src/libraries/WormholeMessages.sol @@ -0,0 +1,600 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.19; + +import {IWormhole} from "wormhole-sdk/interfaces/IWormhole.sol"; +import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol"; +import {toUniversalAddress} from "wormhole-sdk/Utils.sol"; + +//VAA encoding and decoding +// see https://github.com/wormhole-foundation/wormhole/blob/c35940ae9689f6df9e983d51425763509b74a80f/ethereum/contracts/Messages.sol#L147 +//only implements calldata variants, given that VAAs are likely always passed as calldata +library WormholeMessages { + using BytesParsing for bytes; + + // ------------ VAA / CoreBridge ------------ + + error InvalidVersion(uint8 version); + + uint internal constant VAA_VERSION = 1; + uint private constant _VAA_SIGNATURE_ARRAY_OFFSET = 1 /*version*/ + 4 /*guardianSet*/; + uint private constant _VAA_SIGNATURE_SIZE = 1 /*guardianSetIndex*/ + 65 /*signaturesize*/; + uint internal constant VAA_META_SIZE = + 4 /*timestamp*/ + + 4 /*nonce*/ + + 2 /*emitterChainId*/ + + 32 /*emitterAddress*/ + + 8 /*sequence*/ + + 1 /*consistencyLevel*/; + + //see https://github.com/wormhole-foundation/wormhole/blob/c35940ae9689f6df9e983d51425763509b74a80f/ethereum/contracts/Messages.sol#L174 + //origin: https://bitcoin.stackexchange.com/a/102382 + uint private constant _SIGNATURE_RECOVERY_MAGIC = 27; + + function decodeVaaHeaderCdUnchecked( + bytes calldata encodedVaa + ) internal pure returns ( + uint32 guardianSetIndex, + IWormhole.Signature[] memory signatures, + uint offset + ) { unchecked { + uint8 version; + (version, offset) = encodedVaa.asUint8CdUnchecked(0); + if (version != VAA_VERSION) + revert InvalidVersion(version); + + (guardianSetIndex, offset) = encodedVaa.asUint32CdUnchecked(offset); + + uint signersLen; + (signersLen, offset) = encodedVaa.asUint8CdUnchecked(offset); + + signatures = new IWormhole.Signature[](signersLen); + for (uint i = 0; i < signersLen; ++i) { + (signatures[i].guardianIndex, offset) = encodedVaa.asUint8CdUnchecked(offset); + (signatures[i].r, offset) = encodedVaa.asBytes32CdUnchecked(offset); + (signatures[i].s, offset) = encodedVaa.asBytes32CdUnchecked(offset); + (signatures[i].v, offset) = encodedVaa.asUint8CdUnchecked(offset); + signatures[i].v += _SIGNATURE_RECOVERY_MAGIC; + } + }} + + //does not calculate/return the hash that's otherwise included in an IWormhole.VM + function decodeVaaCd( + bytes calldata encodedVaa + ) internal pure returns ( + uint32 timestamp, + uint32 nonce, + uint16 emitterChainId, + bytes32 emitterAddress, + uint64 sequence, + uint8 consistencyLevel, + bytes memory payload + ) { unchecked { + return decodeVaaCd(encodedVaa, skipSignaturesCd(encodedVaa)); + }} + + function decodeVaaCd( + bytes calldata encodedVaa, + uint offset + ) internal pure returns ( + uint32 timestamp, + uint32 nonce, + uint16 emitterChainId, + bytes32 emitterAddress, + uint64 sequence, + uint8 consistencyLevel, + bytes memory payload + ) { unchecked { + (timestamp, nonce, emitterChainId, emitterAddress, sequence, consistencyLevel, offset) = + decodeMetaCd(encodedVaa, offset); + payload = decodePayloadCd(encodedVaa, offset); + }} + + function skipVaaSignaturesCd(bytes calldata encodedVaa) internal pure returns (uint) { unchecked { + (uint sigCount, offset) = encodedVaa.asUint8CdUnchecked(_VAA_SIGNATURE_ARRAY_OFFSET); + return offset + sigCount * _VAA_SIGNATURE_SIZE; + }} + + function decodeVaaMetaCdUnchecked( + bytes calldata encodedVaa + ) internal pure returns ( + uint32 timestamp, + uint32 nonce, + uint16 emitterChainId, + bytes32 emitterAddress, + uint64 sequence, + uint8 consistencyLevel, + uint newOffset + ) { + return decodeMetaCd(encodedVaa, skipSignaturesCd(encodedVaa)); + } + + function decodeVaaMetaCdUnchecked( + bytes calldata encodedVaa, + uint offset + ) internal pure returns ( + uint32 timestamp, + uint32 nonce, + uint16 emitterChainId, + bytes32 emitterAddress, + uint64 sequence, + uint8 consistencyLevel, + uint newOffset + ) { + (timestamp, offset) = encodedVaa.asUint32CdUnchecked(offset); + (nonce, offset) = encodedVaa.asUint32CdUnchecked(offset); + (emitterChainId, offset) = encodedVaa.asUint16CdUnchecked(offset); + (emitterAddress, offset) = encodedVaa.asBytes32CdUnchecked(offset); + (sequence, offset) = encodedVaa.asUint64CdUnchecked(offset); + (consistencyLevel, offset) = encodedVaa.asUint8CdUnchecked(offset); + newOffset = offset; + } + + function decodeVaaPayloadCd( + bytes calldata encodedVaa + ) internal pure returns (bytes memory payload) { unchecked { + return decodePayloadCd(encodedVaa, skipSignaturesCd(encodedVaa) + VAA_META_SIZE); + }} + + function decodeVaaPayloadCd( + bytes calldata encodedVaa, + uint offset + ) internal pure returns (bytes memory payload) { unchecked { + //check to avoid underflow in following subtraction + BytesParsing.checkBound(offset, encodedVaa.length); + (payload, ) = encodedVaa.sliceCdUnchecked(offset, encodedVaa.length - offset); + }} + + //legacy decoder for IWormhole.VM + function decodeVmCd( + bytes calldata encodedVaa + ) internal pure returns (IWormhole.VM memory vm) { unchecked { + uint offset; + (vm.guardianSetIndex, vm.signatures, offset) = decodeHeaderCdUnchecked(encodedVaa); + vm.version = VAA_VERSION; + + BytesParsing.checkBound(offset, encodedVaa.length); + (bytes memory body, ) = encodedVaa.sliceCdUnchecked(offset, encodedVaa.length - offset); + vm.hash = keccak256(abi.encodePacked(keccak256(body))); + + ( vm.timestamp, + vm.nonce, + vm.emitterChainId, + vm.emitterAddress, + vm.sequence, + vm.consistencyLevel, + offset + ) = decodeMetaCd(encodedVaa, offset); + + (vm.payload, ) = decodePayloadCd(encodedVaa, offset); + }} + + //encode should only be relevant for testing + function encode(IWormhole.VM memory vaa) internal pure returns (bytes memory) { unchecked { + bytes memory sigs; + for (uint i = 0; i < vaa.signatures.length; ++i) { + IWormhole.Signature memory sig = vaa.signatures[i]; + uint8 v = sig.v - _SIGNATURE_RECOVERY_MAGIC; + sigs = bytes.concat(sigs, abi.encodePacked(sig.guardianIndex, sig.r, sig.s, v)); + } + + return abi.encodePacked( + vaa.version, + vaa.guardianSetIndex, + uint8(vaa.signatures.length), + sigs, + vaa.timestamp, + vaa.nonce, + vaa.emitterChainId, + vaa.emitterAddress, + vaa.sequence, + vaa.consistencyLevel, + vaa.payload + ); + }} + + // ------------ TokenBridge ------------ + + error InvalidPayloadId(uint8 encoded); + + uint8 internal constant PAYLOAD_ID_TRANSFER = 1; + uint8 internal constant PAYLOAD_ID_ATTEST_META = 2; + uint8 internal constant PAYLOAD_ID_TRANSFER_WITH_PAYLOAD = 3; + + function checkPayloadId(uint8 encoded, uint8 expected) internal pure { + if (encoded != expected) + revert InvalidPayloadId(encoded); + } + + // Transfer payloads + + uint private constant _TRANSFER_COMMON_SIZE = + 32 /*tbNormalizedAmount*/ + + 32 /*tokenOriginAddress*/ + + 2 /*tokenOriginChain*/ + + 32 /*toAddress*/ + + 2 /*toChain*/; + + uint internal constant TRANSFER_WITH_PAYLOAD_META_SIZE = + _TRANSFER_COMMON_SIZE + + 32 /*fromAddress*/; + + function encodeTransfer( + uint256 tbNormalizedAmount, + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + bytes32 toAddress, + uint16 toChain + ) internal pure returns (bytes memory) { + return abi.encodePacked( + PAYLOAD_ID_TRANSFER, + tbNormalizedAmount, + tokenOriginAddress, + tokenOriginChain, + toAddress, + toChain, + uint256(0) //fees are not supported + ); + } + + function encodeTransferWithPayload( + uint256 tbNormalizedAmount, + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + bytes32 toAddress, + uint16 toChain, + bytes memory payload + ) internal pure returns (bytes memory) { + return abi.encodePacked( + PAYLOAD_ID_TRANSFER_WITH_PAYLOAD, + tbNormalizedAmount, + tokenOriginAddress, + tokenOriginChain, + toAddress, + toChain, + payload + ); + } + + // calldata variants + + function decodeTransferCd(bytes calldata vaaPayload) internal pure returns ( + uint256 tbNormalizedAmount, + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + bytes32 toAddress, + uint16 toChain + ) { + return decodeTransferCd(vaaPayload, 0); + } + + function decodeTransferCd(bytes calldata vaaPayload, uint offset) internal pure returns ( + uint256 tbNormalizedAmount, + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + bytes32 toAddress, + uint16 toChain + ) { + (tbNormalizedAmount, tokenOriginAddress, tokenOriginChain, toAddress, toChain, offset) = + decodeTransferCdUnchecked(vaaPayload, offset); + + vaaPayload.checkLengthCd(offset); + } + + function decodeTransferCdUnchecked(bytes calldata vaaPayload, uint offset) internal pure returns( + uint256 tbNormalizedAmount, + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + bytes32 toAddress, + uint16 toChain, + uint newOffset + ) { + (uint8 payloadId, offset) = vaaPayload.asUint8CdUnchecked(offset); + checkPayloadId(payloadId, PAYLOAD_ID_TRANSFER); + + ( + tbNormalizedAmount, + tokenOriginAddress, + tokenOriginChain, + toAddress, + toChain, + offset + ) = decodeTransferCommonCdUnchecked(vaaPayload, offset); + + offset += 32; //skip fee - not supported and always 0 + newOffset = offset; + } + + function decodeTransferWithPayloadMetaCdUnchecked( + bytes calldata vaaPayload, + uint offset + ) internal pure returns ( + uint256 tbNormalizedAmount, + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + bytes32 toAddress, + uint16 toChain, + bytes32 fromAddress, + uint newOffset + ) { + (uint8 payloadId, offset) = vaaPayload.asUint8CdUnchecked(offset); + checkPayloadId(payloadId, PAYLOAD_ID_TRANSFER_WITH_PAYLOAD); + + ( + tbNormalizedAmount, + tokenOriginAddress, + tokenOriginChain, + toAddress, + toChain, + offset + ) = decodeTransferCommonCdUnchecked(vaaPayload, offset); + + (fromAddress, offset) = vaaPayload.asBytes32CdUnchecked(offset); + newOffset = offset; + } + + //only a mother can love this function name + function decodeTransferWithPayloadPayloadCd( + bytes calldata vaaPayload + ) internal pure returns (bytes memory payload) { + return decodeTransferWithPayloadPayloadCd(vaaPayload, _TRANSFER_WITH_PAYLOAD_META_SIZE); + } + + function decodeTransferWithPayloadPayloadCd( + bytes calldata vaaPayload, + uint offset + ) internal pure returns (bytes memory payload) { + BytesParsing.checkBound(offset, vaaPayload.length); + (payload, ) = vaaPayload.sliceCdUnchecked(offset, vaaPayload.length - offset); + } + + function decodeTransferCommonCdUnchecked( + bytes calldata vaaPayload, + uint offset + ) internal pure returns ( + uint256 tbNormalizedAmount, + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + bytes32 toAddress, + uint16 toChain, + uint newOffset + ) { + (tbNormalizedAmount, offset) = vaaPayload.asUint256CdUnchecked(offset); + (tokenOriginAddress, offset) = vaaPayload.asBytes32CdUnchecked(offset); + (tokenOriginChain, offset) = vaaPayload.asUint16CdUnchecked(offset); + (toAddress, offset) = vaaPayload.asBytes32CdUnchecked(offset); + (toChain, offset) = vaaPayload.asUint16CdUnchecked(offset); + newOffset = offset; + } + + // memory variants + + function decodeTransfer(bytes memory vaaPayload) internal pure returns ( + uint256 tbNormalizedAmount, + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + bytes32 toAddress, + uint16 toChain + ) { + return decodeTransfer(vaaPayload, 0); + } + + function decodeTransfer(bytes memory vaaPayload, uint offset) internal pure returns ( + uint256 tbNormalizedAmount, + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + bytes32 toAddress, + uint16 toChain + ) { + (tbNormalizedAmount, tokenOriginAddress, tokenOriginChain, toAddress, toChain, offset) = + decodeTransferUnchecked(vaaPayload, offset); + + vaaPayload.checkLength(offset); + } + + function decodeTransferUnchecked(bytes memory vaaPayload, uint offset) internal pure returns( + uint256 tbNormalizedAmount, + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + bytes32 toAddress, + uint16 toChain, + uint newOffset + ) { + (uint8 payloadId, offset) = vaaPayload.asUint8Unchecked(offset); + checkPayloadId(payloadId, PAYLOAD_ID_TRANSFER); + + ( + tbNormalizedAmount, + tokenOriginAddress, + tokenOriginChain, + toAddress, + toChain, + offset + ) = decodeTransferCommonUnchecked(vaaPayload, offset); + + offset += 32; //skip fee - not supported and always 0 + newOffset = offset; + } + + function decodeTransferWithPayloadMetaUnchecked( + bytes memory vaaPayload, + uint offset + ) internal pure returns ( + uint256 tbNormalizedAmount, + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + bytes32 toAddress, + uint16 toChain, + bytes32 fromAddress, + uint newOffset + ) { + (uint8 payloadId, offset) = vaaPayload.asUint8Unchecked(offset); + checkPayloadId(payloadId, PAYLOAD_ID_TRANSFER_WITH_PAYLOAD); + + ( + tbNormalizedAmount, + tokenOriginAddress, + tokenOriginChain, + toAddress, + toChain, + offset + ) = decodeTransferCommonUnchecked(vaaPayload, offset); + + (fromAddress, offset) = vaaPayload.asBytes32Unchecked(offset); + newOffset = offset; + } + + //only a mother can love this function name + function decodeTransferWithPayloadPayload( + bytes memory vaaPayload + ) internal pure returns (bytes memory payload) { + return decodeTransferWithPayloadPayload(vaaPayload, _TRANSFER_WITH_PAYLOAD_META_SIZE); + } + + function decodeTransferWithPayloadPayload( + bytes memory vaaPayload, + uint offset + ) internal pure returns (bytes memory payload) { + BytesParsing.checkBound(offset, vaaPayload.length); + (payload, ) = vaaPayload.sliceUnchecked(offset, vaaPayload.length - offset); + } + + function decodeTransferWithPayloadPayload( + bytes memory vaaPayload, + uint offset + ) internal pure returns (bytes memory payload) { + BytesParsing.checkBound(offset, vaaPayload.length); + (payload, ) = vaaPayload.sliceUnchecked(offset, vaaPayload.length - offset); + } + + function decodeTransferCommonUnchecked( + bytes memory vaaPayload, + uint offset + ) internal pure returns ( + uint256 tbNormalizedAmount, + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + bytes32 toAddress, + uint16 toChain, + uint newOffset + ) { + (tbNormalizedAmount, offset) = vaaPayload.asUint256Unchecked(offset); + (tokenOriginAddress, offset) = vaaPayload.asBytes32Unchecked(offset); + (tokenOriginChain, offset) = vaaPayload.asUint16Unchecked(offset); + (toAddress, offset) = vaaPayload.asBytes32Unchecked(offset); + (toChain, offset) = vaaPayload.asUint16Unchecked(offset); + newOffset = offset; + } + + // Attest meta payloads + + function encodeAttestMeta( + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + uint8 decimals, + bytes32 symbol, + bytes32 name + ) internal pure returns (bytes memory) { + return abi.encodePacked( + PAYLOAD_ID_ATTEST_META, + tokenOriginAddress, + tokenOriginChain, + decimals, + symbol, + name + ); + } + + //calldata variants + + function decodeAttestMetaCd(bytes calldata vaaPayload) internal pure returns ( + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + uint8 decimals, + bytes32 symbol, + bytes32 name + ) { + return decodeAttestMetaCd(vaaPayload, 0); + } + + function decodeAttestMetaCd(bytes calldata vaaPayload, uint offset) internal pure returns ( + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + uint8 decimals, + bytes32 symbol, + bytes32 name + ) { + (tokenOriginAddress, tokenOriginChain, decimals, symbol, name, offset) = + decodeAttestMetaCdUnchecked(vaaPayload, offset); + + vaaPayload.checkLengthCd(offset); + } + + function decodeAttestMetaCdUnchecked( + bytes calldata vaaPayload, + uint offset + ) internal pure returns ( + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + uint8 decimals, + bytes32 symbol, + bytes32 name, + uint newOffset + ) { + (uint8 payloadId, offset) = vaaPayload.asUint8CdUnchecked(offset); + checkPayloadId(payloadId, PAYLOAD_ID_ATTEST_META); + + (tokenOriginAddress, offset) = vaaPayload.asBytes32CdUnchecked(offset); + (tokenOriginChain, offset) = vaaPayload.asUint16CdUnchecked(offset); + (decimals, offset) = vaaPayload.asUint8CdUnchecked(offset); + (symbol, offset) = vaaPayload.asBytes32CdUnchecked(offset); + (name, offset) = vaaPayload.asBytes32CdUnchecked(offset); + newOffset = offset; + } + + //memory variants + + function decodeAttestMeta(bytes memory vaaPayload) internal pure returns ( + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + uint8 decimals, + bytes32 symbol, + bytes32 name + ) { + return decodeAttestMetaUnchecked(vaaPayload, 0); + } + + function decodeAttestMeta(bytes memory vaaPayload, uint offset) internal pure returns ( + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + uint8 decimals, + bytes32 symbol, + bytes32 name + ) { + (tokenOriginAddress, tokenOriginChain, decimals, symbol, name, offset) = + decodeAttestMetaUnchecked(vaaPayload, offset); + + vaaPayload.checkLength(offset); + } + + function decodeAttestMetaUnchecked( + bytes memory vaaPayload, + uint offset + ) internal pure returns ( + bytes32 tokenOriginAddress, + uint16 tokenOriginChain, + uint8 decimals, + bytes32 symbol, + bytes32 name, + uint newOffset + ) { + (uint8 payloadId, offset) = vaaPayload.asUint8Unchecked(offset); + checkPayloadId(payloadId, PAYLOAD_ID_ATTEST_META); + + (tokenOriginAddress, offset) = vaaPayload.asBytes32Unchecked(offset); + (tokenOriginChain, offset) = vaaPayload.asUint16Unchecked(offset); + (decimals, offset) = vaaPayload.asUint8Unchecked(offset); + (symbol, offset) = vaaPayload.asBytes32Unchecked(offset); + (name, offset) = vaaPayload.asBytes32Unchecked(offset); + newOffset = offset; + } +} diff --git a/src/proxy/Proxy.sol b/src/proxy/Proxy.sol index 70fb3a8..bab5ba9 100644 --- a/src/proxy/Proxy.sol +++ b/src/proxy/Proxy.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; -import { implementationState } from "./Eip1967Implementation.sol"; +import {implementationState} from "./Eip1967Implementation.sol"; error ProxyConstructionFailed(bytes revertData); diff --git a/src/proxy/ProxyBase.sol b/src/proxy/ProxyBase.sol index 43f4e92..34de79e 100644 --- a/src/proxy/ProxyBase.sol +++ b/src/proxy/ProxyBase.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; -import { implementationState } from "./Eip1967Implementation.sol"; +import {implementationState} from "./Eip1967Implementation.sol"; error InvalidSender(); error IdempotentUpgrade(); diff --git a/src/testing/CctpMessages.sol b/src/testing/CctpMessages.sol deleted file mode 100644 index 8a99ea7..0000000 --- a/src/testing/CctpMessages.sol +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-License-Identifier: Apache 2 -pragma solidity ^0.8.19; - -import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol"; - -//Message format emitted by Circle MessageTransmitter - akin to Wormhole CoreBridge -// see: https://github.com/circlefin/evm-cctp-contracts/blob/master/src/messages/Message.sol -// -//Unlike the Wormhole CoreBridge which broadcasts, Circle Messages always have an intended -// destination and recipient. -// -//Cctp messages are "redeemed" by calling receiveMessage() on the Circle Message Transmitter -// which in turn invokes handleReceiveMessage() on the recipient of the message: -// see: https://github.com/circlefin/evm-cctp-contracts/blob/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/src/MessageTransmitter.sol#L294-L295 -//So even messages that originate from the TokenMessenger are first sent to the MessageTransmitter -// whereas Wormhole TokenBridge messages must be redeemed with the TokenBridge, which internally -// verifies the veracity of the VAA with the CoreBridge. -//To provide a similar restriction like the TokenBridge's redeemWithPayload() function which can -// only be called by the recipient of the TokenBridge transferWithPayload message, Circle provides -// an additional, optional field named destinationCaller which must be the caller of -// receiveMessage() when it has been specified (i.e. the field is != 0). -struct CctpHeader { - //uint32 headerVersion; - uint32 sourceDomain; - uint32 destinationDomain; - uint64 nonce; - //caller of the Circle Message Transmitter -> for us always the foreign TokenMessenger - bytes32 sender; - //caller of the Circle Message Transmitter -> for us always the local TokenMessenger - bytes32 recipient; - bytes32 destinationCaller; -} - -struct CctpMessage { - CctpHeader header; - bytes messageBody; -} - -struct CctpTokenBurnMessage { - CctpHeader header; - //uint32 bodyVersion; - //the address of the USDC contract on the foreign domain whose tokens were burned - bytes32 burnToken; - //always our local WormholeCctpTokenMessenger contract (e.g. CircleIntegration, TokenRouter)a - bytes32 mintRecipient; - uint256 amount; - //address of caller of depositAndBurn on the foreign chain - for us always foreignCaller - bytes32 messageSender; -} - -library CctpMessages { - using BytesParsing for bytes; - - uint private constant _CCTP_HEADER_SIZE = 3*4 + 8 + 3*32; - uint private constant _CCTP_TOKEN_BURN_MESSAGE_SIZE = _CCTP_HEADER_SIZE + 4 + 4*32; - - //returned by MessageTransmitter.version() - see here: - //https://github.com/circlefin/evm-cctp-contracts/blob/1662356f9e60bb3f18cb6d09f95f628f0cc3637f/src/MessageTransmitter.sol#L238 - uint32 constant MESSAGE_TRANSMITTER_HEADER_VERSION = 0; - - //returned by TokenMessenger.messageBodyVersion() - see here: - //https://github.com/circlefin/evm-cctp-contracts/blob/1662356f9e60bb3f18cb6d09f95f628f0cc3637f/src/TokenMessenger.sol#L107 - uint32 constant TOKEN_MESSENGER_BODY_VERSION = 0; - - function encode(CctpHeader memory header) internal pure returns (bytes memory) { - return abi.encodePacked( - MESSAGE_TRANSMITTER_HEADER_VERSION, - header.sourceDomain, - header.destinationDomain, - header.nonce, - header.sender, - header.recipient, - header.destinationCaller - ); - } - - function encode(CctpMessage memory message) internal pure returns (bytes memory) { - return abi.encodePacked( - encode(message.header), - message.messageBody - ); - } - - function encode(CctpTokenBurnMessage memory burnMsg) internal pure returns (bytes memory) { - return abi.encodePacked( - encode(burnMsg.header), - TOKEN_MESSENGER_BODY_VERSION, - burnMsg.burnToken, - burnMsg.mintRecipient, - burnMsg.amount, - burnMsg.messageSender - ); - } - - function isCctpTokenBurnMessage(bytes memory encoded) internal pure returns (bool) { - if (encoded.length != _CCTP_TOKEN_BURN_MESSAGE_SIZE) - return false; - - (uint headerVersion,) = encoded.asUint32Unchecked(0); - (uint bodyVersion, ) = encoded.asUint32Unchecked(_CCTP_HEADER_SIZE); - return headerVersion == MESSAGE_TRANSMITTER_HEADER_VERSION && - bodyVersion == TOKEN_MESSENGER_BODY_VERSION; - } - - function decodeCctpHeader( - bytes memory encoded - ) internal pure returns (CctpHeader memory ret) { - uint offset; - uint32 version; - (version, offset) = encoded.asUint32Unchecked(offset); - require(version == MESSAGE_TRANSMITTER_HEADER_VERSION, "cctp msg header version mismatch"); - (ret.sourceDomain, offset) = encoded.asUint32Unchecked(offset); - (ret.destinationDomain, offset) = encoded.asUint32Unchecked(offset); - (ret.nonce, offset) = encoded.asUint64Unchecked(offset); - (ret.sender, offset) = encoded.asBytes32Unchecked(offset); - (ret.recipient, offset) = encoded.asBytes32Unchecked(offset); - (ret.destinationCaller, offset) = encoded.asBytes32Unchecked(offset); - encoded.checkLength(offset); - } - - function decodeCctpMessage( - bytes memory encoded - ) internal pure returns (CctpMessage memory ret) { - (bytes memory encHeader, uint offset) = encoded.sliceUnchecked(0, _CCTP_HEADER_SIZE); - ret.header = decodeCctpHeader(encHeader); - (ret.messageBody, offset) = encoded.slice(offset, encoded.length - offset); //checked! - return ret; - } - - function decodeCctpTokenBurnMessage( - bytes memory encoded - ) internal pure returns (CctpTokenBurnMessage memory ret) { - (bytes memory encHeader, uint offset) = encoded.sliceUnchecked(0, _CCTP_HEADER_SIZE); - ret.header = decodeCctpHeader(encHeader); - uint32 version; - (version, offset) = encoded.asUint32Unchecked(offset); - require(version == TOKEN_MESSENGER_BODY_VERSION, "cctp msg body version mismatch"); - (ret.burnToken, offset) = encoded.asBytes32Unchecked(offset); - (ret.mintRecipient, offset) = encoded.asBytes32Unchecked(offset); - (ret.amount, offset) = encoded.asUint256Unchecked(offset); - (ret.messageSender, offset) = encoded.asBytes32Unchecked(offset); - encoded.checkLength(offset); - return ret; - } -} \ No newline at end of file diff --git a/src/testing/CctpOverride.sol b/src/testing/CctpOverride.sol index c2ee1f5..aa7ede5 100644 --- a/src/testing/CctpOverride.sol +++ b/src/testing/CctpOverride.sol @@ -4,9 +4,15 @@ pragma solidity ^0.8.19; import "forge-std/Vm.sol"; import {IMessageTransmitter} from "wormhole-sdk/interfaces/cctp/IMessageTransmitter.sol"; -import {VM_ADDRESS, DEVNET_GUARDIAN_PRIVATE_KEY} from "./Constants.sol"; -import "./CctpMessages.sol"; -import "./LogUtils.sol"; +import {LogUtils} from "wormhole-sdk/testing/LogUtils.sol"; +import { + CctpMessages, + CctpTokenBurnMessage +} from "wormhole-sdk/libraries/CctpMessages.sol"; +import { + VM_ADDRESS, + DEVNET_GUARDIAN_PRIVATE_KEY +} from "wormhole-sdk/testing/Constants.sol"; //create fake CCTP attestations for forge tests library CctpOverride { diff --git a/src/testing/UsdcDealer.sol b/src/testing/UsdcDealer.sol index 4611db4..2919726 100644 --- a/src/testing/UsdcDealer.sol +++ b/src/testing/UsdcDealer.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.19; import {Vm} from "forge-std/Vm.sol"; import "IERC20/IERC20.sol"; -import {VM_ADDRESS} from "./Constants.sol"; +import {VM_ADDRESS} from "wormhole-sdk/testing/Constants.sol"; interface IUSDC is IERC20 { function masterMinter() external view returns (address); diff --git a/src/testing/WormholeCctpSimulator.sol b/src/testing/WormholeCctpSimulator.sol index 5b2359f..3dbc27e 100644 --- a/src/testing/WormholeCctpSimulator.sol +++ b/src/testing/WormholeCctpSimulator.sol @@ -3,15 +3,20 @@ pragma solidity ^0.8.19; import {Vm} from "forge-std/Vm.sol"; -import "wormhole-sdk/interfaces/cctp/ITokenMessenger.sol"; - -import "wormhole-sdk/interfaces/IWormhole.sol"; +import {IWormhole} from "wormhole-sdk/interfaces/IWormhole.sol"; +import {IMessageTransmitter} from "wormhole-sdk/interfaces/cctp/IMessageTransmitter.sol"; +import {ITokenMessenger} from "wormhole-sdk/interfaces/cctp/ITokenMessenger.sol"; +import {ITokenMinter} from "wormhole-sdk/interfaces/cctp/ITokenMinter.sol"; + +import { + CctpMessages, + CctpTokenBurnMessage +} from "wormhole-sdk/libraries/CctpMessages.sol"; import {WormholeCctpMessages} from "wormhole-sdk/libraries/WormholeCctpMessages.sol"; -import {toUniversalAddress} from "wormhole-sdk/Utils.sol"; - -import {VM_ADDRESS} from "wormhole-sdk/testing/Constants.sol"; -import "wormhole-sdk/testing/CctpOverride.sol"; -import "wormhole-sdk/testing/WormholeOverride.sol"; +import {toUniversalAddress} from "wormhole-sdk/Utils.sol"; +import {VM_ADDRESS} from "wormhole-sdk/testing/Constants.sol"; +import {CctpOverride} from "wormhole-sdk/testing/CctpOverride.sol"; +import {WormholeOverride} from "wormhole-sdk/testing/WormholeOverride.sol"; //faked foreign call chain: // foreignCaller -> foreignSender -> FOREIGN_TOKEN_MESSENGER -> foreign MessageTransmitter diff --git a/src/testing/WormholeOverride.sol b/src/testing/WormholeOverride.sol index 85b8478..420ff7b 100644 --- a/src/testing/WormholeOverride.sol +++ b/src/testing/WormholeOverride.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.24; import {Vm} from "forge-std/Vm.sol"; -import {WORD_SIZE, WORD_SIZE_MINUS_ONE} from "wormhole-sdk/constants/Common.sol"; -import {IWormhole} from "wormhole-sdk/interfaces/IWormhole.sol"; -import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol"; -import {toUniversalAddress} from "wormhole-sdk/Utils.sol"; - +import {WORD_SIZE, WORD_SIZE_MINUS_ONE} from "wormhole-sdk/constants/Common.sol"; +import {IWormhole} from "wormhole-sdk/interfaces/IWormhole.sol"; +import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol"; +import {WormholeMessages} from "wormhole-sdk/libraries/WormholeMessages.sol"; +import {toUniversalAddress} from "wormhole-sdk/Utils.sol"; import {VM_ADDRESS, DEVNET_GUARDIAN_PRIVATE_KEY} from "wormhole-sdk/testing/Constants.sol"; import {LogUtils} from "wormhole-sdk/testing/LogUtils.sol"; @@ -25,32 +25,6 @@ struct PublishedMessage { bytes payload; } -//use `using VaaEncoding for IWormhole.VM;` to convert VAAs to bytes via .encode() -library VaaEncoding { - function encode(IWormhole.VM memory vaa) internal pure returns (bytes memory) { unchecked { - bytes memory sigs; - for (uint i = 0; i < vaa.signatures.length; ++i) { - IWormhole.Signature memory sig = vaa.signatures[i]; - uint8 v = sig.v - 27; //see https://github.com/wormhole-foundation/wormhole/blob/c35940ae9689f6df9e983d51425763509b74a80f/ethereum/contracts/Messages.sol#L174 - sigs = bytes.concat(sigs, abi.encodePacked(sig.guardianIndex, sig.r, sig.s, v)); - } - - return abi.encodePacked( - vaa.version, - vaa.guardianSetIndex, - uint8(vaa.signatures.length), - sigs, - vaa.timestamp, - vaa.nonce, - vaa.emitterChainId, - vaa.emitterAddress, - vaa.sequence, - vaa.consistencyLevel, - vaa.payload - ); - }} -} - //simple version of the library - should be sufficient for most use cases library WormholeOverride { using AdvancedWormholeOverride for IWormhole; @@ -111,11 +85,11 @@ library WormholeOverride { //────────────────────────────────────────────────────────────────────────────────────────────────── //more complex superset of WormholeOverride for more advanced tests -library AdvancedWormholeOverride { +library AdvancedWormholeOverride { using { toUniversalAddress } for address; using BytesParsing for bytes; using LogUtils for Vm.Log[]; - using VaaEncoding for IWormhole.VM; + using WormholeMessages for IWormhole.VM; Vm constant vm = Vm(VM_ADDRESS); @@ -165,7 +139,7 @@ library AdvancedWormholeOverride { // multiple, different instances of the core bridge uint256 private constant _OVERRIDE_STATE_SLOT = 0x2e44eb2c79e88410071ac52f3c0e5ab51396d9208c2c783cdb8e12f39b763de8; - + //extra data (ors = _OVERRIDE_STATE_SLOT): // slot │ type │ name // ───────┼───────────┼──────────────────────────── @@ -215,7 +189,7 @@ library AdvancedWormholeOverride { bytes32(_OVERRIDE_STATE_SLOT + _OR_NONCE_OFFSET) ))); }} - + function setConsistencyLevel(IWormhole wormhole, uint8 consistencyLevel) internal { vm.store( address(wormhole), @@ -292,7 +266,7 @@ library AdvancedWormholeOverride { uint8 curIdx = signingIndices[i]; assembly ("memory-safe") { mstore8(add(add(packedIndices, WORD_SIZE), i), curIdx) } } - + uint fullSlots = packedIndices.length / WORD_SIZE; for (uint i = 0; i < fullSlots; ++i) { (bytes32 val,) = packedIndices.asBytes32Unchecked(i * WORD_SIZE); @@ -302,7 +276,7 @@ library AdvancedWormholeOverride { val ); } - + uint remaining = packedIndices.length % WORD_SIZE; if (remaining > 0) { (uint256 val, ) = packedIndices.asUint256Unchecked(fullSlots * WORD_SIZE); @@ -334,7 +308,7 @@ library AdvancedWormholeOverride { address(wormhole), bytes32(_arraySlot(_OVERRIDE_STATE_SLOT + _OR_SIGNING_INDICES_OFFSET) + i) ); - + bytes memory packed = abi.encodePacked(individualSlots); assembly ("memory-safe") { mstore(packed, len) } return packed; @@ -375,7 +349,7 @@ library AdvancedWormholeOverride { if (guardianPrivateKeys.length == 0) revert ("no guardian private keys provided"); - + if (guardianPrivateKeys.length > type(uint8).max) revert ("too many guardians, core bridge enforces upper bound of 255"); @@ -389,7 +363,7 @@ library AdvancedWormholeOverride { bytes32(curGuardianSetSlot + _GUARDIAN_SET_STRUCT_EXPIRATION_OFFSET), bytes32(block.timestamp + 1 days) ); - + uint32 newGuardianSetIndex = curGuardianSetIndex + 1; uint256 newGuardianSetSlot = _guardianSetSlot(newGuardianSetIndex); @@ -409,7 +383,7 @@ library AdvancedWormholeOverride { bytes32(_arraySlot(newGuardianSetSlot) + i), bytes32(uint256(uint160(vm.addr(guardianPrivateKeys[i])))) ); - + //initialize override state with default values setSequence(wormhole, 0); setNonce(wormhole, 0); diff --git a/src/testing/WormholeRelayer/MockOffchainRelayer.sol b/src/testing/WormholeRelayer/MockOffchainRelayer.sol index 68abfc7..556cc52 100644 --- a/src/testing/WormholeRelayer/MockOffchainRelayer.sol +++ b/src/testing/WormholeRelayer/MockOffchainRelayer.sol @@ -140,7 +140,7 @@ contract MockOffchainRelayer { vaas[i].emitterChainId, vaas[i].emitterAddress.fromUniversalAddress() ); - + genericRelay( vaas[i], vaas,