Skip to content

FeeQuoter support for token transfers - #872

Open
vicentevieytes wants to merge 6 commits into
mainfrom
vv/fee-quoter-token-transfers
Open

vicentevieytes wants to merge 6 commits into
mainfrom
vv/fee-quoter-token-transfers

Conversation

@vicentevieytes

Copy link
Copy Markdown
Collaborator

No description provided.

@vicentevieytes
vicentevieytes marked this pull request as ready for review September 8, 2026 13:21
@vicentevieytes
vicentevieytes requested a review from a team as a code owner September 8, 2026 13:21

@duck-types duck-types 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.

The fee calculation logic looks correct compared to EVM's, but I've left some small suggestions

// SnakedCell<T> (= cell) is passed directly as this content cell - no extra ref indirection, since
// get-method args aren't loaded via a struct's loadRef()-based field deserialization.
function buildSnakedCellOf<T>(v: SnakedCell<T>, storeFn_T: StoreCallback<T>): c.Cell {
if (v.length === 0) {

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.

Nice catch. nit: remove the LLM reasoning comment

var tokenCount = 0;

var tokenAmountsIter = msg.tokenAmounts.iter();
if (tokenAmountsIter.empty()) {

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.

Invert this if-else clauses for code to be more similar to EVM's

Comment on lines +369 to +371
if (overrideEntry.isFound) {
val config = overrideEntry.loadValue();
if (config.isEnabled) {

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.

There are too many anidated ifs and it gets difficult to follow the logic. We could have a helper function to fetch the config and check if it is enabled, and return TokenTransferFeeConfig?, then we do

Suggested change
if (overrideEntry.isFound) {
val config = overrideEntry.loadValue();
if (config.isEnabled) {
if (config == null) {
// return defaults
}
// rest of the calculations

Comment on lines +386 to +390
if (premiumFeeUsdWei < minFeeUsdWei) {
premiumFeeUsdWei = minFeeUsdWei;
} else if (premiumFeeUsdWei > maxFeeUsdWei) {
premiumFeeUsdWei = maxFeeUsdWei;
}

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 know this is what EVM does, but it could use a min and max functions, right? We have thoes in the std lib in Tolk

// + tokenCount as uint256 * TON_2_EVM_MESSAGE_FIXED_BYTES_PER_TOKEN
// + tokenTransferBytesOverhead
+ msgDataLen
+ mustProd(tokenCount, TON_2_EVM_MESSAGE_FIXED_BYTES_PER_TOKEN, FeeQuoter_Error.DataAvailabilityCostOverflow)

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 probably use mustAdd() here, right?

@@ -368,7 +427,6 @@
fun validateMessageAndResolveGasLimitForDestination(extraArgs: Cell<ExtraArgs>, config: FeeQuoterDestChainConfig, message: Router_CCIPSend, msgDataLen: uint256): int {

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.

F2 (correctness gap): SVM/SUI maxDataBytes check ignores tokens

contract.tolk:427–497 (validateMessageAndResolveGasLimitForDestination): EVM v1.6.3 expands the payload before the maxDataBytes assert — numberOfTokens × SVM_TOKEN_TRANSFER_DATA_OVERHEAD (v1.6.3:1145–1155) and × SUI_TOKEN_TRANSFER_DATA_OVERHEAD (:1088) plus a per-token destBytesOverhead loop (:1150–1160). TON's validate function has neither, even though SVM_TOKEN_TRANSFER_DATA_OVERHEAD and SUI_TOKEN_TRANSFER_DATA_OVERHEAD already exist in types.tolk:108,126 unused.

Pre-PR this was theoretical (tokens rejected with TokenTransfersNotSupported). This PR allows tokens, so a SVM/SUI-destined message with N tokens now passes maxDataBytes while actually being N × (36 + destBytesOverhead) bytes larger — the exact overrun the EVM check exists to prevent.

while (!tokenAmountsIter.empty()) {
val tokenAmount = tokenAmountsIter.next();
tokenCount += 1;
assert (tokenCount <= destChainConfig.config.maxNumberOfTokensPerMsg) throw FeeQuoter_Error.UnsupportedNumberOfTokens;

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.

F3 (error-precedence deviation): maxNumberOfTokensPerMsg checked inside the fee loop

contract.tolk:293–294 bounds tokenCount inside the tokenAmounts iteration. EVM checks it once in the validate step (v1.6.3:1032, "too many tokens" fires before extraArgs/address validation and before any fee math). Consequences:

  • A message that is both oversized and, say, has an invalid SVM receiver now reverts with UnsupportedNumberOfTokens on TON but InvalidSVMReceiverAddress-class errors on EVM — fine for users, but it silently skips the maxDataBytes expansion (F2) in cases EVM would still measure.
  • It's O(n) checks where one would do, and it forces the loop to run per-token fee math for tokens 1..max before rejecting token max+1 — EVM rejects before touching any fee state.

}
}

fun updateTokenTransferFeeConfigs(mutate st: Storage, msg: FeeQuoter_UpdateTokenTransferFeeConfigs) {

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.

F4 (missing admin validation vs EVM): updateTokenTransferFeeConfigs accepts invalid configs

contract.tolk:127–154. EVM v1.6.3/2.0 _updateTokenTransferInitialConfigs/ applyUpdateConfig revert on:

  • TokenTransferConfigMustBeEnabled — you can't store a config with isEnabled == false (v1.6.3:~1310); disabling is done by removing;
  • InvalidDestBytesOverheaddestBytesOverhead < CCIP_LOCK_OR_BURN_V1_RET_BYTES (32) rejected;
  • dest chain with config.selector == 0 rejected (InvalidDestChainConfig).

TON accepts isEnabled=false entries with arbitrary values (they're then silently treated as "no config" at billing time — a stored-but-inert state EVM forbids), stores destBytesOverhead < 32 (which under-counts DA/calldata bytes forever), and noisily no-ops for unknown destChainSelector (replaceIfExists at :151 swallows the whole update).

Owner-only, so severity is low, but these are exactly the foot-guns EVM added reverts for, and "2.0 parity" is a stated goal. Ask: add the three asserts (FeeQuoter_Error extensions or reuse InvalidDestChainConfig-style codes), and change the unknown-selector path to throw instead of silently dropping — an operator who "successfully" updates a nonexistent lane will otherwise pay for gas and get nothing, and updateDestChainConfigs in the same file sets the precedent for how you want this to behave.

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.

done

tokenCount: uint256,
tokenTransferBytesOverhead: uint256,
): uint256 {
assert (tokenCount == 0) throw FeeQuoter_Error.TokenTransfersNotSupported;

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.

F7 (hygiene): stale error comments and a now-dead error code

errors.tolk:24–29: TokenTransfersNotSupported has no throw site anymore (grep: only the enum decl), and PremiumFeeOverflow / DataAvailabilityCostOverflow / FeeCalculationOverflow carry // Unreachable comments your own tests disprove (the 2^119 × 2^223 case exercises PremiumFeeOverflow). Ask: delete the // Unreachable annotations, and remove TokenTransfersNotSupported

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants