Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactored and extended decoding functionality, added CoreBridgeLib #77

Merged
merged 7 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/RawDispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Contracts using this base class have to override the associated virtual function

Of course, integrations have to actually make use of these custom dispatch functions to reap their benefits. To this end, contracts using the RawDispatcher pattern/base call should come with two additional "SDKs":
1. For on-chain integrations: A Solidity integrator `library` that fills the role of what is otherwise provided by an `interface`. That is, a set of encoding and decoding functions that mirror the contract's ABI but implement the custom call format of the contract decoding its returned `bytes` under the hood.
2. For off-chain integrations: A Typescript analog of the integrator library. The [layouting mechanism in the Wormhole Typescript SDK](https://github.com/wormhole-foundation/wormhole-sdk-ts/tree/main/core/base/src/utils/layout) offers an easy, declarative way to specify such custom encodings. It also provides [definitions of common types](https://github.com/wormhole-foundation/wormhole-sdk-ts/tree/main/core/definitions/src/layout-items) and various examples of its use can be found in [the protocols defined in the SDK itself](https://github.com/wormhole-foundation/wormhole-sdk-ts/tree/main/core/definitions/src/protocols) (e.g. [TokenBridge Messages](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/main/core/definitions/src/protocols/tokenBridge/tokenBridgeLayout.ts), [WormholeRelayer Messages](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/main/core/definitions/src/protocols/relayer/relayerLayout.ts), [CCTP messages](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/main/core/definitions/src/protocols/circleBridge/circleBridgeLayout.ts)) or strewn throughout the various example repos e.g. [example-swap-layer](https://github.com/wormhole-foundation/example-swap-layer/blob/main/evm/ts-sdk/src/layout.ts) or [example-native-token-transfers](https://github.com/wormhole-foundation/example-native-token-transfers/tree/main/sdk/definitions/src/layouts). (A proper, standalone tutorial is sadly still outstanding at the time of writing.)
2. For off-chain integrations: A Typescript analog of the integrator library. The [layouting package](https://www.npmjs.com/package/binary-layout) offers an easy, declarative way to specify such custom encodings. It is also used in [the Wormhole Typescript SDK](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/main/core/base/src/utils/layout.ts) to [define common types](https://github.com/wormhole-foundation/wormhole-sdk-ts/tree/main/core/definitions/src/layout-items) and various other layout examples can be found in [the protocols defined within the SDK itself](https://github.com/wormhole-foundation/wormhole-sdk-ts/tree/main/core/definitions/src/protocols) (e.g. [TokenBridge Messages](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/main/core/definitions/src/protocols/tokenBridge/tokenBridgeLayout.ts), [WormholeRelayer Messages](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/main/core/definitions/src/protocols/relayer/relayerLayout.ts), [CCTP messages](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/main/core/definitions/src/protocols/circleBridge/circleBridgeLayout.ts)) or strewn throughout the various example repos e.g. [example-swap-layer](https://github.com/wormhole-foundation/example-swap-layer/blob/main/evm/ts-sdk/src/layout.ts) or [example-native-token-transfers](https://github.com/wormhole-foundation/example-native-token-transfers/tree/main/sdk/definitions/src/layouts).

### Limitations

Expand Down
4 changes: 0 additions & 4 deletions docs/Testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion gen/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ cctpDomains_TARGET = constants/CctpDomains.sol
fnGeneratorTarget = ../src/$($(1)_TARGET)
GENERATOR_TARGETS = $(foreach generator,$(GENERATORS),$(call fnGeneratorTarget,$(generator)))

TEST_WRAPPERS = BytesParsing QueryResponse
TEST_WRAPPERS = BytesParsing QueryResponse VaaLib TokenBridgeMessages CctpMessages

fnTestWrapperTarget = ../test/generated/$(1)TestWrapper.sol
TEST_WRAPPER_TARGETS =\
Expand Down
73 changes: 20 additions & 53 deletions src/Utils.sol
Original file line number Diff line number Diff line change
@@ -1,56 +1,23 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.4;

import { WORD_SIZE, SCRATCH_SPACE_PTR, FREE_MEMORY_PTR } from "./constants/Common.sol";

import {tokenOrNativeTransfer} from "wormhole-sdk/utils/Transfer.sol";
import {reRevert} from "wormhole-sdk/utils/Revert.sol";
import {toUniversalAddress, fromUniversalAddress} from "wormhole-sdk/utils/UniversalAddress.sol";

//see Optimization.md for rationale on avoiding short-circuiting
function eagerAnd(bool lhs, bool rhs) pure returns (bool ret) {
/// @solidity memory-safe-assembly
assembly {
ret := and(lhs, rhs)
}
}

//see Optimization.md for rationale on avoiding short-circuiting
function eagerOr(bool lhs, bool rhs) pure returns (bool ret) {
/// @solidity memory-safe-assembly
assembly {
ret := or(lhs, rhs)
}
}

function keccak256Word(bytes32 word) pure returns (bytes32 hash) {
/// @solidity memory-safe-assembly
assembly {
mstore(SCRATCH_SPACE_PTR, word)
hash := keccak256(SCRATCH_SPACE_PTR, WORD_SIZE)
}
}

function keccak256SliceUnchecked(
bytes memory encoded,
uint offset,
uint length
) pure returns (bytes32 hash) {
/// @solidity memory-safe-assembly
assembly {
// The length of the bytes type `length` field is that of a word in memory
let ptr := add(add(encoded, offset), WORD_SIZE)
hash := keccak256(ptr, length)
}
}

function keccak256Cd(
bytes calldata encoded
) pure returns (bytes32 hash) {
/// @solidity memory-safe-assembly
assembly {
let freeMemory := mload(FREE_MEMORY_PTR)
calldatacopy(freeMemory, encoded.offset, encoded.length)
hash := keccak256(freeMemory, encoded.length)
}
}
import {
tokenOrNativeTransfer
} from "wormhole-sdk/utils/Transfer.sol";
import {
reRevert
} from "wormhole-sdk/utils/Revert.sol";
import {
NotAnEvmAddress,
toUniversalAddress,
fromUniversalAddress
} from "wormhole-sdk/utils/UniversalAddress.sol";
import {
keccak256Word,
keccak256SliceUnchecked,
keccak256Cd
} from "wormhole-sdk/utils/Keccak.sol";
import {
eagerAnd,
eagerOr
} from "wormhole-sdk/utils/EagerOps.sol";
2 changes: 1 addition & 1 deletion src/components/dispatcher/SweepTokens.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pragma solidity ^0.8.4;

import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol";
import {tokenOrNativeTransfer} from "wormhole-sdk/utils/Transfer.sol";
import {tokenOrNativeTransfer} from "wormhole-sdk/Utils.sol";
import {senderAtLeastAdmin} from "wormhole-sdk/components/dispatcher/AccessControl.sol";
import {SWEEP_TOKENS_ID} from "wormhole-sdk/components/dispatcher/Ids.sol";

Expand Down
19 changes: 19 additions & 0 deletions src/legacy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Legacy Directory

The legacy directory is a dumping ground for all files that are kept for backwards compatibility but that are not kept up to the standards of the rest of the SDK.

# WormholeCctp

The WormholeCctpTokenMessenger is a standalone implementation of [WormholeCircleIntegration](https://github.com/wormhole-foundation/wormhole-circle-integration/).

Its has two associated files:
1. WormholeCctpMessages (message encoding/decoding)
2. WormholeCctpSimulator (for forge testing)

WormholeCctp functionality was extracted during the [liquidity layer](https://github.com/wormhole-foundation/example-liquidity-layer/blob/main/evm/src/shared/WormholeCctpTokenMessenger.sol) development process when it was recognized that going through the circle integration contract was adding additional, unnecessary overhead (gas, deployment, registration).

It later got moved to the Solidity SDK and used for testing in the [swap layer](https://github.com/wormhole-foundation/example-swap-layer).

The implementation is now considered legacy because it's unlikely to see any future use and thus keeping it up to date is not worth the effort.

A proper overhaul, besides introducing common optimizations in the rest of the SDK, would also rework the message format. Currently, most of the information that's in the VAA is actually redundant and could simply be taken from the CctpBurnTokenMessage. The only 2 pieces of information that should actually go into the VAA to uniquely link it to its associated CCTP messages is the CCTP nonce and the sourceDomain. But changing the message format would break backwards compatibility, thus interfering with its only expected use case.
176 changes: 176 additions & 0 deletions src/legacy/WormholeCctpMessages.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.19;

import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol";

// ╭───────────────────────────────────────────────────────────────────────╮
// │ Library for encoding and decoding WormholeCctpTokenMessenger messages │
// ╰───────────────────────────────────────────────────────────────────────╯

library WormholeCctpMessageLib {
using BytesParsing for bytes;
using {BytesParsing.checkLength} for uint;

uint8 internal constant PAYLOAD_ID_DEPOSIT = 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 InvalidPayloadId(uint8);

function encodeDeposit(
bytes32 universalTokenAddress,
uint256 amount,
uint32 sourceCctpDomain,
uint32 targetCctpDomain,
uint64 cctpNonce,
bytes32 burnSource,
bytes32 mintRecipient,
bytes memory payload
) internal pure returns (bytes memory) {
uint payloadLen = payload.length;
if (payloadLen > type(uint16).max)
revert PayloadTooLarge(payloadLen);

return abi.encodePacked(
PAYLOAD_ID_DEPOSIT,
universalTokenAddress,
amount,
sourceCctpDomain,
targetCctpDomain,
cctpNonce,
burnSource,
mintRecipient,
uint16(payloadLen),
payload
);
}

function decodeDepositCd(bytes calldata vaaPayload) internal pure returns (
bytes32 token,
uint256 amount,
uint32 sourceCctpDomain,
uint32 targetCctpDomain,
uint64 cctpNonce,
bytes32 burnSource,
bytes32 mintRecipient,
bytes calldata payload
) {
uint offset = 0;
( token,
amount,
sourceCctpDomain,
targetCctpDomain,
cctpNonce,
burnSource,
mintRecipient,
offset
) = decodeDepositHeaderCdUnchecked(vaaPayload, offset);

(payload, offset) = decodeDepositPayloadCdUnchecked(vaaPayload, offset);
vaaPayload.length.checkLength(offset);
}

function decodeDepositHeaderCdUnchecked(
bytes calldata encoded,
uint offset
) internal pure returns (
bytes32 token,
uint256 amount,
uint32 sourceCctpDomain,
uint32 targetCctpDomain,
uint64 cctpNonce,
bytes32 burnSource,
bytes32 mintRecipient,
uint payloadOffset
) {
uint8 payloadId;
(payloadId, offset) = encoded.asUint8CdUnchecked(offset);
checkPayloadId(payloadId, PAYLOAD_ID_DEPOSIT);
(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);
payloadOffset = offset;
}

function decodeDepositPayloadCdUnchecked(
bytes calldata encoded,
uint offset
) internal pure returns (bytes calldata payload, uint newOffset) {
(payload, newOffset) = encoded.sliceUint16PrefixedCdUnchecked(offset);
}

function decodeDepositMem(bytes memory vaaPayload) internal pure returns (
bytes32 token,
uint256 amount,
uint32 sourceCctpDomain,
uint32 targetCctpDomain,
uint64 cctpNonce,
bytes32 burnSource,
bytes32 mintRecipient,
bytes memory payload
) {
uint offset = 0;
( token,
amount,
sourceCctpDomain,
targetCctpDomain,
cctpNonce,
burnSource,
mintRecipient,
offset
) = decodeDepositHeaderMemUnchecked(vaaPayload, 0);

(payload, offset) = decodeDepositPayloadMemUnchecked(vaaPayload, offset);
vaaPayload.length.checkLength(offset);
}

function decodeDepositHeaderMemUnchecked(
bytes memory encoded,
uint offset
) internal pure returns (
bytes32 token,
uint256 amount,
uint32 sourceCctpDomain,
uint32 targetCctpDomain,
uint64 cctpNonce,
bytes32 burnSource,
bytes32 mintRecipient,
uint payloadOffset
) {
uint8 payloadId;
(payloadId, offset) = encoded.asUint8MemUnchecked(offset);
checkPayloadId(payloadId, PAYLOAD_ID_DEPOSIT);
(token, offset) = encoded.asBytes32MemUnchecked(offset);
(amount, offset) = encoded.asUint256MemUnchecked(offset);
(sourceCctpDomain, offset) = encoded.asUint32MemUnchecked(offset);
(targetCctpDomain, offset) = encoded.asUint32MemUnchecked(offset);
(cctpNonce, offset) = encoded.asUint64MemUnchecked(offset);
(burnSource, offset) = encoded.asBytes32MemUnchecked(offset);
(mintRecipient, offset) = encoded.asBytes32MemUnchecked(offset);
payloadOffset = offset;
}

function decodeDepositPayloadMemUnchecked(
bytes memory encoded,
uint offset
) internal pure returns (bytes memory payload, uint newOffset) {
(payload, newOffset) = encoded.sliceUint16PrefixedMemUnchecked(offset);
}

function checkPayloadId(uint8 encoded, uint8 expected) internal pure {
if (encoded != expected)
revert InvalidPayloadId(encoded);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@ 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 {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 {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 {
CctpMessageLib,
CctpTokenBurnMessage
} from "wormhole-sdk/libraries/CctpMessages.sol";
import {WormholeCctpMessageLib} from "wormhole-sdk/legacy/WormholeCctpMessages.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
//example:
// foreignCaller = swap layer
// foreignSender = liquidity layer - implements WormholeCctpTokenMessenger
// emits WormholeCctpMessages.Deposit VAA with a RedeemFill payload
// emits WormholeCctpMessageLib.Deposit VAA with a RedeemFill payload

//local call chain using faked vaa and circle attestation:
// test -> intermediate contract(s) -> mintRecipient -> MessageTransmitter -> TokenMessenger
Expand All @@ -36,9 +41,8 @@ bytes32 constant FOREIGN_USDC =
//simulates a foreign WormholeCctpTokenMessenger
contract WormholeCctpSimulator {
using WormholeOverride for IWormhole;
using CctpMessages for CctpTokenBurnMessage;
using CctpOverride for IMessageTransmitter;
using CctpMessages for bytes;
using CctpMessageLib for bytes;
using { toUniversalAddress } for address;

Vm constant vm = Vm(VM_ADDRESS);
Expand Down Expand Up @@ -138,14 +142,14 @@ contract WormholeCctpSimulator {
encodedVaa = wormhole.craftVaa(
foreignChain,
foreignSender,
WormholeCctpMessages.encodeDeposit(
burnMsg.burnToken,
WormholeCctpMessageLib.encodeDeposit(
burnMsg.body.burnToken,
amount,
burnMsg.header.sourceDomain,
burnMsg.header.destinationDomain,
burnMsg.header.nonce,
foreignCaller,
burnMsg.mintRecipient,
burnMsg.body.mintRecipient,
payload
)
);
Expand All @@ -165,10 +169,10 @@ contract WormholeCctpSimulator {
burnMsg.header.sender = FOREIGN_TOKEN_MESSENGER;
burnMsg.header.recipient = address(tokenMessenger).toUniversalAddress();
burnMsg.header.destinationCaller = destinationCaller.toUniversalAddress();
burnMsg.burnToken = FOREIGN_USDC;
burnMsg.mintRecipient = mintRecipient.toUniversalAddress();
burnMsg.amount = amount;
burnMsg.messageSender = foreignSender;
burnMsg.body.burnToken = FOREIGN_USDC;
burnMsg.body.mintRecipient = mintRecipient.toUniversalAddress();
burnMsg.body.amount = amount;
burnMsg.body.messageSender = foreignSender;

encodedCctpMessage = burnMsg.encode();
cctpAttestation = messageTransmitter.sign(burnMsg);
Expand Down
Loading