Skip to content

Commit 67df547

Browse files
authored
NativeTokenTransfer additional payload (#522)
* docs: NativeTokenTransfer additional payload * evm: NativeTokenTransfer additional payload * solana: NativeTokenTransfer additional payload * sdk: NativeTokenTransfer additional payload * evm: additional and custom payload hooks * docs: integrator notes for NativeTokenTransfer additional payload * docs: fixup comments * evm: more flexible additional payload generation * solana: fix anchor test * evm: update _handleAdditionalPayload params * solana: NativeTokenTransfer written_size logic mirrors write * docs: more additional payload info * evm: update _prepareNativeTokenTransfer and _handleAdditionalPayload for integrator flexibility * evm: MockNttManagerAdditionalPayload and TestAdditionalPayload * evm: _prepareNativeTokenTransfer docstring update
1 parent 68a7ca4 commit 67df547

31 files changed

+1031
-71
lines changed

docs/NttManager.md

+17-6
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,24 @@ uint16 payload_len // length of the payload
1717

1818
### Payloads
1919

20+
> Note: Integrators who need to send different types of payloads should also use a unique 4-byte prefix to distinguish them from `NativeTokenTransfer` and one another.
21+
2022
#### NativeTokenTransfer
2123

2224
```go
23-
[4]byte prefix = 0x994E5454 // 0x99'N''T''T'
24-
uint8 decimals // number of decimals for the amount
25-
uint64 amount // amount being transferred
26-
[32]byte source_token // source chain token address
27-
[32]byte recipient_address // the address of the recipient
28-
uint16 recipient_chain // the Wormhole Chain ID of the recipient
25+
[4]byte prefix = 0x994E5454 // 0x99'N''T''T'
26+
uint8 decimals // number of decimals for the amount
27+
uint64 amount // amount being transferred
28+
[32]byte source_token // source chain token address
29+
[32]byte recipient_address // the address of the recipient
30+
uint16 recipient_chain // the Wormhole Chain ID of the recipient
31+
```
32+
33+
To support integrators who may want to send additional, custom data with their transfers, this format may be extended to also include these additional, optional fields. Customizing transfers in this way ensures compatibility of the canonical portion of the payload across the ecosystem (Connect, explorers, NTT Global Accountant, etc).
34+
35+
In order to aid parsers in identifying your additional payload, it is recommended to start it with a unique 4-byte prefix.
36+
37+
```go
38+
uint16 additional_payload_len // length of the custom payload
39+
[]byte additional_payload // custom payload - recommended that the first 4 bytes are a unique prefix
2940
```

docs/Transceiver.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ uint16 transceiver_payload_length
2727
#### TransceiverMessage
2828

2929
```go
30-
prefix = 0x9945FF10 // 0x99'E''W''H'
30+
prefix = 0x9945FF10
3131
```
3232

3333
#### Initialize Transceiver

evm/README.md

+37-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
# Message Lifecycle (EVM)
1+
# EVM
2+
3+
## Message Lifecycle (EVM)
24

35
1. **Transfer**
46

@@ -205,9 +207,37 @@ $ anvil --help
205207
$ cast --help
206208
```
207209

208-
### Deploy Wormhole NTT
210+
## Message Customization
211+
212+
See the [NttManager](../docs/NttManager.md) doc for wire format details.
213+
214+
> Note: The size of `NttManager` is significantly higher (and very close to the contract size limit) than `NttManagerNoRateLimiting`. See [No-Rate-Limiting](#no-rate-limiting) for more detail.
215+
216+
### NativeTokenTransfer Additional Payload
217+
218+
Your contract can extend `NttManagerNoRateLimiting` to provide an additional payload on the `NativeTokenTransfer` message. Override the following:
219+
220+
- `_prepareNativeTokenTransfer`
221+
- `_handleAdditionalPayload`
222+
223+
Be sure to review the code to ensure that they are called at an appropriate place in the flow for your use case.
224+
225+
> Note: This is _not_ supported when RateLimiting is enabled, as this implementation does not propagate the additional payloads on queued messages. Use either with `NttManager` when `_rateLimitDuration` is set to `0` and `_skipRateLimiting` is set to `true` in the constructor or with `NttManagerNoRateLimiting`.
226+
227+
### Custom Manager Payloads
228+
229+
`NttManager` (or `NttManagerNoRateLimiting`) can also be extended to exchange other kinds of custom messages. To send custom messages, create a new public function and roughly follow the steps in `_transfer`. What exactly you need to do will vary on your use case, but in order to leverage the existing transceivers, you will roughly need to
230+
231+
- call `_prepareForTransfer`
232+
- construct and encode your payload
233+
- construct the `NttManagerMessage` payload
234+
- call `_sendMessageToTransceivers`
235+
236+
On the receiving side, override `_handleMsg` and adjust its code based on your custom payload prefixes, defaulting to `_handleTransfer`.
237+
238+
## Deploy Wormhole NTT
209239

210-
#### Environment Setup
240+
### Environment Setup
211241

212242
Note: **All Chain IDs set in the deployment environment files and configuration files should be the Wormhole Chain ID**
213243

@@ -222,7 +252,7 @@ Do this for each blockchain network that the `NTTManager` and `WormholeTransceiv
222252

223253
Currently the `MAX_OUTBOUND_LIMIT` is set to zero in the sample `.env` file. This means that all outbound transfers will be queued by the rate limiter.
224254

225-
#### Config Setup
255+
### Config Setup
226256

227257
Before deploying the contracts, navigate to the `evm/cfg` directory and copy the sample file. Make sure to preserve the existing name:
228258

@@ -242,7 +272,7 @@ Configure each network to your liking (including adding/removing networks). We w
242272

243273
Currently the per-chain `inBoundLimit` is set to zero by default. This means all inbound transfers will be queued by the rate limiter. Set this value accordingly.
244274

245-
#### Deploy
275+
### Deploy
246276

247277
Deploy the `NttManager` and `WormholeTransceiver` contracts by running the following command for each target network:
248278

@@ -258,7 +288,7 @@ bash sh/deploy_wormhole_ntt.sh -n NETWORK_TYPE -c CHAIN_NAME -k PRIVATE_KEY
258288

259289
Save the deployed proxy contract addresses (see the forge script output) in the `WormholeNttConfig.json` file.
260290

261-
#### Configuration
291+
### Configuration
262292

263293
Once all of the contracts have been deployed and the addresses have been saved, run the following command for each target network:
264294

@@ -272,7 +302,7 @@ bash sh/configure_wormhole_ntt.sh -n NETWORK_TYPE -c CHAIN_NAME -k PRIVATE_KEY
272302
-c avalanche, ethereum, sepolia
273303
```
274304

275-
#### Additional Notes
305+
### Additional Notes
276306

277307
Tokens powered by NTT in **burn** mode require the `burn` method to be present. This method is not present in the standard ERC20 interface, but is found in the `ERC20Burnable` interface.
278308

evm/src/NttManager/NttManager.sol

+65-3
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,27 @@ contract NttManager is INttManager, RateLimiter, ManagerBase {
210210
return;
211211
}
212212

213+
_handleMsg(sourceChainId, sourceNttManagerAddress, message, digest);
214+
}
215+
216+
/// @dev Override this function to handle custom NttManager payloads.
217+
/// This can also be used to customize transfer logic by using your own
218+
/// _handleTransfer implementation.
219+
function _handleMsg(
220+
uint16 sourceChainId,
221+
bytes32 sourceNttManagerAddress,
222+
TransceiverStructs.NttManagerMessage memory message,
223+
bytes32 digest
224+
) internal virtual {
225+
_handleTransfer(sourceChainId, sourceNttManagerAddress, message, digest);
226+
}
227+
228+
function _handleTransfer(
229+
uint16 sourceChainId,
230+
bytes32 sourceNttManagerAddress,
231+
TransceiverStructs.NttManagerMessage memory message,
232+
bytes32 digest
233+
) internal {
213234
TransceiverStructs.NativeTokenTransfer memory nativeTokenTransfer =
214235
TransceiverStructs.parseNativeTokenTransfer(message.payload);
215236

@@ -231,9 +252,28 @@ contract NttManager is INttManager, RateLimiter, ManagerBase {
231252
return;
232253
}
233254

255+
_handleAdditionalPayload(
256+
sourceChainId, sourceNttManagerAddress, message.id, message.sender, nativeTokenTransfer
257+
);
258+
234259
_mintOrUnlockToRecipient(digest, transferRecipient, nativeTransferAmount, false);
235260
}
236261

262+
/// @dev Override this function to process an additional payload on the NativeTokenTransfer
263+
/// For integrator flexibility, this function is *not* marked pure or view
264+
/// @param - The Wormhole chain id of the sender
265+
/// @param - The address of the sender's NTT Manager contract.
266+
/// @param - The message id from the NttManagerMessage.
267+
/// @param - The original message sender address from the NttManagerMessage.
268+
/// @param - The parsed NativeTokenTransfer, which includes the additionalPayload field
269+
function _handleAdditionalPayload(
270+
uint16, // sourceChainId
271+
bytes32, // sourceNttManagerAddress
272+
bytes32, // id
273+
bytes32, // sender
274+
TransceiverStructs.NativeTokenTransfer memory // nativeTokenTransfer
275+
) internal virtual {}
276+
237277
function _enqueueOrConsumeInboundRateLimit(
238278
bytes32 digest,
239279
uint16 sourceChainId,
@@ -500,9 +540,8 @@ contract NttManager is INttManager, RateLimiter, ManagerBase {
500540
// push it on the stack again to avoid a stack too deep error
501541
uint64 seq = sequence;
502542

503-
TransceiverStructs.NativeTokenTransfer memory ntt = TransceiverStructs.NativeTokenTransfer(
504-
amount, toWormholeFormat(token), recipient, recipientChain
505-
);
543+
TransceiverStructs.NativeTokenTransfer memory ntt =
544+
_prepareNativeTokenTransfer(amount, token, recipient, recipientChain, seq, sender);
506545

507546
// construct the NttManagerMessage payload
508547
bytes memory encodedNttManagerPayload = TransceiverStructs.encodeNttManagerMessage(
@@ -547,6 +586,29 @@ contract NttManager is INttManager, RateLimiter, ManagerBase {
547586
return seq;
548587
}
549588

589+
/// @dev Override this function to provide an additional payload on the NativeTokenTransfer
590+
/// For integrator flexibility, this function is *not* marked pure or view
591+
/// @param amount TrimmedAmount of the transfer
592+
/// @param token Address of the token that this NTT Manager is tied to
593+
/// @param recipient The recipient address
594+
/// @param recipientChain The Wormhole chain ID for the destination
595+
/// @param - The sequence number for the manager message (unused, provided for overriding integrators)
596+
/// @param - The sender of the funds (unused, provided for overriding integrators). If releasing
597+
/// queued transfers, when rate limiting is used, then this value could be different from msg.sender.
598+
/// @return - The TransceiverStructs.NativeTokenTransfer struct
599+
function _prepareNativeTokenTransfer(
600+
TrimmedAmount amount,
601+
address token,
602+
bytes32 recipient,
603+
uint16 recipientChain,
604+
uint64, // sequence
605+
address // sender
606+
) internal virtual returns (TransceiverStructs.NativeTokenTransfer memory) {
607+
return TransceiverStructs.NativeTokenTransfer(
608+
amount, toWormholeFormat(token), recipient, recipientChain, ""
609+
);
610+
}
611+
550612
function _mintOrUnlockToRecipient(
551613
bytes32 digest,
552614
address recipient,

evm/src/Transceiver/WormholeTransceiver/WormholeTransceiverState.sol

-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ abstract contract WormholeTransceiverState is IWormholeTransceiverState, Transce
3636
// ==================== Constants ================================================
3737

3838
/// @dev Prefix for all TransceiverMessage payloads
39-
/// This is 0x99'E''W''H'
4039
/// @notice Magic string (constant value set by messaging provider) that idenfies the payload as an transceiver-emitted payload.
4140
/// Note that this is not a security critical field. It's meant to be used by messaging providers to identify which messages are Transceiver-related.
4241
bytes4 constant WH_TRANSCEIVER_PAYLOAD_PREFIX = 0x9945FF10;

evm/src/libraries/TransceiverStructs.sol

+29
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ library TransceiverStructs {
102102
/// - sourceToken - 32 bytes
103103
/// - to - 32 bytes
104104
/// - toChain - 2 bytes
105+
/// - additionalPayloadLength - 2 bytes, optional
106+
/// - additionalPayload - `additionalPayloadLength` bytes
105107
struct NativeTokenTransfer {
106108
/// @notice Amount being transferred (big-endian u64 and u8 for decimals)
107109
TrimmedAmount amount;
@@ -111,6 +113,9 @@ library TransceiverStructs {
111113
bytes32 to;
112114
/// @notice Chain ID of the recipient
113115
uint16 toChain;
116+
/// @notice Custom payload
117+
/// @dev Recommended that the first 4 bytes are a unique prefix
118+
bytes additionalPayload;
114119
}
115120

116121
function encodeNativeTokenTransfer(
@@ -119,6 +124,22 @@ library TransceiverStructs {
119124
// The `amount` and `decimals` fields are encoded in reverse order compared to how they are declared in the
120125
// `TrimmedAmount` type. This is consistent with the Rust NTT implementation.
121126
TrimmedAmount transferAmount = m.amount;
127+
if (m.additionalPayload.length > 0) {
128+
if (m.additionalPayload.length > type(uint16).max) {
129+
revert PayloadTooLong(m.additionalPayload.length);
130+
}
131+
uint16 additionalPayloadLength = uint16(m.additionalPayload.length);
132+
return abi.encodePacked(
133+
NTT_PREFIX,
134+
transferAmount.getDecimals(),
135+
transferAmount.getAmount(),
136+
m.sourceToken,
137+
m.to,
138+
m.toChain,
139+
additionalPayloadLength,
140+
m.additionalPayload
141+
);
142+
}
122143
return abi.encodePacked(
123144
NTT_PREFIX,
124145
transferAmount.getDecimals(),
@@ -153,6 +174,14 @@ library TransceiverStructs {
153174
(nativeTokenTransfer.sourceToken, offset) = encoded.asBytes32Unchecked(offset);
154175
(nativeTokenTransfer.to, offset) = encoded.asBytes32Unchecked(offset);
155176
(nativeTokenTransfer.toChain, offset) = encoded.asUint16Unchecked(offset);
177+
// The additional payload may be omitted, but if it is included, it is prefixed by a u16 for its length.
178+
// If there are at least 2 bytes remaining, attempt to parse the additional payload.
179+
if (encoded.length >= offset + 2) {
180+
uint256 payloadLength;
181+
(payloadLength, offset) = encoded.asUint16Unchecked(offset);
182+
(nativeTokenTransfer.additionalPayload, offset) =
183+
encoded.sliceUnchecked(offset, payloadLength);
184+
}
156185
encoded.checkLength(offset);
157186
}
158187

0 commit comments

Comments
 (0)