TokenTransfer onramp flow skeleton#754
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces the initial “token transfer onramp” flow for CCIP-on-TON: token transfers are initiated via Jetton transfer_notification, routed through Router -> OnRamp -> CCIPSendExecutor, and resolved via a new TokenRegistry that returns the token’s configured pool for lock/burn.
Changes:
- Add token-transfer message plumbing (
Common_JettonTransferNotification,Router_LockOrBurn,OnRamp_ExecutorRequestsLockOrBurn) and update CCIPSendExecutor state machine to include token-registry lookup + lock/burn confirmation. - Add
TokenRegistrycontract + wrapper, plus aMockTokenPooltest contract/wrapper to simulate lock/burn. - Update FeeQuoter to accept token transfers (currently priced like token-less messages) and add/adjust tests including a new end-to-end token-transfer test.
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| contracts/wrappers/gen/ccip/Router.ts | Regenerated wrapper: adds structs/messages for jetton transfer notification and lock/burn flow + new error IDs. |
| contracts/wrappers/gen/ccip/OnRamp.ts | Regenerated wrapper: adds Router_LockOrBurn, OnRamp_ExecutorRequestsLockOrBurn, storage field tokenRegistry, and tokenAmounts snake typing. |
| contracts/wrappers/gen/ccip/FeeQuoter.ts | Regenerated wrapper: updated CodeCell; error mapping adjusted after token-transfer acceptance. |
| contracts/wrappers/gen/ccip/CCIPSendExecutor.ts | Regenerated wrapper: adds TokenRegistry and MockTokenPool message types, new executor states, and new errors. |
| contracts/wrappers/ccip/TokenRegistry.ts | New TS wrapper for TokenRegistry deployment/config. |
| contracts/wrappers/ccip/Router.ts | Adds lockOrBurn opcode constant for Router wrapper. |
| contracts/wrappers/ccip/OnRamp.ts | Adds optional tokenRegistry to storage encoding/decoding and new opcode constant. |
| contracts/wrappers/ccip/MockTokenPool.ts | New TS wrapper for the mock token pool used in tests. |
| contracts/wrappers/ccip/CCIPSendExecutor.ts | Updates Addresses/Config to carry tokenRegistry and adds inbound opcodes for new messages. |
| contracts/wrappers/ccip.TokenRegistry.compile.ts | New blueprint compile config for TokenRegistry. |
| contracts/wrappers/ccip.test.mockTokenPool.compile.ts | New blueprint compile config for mock token pool. |
| contracts/tests/Logs.ts | Decodes tokenAmounts snake cell into arrays for log matching. |
| contracts/tests/ccip/router/Router.Setup.ts | Allows injecting TokenRegistry address into OnRamp deployment during test setup. |
| contracts/tests/ccip/feequoter/FeeQuoter.getValidatedFee.spec.ts | Updates test expectations: token transfers now accepted and priced like token-less messages. |
| contracts/tests/ccip/e2e/CCIPSendWithTokenTransfer.spec.ts | New E2E test for jetton-initiated CCIP send with token transfer path. |
| contracts/contracts/ccip/token_registry/types.tolk | New TokenRegistry types. |
| contracts/contracts/ccip/token_registry/storage.tolk | New TokenRegistry storage definition/load/store. |
| contracts/contracts/ccip/token_registry/messages.tolk | New TokenRegistry request/response message schema. |
| contracts/contracts/ccip/token_registry/contract.tolk | New TokenRegistry contract implementation. |
| contracts/contracts/ccip/test/tokenPool/messages.tolk | New mock token pool message schema. |
| contracts/contracts/ccip/test/tokenPool/contract.tolk | New mock token pool contract implementation. |
| contracts/contracts/ccip/router/messages.tolk | Router now accepts Router_LockOrBurn and Common_JettonTransferNotification. |
| contracts/contracts/ccip/router/errors.tolk | Adds TokenTransferNotThroughNotification router error. |
| contracts/contracts/ccip/router/contract.tolk | Implements token-transfer notification path and lock/burn forwarding. |
| contracts/contracts/ccip/onramp/types.tolk | Changes TVM ramp message body tokenAmounts to SnakedCell<TokenAmount>. |
| contracts/contracts/ccip/onramp/storage.tolk | Adds tokenRegistry: address? to OnRamp storage. |
| contracts/contracts/ccip/onramp/messages.tolk | Adds OnRamp_ExecutorRequestsLockOrBurn message. |
| contracts/contracts/ccip/onramp/contract.tolk | Computes tokenRegistry for token sends and forwards executor lock/burn requests to Router. |
| contracts/contracts/ccip/fee_quoter/contract.tolk | Removes “no tokens supported” asserts; token transfers now allowed (extra fee ignored for now). |
| contracts/contracts/ccip/ccipsend_executor/types.tolk | Adds tokenRegistry address, new executor states for token flow. |
| contracts/contracts/ccip/ccipsend_executor/messages.tolk | Adds inbound messages for TokenRegistry + MockTokenPool callbacks. |
| contracts/contracts/ccip/ccipsend_executor/errors.tolk | Adds TokenNotEnabled executor error. |
| contracts/contracts/ccip/ccipsend_executor/contract.tolk | Implements token registry query + lock/burn request/confirmation path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| // Sender must be a Jetton Wallet owned by the Router | ||
| Common_JettonTransferNotification => { | ||
| //TODO: Validate sender is owned jetton transfer |
There was a problem hiding this comment.
This would be done by the executor/registry, right?
There was a problem hiding this comment.
Yeah, I don't see a way to do it directly in the Router
patricios-space
left a comment
There was a problem hiding this comment.
I like this structure! I've left some general comments. I've only reviewed the contracts for now.
About the validation of the wallet, we should be forwarding that address from the Router through the OnRamp and to the SendExecutor so it can do the validation. We should name this fields untrustedWalletAddress or something similar, to remember that they must be verified
| } | ||
|
|
||
| // Token transfer messages can only come in through jetton transfer notifications | ||
| assert(msg.tokenAmounts.empty()) throw Router_Error.TokenTransferNotThroughNotification; //TBD shuold this be sendMessageRejected instead of throw? |
There was a problem hiding this comment.
Yes, we should be returning sendMessageRejected. There are other asserts that could be changed for returning rejected message
Edit: We should be returning sendMessageRejected through a refund
There was a problem hiding this comment.
Yes, I think this is a task on it's own. I'll create a ticket
There was a problem hiding this comment.
NONEVM-5291
| // Fees are paid in native TON, not in the transferred token. metadata.value must be the | ||
| // native TON attached to the transfer notification (used downstream to cover fee + | ||
| // execution costs), mirroring the plain-message path which uses in.valueCoins. | ||
| val onRampSend = createMessage({ |
There was a problem hiding this comment.
Have you consider merging this handler this in onCCCIPSend function?
This lacks checks for min-value in entrypoint and cursed lanes, for example. I might be missing something else. By reducing duplicated code, we can be sure that all checks that must be done for both entry points are verified.
There was a problem hiding this comment.
done, take a look please, i left the min value validation outside the function because that's how it's done for all hanlders
| addresses: CCIPSendExecutor_Addresses { | ||
| onramp: st.onramp, | ||
| feeQuoter: config.feeQuoter, | ||
| tokenRegistry: config.tokenRegistry |
There was a problem hiding this comment.
SendExecutor doesn't load this config lazily, so I think this is going to be a breaking change. I think this should be good any way because old SendExecutors are not going to receive new messages from the OnRamp. It's only the new OnRamp that will receive messages from the old SendExecutors
There was a problem hiding this comment.
I guess the update order would be:
- update sendExecutorCode on the onRamp
- upgrade the onramp
Issue there is if the sendExecutor code is updated, and before the onRamp code is updated the onRamp tries to deploy a sendExecutor with the outdated config (that does not have the tokenRegistry)
There was a problem hiding this comment.
Maybe we can separate the initialization in two steps...
The new SendExecutor receives CCIPSendExecutor_Execute first, and we don't change that message type.
But, on the new handler if a TokenAmount is present then the executor blocks and waits for another message CCIPSendExecutor_InitTokenTransfer which passes the token registry address.
The Onramp sends only Execute when there are no token amounts , and Execute followed by InitTokenTransfer when there are token amounts
There was a problem hiding this comment.
The other option is making the executor able to handle both the old and the new config, we keep CCIPSendExecutor_Execute and add CCIPSendExecutor_ExecuteV2. The new SendExecutor is able to handle both but does not do any tokenTransfer related execution with the first one
There was a problem hiding this comment.
I think I lean towards the second option
|
|
||
| match (msg) { | ||
| TokenRegistry_GetTokenInfo => { | ||
| onGetTokenInfo(msg, in.senderAddress); |
There was a problem hiding this comment.
Missing assertion on incomming message value
There was a problem hiding this comment.
I'll add a TODO, this implementation of the TokenRegistry is incomplete and its just to get us an e2e test running. We don't know how much value is required here. In fact we should re-benchmark all of the contracts with the recent changes to the TON fees.
|
|
||
| fun CCIPSendExecutor<CCIPSendExecutor_State_TokenRegistryAccess>.onTokenInfoReceived(mutate self, msg: TokenRegistry_ReturnTokenInfo) { | ||
| //TODO validate sender wallet address by querying the jettonMinter | ||
| assert(msg.tokenInfo.enabled) throw CCIPSendExecutor_Error.TokenNotEnabled; |
There was a problem hiding this comment.
We can use the type system to represent this. Maybe the registry should return a different message if the token is not enabled OR it returns an optional TokenInfo. In this way, we don't even have the tokenPool address here, preventing us from trying to lock anyway
There was a problem hiding this comment.
I think I prefer having just one request and response instead of two different response types, but I can do the two messages if you think it's important to leave that tokenPool field out when it won't be used.
Maybe we should also handle bounced messages from Executor->Registry which would indicate that the token has not been enabled either.
I think handling this could be part of the scope of the task to handle the failure path on the CCIPSend TokenTransfer flow. I'll add a TODO
No description provided.