From 8c91b470eb009414374e63f34d480d4e2c3a7867 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Fri, 3 Jul 2026 14:41:28 -0300 Subject: [PATCH] initial flow --- .../ccip/ccipsend_executor/contract.tolk | 6 +- .../ccip/ccipsend_executor/messages.tolk | 2 +- .../ccip/lib/token_pool/messages.tolk | 30 ++++ .../contracts/ccip/lib/token_pool/types.tolk | 128 ++++++++++++++++++ .../contracts/ccip/offramp/contract.tolk | 35 +++++ contracts/contracts/ccip/offramp/errors.tolk | 1 + .../contracts/ccip/offramp/messages.tolk | 9 ++ contracts/contracts/ccip/offramp/types.tolk | 15 +- contracts/contracts/ccip/pools/messages.tolk | 1 - .../ccip/receive_executor/contract.tolk | 90 +++++++++++- .../ccip/receive_executor/errors.tolk | 3 + .../ccip/receive_executor/messages.tolk | 7 +- .../ccip/receive_executor/storage.tolk | 5 +- .../ccip/receive_executor/types.tolk | 2 + contracts/contracts/ccip/router/contract.tolk | 25 +--- .../ccip/test/tokenPool/contract.tolk | 36 ++++- .../ccip/test/tokenPool/messages.tolk | 10 +- .../contracts/ccip/token_registry/types.tolk | 16 +++ 18 files changed, 377 insertions(+), 44 deletions(-) create mode 100644 contracts/contracts/ccip/lib/token_pool/messages.tolk create mode 100644 contracts/contracts/ccip/lib/token_pool/types.tolk delete mode 100644 contracts/contracts/ccip/pools/messages.tolk diff --git a/contracts/contracts/ccip/ccipsend_executor/contract.tolk b/contracts/contracts/ccip/ccipsend_executor/contract.tolk index ede792b142..3c2a656f79 100644 --- a/contracts/contracts/ccip/ccipsend_executor/contract.tolk +++ b/contracts/contracts/ccip/ccipsend_executor/contract.tolk @@ -1,3 +1,5 @@ +import "@stdlib/gas-payments" + import "types" import "messages" import "storage" @@ -7,10 +9,8 @@ import "../onramp/messages" import "../../lib/utils" import "../router/messages" import "../fee_quoter/types" -import "@stdlib/gas-payments" -import "../common/types" import "../token_registry/messages" -import "../pools/messages" +import "../lib/token_pool/messages" tolk 1.4.1 diff --git a/contracts/contracts/ccip/ccipsend_executor/messages.tolk b/contracts/contracts/ccip/ccipsend_executor/messages.tolk index 4b8a0cee4c..9f4faf1e0d 100644 --- a/contracts/contracts/ccip/ccipsend_executor/messages.tolk +++ b/contracts/contracts/ccip/ccipsend_executor/messages.tolk @@ -2,7 +2,7 @@ import "types" import "../onramp/messages" import "../fee_quoter/messages" import "../token_registry/messages" -import "../pools/messages" +import "../lib/token_pool/messages" type CCIPSendExecutor_InMessage = | CCIPSendExecutor_Execute diff --git a/contracts/contracts/ccip/lib/token_pool/messages.tolk b/contracts/contracts/ccip/lib/token_pool/messages.tolk new file mode 100644 index 0000000000..dda32870f0 --- /dev/null +++ b/contracts/contracts/ccip/lib/token_pool/messages.tolk @@ -0,0 +1,30 @@ +import "../../common/types" +import "types" + +type TokenPool_InMessage = + | TokenPool_LockOrBurn + | TokenPool_ReleaseOrMint + ; + +/// Incoming messages + +struct (0x7dd8f942) TokenPool_LockOrBurn { + tokenAmount: TokenAmount, // TODO LockOrBurn doesn't need to know the token address + notify: address, +} + +struct (0x351f77e3) TokenPool_ReleaseOrMint { + queryId: uint64; + request: Cell; + requestedFinalityConfig: uint32; + replyTo: address? = null; +} + +/// Outgoing messages + +struct (0x4c700579) TokenPool_NotifySuccessfulLockOrBurn {} + +struct (0xe0e882f5) TokenPool_ReleaseOrMintFinished { + queryId: uint64; + out: Cell; // TBD do we need this info? +} diff --git a/contracts/contracts/ccip/lib/token_pool/types.tolk b/contracts/contracts/ccip/lib/token_pool/types.tolk new file mode 100644 index 0000000000..9fae4d1c2f --- /dev/null +++ b/contracts/contracts/ccip/lib/token_pool/types.tolk @@ -0,0 +1,128 @@ +import "../../rmn_remote/lib" +import "../../common/types" + +// SPDX-License-Identifier: BUSL-1.1 + +const TOKEN_POOL_WAIT_FOR_FINALITY_FLAG: uint32 = 0; +const TOKEN_POOL_DEFAULT_FINALITY: uint32 = TOKEN_POOL_WAIT_FOR_FINALITY_FLAG; +/// The division factor for bps. This also represents the maximum bps fee. +const TOKEN_POOL_BPS_DIVIDER: uint256 = 10000; + +// The number of bytes in the return data for a pool v1 releaseOrMint call. +// This should match the size of the ReleaseOrMintOutV1 struct. +const CCIP_POOL_V1_RET_BYTES = 32; + +// The default max number of bytes in the return data for a pool v1 lockOrBurn call. +// This data can be used to send information to the destination chain token pool. Can be overwritten +// in the TokenTransferFeeConfig.destBytesOverhead if more data is required. +const CCIP_LOCK_OR_BURN_V1_RET_BYTES = 32; + +struct TokenPool_DynamicConfig { + router: address; + rateLimitAdmin: address?; + feeAdmin: address?; +} + +struct TokenPool_MirroredPolicy { + onRamps: map; + offRamps: map; + cursedSubjects: CursedSubjects; +} + +struct TokenPool_RampUpdate { + remoteChainSelector: uint64; + onRamp: address? = null; + offRamp: address? = null; +} + +/// Struct with args for setting the token transfer fee configurations for a destination chain and a set of tokens. +struct TokenPool_TokenTransferFeeConfigArgs { + destChainSelector: uint64; // Destination chain selector. + tokenTransferFeeConfig: TokenPool_TokenTransferFeeConfig; // Token transfer fee configuration. +} + +// TODO: triage new types below + +struct TokenPool_LockOrBurnPrepared { + feeAmount: uint256; + destTokenAmount: uint256; + out: TokenPool_LockOrBurnOutV1; +} + +struct TokenPool_ReleaseOrMintPrepared { + requestedFinalityConfig: uint32; + localAmount: uint256; + out: TokenPool_ReleaseOrMintOutV1; +} + +// IPoolV2 + +struct TokenPool_TokenTransferFeeConfig { + destGasOverhead: uint32; // Gas charged to execute the token transfer on the destination chain. + destBytesOverhead: uint32; // Data availability bytes. + finalityFeeUSDCents: uint256; // Fee to charge for token transfer with default (wait-for-finality) finality, multiples of 0.01 USD. + fastFinalityFeeUSDCents: uint256; // Fee to charge for token transfer with fast finality (FTF), multiples of 0.01 USD. + // The following two fee is deducted from the transferred asset, not added on top. + finalityTransferFeeBps: uint16; // Fee in basis points for default finality transfers [0-10_000]. + fastFinalityTransferFeeBps: uint16; // Fee in basis points for custom finality transfers [0-10_000]. + isEnabled: bool; // Whether this config is enabled. +} + +enum TokenPool_MessageDirection: uint8 { + Outbound = 0 + Inbound = 1 +} + +struct TokenPool_Transfer { + id : uint256; // Unique transfer ID for this TP operation. + details: Cell>; // Details of the transfer. +} + +struct TokenPool_TransferDetails { + receiver: R; // The recipient of the tokens on the destination chain. For EVM source chains, this is abi-encoded (32 bytes). + remoteChainSelector: uint64; // ─╮ The chain ID of the destination chain. + originalSender: S; // ─╯ The original sender of the tx on the source chain. + amount: uint256; // The amount of tokens to process, denominated in the source token's decimals. + localToken: address; // The address on this chain of the token to process. +} + +// Typed transfers for both directions +type TokenPool_LockOrBurnTransfer = TokenPool_Transfer< + address, // sender addr + Cell, // receiver addr +>; +type TokenPool_ReleaseOrMintTransfer = TokenPool_Transfer< + Cell, // sender addr + address // receiver addr +>; + +struct TokenPool_LockOrBurnInV1 { + transfer: TokenPool_LockOrBurnTransfer; // The transfer id/details for the lock or burn operation. +} + +struct TokenPool_LockOrBurnOutV1 { + // The address of the destination token, abi encoded in the case of EVM chains. + // This value is UNTRUSTED as any pool owner can return whatever value they want. + destTokenAddress: Cell; + // Optional pool data to be transferred to the destination chain. Be default this is capped at + // CCIP_LOCK_OR_BURN_V1_RET_BYTES bytes. If more data is required, the TokenTransferFeeConfig.destBytesOverhead + // has to be set for the specific token. + destPoolData: cell; +} + +struct TokenPool_ReleaseOrMintInV1 { + transfer: TokenPool_ReleaseOrMintTransfer; // The transfer id/details for the release or mint operation. + /// @dev WARNING: sourcePoolAddress should be checked prior to any processing of funds. Make sure it matches the + /// expected pool address for the given remoteChainSelector. + sourcePoolAddress: Cell; // The address of the source pool, abi encoded in the case of EVM chains. + sourcePoolData: cell?; // The data received from the source pool to process the release or mint. + /// @dev WARNING: offchainTokenData is untrusted data. + offchainTokenData: cell?; // The offchain data to process the release or mint. +} + +struct TokenPool_ReleaseOrMintOutV1 { + // The number of tokens released or minted on the destination chain, denominated in the local token's decimals. + // This value is expected to be equal to the ReleaseOrMintInV1.amount in the case where the source and destination + // chain have the same number of decimals. + destinationAmount: uint256; +} diff --git a/contracts/contracts/ccip/offramp/contract.tolk b/contracts/contracts/ccip/offramp/contract.tolk index 7100ebac04..f2de01bc95 100644 --- a/contracts/contracts/ccip/offramp/contract.tolk +++ b/contracts/contracts/ccip/offramp/contract.tolk @@ -19,6 +19,8 @@ import "../merkle_root/storage" import "../receive_executor/messages" import "../receive_executor/storage" import "../router/messages" +import "../token_registry/types" +import "../lib/token_pool/messages" import "messages" import "errors" @@ -66,6 +68,12 @@ fun onInternalMessage(in:InMessage) { assertSenderIsReceiverExecutor(st, msg.execId, in.senderAddress); onDispatchValidated(st, msg) } + // Sender must be ReceiveExecutor + OffRamp_ReleaseOrMint => { + val st = lazy Storage.load(); + assertSenderIsReceiverExecutor(st, msg.execId, in.senderAddress); + onReleaseOrMint(in.senderAddress, msg) + } // Permissionless OffRamp_ManuallyExecute => { onManuallyExecute(msg, in.valueCoins) @@ -438,6 +446,21 @@ fun blockMsg(st: Storage, execId: uint192, receiver: address) { receiveBounced.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); } +fun onReleaseOrMint(sendExecutor: address, msg: OffRamp_ReleaseOrMint) { + val releaseOrMintMsg = createMessage({ + bounce: BounceMode.RichBounce, + value: 0, + dest: msg.tokenPool, + body: TokenPool_ReleaseOrMint { + queryId: msg.queryId, + request: msg.request, + requestedFinalityConfig: msg.requestedFinalityConfig, + replyTo: sendExecutor, + } + }); + releaseOrMintMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); +} + // Sets a new OCR3 configuration for the contract. Only the contract owner can call this function. fun onSetOCR3Config(msg: OCR3Base_SetOCR3Config) { var st = lazy Storage.load(); @@ -800,6 +823,8 @@ fun onExecuteValidated(st: Storage, msg: OffRamp_ExecuteValidated, sender: addre val executorAddress: AutoDeployAddress = getReceiverExecutorDeployAddress(st, execId); + val tokenAdminRegistry = getTokenAdminRegistry(st, message.tokenAmounts); + // Check if this is the first time the exec flow is running given that in that case we need to // initialize the executor if (msg.executionState == ExecutionState.Untouched) { @@ -815,6 +840,7 @@ fun onExecuteValidated(st: Storage, msg: OffRamp_ExecuteValidated, sender: addre message: message.toCell(), root: sender, execId, + tokenAdminRegistry, }.toCell(), } }, @@ -838,6 +864,15 @@ fun onExecuteValidated(st: Storage, msg: OffRamp_ExecuteValidated, sender: addre initExecMsg.send(SEND_MODE_PAY_FEES_SEPARATELY); } +fun getTokenAdminRegistry(st: Storage, tokenAmounts: SnakedCell?): Cell
? { + if (tokenAmounts == null) { + return null; + } + val tokenTransfer = tokenAmounts.getTokenTransfer(Error.UnsupportedNumberOfTokens); + val registryAddress = TokenRegistry_DeriveAddress( st.deployables.load().deployer, tokenTransfer.token).calculateAddress(); + return registryAddress.toCell(); +} + /* Retrieves the Router address associated with a given sourceChainSelector. Throws an error if the source chain is not enabled in the stored configs. diff --git a/contracts/contracts/ccip/offramp/errors.tolk b/contracts/contracts/ccip/offramp/errors.tolk index 4746d17d07..e2dd506e93 100644 --- a/contracts/contracts/ccip/offramp/errors.tolk +++ b/contracts/contracts/ccip/offramp/errors.tolk @@ -20,4 +20,5 @@ enum Error { OnRampAddressMismatch EmptyCommitReport MerkleRootCannotBeZero + UnsupportedNumberOfTokens } diff --git a/contracts/contracts/ccip/offramp/messages.tolk b/contracts/contracts/ccip/offramp/messages.tolk index ff1fbfdd7d..8ab2f60ed2 100644 --- a/contracts/contracts/ccip/offramp/messages.tolk +++ b/contracts/contracts/ccip/offramp/messages.tolk @@ -12,6 +12,7 @@ import "../merkle_root/messages" import "../receive_executor/messages" import "../fee_quoter/messages" import "../router/messages" +import "../lib/token_pool/types" type OffRamp_InMessage = | OffRamp_Commit @@ -19,6 +20,7 @@ type OffRamp_InMessage = | OffRamp_ExecuteValidated | OffRamp_ManuallyExecute | OffRamp_DispatchValidated + | OffRamp_ReleaseOrMint | OffRamp_UpdateSourceChainConfigs | OffRamp_CCIPReceiveConfirm | OffRamp_CCIPReceiveBounced @@ -119,6 +121,13 @@ struct (0xa015e0e2) OffRamp_UpdateDeployables { merkleRootCode: cell?; } +struct (0x351f77e3) OffRamp_ReleaseOrMint { + queryId: uint64 + tokenPool: address + requestedFinalityConfig: uint32 + request: Cell +} + struct OffRamp_Costs{} fun OffRamp_Costs.executeValidated(): int { diff --git a/contracts/contracts/ccip/offramp/types.tolk b/contracts/contracts/ccip/offramp/types.tolk index 1911973cc5..caad12097d 100644 --- a/contracts/contracts/ccip/offramp/types.tolk +++ b/contracts/contracts/ccip/offramp/types.tolk @@ -66,6 +66,15 @@ struct Any2TVMRampMessage { // maybe mark these amounts as slice remaining then parse them by hand to avoid requiring this to be a map<> at send time? } +// Helper to extract the single token transfer from the message, and assert that there is only one token transfer +fun SnakedCell.getTokenTransfer(self, error: int): Any2TVMTokenTransfer { + var it = self.iter(); + assert (!it.empty(), error); + val token = it.next(); + assert (it.empty(), error); + return token +} + fun Any2TVMRampMessage.generateMessageId(self, metadataHash: uint256): uint256 { // Loosely based on https://github.com/smartcontractkit/chainlink-ccip/blob/2d2ce6abb3f7626dbd3008e4232da4faa839feff/chains/evm/contracts/libraries/Internal.sol#L113-L134 return beginCell() @@ -99,10 +108,10 @@ struct MerkleRoot { struct Any2TVMTokenTransfer { sourcePoolAddress: Cell; - destPoolAddress: address; - destGasAmount: uint32; + token: address; + destGasAmount: uint32; // TBD: This field was removed in CCIP 2.0 and it's purpose/utility in TON is unclear. extraData: cell; - amount: uint256; + amount: coins; // TBD: Does this change break any message ID we set in the past? It would but we didn't support token transfers then } struct OffRamp_Deployables { diff --git a/contracts/contracts/ccip/pools/messages.tolk b/contracts/contracts/ccip/pools/messages.tolk deleted file mode 100644 index 0a0aefba54..0000000000 --- a/contracts/contracts/ccip/pools/messages.tolk +++ /dev/null @@ -1 +0,0 @@ -struct (0x4c700579) TokenPool_NotifySuccessfulLockOrBurn {} diff --git a/contracts/contracts/ccip/receive_executor/contract.tolk b/contracts/contracts/ccip/receive_executor/contract.tolk index c5d294dd45..bf65d0e92d 100644 --- a/contracts/contracts/ccip/receive_executor/contract.tolk +++ b/contracts/contracts/ccip/receive_executor/contract.tolk @@ -6,14 +6,16 @@ import "types" import "errors" import "../offramp/messages" import "../../lib/utils" +import "../token_registry/messages" +import "../lib/token_pool/types" tolk 1.4.1 -const CONTRACT_VERSION = "1.6.2".literalSlice(); +const CONTRACT_VERSION = "1.7.0".literalSlice(); contract ReceiveExecutor { author: "SmartContract Chainlink Limited SEZC" - version: "1.6.2" + version: "1.7.0" description: "link.chain.ton.ccip.ReceiveExecutor" storage: ReceiveExecutor_Storage @@ -37,6 +39,27 @@ fun onInternalMessage(in: InMessage) { onCCIPReceiveConfirm(mutate st, msg); st.store(); } + // Sender must be the TokenAdminRegistry + TokenRegistry_ReturnTokenInfo => { + var st = ReceiveExecutor_Storage.load(); + assert(st.tokenAdminRegistry != null, Error.Unauthorized); + val tokenAdminRegistry = st.tokenAdminRegistry.load(); + assert(in.senderAddress == tokenAdminRegistry, Error.Unauthorized); + // TBD: are we missing some asserts on st.state in other handlers? + assert(st.state == ReceiveExecutor_MessageState.TokenAdminRegistryQuery, Error.TokenAdminRegistryUnexpectedResponse); + onTokenAdminRegistryResponse(mutate st, msg); + st.store(); + } + // Sender must be the TokenPool + TokenPool_ReleaseOrMintFinished => { + var st = ReceiveExecutor_Storage.load(); + assert(st.tokenPool != null && in.senderAddress == st.tokenPool, Error.Unauthorized); + assert(st.state == ReceiveExecutor_MessageState.TokenTransfer, Error.TokenPoolUnexpectedResponse); + val shouldOverrideGas = st.shouldOverrideGas; + st.shouldOverrideGas = null; + execute(mutate st, shouldOverrideGas); + st.store(); + } // Sender must be the owner (OffRamp) ReceiveExecutor_Bounced => { var st = ReceiveExecutor_Storage.load(); @@ -52,11 +75,72 @@ fun onInternalMessage(in: InMessage) { } fun onInitExecute(mutate st: ReceiveExecutor_Storage, msg: ReceiveExecutor_InitExecute) { - st.lastExecutionTimestamp = blockchain.now(); + if (st.tokenAdminRegistry != null) { + st.shouldOverrideGas = msg.gasOverride; + return queryTokenAdminRegistry(mutate st, st.tokenAdminRegistry!.load()); + } execute(mutate st, msg.gasOverride); } +fun queryTokenAdminRegistry(mutate st: ReceiveExecutor_Storage, tokenAdminRegistry: address) { + st.state = ReceiveExecutor_MessageState.TokenAdminRegistryQuery; + createMessage( + { + bounce: true, + value: 0, + dest: tokenAdminRegistry, + body: TokenRegistry_GetTokenInfo { + } + } + ).send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); +} + +fun onTokenAdminRegistryResponse(mutate st: ReceiveExecutor_Storage, msg: TokenRegistry_ReturnTokenInfo) { + val rampMsg = lazy st.message.load(); + assert (rampMsg.tokenAmounts != null) throw Error.NoTokenAmountsInMessage; + val token = rampMsg.tokenAmounts.getTokenTransfer(Error.UnsupportedNumberOfTokens); + // TODO + // if (msg.tokenInfo.tokenPool == null) { + // throw Error.TokenNotEnabled; // TODO check what error is thrown in EVM + // } + releaseOrMint(mutate st, rampMsg, msg.tokenInfo.tokenPool!, token); +} + +fun releaseOrMint(mutate st: ReceiveExecutor_Storage, rampMsg: Any2TVMRampMessage, tokenPool: address, tokenAmount: Any2TVMTokenTransfer) { + st.tokenPool = tokenPool; + st.state = ReceiveExecutor_MessageState.TokenTransfer; + createMessage( + { + bounce: true, + value: 0, // TBD should we be using tokenAmoun.destGasAmount instead? It's not clear what this field is for in TON, and it was removed in CCIP 2.0 + dest: st.owner, + body: OffRamp_ReleaseOrMint { + queryId: 0, + token: tokenAmount.token, + tokenPool, + requestedFinalityConfig: TOKEN_POOL_WAIT_FOR_FINALITY_FLAG, + request: TokenPool_ReleaseOrMintInV1 { + transfer: TokenPool_Transfer { + id: 0, // TBD what should go here? Seqnum? + details: TokenPool_TransferDetails { + receiver: rampMsg.receiver, + remoteChainSelector: rampMsg.header.sourceChainSelector, + originalSender: rampMsg.sender, + amount: tokenAmount.amount, + localToken: tokenAmount.token, + }.toCell(), + }, + sourcePoolAddress: tokenAmount.sourcePoolAddress, + sourcePoolData: null, // TBD Any2TVMTokenTransfer doesn't have this. + offchainTokenData: null, // TBD Any2TVMTokenTransfer doesn't have this. + }.toCell(), + } + } + ).send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); +} + fun execute(mutate st: ReceiveExecutor_Storage, gasOverride: coins?) { + st.lastExecutionTimestamp = blockchain.now(); st.state = ReceiveExecutor_MessageState.Execute; createMessage( { diff --git a/contracts/contracts/ccip/receive_executor/errors.tolk b/contracts/contracts/ccip/receive_executor/errors.tolk index f704679381..b474c7c76c 100644 --- a/contracts/contracts/ccip/receive_executor/errors.tolk +++ b/contracts/contracts/ccip/receive_executor/errors.tolk @@ -6,4 +6,7 @@ enum Error { UpdatingStateOfNonExecutedMessage NotificationFromInvalidReceiver Unauthorized + UnsupportedNumberOfTokens + NoTokenAmountsInMessage + TokenAdminRegistryUnexpectedResponse } diff --git a/contracts/contracts/ccip/receive_executor/messages.tolk b/contracts/contracts/ccip/receive_executor/messages.tolk index 4d83a4820f..61c705b91d 100644 --- a/contracts/contracts/ccip/receive_executor/messages.tolk +++ b/contracts/contracts/ccip/receive_executor/messages.tolk @@ -1,10 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 import "../offramp/messages" +import "../token_registry/messages" +import "../lib/token_pool/messages" type ReceiveExecutor_InMessage = | ReceiveExecutor_InitExecute | ReceiveExecutor_Bounced - | ReceiveExecutor_Confirm; + | TokenRegistry_ReturnTokenInfo + | TokenPool_ReleaseOrMintFinished + | ReceiveExecutor_Confirm + ; struct (0x64cd2fd2) ReceiveExecutor_InitExecute { gasOverride: coins? = null; diff --git a/contracts/contracts/ccip/receive_executor/storage.tolk b/contracts/contracts/ccip/receive_executor/storage.tolk index a81fdaac7a..fae5ea3a79 100644 --- a/contracts/contracts/ccip/receive_executor/storage.tolk +++ b/contracts/contracts/ccip/receive_executor/storage.tolk @@ -3,12 +3,15 @@ import "../offramp/types" import "types" struct ReceiveExecutor_Storage { - owner: address; // MerkleRoot contract + owner: address; // OffRamp message: Cell; root: address; execId: uint192; state: ReceiveExecutor_MessageState = ReceiveExecutor_MessageState.Untouched; lastExecutionTimestamp: uint64 = 0; + tokenAdminRegistry: Cell
?; + shouldOverrideGas: coins? = null; // TODO move this to state attribute + tokenPool: Cell
?; // TODO move this to state attribute } fun ReceiveExecutor_Storage.load(): ReceiveExecutor_Storage { diff --git a/contracts/contracts/ccip/receive_executor/types.tolk b/contracts/contracts/ccip/receive_executor/types.tolk index f54a6135ce..4a5d62c4bb 100644 --- a/contracts/contracts/ccip/receive_executor/types.tolk +++ b/contracts/contracts/ccip/receive_executor/types.tolk @@ -7,6 +7,8 @@ const ReceiveExecutor_FACILITY_ID = ReceiveExecutor_FACILITY_NAME.crc32() % 640 // State stores the general state machine indicating which phase we're in enum ReceiveExecutor_MessageState { Untouched = 0 + TokenAdminRegistryQuery = 1 + TokenTransfer Execute ExecuteFailed Success diff --git a/contracts/contracts/ccip/router/contract.tolk b/contracts/contracts/ccip/router/contract.tolk index 0f84849b53..e40524045c 100644 --- a/contracts/contracts/ccip/router/contract.tolk +++ b/contracts/contracts/ccip/router/contract.tolk @@ -7,6 +7,7 @@ import "../../lib/utils" import "../../lib/versioning/upgradeable" import "../../lib/receiver/types" import "../../lib/receiver/messages" +import "../lib/token_pool/messages" import "../rmn_remote/lib.tolk" import "../onramp/messages" @@ -14,7 +15,6 @@ import "../onramp/types" import "../offramp/messages" import "../offramp/types" import "../common/messages" -import "../test/tokenPool/messages" import "types" import "messages" @@ -25,6 +25,7 @@ import "../../lib/deployable/namespace" import "../token_registry/storage" import "../token_registry/messages" import "../../lib/deployable/types" +import "../token_registry/types" tolk 1.4.1 @@ -199,7 +200,7 @@ fun onInternalMessage(in: InMessage) { } Router_TokenRegistrySetTokenInfo => { val st = Storage.load(); - val registryAddress = getTokenRegistryDeployAddress(st, msg.tokenAddress); + val registryAddress = TokenRegistry_DeriveAddress(st.tokenRegistryDeployment.load().tokenRegistryCode, msg.tokenAddress); if (msg.isNewEntry) { val deployMsg = createMessage({ bounce: false, @@ -560,7 +561,7 @@ fun onCCIPSend(msg: Router_CCIPSend, sender: address, value: coins) { if (!msg.tokenAmounts.empty()) { val tokenAmount = msg.tokenAmounts.iter().next(); // Only one tokenAmount per msg is supported //TODO should we assert that there is only one token in the tokenAmounts? Or do we just ignore after the first one? - tokenRegistry = getTokenRegistryDeployAddress(st, tokenAmount.token).calculateAddress(); + tokenRegistry = TokenRegistry_DeriveAddress(st.tokenRegistryDeployment.load().tokenRegistryCode, tokenAmount.token).calculateAddress(); } val onRampResult = st.onRamps.get(msg.destChainSelector); @@ -714,7 +715,7 @@ fun onLockOrBurn(msg: Router_LockOrBurn) { bounce: true, value: 0, dest: msg.tokenPool, - body: MockTokenPool_LockOrBurn { + body: TokenPool_LockOrBurn { tokenAmount: msg.tokenAmount, notify: msg.executorAddress, } @@ -738,22 +739,6 @@ fun assertSenderIsOnRamp(st: Storage, destChainSelector: uint64, sender: address assert (sender == onRampAddress) throw Router_Error.NotOnRamp; } -/* -Derives the token registry contract address from the token address -*/ -fun getTokenRegistryDeployAddress(st: Storage, tokenAddress: address): AutoDeployAddress { - return { - stateInit: { - code: st.tokenRegistryDeployment.load().deployableCode, - data: NamespacedDeployableData({ - namespace: DeployableNamespace_TokenRegistry, - owner: contract.getAddress(), - id: tokenAddress, - }), - } - } -} - @method_id(1000) fun migrate(storage: cell, version: slice): cell { requireSupportedVersion(version); diff --git a/contracts/contracts/ccip/test/tokenPool/contract.tolk b/contracts/contracts/ccip/test/tokenPool/contract.tolk index 5bbf79341e..0107175ed9 100644 --- a/contracts/contracts/ccip/test/tokenPool/contract.tolk +++ b/contracts/contracts/ccip/test/tokenPool/contract.tolk @@ -1,17 +1,32 @@ import "messages" -import "../../pools/messages" +import "../../lib/token_pool/messages" +import "../../lib/token_pool/types" contract MockTokenPool { author: "SmartContract Chainlink Limited SEZC" version: "1.6.1" description: "link.chain.ton.ccip.test.TokenPool" - incomingMessages: MockTokenPool_LockOrBurn + incomingMessages: MockTokenPool_InMessage } const CONTRACT_VERSION = "0.0.1".literalSlice(); fun onInternalMessage(in: InMessage) { - val msg = lazy MockTokenPool_LockOrBurn.fromSlice(in.body); + val msg = lazy MockTokenPool_InMessage.fromSlice(in.body); + match (msg) { + TokenPool_LockOrBurn => { + onLockOrBurn(msg); + } + TokenPool_ReleaseOrMint => { + onReleaseOrMint(msg); + } + else => { + assert (in.body.isEmpty()) throw 0xFFFF + } + } +} + +fun onLockOrBurn(msg: TokenPool_LockOrBurn) { val notification = createMessage({ bounce: false, value: 0, @@ -20,3 +35,18 @@ fun onInternalMessage(in: InMessage) { }); notification.send(SEND_MODE_CARRY_ALL_BALANCE); } + +fun onReleaseOrMint(msg: TokenPool_ReleaseOrMint) { + val notification = createMessage({ + bounce: false, + value: 0, + dest: msg.replyTo!, + body: TokenPool_ReleaseOrMintFinished { + queryId: msg.queryId, + out: TokenPool_ReleaseOrMintOutV1 { + destinationAmount: msg.request.load().transfer.details.load().amount, + }.toCell(), + }, + }); + notification.send(SEND_MODE_CARRY_ALL_BALANCE); +} diff --git a/contracts/contracts/ccip/test/tokenPool/messages.tolk b/contracts/contracts/ccip/test/tokenPool/messages.tolk index 0495fee95e..677a854d09 100644 --- a/contracts/contracts/ccip/test/tokenPool/messages.tolk +++ b/contracts/contracts/ccip/test/tokenPool/messages.tolk @@ -1,9 +1,3 @@ -import "../../common/types" - -struct (0x7dd8f942) MockTokenPool_LockOrBurn { - tokenAmount: TokenAmount, - notify: address, -} - - +import "../../lib/token_pool/messages" +type MockTokenPool_InMessage = TokenPool_InMessage; diff --git a/contracts/contracts/ccip/token_registry/types.tolk b/contracts/contracts/ccip/token_registry/types.tolk index 8ebddf3ca8..40cbd75cb1 100644 --- a/contracts/contracts/ccip/token_registry/types.tolk +++ b/contracts/contracts/ccip/token_registry/types.tolk @@ -1,5 +1,21 @@ +import "../../lib/deployable/namespace" + struct TokenRegistry_TokenInfo { tokenPool: address minterAddress: address enabled: bool } + +/// Derives the token registry contract address from the token address +fun TokenRegistry_DeriveAddress(code: cell, tokenAddress: address): AutoDeployAddress { + return { + stateInit: { + code, + data: NamespacedDeployableData({ + namespace: DeployableNamespace_TokenRegistry, + owner: contract.getAddress(), + id: tokenAddress, + }), + } + } +}