Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions contracts/contracts/ccip/router/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,15 @@ fun onCCIPReceiveBounced(execId: ReceiveExecutorId, sender: address) {
fun onGetValidatedFee(msg: Router_GetValidatedFee<RemainingBitsAndRefs>, sender: address) {
val st = lazy Storage.load();
val ccipSend = lazy msg.ccipSend.load();
val onRamp = st.onRamps.mustGet(ccipSend.destChainSelector, Router_Error.DestChainNotEnabled as int);
val onRampResult = st.onRamps.get(ccipSend.destChainSelector);
if (!onRampResult.isFound) {
return sendMessageValidationFailed(sender, Router_Error.DestChainNotEnabled as uint256, msg.ccipSend, msg.context);
}

val onRamp = onRampResult.loadValue();
if (onRamp.isZero()) {
return sendMessageValidationFailed(sender, Router_Error.DestChainNotEnabled as uint256, msg.ccipSend, msg.context);
};

val feeQuoterMsg = createMessage({
bounce: true,
Expand Down Expand Up @@ -401,14 +409,18 @@ fun onMessageValidatedFromOnRamp(msg: OnRamp_MessageValidated<Router_GetValidate
// Router ->> SenderAcc: Router_MessageValidationFailed
fun onMessageValidationFailedFromOnRamp(msg: OnRamp_MessageValidationFailed<Router_GetValidatedFeeContext>) {
val userAddress = msg.context.routerContext;
sendMessageValidationFailed(userAddress, msg.error, msg.msg, msg.context.userContext);
}

fun sendMessageValidationFailed(userAddress: address, error: uint256, msg: Cell<Router_CCIPSend>, userContext: RemainingBitsOrRef<RemainingBitsAndRefs>) {
val replyMsg = createMessage({
bounce: true,
value: 0,
dest: userAddress,
body: Router_MessageValidationFailed {
error: msg.error,
msg: msg.msg,
context: msg.context.userContext,
error: error,
msg: msg,
context: userContext,
}
});
replyMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
Expand All @@ -429,11 +441,14 @@ fun onCCIPSend(msg: Router_CCIPSend, sender: address, value: coins) {
msg.feeToken = st.wrappedNative
}

val onRamp = st.onRamps.mustGet(msg.destChainSelector, Router_Error.DestChainNotEnabled as int);
val onRampResult = st.onRamps.get(msg.destChainSelector);
if (!onRampResult.isFound || onRampResult.loadValue().isZero()) {
return sendMessageRejected(sender, msg.queryID, Router_Error.DestChainNotEnabled as uint256);
}
val sendMsg = createMessage({
bounce: true,
value: 0,
dest: onRamp,
dest: onRampResult.loadValue(),
body: OnRamp_Send { msg: msg.toCell(), metadata: Metadata{ sender, value } },
});
sendMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
Expand Down Expand Up @@ -546,13 +561,17 @@ fun onSendACK(msg: Router_MessageSent) {
// OnRamp ->> Router: Router_MessageRejected
// Router ->> SenderAcc: Router_CCIPSendNACK
fun onSendNACK(msg: Router_MessageRejected) {
sendMessageRejected(msg.sender, msg.queryID, msg.error);
}

fun sendMessageRejected(sender: address, queryID: uint64, error: uint256) {
val nackMsg = createMessage({
bounce: true,
value: 0,
dest: msg.sender,
dest: sender,
body: Router_CCIPSendNACK {
queryID: msg.queryID,
error: msg.error,
queryID: queryID,
error: error,
}
});

Expand Down
60 changes: 57 additions & 3 deletions contracts/tests/ccip/router/Router.ccipSend.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
TEST_TOKEN_ADDR,
contractsCoverageConfig,
} from './Router.Setup'
import { ZERO_ADDRESS } from '../../../src/utils'

describe('Router', () => {
let blockchain: Blockchain
Expand Down Expand Up @@ -78,7 +79,7 @@ describe('Router', () => {
})
})

it('should reject message for disabled dest chain', async () => {
it('should reject message for disabled dest chain (missing OnRamp)', async () => {
const badMsg = { ...msg, destChainSelector: msg.destChainSelector + 1n }
const result = await router.sendCcipSend(sender.getSender(), {
value: toNano('1'),
Expand All @@ -88,8 +89,61 @@ describe('Router', () => {
expect(result.transactions).toHaveTransaction({
from: sender.address,
to: router.address,
success: false,
exitCode: rt.RouterError.DestChainNotEnabled,
success: true,
})

expect(result.transactions).toHaveTransaction({
from: router.address,
to: sender.address,
op: rt.opcodes.out.ccipSendNACK,
body(x) {
if (!x) return false
const decoded = rt.builder.message.out.ccipSendNACK.load(x.beginParse())
return decoded.error === BigInt(rt.RouterError.DestChainNotEnabled)
},
})
})

it('should reject message for disabled dest chain (zero address)', async () => {
// Disable the onRamp for the chain
{
const result = await router.sendApplyRampUpdatesSetRamps(deployer.getSender(), {
value: toNano('1'),
data: {
queryID: 1n,
onRamps: {
destChainSelectors: [CHAINSEL_EVM_TEST_90000001],
onRamp: ZERO_ADDRESS,
},
},
})
expect(result.transactions).toHaveTransaction({
from: deployer.address,
to: router.address,
success: true,
})
}

const result = await router.sendCcipSend(sender.getSender(), {
value: toNano('1'),
body: msg,
})

expect(result.transactions).toHaveTransaction({
from: sender.address,
to: router.address,
success: true,
})

expect(result.transactions).toHaveTransaction({
from: router.address,
to: sender.address,
op: rt.opcodes.out.ccipSendNACK,
body(x) {
if (!x) return false
const decoded = rt.builder.message.out.ccipSendNACK.load(x.beginParse())
return decoded.error === BigInt(rt.RouterError.DestChainNotEnabled)
},
})
})

Expand Down
78 changes: 75 additions & 3 deletions contracts/tests/ccip/router/Router.getFee.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
TEST_TOKEN_ADDR,
contractsCoverageConfig,
} from './Router.Setup'
import { ZERO_ADDRESS } from '../../../src/utils'

describe('Router', () => {
let blockchain: Blockchain
Expand Down Expand Up @@ -92,7 +93,7 @@ describe('Router', () => {
})
})

it('should reject getValidatedFee for disabled dest chain', async () => {
it('should reject getValidatedFee for disabled dest chain (missing OnRamp)', async () => {
const badMsg = {
queryID: 1,
destChainSelector: CHAINSEL_EVM_TEST_90000001 + 1n,
Expand All @@ -118,8 +119,79 @@ describe('Router', () => {
expect(result.transactions).toHaveTransaction({
from: sender.address,
to: router.address,
success: false,
exitCode: rt.RouterError.DestChainNotEnabled,
success: true,
})

expect(result.transactions).toHaveTransaction({
from: router.address,
to: sender.address,
op: rt.opcodes.out.messageValidationFailed,
body(x) {
if (!x) return false
const decoded = rt.builder.message.out.messageValidationFailed.load(x.beginParse())
return decoded.error === BigInt(rt.RouterError.DestChainNotEnabled)
},
})
})

it('should reject getValidatedFee for disabled dest chain (zero address)', async () => {
// Disable the onRamp for the chain
{
const result = await router.sendApplyRampUpdatesSetRamps(deployer.getSender(), {
value: toNano('1'),
data: {
queryID: 1n,
onRamps: {
destChainSelectors: [CHAINSEL_EVM_TEST_90000001],
onRamp: ZERO_ADDRESS,
},
},
})

expect(result.transactions).toHaveTransaction({
from: deployer.address,
to: router.address,
success: true,
})
}

const badMsg = {
queryID: 1,
destChainSelector: CHAINSEL_EVM_TEST_90000001,
receiver: EVM_ADDRESS,
data: Cell.EMPTY,
tokenAmounts: [],
feeToken: TEST_TOKEN_ADDR,
extraArgs: rt.builder.data.extraArgs
.encode({
kind: 'generic-v2',
gasLimit: 100n,
allowOutOfOrderExecution: true,
})
.asCell(),
}
const result = await router.sendGetValidatedFee(
sender.getSender(),
toNano('0.5'),
badMsg,
beginCell().asSlice(),
)

expect(result.transactions).toHaveTransaction({
from: sender.address,
to: router.address,
success: true,
})

expect(result.transactions).toHaveTransaction({
from: router.address,
to: sender.address,
op: rt.opcodes.out.messageValidationFailed,
body(x) {
if (!x) return false
const decoded = rt.builder.message.out.messageValidationFailed.load(x.beginParse())
return decoded.error === BigInt(rt.RouterError.DestChainNotEnabled)
},
})
})

Expand Down
Loading