Skip to content

TokenTransfer onramp flow skeleton#754

Merged
vicentevieytes merged 32 commits into
mainfrom
vv/tt/onramp-flow-skeleton
Jun 18, 2026
Merged

TokenTransfer onramp flow skeleton#754
vicentevieytes merged 32 commits into
mainfrom
vv/tt/onramp-flow-skeleton

Conversation

@vicentevieytes

Copy link
Copy Markdown
Collaborator

No description provided.

@vicentevieytes vicentevieytes changed the title add tokenRegistry to storages TokenTransfer onramp flow skeleton Jun 8, 2026
@vicentevieytes vicentevieytes marked this pull request as ready for review June 9, 2026 02:29
@vicentevieytes vicentevieytes requested a review from a team as a code owner June 9, 2026 02:29
Copilot AI review requested due to automatic review settings June 9, 2026 02:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TokenRegistry contract + wrapper, plus a MockTokenPool test 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.

Comment thread contracts/contracts/ccip/router/contract.tolk Outdated
Comment thread contracts/contracts/ccip/router/contract.tolk
Comment thread contracts/contracts/ccip/router/contract.tolk Outdated
Comment thread contracts/contracts/ccip/onramp/contract.tolk
Comment thread contracts/wrappers/ccip/CCIPSendExecutor.ts
Comment thread contracts/contracts/ccip/router/contract.tolk Outdated

@patricios-space patricios-space left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread contracts/contracts/ccip/router/contract.tolk Outdated
Comment thread contracts/contracts/ccip/router/contract.tolk Outdated
Comment thread contracts/contracts/ccip/router/contract.tolk Outdated
Comment thread contracts/contracts/ccip/router/contract.tolk Outdated
Comment thread contracts/contracts/ccip/ccipsend_executor/contract.tolk
Comment thread contracts/contracts/ccip/token_registry/contract.tolk

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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@vicentevieytes vicentevieytes Jun 12, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I don't think you need to different opcodes. The msg struct could be:

struct (0xddccddb5) TokenRegistry_ReturnTokenInfo {
    minterAddress: address
    tokenPool: address?
}

In this way, msg.tokenPool == null means that it is disabled

Comment thread contracts/contracts/ccip/ccipsend_executor/contract.tolk Outdated
Comment thread contracts/contracts/ccip/ccipsend_executor/contract.tolk Outdated
Comment thread contracts/contracts/ccip/onramp/contract.tolk Outdated
Comment thread contracts/contracts/ccip/ccipsend_executor/contract.tolk
Comment thread contracts/contracts/ccip/ccipsend_executor/contract.tolk Outdated
bounce: true,
value: 0,
dest: tokenRegistry,
body: TokenRegistry_GetTokenInfo {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we do this earlier in onExecute? We can send to FQ and TR at the same time.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, maybe we could do that, it makes the state machine more complex though. You would have to wait for two separate messages, you don't know which response will arrive first. You'd have to block on which ever comes first and then wait for the second one to do the same handling.

Besides, it's the same amount of messages, two outgoing one incoming, yeah it's sequential instead of parallel, but I don't think parallelizing this would be a meaningful optimization.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @vicentevieytes here. I don't think the extra complexity is worth the small gains. With 0.6 seconds per TX, we would be shaving 1.2 seconds from total latency.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that 1.2s is a lot :)

I also think that keeping state and counting to 2 in a sharded contract should be straightforward and the default thing to do here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a TODO to keep it as an optimization to consider

nackMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
}

//TODO Should be a jetton transfer to the TokenPool wallet, and the LockOrBurn message should go in the forwardPayload of that jetton transfer

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think I would move this to the withdraw patter - instead of sending a Jetton, we'd send TokenPool_LockOrBurn to the TP and allow it to withdraw.

The thing is at this point we still didn't go through all the preflight checks - (1) gas check for TP processing - unique per TP/Jetton, (2) rate limiter check, (3) advanced hooks check

We don't want to move funds then return if any of those fail, but ideally we do all the checks and withdraw funds sequentially.

@krebernisak krebernisak Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the withdraw path should go through the executor - so the TP needs to withdraw -> executor, which can verify the token amounts and will withdraw -> on-ramp, which can verify the executor. Finally the on-ramp withdraws from the Router - the TP accepts only transfers from the Router JettonWallet.

TokenPool_LockOrBurn msg needs to have an address of an executor to use for withdrawals.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, the token pool first does all the pre-flight checks then starts the withdraw flow which ends up with a transfer from the Router to the TP.

For now I'll just update the TODO and we can integrate this flow when we replace the mockTokenPool with the real one

import "types"

struct TokenRegistry_Storage {
info: TokenRegistry_TokenInfo

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we need to follow the EVM implementation closely.

Deployment of the TokenRegistry by the Router through
Administration of the token through the Router

Not sure I understand these requirements to deploy and admin TR through the Router, why is that needed?

Comment thread contracts/contracts/ccip/router/contract.tolk
Comment thread contracts/wrappers/ccip/MockTokenPool.ts Outdated
Comment thread contracts/contracts/ccip/onramp/contract.tolk
Comment thread contracts/contracts/ccip/ccipsend_executor/types.tolk
Comment thread contracts/wrappers/ccip/CCIPSendExecutor.ts
Comment thread contracts/contracts/ccip/fee_quoter/contract.tolk Outdated
Comment thread contracts/tests/ccip/feequoter/FeeQuoter.getValidatedFee.spec.ts
Comment thread contracts/tests/ccip/e2e/CCIPSendWithTokenTransfer.spec.ts
@patricios-space

Copy link
Copy Markdown
Collaborator

I think we can use this as a base. Can you address some of the latest comments before merging this?

@vicentevieytes vicentevieytes enabled auto-merge (squash) June 18, 2026 16:17
// Option 1: split the initialization in two messages to maintain backwards compatibility.
// 1. CCIPSendExecutor_Execute stays the same as in 1.6.1 (that means not changing Config)
// 2. CCIPSendExecutor_InitTokenTransfer passes the token registry address and other necessary configs
// That way we can upgrade the SendExecutor code in the OnRamp and then upgrade the OnRamp itself, if an executor is deployed by the OnRamp between upgrades the new SendExecutor will still be able to handle the initialization message.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to do this atomically if we use Acton to build the payload. It allows us to hardcode a constant in the code that can be used in the migrate function to update the value

val minValue = Router_Costs.CCIPSend();
assert (in.valueCoins >= minValue) throw Router_Error.InsufficientFee;
// 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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. We could wrap these in a try-catch and send the error back instad of calling sendMessageRejected on every line

@patricios-space patricios-space left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@vicentevieytes vicentevieytes merged commit 31f7db6 into main Jun 18, 2026
35 checks passed
@vicentevieytes vicentevieytes deleted the vv/tt/onramp-flow-skeleton branch June 18, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants