FeeQuoter support for token transfers - #872
vicentevieytes wants to merge 6 commits into
Conversation
… vv/fee-quoter-token-transfers
duck-types
left a comment
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Nice catch. nit: remove the LLM reasoning comment
| var tokenCount = 0; | ||
|
|
||
| var tokenAmountsIter = msg.tokenAmounts.iter(); | ||
| if (tokenAmountsIter.empty()) { |
There was a problem hiding this comment.
Invert this if-else clauses for code to be more similar to EVM's
| if (overrideEntry.isFound) { | ||
| val config = overrideEntry.loadValue(); | ||
| if (config.isEnabled) { |
There was a problem hiding this comment.
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
| if (overrideEntry.isFound) { | |
| val config = overrideEntry.loadValue(); | |
| if (config.isEnabled) { | |
| if (config == null) { | |
| // return defaults | |
| } | |
| // rest of the calculations |
| if (premiumFeeUsdWei < minFeeUsdWei) { | ||
| premiumFeeUsdWei = minFeeUsdWei; | ||
| } else if (premiumFeeUsdWei > maxFeeUsdWei) { | ||
| premiumFeeUsdWei = maxFeeUsdWei; | ||
| } |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
We should probably use mustAdd() here, right?
| @@ -368,7 +427,6 @@ | |||
| fun validateMessageAndResolveGasLimitForDestination(extraArgs: Cell<ExtraArgs>, config: FeeQuoterDestChainConfig, message: Router_CCIPSend, msgDataLen: uint256): int { | |||
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
UnsupportedNumberOfTokenson TON butInvalidSVMReceiverAddress-class errors on EVM — fine for users, but it silently skips themaxDataBytesexpansion (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) { |
There was a problem hiding this comment.
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 withisEnabled == false(v1.6.3:~1310); disabling is done by removing;InvalidDestBytesOverhead—destBytesOverhead < CCIP_LOCK_OR_BURN_V1_RET_BYTES (32)rejected;- dest chain with
config.selector == 0rejected (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.
| tokenCount: uint256, | ||
| tokenTransferBytesOverhead: uint256, | ||
| ): uint256 { | ||
| assert (tokenCount == 0) throw FeeQuoter_Error.TokenTransfersNotSupported; |
There was a problem hiding this comment.
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
No description provided.