Skip to content

Commit 949a001

Browse files
[NONEVM-2960] [Router] get fee before calling CCIPSend (offchain) (#333)
* ref: cross-chain address encoding and expose feequoter addr getter on OnRamp * feat: off chain getValidatedFee * chore: rm unsued function * ref: use stack getter * ref: stack codec * ref: move offchain get fee helper
1 parent ecb6913 commit 949a001

File tree

8 files changed

+121
-68
lines changed

8 files changed

+121
-68
lines changed

contracts/contracts/ccip/fee_quoter/contract.tolk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,11 @@ fun DestChainConfig.getValidatedGasPrice(self): (uint112, uint112) {
220220
return (price.executionGasPrice, price.dataAvailabilityGasPrice)
221221
}
222222

223+
// Note: Equivalent to validatedFee for cell encoding instead of using stack
223224
get fun validatedFeeCell(msg: Cell<Router_CCIPSend>): coins {
224225
return validatedFee(msg.load());
225226
}
226227

227-
228228
get fun validatedFee(msg: Router_CCIPSend): coins {
229229
var st = lazy Storage.load();
230230

contracts/contracts/ccip/onramp/contract.tolk

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,13 @@ get fun destChainConfig(destChainSelector: uint64): OnRamp_DestChainConfig {
274274
return st.destChainConfigs.mustGet(destChainSelector,Error.UnknownDestChainSelector as int);
275275
}
276276

277+
// We are taking destChainSelector as param to enable future per-destination feeQuoters
278+
get fun feeQuoter(destChainSelector: int): address {
279+
val st = lazy OnRamp_Storage.load();
280+
val config = st.config.load();
281+
return config.feeQuoter;
282+
}
283+
277284
// vec<address>
278285
get fun allowedSendersList(destChainSelector: uint64): tuple? {
279286
val st = lazy OnRamp_Storage.load();

contracts/src/ccipSend/fee.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Address, ContractProvider } from '@ton/core'
2+
import * as rt from '../../wrappers/ccip/Router'
3+
import * as onr from '../../wrappers/ccip/OnRamp'
4+
import * as fq from '../../wrappers/ccip/FeeQuoter'
5+
import { Blockchain } from '@ton/sandbox'
6+
7+
// Gets the validated fee for a CCIPSend message with off-chain getters
8+
export async function getValidatedFee(
9+
blockchain: Blockchain,
10+
router: Address,
11+
msg: rt.CCIPSend,
12+
): Promise<bigint> {
13+
const routerContract = blockchain.openContract(rt.Router.createFromAddress(router))
14+
const orAddress = await routerContract.getOnRamp(msg.destChainSelector)
15+
const onRampContract = blockchain.openContract(onr.OnRamp.createFromAddress(orAddress))
16+
const feeQuoterAddress = await onRampContract.getFeeQuoter(msg.destChainSelector)
17+
const feeQuoterContract = blockchain.openContract(
18+
fq.FeeQuoter.createFromAddress(feeQuoterAddress),
19+
)
20+
const fee = await feeQuoterContract.getValidatedFee(msg)
21+
return fee
22+
}

contracts/tests/ccip/CCIPRouter.spec.ts

Lines changed: 5 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import * as ownable2step from '../../wrappers/libraries/access/Ownable2Step'
2929
import * as UpgradeableSpec from '../lib/versioning/UpgradeableSpec'
3030
import * as TypeAndVersionSpec from '../lib/versioning/TypeAndVersionSpec'
3131
import { dump } from '../utils/prettyPrint'
32+
import { getValidatedFee } from '../../src/ccipSend/fee'
3233

3334
const CHAINSEL_EVM_TEST_90000001 = 909606746561742123n
3435
const CHAINSEL_EVM_TEST_90000002 = 5548718428018410741n
@@ -119,7 +120,7 @@ describe('Router', () => {
119120
print: true,
120121
blockchainLogs: false,
121122
vmLogs: 'none',
122-
debugLogs: false,
123+
debugLogs: true,
123124
}
124125
deployer = await blockchain.treasury('deployer')
125126
sender = await blockchain.treasury('sender')
@@ -648,9 +649,9 @@ describe('Router', () => {
648649
.asCell(),
649650
}
650651

651-
const amount = await getValidatedFee(sender.getSender(), feeQuoter, ccipSend, Cell.EMPTY)
652-
console.log('Validated fee:', amount.fee, 'TON')
653-
const totalSendValue = amount.fee + toNano('0.5')
652+
const fee = await getValidatedFee(blockchain, router.address, ccipSend)
653+
console.log('Validated fee:', fee, 'TON')
654+
const totalSendValue = fee + toNano('0.5')
654655

655656
// router.ccipSend
656657
{
@@ -1054,49 +1055,3 @@ function verifyBodyIsRouterCCIPSendACK(
10541055

10551056
return verifyBodyMessage(body, rt.builder.message.out.ccipSendACK, validations)
10561057
}
1057-
1058-
/**
1059-
* Requests validateMessage
1060-
*/
1061-
async function getValidatedFee(
1062-
sender: Sender,
1063-
feeQuoter: SandboxContract<fq.FeeQuoter>,
1064-
msg: rt.CCIPSend,
1065-
metadata: Cell,
1066-
): Promise<sendExecutor.MessageValidated> {
1067-
const res = await feeQuoter.sendGetValidatedFee(sender, {
1068-
value: toNano('1'),
1069-
msg: {
1070-
msg,
1071-
metadata,
1072-
},
1073-
})
1074-
1075-
// request
1076-
expect(res.transactions).toHaveTransaction({
1077-
from: sender.address,
1078-
to: feeQuoter.address,
1079-
success: true,
1080-
})
1081-
// response
1082-
expect(res.transactions).toHaveTransaction({
1083-
from: feeQuoter.address,
1084-
to: sender.address,
1085-
success: true,
1086-
})
1087-
1088-
const tx = res.transactions.find(
1089-
(tx) =>
1090-
tx.inMessage?.info.type === 'internal' && tx.inMessage.info.src.equals(feeQuoter.address),
1091-
)
1092-
1093-
if (!tx || tx.inMessage === undefined || tx.inMessage?.info.type !== 'internal') {
1094-
throw new Error('Failed to find response transaction')
1095-
}
1096-
const resp = tx.inMessage
1097-
1098-
const body = resp.body.beginParse()
1099-
expect(body.preloadUint(32)).toBe(sendExecutor.Opcodes.messageValidated)
1100-
const messageValidated = fq.builder.message.out.messageValidated.load(resp.body.beginParse())
1101-
return messageValidated
1102-
}

contracts/wrappers/ccip/FeeQuoter.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import {
1212
SendMode,
1313
Builder,
1414
Slice,
15+
TupleItem,
1516
} from '@ton/core'
1617

1718
import * as ownable2step from '../libraries/access/Ownable2Step'
1819
import * as withdrawable from '../libraries/funding/Withdrawable'
19-
import { CellCodec } from '../utils'
20+
import { CellCodec, StackCodec } from '../utils'
2021
import { asSnakeData, fromSnakeData } from '../../src/utils'
2122
import * as upgradeable from '../libraries/versioning/Upgradeable'
2223
import * as typeAndVersion from '../libraries/versioning/TypeAndVersion'
@@ -387,6 +388,36 @@ export const builder = {
387388
}
388389
})(),
389390
}
391+
392+
export const stackBuilder = {
393+
data: {
394+
ccipSend: ((): StackCodec<rt.CCIPSend> => {
395+
return {
396+
encode: function (data: rt.CCIPSend): TupleItem[] {
397+
return [
398+
{ type: 'int', value: BigInt(data.queryID ?? 0) },
399+
{ type: 'int', value: data.destChainSelector },
400+
{
401+
type: 'slice',
402+
cell: beginCell().storeBuffer(data.receiver, data.receiver.length).endCell(),
403+
},
404+
{ type: 'cell', cell: data.data },
405+
{
406+
type: 'cell',
407+
cell: asSnakeData(data.tokenAmounts, rt.builder.data.tokenAmount.encode),
408+
},
409+
{ type: 'slice', cell: beginCell().storeAddress(data.feeToken).endCell() },
410+
{ type: 'cell', cell: data.extraArgs },
411+
]
412+
},
413+
load: function (src: TupleItem[]): rt.CCIPSend {
414+
throw new Error('Function not implemented.')
415+
},
416+
}
417+
})(),
418+
},
419+
}
420+
390421
export abstract class Params {}
391422

392423
export abstract class Opcodes {
@@ -504,6 +535,20 @@ export class FeeQuoter
504535
return upgradeable.sendUpgrade(provider, via, value, body)
505536
}
506537

538+
async getValidatedFeeCell(provider: ContractProvider, msg: rt.CCIPSend): Promise<bigint> {
539+
const result = await provider.get('validatedFeeCell', [
540+
{ type: 'cell', cell: rt.builder.message.in.ccipSend.encode(msg).asCell() },
541+
])
542+
543+
return result.stack.readBigNumber()
544+
}
545+
546+
async getValidatedFee(provider: ContractProvider, msg: rt.CCIPSend): Promise<bigint> {
547+
const result = await provider.get('validatedFee', stackBuilder.data.ccipSend.encode(msg))
548+
549+
return result.stack.readBigNumber()
550+
}
551+
507552
getTypeAndVersion(provider: ContractProvider): Promise<{ type: string; version: string }> {
508553
return typeAndVersion.getTypeAndVersion(provider)
509554
}

contracts/wrappers/ccip/OnRamp.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,12 @@ export class OnRamp implements Contract, withdrawable.Interface {
267267
return upgradeable.sendUpgrade(provider, via, value, body)
268268
}
269269

270+
getFeeQuoter(provider: ContractProvider, destChainSelector: bigint): Promise<Address> {
271+
return provider.get('feeQuoter', [{ type: 'int', value: destChainSelector }]).then((res) => {
272+
return res.stack.readAddress()
273+
})
274+
}
275+
270276
getTypeAndVersion(provider: ContractProvider): Promise<{ type: string; version: string }> {
271277
return typeAndVersion.getTypeAndVersion(provider)
272278
}

contracts/wrappers/ccip/Router.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,22 @@ export type CCIPReceiveConfirm = {
390390
rootId: bigint
391391
}
392392

393+
const crossChainAddressCodec: CellCodec<Buffer> = {
394+
encode: (addr: Buffer): Builder => {
395+
if (addr.byteLength > 64) {
396+
throw new Error('CrossChainAddress too long')
397+
}
398+
return beginCell().storeUint(addr.length, 8).storeBuffer(addr, addr.length)
399+
},
400+
load: (src: Slice): Buffer => {
401+
const len = Number(src.loadUint(8))
402+
if (len > 64) {
403+
throw new Error('CrossChainAddress too long')
404+
}
405+
return src.loadBuffer(len)
406+
},
407+
}
408+
393409
export const builder = {
394410
data: (() => {
395411
const contractData: CellCodec<Storage> = {
@@ -496,33 +512,30 @@ export const builder = {
496512
extraArgs,
497513
onRamps,
498514
offRamps,
515+
crossChainAddress: crossChainAddressCodec,
499516
}
500517
})(),
501518
message: {
502519
in: (() => {
503520
const ccipSend: CellCodec<CCIPSend> = {
504521
encode: (opts: CCIPSend): Builder => {
505-
return (
506-
beginCell()
507-
.storeUint(Opcodes.ccipSend, 32)
508-
.storeUint(opts.queryID ?? 0, 64)
509-
.storeUint(opts.destChainSelector, 64)
510-
// CrossChainAddress TODO: assert =< 64
511-
.storeUint(opts.receiver.byteLength, 8)
512-
.storeBuffer(opts.receiver, opts.receiver.byteLength)
513-
.storeRef(opts.data)
514-
.storeRef(asSnakeData(opts.tokenAmounts, tokenAmountCodec.encode)) // TODO: pack inputs
515-
.storeAddress(opts.feeToken)
516-
517-
.storeRef(opts.extraArgs)
518-
)
522+
return beginCell()
523+
.storeUint(Opcodes.ccipSend, 32)
524+
.storeUint(opts.queryID ?? 0, 64)
525+
.storeUint(opts.destChainSelector, 64)
526+
.storeBuilder(crossChainAddressCodec.encode(opts.receiver))
527+
.storeRef(opts.data)
528+
.storeRef(asSnakeData(opts.tokenAmounts, tokenAmountCodec.encode)) // TODO: pack inputs
529+
.storeAddress(opts.feeToken)
530+
531+
.storeRef(opts.extraArgs)
519532
},
520533
load: function (src: Slice): CCIPSend {
521534
src.skip(32)
522535
return {
523536
queryID: src.loadUint(64),
524537
destChainSelector: src.loadUintBig(64),
525-
receiver: src.loadBuffer(src.loadUint(8)),
538+
receiver: crossChainAddressCodec.load(src),
526539
data: src.loadRef(),
527540
tokenAmounts: fromSnakeData(src.loadRef(), tokenAmountCodec.load),
528541
feeToken: src.loadAddress(),

contracts/wrappers/utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Builder, Slice } from '@ton/core'
1+
import { Builder, Slice, TupleItem } from '@ton/core'
22
import { createHash } from 'crypto'
33

44
/// Returns the facility ID for the given CRC16 key (e.g. stringCrc16("com.chainlink.ton.mcms.Timelock")).
@@ -29,3 +29,8 @@ export interface CellCodec<T> {
2929
encode: (data: T) => Builder
3030
load: (src: Slice) => T
3131
}
32+
33+
export interface StackCodec<T> {
34+
encode: (data: T) => TupleItem[]
35+
load: (src: TupleItem[]) => T
36+
}

0 commit comments

Comments
 (0)