Skip to content

Commit 9206704

Browse files
authored
refactored and extended decoding functionality, added CoreBridgeLib (#77)
* added/unified documentation of format libraries * added VaaLib and associated structs for efficient decoding of VAAs * implemented CoreBridgeLib for more gas efficient VAA verification (~26k gas or 20%) * added TokenBridgeMessageLib and associated structs for efficient decoding of TokenBridge payloads * updated PermitParsing to new standards and added `calldata` variants * reworked, enhanced, and unified parsing of CCTP messages * moved WormholeCctp components into newly created legacy directory * added calldata variants to QueryResponseLib * QueryResponseLib now uses CoreBridgeLib under the hood * added `minSigsForQuorum` utility function * unified function naming (`parse` -> `decode` to be in line with `abi.decode`) * unified code style throughout the library * added tests for VaaLib and WormholeMessages
1 parent 575181b commit 9206704

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+6422
-1595
lines changed

docs/RawDispatcher.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ Contracts using this base class have to override the associated virtual function
116116

117117
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":
118118
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.
119-
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.)
119+
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).
120120

121121
### Limitations
122122

docs/Testing.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,6 @@ The `WormholeCctpSimulator` contract can be deployed to simulate a virtual `Worm
4242

4343
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.
4444

45-
### CctpMessages
46-
47-
Library to parse CCTP messages composed/emitted by Circle's `TokenMessenger` and `MessageTransmitter` contracts. Used in `CctpOverride` and `WormholeCctpSimulator`.
48-
4945
### ERC20Mock
5046

5147
Copy of SolMate's ERC20 Mock token that uses the overrideable `IERC20` interface of this SDK to guarantee compatibility.
File renamed without changes.

gen/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ cctpDomains_TARGET = constants/CctpDomains.sol
55
fnGeneratorTarget = ../src/$($(1)_TARGET)
66
GENERATOR_TARGETS = $(foreach generator,$(GENERATORS),$(call fnGeneratorTarget,$(generator)))
77

8-
TEST_WRAPPERS = BytesParsing QueryResponse
8+
TEST_WRAPPERS = BytesParsing QueryResponse VaaLib TokenBridgeMessages CctpMessages
99

1010
fnTestWrapperTarget = ../test/generated/$(1)TestWrapper.sol
1111
TEST_WRAPPER_TARGETS =\

src/Utils.sol

Lines changed: 20 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,23 @@
11
// SPDX-License-Identifier: Apache 2
22
pragma solidity ^0.8.4;
33

4-
import { WORD_SIZE, SCRATCH_SPACE_PTR, FREE_MEMORY_PTR } from "./constants/Common.sol";
5-
6-
import {tokenOrNativeTransfer} from "wormhole-sdk/utils/Transfer.sol";
7-
import {reRevert} from "wormhole-sdk/utils/Revert.sol";
8-
import {toUniversalAddress, fromUniversalAddress} from "wormhole-sdk/utils/UniversalAddress.sol";
9-
10-
//see Optimization.md for rationale on avoiding short-circuiting
11-
function eagerAnd(bool lhs, bool rhs) pure returns (bool ret) {
12-
/// @solidity memory-safe-assembly
13-
assembly {
14-
ret := and(lhs, rhs)
15-
}
16-
}
17-
18-
//see Optimization.md for rationale on avoiding short-circuiting
19-
function eagerOr(bool lhs, bool rhs) pure returns (bool ret) {
20-
/// @solidity memory-safe-assembly
21-
assembly {
22-
ret := or(lhs, rhs)
23-
}
24-
}
25-
26-
function keccak256Word(bytes32 word) pure returns (bytes32 hash) {
27-
/// @solidity memory-safe-assembly
28-
assembly {
29-
mstore(SCRATCH_SPACE_PTR, word)
30-
hash := keccak256(SCRATCH_SPACE_PTR, WORD_SIZE)
31-
}
32-
}
33-
34-
function keccak256SliceUnchecked(
35-
bytes memory encoded,
36-
uint offset,
37-
uint length
38-
) pure returns (bytes32 hash) {
39-
/// @solidity memory-safe-assembly
40-
assembly {
41-
// The length of the bytes type `length` field is that of a word in memory
42-
let ptr := add(add(encoded, offset), WORD_SIZE)
43-
hash := keccak256(ptr, length)
44-
}
45-
}
46-
47-
function keccak256Cd(
48-
bytes calldata encoded
49-
) pure returns (bytes32 hash) {
50-
/// @solidity memory-safe-assembly
51-
assembly {
52-
let freeMemory := mload(FREE_MEMORY_PTR)
53-
calldatacopy(freeMemory, encoded.offset, encoded.length)
54-
hash := keccak256(freeMemory, encoded.length)
55-
}
56-
}
4+
import {
5+
tokenOrNativeTransfer
6+
} from "wormhole-sdk/utils/Transfer.sol";
7+
import {
8+
reRevert
9+
} from "wormhole-sdk/utils/Revert.sol";
10+
import {
11+
NotAnEvmAddress,
12+
toUniversalAddress,
13+
fromUniversalAddress
14+
} from "wormhole-sdk/utils/UniversalAddress.sol";
15+
import {
16+
keccak256Word,
17+
keccak256SliceUnchecked,
18+
keccak256Cd
19+
} from "wormhole-sdk/utils/Keccak.sol";
20+
import {
21+
eagerAnd,
22+
eagerOr
23+
} from "wormhole-sdk/utils/EagerOps.sol";

src/components/dispatcher/SweepTokens.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
pragma solidity ^0.8.4;
44

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

src/legacy/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Legacy Directory
2+
3+
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.
4+
5+
# WormholeCctp
6+
7+
The WormholeCctpTokenMessenger is a standalone implementation of [WormholeCircleIntegration](https://github.com/wormhole-foundation/wormhole-circle-integration/).
8+
9+
Its has two associated files:
10+
1. WormholeCctpMessages (message encoding/decoding)
11+
2. WormholeCctpSimulator (for forge testing)
12+
13+
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).
14+
15+
It later got moved to the Solidity SDK and used for testing in the [swap layer](https://github.com/wormhole-foundation/example-swap-layer).
16+
17+
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.
18+
19+
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.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// SPDX-License-Identifier: Apache 2
2+
pragma solidity ^0.8.19;
3+
4+
import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol";
5+
6+
// ╭───────────────────────────────────────────────────────────────────────╮
7+
// │ Library for encoding and decoding WormholeCctpTokenMessenger messages │
8+
// ╰───────────────────────────────────────────────────────────────────────╯
9+
10+
library WormholeCctpMessageLib {
11+
using BytesParsing for bytes;
12+
using {BytesParsing.checkLength} for uint;
13+
14+
uint8 internal constant PAYLOAD_ID_DEPOSIT = 1;
15+
16+
// uint private constant _DEPOSIT_META_SIZE =
17+
// 32 /*universalTokenAddress*/ +
18+
// 32 /*amount*/ +
19+
// 4 /*sourceCctpDomain*/ +
20+
// 4 /*targetCctpDomain*/ +
21+
// 8 /*cctpNonce*/ +
22+
// 32 /*burnSource*/ +
23+
// 32 /*mintRecipient*/;
24+
25+
error PayloadTooLarge(uint256);
26+
error InvalidPayloadId(uint8);
27+
28+
function encodeDeposit(
29+
bytes32 universalTokenAddress,
30+
uint256 amount,
31+
uint32 sourceCctpDomain,
32+
uint32 targetCctpDomain,
33+
uint64 cctpNonce,
34+
bytes32 burnSource,
35+
bytes32 mintRecipient,
36+
bytes memory payload
37+
) internal pure returns (bytes memory) {
38+
uint payloadLen = payload.length;
39+
if (payloadLen > type(uint16).max)
40+
revert PayloadTooLarge(payloadLen);
41+
42+
return abi.encodePacked(
43+
PAYLOAD_ID_DEPOSIT,
44+
universalTokenAddress,
45+
amount,
46+
sourceCctpDomain,
47+
targetCctpDomain,
48+
cctpNonce,
49+
burnSource,
50+
mintRecipient,
51+
uint16(payloadLen),
52+
payload
53+
);
54+
}
55+
56+
function decodeDepositCd(bytes calldata vaaPayload) internal pure returns (
57+
bytes32 token,
58+
uint256 amount,
59+
uint32 sourceCctpDomain,
60+
uint32 targetCctpDomain,
61+
uint64 cctpNonce,
62+
bytes32 burnSource,
63+
bytes32 mintRecipient,
64+
bytes calldata payload
65+
) {
66+
uint offset = 0;
67+
( token,
68+
amount,
69+
sourceCctpDomain,
70+
targetCctpDomain,
71+
cctpNonce,
72+
burnSource,
73+
mintRecipient,
74+
offset
75+
) = decodeDepositHeaderCdUnchecked(vaaPayload, offset);
76+
77+
(payload, offset) = decodeDepositPayloadCdUnchecked(vaaPayload, offset);
78+
vaaPayload.length.checkLength(offset);
79+
}
80+
81+
function decodeDepositHeaderCdUnchecked(
82+
bytes calldata encoded,
83+
uint offset
84+
) internal pure returns (
85+
bytes32 token,
86+
uint256 amount,
87+
uint32 sourceCctpDomain,
88+
uint32 targetCctpDomain,
89+
uint64 cctpNonce,
90+
bytes32 burnSource,
91+
bytes32 mintRecipient,
92+
uint payloadOffset
93+
) {
94+
uint8 payloadId;
95+
(payloadId, offset) = encoded.asUint8CdUnchecked(offset);
96+
checkPayloadId(payloadId, PAYLOAD_ID_DEPOSIT);
97+
(token, offset) = encoded.asBytes32CdUnchecked(offset);
98+
(amount, offset) = encoded.asUint256CdUnchecked(offset);
99+
(sourceCctpDomain, offset) = encoded.asUint32CdUnchecked(offset);
100+
(targetCctpDomain, offset) = encoded.asUint32CdUnchecked(offset);
101+
(cctpNonce, offset) = encoded.asUint64CdUnchecked(offset);
102+
(burnSource, offset) = encoded.asBytes32CdUnchecked(offset);
103+
(mintRecipient, offset) = encoded.asBytes32CdUnchecked(offset);
104+
payloadOffset = offset;
105+
}
106+
107+
function decodeDepositPayloadCdUnchecked(
108+
bytes calldata encoded,
109+
uint offset
110+
) internal pure returns (bytes calldata payload, uint newOffset) {
111+
(payload, newOffset) = encoded.sliceUint16PrefixedCdUnchecked(offset);
112+
}
113+
114+
function decodeDepositMem(bytes memory vaaPayload) internal pure returns (
115+
bytes32 token,
116+
uint256 amount,
117+
uint32 sourceCctpDomain,
118+
uint32 targetCctpDomain,
119+
uint64 cctpNonce,
120+
bytes32 burnSource,
121+
bytes32 mintRecipient,
122+
bytes memory payload
123+
) {
124+
uint offset = 0;
125+
( token,
126+
amount,
127+
sourceCctpDomain,
128+
targetCctpDomain,
129+
cctpNonce,
130+
burnSource,
131+
mintRecipient,
132+
offset
133+
) = decodeDepositHeaderMemUnchecked(vaaPayload, 0);
134+
135+
(payload, offset) = decodeDepositPayloadMemUnchecked(vaaPayload, offset);
136+
vaaPayload.length.checkLength(offset);
137+
}
138+
139+
function decodeDepositHeaderMemUnchecked(
140+
bytes memory encoded,
141+
uint offset
142+
) internal pure returns (
143+
bytes32 token,
144+
uint256 amount,
145+
uint32 sourceCctpDomain,
146+
uint32 targetCctpDomain,
147+
uint64 cctpNonce,
148+
bytes32 burnSource,
149+
bytes32 mintRecipient,
150+
uint payloadOffset
151+
) {
152+
uint8 payloadId;
153+
(payloadId, offset) = encoded.asUint8MemUnchecked(offset);
154+
checkPayloadId(payloadId, PAYLOAD_ID_DEPOSIT);
155+
(token, offset) = encoded.asBytes32MemUnchecked(offset);
156+
(amount, offset) = encoded.asUint256MemUnchecked(offset);
157+
(sourceCctpDomain, offset) = encoded.asUint32MemUnchecked(offset);
158+
(targetCctpDomain, offset) = encoded.asUint32MemUnchecked(offset);
159+
(cctpNonce, offset) = encoded.asUint64MemUnchecked(offset);
160+
(burnSource, offset) = encoded.asBytes32MemUnchecked(offset);
161+
(mintRecipient, offset) = encoded.asBytes32MemUnchecked(offset);
162+
payloadOffset = offset;
163+
}
164+
165+
function decodeDepositPayloadMemUnchecked(
166+
bytes memory encoded,
167+
uint offset
168+
) internal pure returns (bytes memory payload, uint newOffset) {
169+
(payload, newOffset) = encoded.sliceUint16PrefixedMemUnchecked(offset);
170+
}
171+
172+
function checkPayloadId(uint8 encoded, uint8 expected) internal pure {
173+
if (encoded != expected)
174+
revert InvalidPayloadId(encoded);
175+
}
176+
}

src/testing/WormholeCctpSimulator.sol renamed to src/legacy/WormholeCctpSimulator.sol

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,27 @@ pragma solidity ^0.8.19;
33

44
import {Vm} from "forge-std/Vm.sol";
55

6-
import "wormhole-sdk/interfaces/cctp/ITokenMessenger.sol";
7-
8-
import "wormhole-sdk/interfaces/IWormhole.sol";
9-
import {WormholeCctpMessages} from "wormhole-sdk/libraries/WormholeCctpMessages.sol";
10-
import {toUniversalAddress} from "wormhole-sdk/Utils.sol";
11-
12-
import {VM_ADDRESS} from "wormhole-sdk/testing/Constants.sol";
13-
import "wormhole-sdk/testing/CctpOverride.sol";
14-
import "wormhole-sdk/testing/WormholeOverride.sol";
6+
import {IWormhole} from "wormhole-sdk/interfaces/IWormhole.sol";
7+
import {IMessageTransmitter} from "wormhole-sdk/interfaces/cctp/IMessageTransmitter.sol";
8+
import {ITokenMessenger} from "wormhole-sdk/interfaces/cctp/ITokenMessenger.sol";
9+
import {ITokenMinter} from "wormhole-sdk/interfaces/cctp/ITokenMinter.sol";
10+
11+
import {
12+
CctpMessageLib,
13+
CctpTokenBurnMessage
14+
} from "wormhole-sdk/libraries/CctpMessages.sol";
15+
import {WormholeCctpMessageLib} from "wormhole-sdk/legacy/WormholeCctpMessages.sol";
16+
import {toUniversalAddress} from "wormhole-sdk/Utils.sol";
17+
import {VM_ADDRESS} from "wormhole-sdk/testing/Constants.sol";
18+
import {CctpOverride} from "wormhole-sdk/testing/CctpOverride.sol";
19+
import {WormholeOverride} from "wormhole-sdk/testing/WormholeOverride.sol";
1520

1621
//faked foreign call chain:
1722
// foreignCaller -> foreignSender -> FOREIGN_TOKEN_MESSENGER -> foreign MessageTransmitter
1823
//example:
1924
// foreignCaller = swap layer
2025
// foreignSender = liquidity layer - implements WormholeCctpTokenMessenger
21-
// emits WormholeCctpMessages.Deposit VAA with a RedeemFill payload
26+
// emits WormholeCctpMessageLib.Deposit VAA with a RedeemFill payload
2227

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

4448
Vm constant vm = Vm(VM_ADDRESS);
@@ -138,14 +142,14 @@ contract WormholeCctpSimulator {
138142
encodedVaa = wormhole.craftVaa(
139143
foreignChain,
140144
foreignSender,
141-
WormholeCctpMessages.encodeDeposit(
142-
burnMsg.burnToken,
145+
WormholeCctpMessageLib.encodeDeposit(
146+
burnMsg.body.burnToken,
143147
amount,
144148
burnMsg.header.sourceDomain,
145149
burnMsg.header.destinationDomain,
146150
burnMsg.header.nonce,
147151
foreignCaller,
148-
burnMsg.mintRecipient,
152+
burnMsg.body.mintRecipient,
149153
payload
150154
)
151155
);
@@ -165,10 +169,10 @@ contract WormholeCctpSimulator {
165169
burnMsg.header.sender = FOREIGN_TOKEN_MESSENGER;
166170
burnMsg.header.recipient = address(tokenMessenger).toUniversalAddress();
167171
burnMsg.header.destinationCaller = destinationCaller.toUniversalAddress();
168-
burnMsg.burnToken = FOREIGN_USDC;
169-
burnMsg.mintRecipient = mintRecipient.toUniversalAddress();
170-
burnMsg.amount = amount;
171-
burnMsg.messageSender = foreignSender;
172+
burnMsg.body.burnToken = FOREIGN_USDC;
173+
burnMsg.body.mintRecipient = mintRecipient.toUniversalAddress();
174+
burnMsg.body.amount = amount;
175+
burnMsg.body.messageSender = foreignSender;
172176

173177
encodedCctpMessage = burnMsg.encode();
174178
cctpAttestation = messageTransmitter.sign(burnMsg);

0 commit comments

Comments
 (0)