Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
21 changes: 19 additions & 2 deletions contracts/contracts/ccip/merkle_root/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "../offramp/messages"
import "../offramp/types"
import "../../lib/utils"

const CONTRACT_VERSION = "0.0.3";
const CONTRACT_VERSION = "0.0.4";

fun onInternalMessage(in: InMessage) {
val msg = lazy MerkleRoot_InMessage.fromSlice(in.body);
Expand Down Expand Up @@ -85,7 +85,24 @@ fun _validateAndExecute(msg: MerkleRoot_Validate, sender: address) {
var st = MerkleRoot_Storage.load();

assert(sender == st.owner, Error.NotOwner);
assert(st.state == STATE_UNTOUCHED, Error.StateIsNotUntouched);
assert(st.state == STATE_UNTOUCHED || st.state == STATE_EXECUTE_FAILED, Error.SkippedAlreadyExecutedMessage);

val isManualExecute = msg.gasOverride != null;
if (isManualExecute) {
// TODO: validate limits
val isOldCommitReport = (blockchain.now() - st.timestamp < msg.permissionlessExecutionThresholdSeconds);

// Manual execution is fine if we previously failed or if the commit report is just too old.
// (In that case the report will still be in UNTOUCHED state)
assert(isOldCommitReport || st.state == STATE_EXECUTE_FAILED, Error.ManualExecutionNotYetEnabled);

if (msg.gasOverride! != 0) {
// TODO: specify this as gas value on execution
}
} else {
// DON can only execute a message once.
assert(st.state == STATE_UNTOUCHED, Error.AlreadyAttempted);
}

val ccipMessage = msg.message;

Expand Down
5 changes: 3 additions & 2 deletions contracts/contracts/ccip/merkle_root/errors.tolk
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import "../../lib/utils"

enum Error {
StateIsNotUntouched = 47900; // Facility ID * 100
AlreadyAttempted = 47900; // Facility ID * 100
UpdatingStateOfNonExecutedMessage;
NotificationFromInvalidReceiver;
NotOwner;
ManualExecutionNotYetEnabled;
SkippedAlreadyExecutedMessage;
}


4 changes: 2 additions & 2 deletions contracts/contracts/ccip/merkle_root/messages.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ type MerkleRoot_InMessage = MerkleRoot_Validate | MerkleRoot_CCIPReceiveBounced
//crc32('MerkleRoot_Validate')
struct (0x38ede91) MerkleRoot_Validate {
message: Any2TVMRampMessage,
permissionlessExecutionThresholdSeconds: uint32,
//TODO: token data
gasOverride: coins?,
}

//crc32('MerkleRoot_CCIPReceiveConfirm')
Expand All @@ -18,5 +20,3 @@ struct(0x845e303f) MerkleRoot_CCIPReceiveBounced {
receiver: address
}



1 change: 1 addition & 0 deletions contracts/contracts/ccip/merkle_root/storage.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "../common/types";
struct MerkleRoot_Storage {
rootId: uint224;
owner: address;
timestamp: uint64;
state: uint8 = 0;
executionState: uint8 = 0;
tokenBalance: TokenBalance = TokenBalance{};
Expand Down
47 changes: 33 additions & 14 deletions contracts/contracts/ccip/offramp/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import "../merkle_root/messages"
import "../../lib/receiver/messages"
import "../merkle_root/storage"

const CONTRACT_VERSION = "0.0.3";
const CONTRACT_VERSION = "0.0.4";

fun onInternalMessage(in:InMessage) {
val msg = lazy OffRamp_InMessage.fromSlice(in.body);
Expand All @@ -27,6 +27,9 @@ fun onInternalMessage(in:InMessage) {
OffRamp_Execute => {
_execute(msg, in.senderAddress)
}
OffRamp_ManuallyExecute => {
_manually_execute(msg, in.senderAddress)
}
OCR3Base_SetOCR3Config => {
_setOCR3Config(msg, in.senderAddress)
}
Expand Down Expand Up @@ -262,6 +265,7 @@ fun _commit(msg: OffRamp_Commit, sender: address) {
data: MerkleRoot_Storage {
rootId: rootId.endCell().beginParse().loadUint(224),
owner: contract.getAddress(),
timestamp: blockchain.now(),
state: 0,
executionState: 0,
tokenBalance: TokenBalance{}
Expand Down Expand Up @@ -295,13 +299,36 @@ fun _commit(msg: OffRamp_Commit, sender: address) {
);
}

fun _manually_execute(msg:OffRamp_ManuallyExecute, sender: address) {
var st = Storage.load();

val report = msg.report;

// when_chain_not_forked assert

_execute_single_report(msg.report, msg.gasOverride, sender);

}

fun _execute(msg: OffRamp_Execute, sender: address) {
_execute_single_report(msg.report, null, sender);

var st = Storage.load();

// TODO: manual execution flag
// TODO: check if chain was cursed by RMNRemote
st.ocr3Base.load().transmit(
sender,
OCR_PLUGIN_TYPE_EXECUTE,
msg.reportContext,
msg.report.toCell(),
beginCell().endCell(),
);

val report = msg.report;
}

fun _execute_single_report(report: ExecutionReport, gasOverride: coins?, sender: address) {
var st = Storage.load();

// TODO: check if chain was cursed by RMNRemote

var sourceChainConfigResult = st.sourceChainConfigs.get(report.sourceChainSelector);
assert(sourceChainConfigResult.isFound, Error.SourceChainNotEnabled);
Expand Down Expand Up @@ -352,20 +379,12 @@ fun _execute(msg: OffRamp_Execute, sender: address) {

body: MerkleRoot_Validate {
message: Any2TVMRampMessage.fromCell(report.messages),
permissionlessExecutionThresholdSeconds: st.permissionlessExecutionThresholdSeconds,
gasOverride,
},
});
executeMsg.send(SEND_MODE_REGULAR);

// TODO: how do we handle incrementing the nonce? since execution is async this might take a while

st.ocr3Base.load().transmit(
sender,
OCR_PLUGIN_TYPE_EXECUTE,
msg.reportContext,
msg.report.toCell(),
beginCell().endCell(),
);

}

//todo: maybe it should be chainConfigs and take an array?
Expand Down
2 changes: 0 additions & 2 deletions contracts/contracts/ccip/offramp/errors.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,3 @@ enum Error {
InvalidOnRampUpdate
}



8 changes: 8 additions & 0 deletions contracts/contracts/ccip/offramp/messages.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "../common/types.tolk"
type OffRamp_InMessage =
| OffRamp_Commit
| OffRamp_Execute
| OffRamp_ManuallyExecute
| OffRamp_DispatchValidated
| OffRamp_UpdateSourceChainConfig
| OCR3Base_SetOCR3Config
Expand All @@ -33,6 +34,13 @@ struct (0x27bdac33) OffRamp_Execute {
report: ExecutionReport;
}

//crc32('OffRamp_ManuallyExecute')
struct (0xa00785cf) OffRamp_ManuallyExecute {
queryId: uint64;
report: ExecutionReport;
gasOverride: coins; // TODO: should this just be part of the added gas value passed into the call?
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can just pass through all TON value specified on the call here and avoid this override

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, but then we have to change the logic where we check msg.gasOverride != null and instead add a specific handler for manual exec in the MerkleRoot

}

//crc32('OffRamp_UpdateSourceChainConfig')
struct (0xb98c95e3) OffRamp_UpdateSourceChainConfig {
queryId: uint64;
Expand Down
18 changes: 14 additions & 4 deletions contracts/contracts/ccip/test/receiver/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ import "../../../lib/receiver/types";
import "../../../lib/utils";
import "../../../ccip/offramp/messages";

const CONTRACT_VERSION = "0.0.1";
const CONTRACT_VERSION = "0.0.2";

const RECEIVED_MESSAGE_TOPIC = 0xc5a40ab3; //crc32('Receiver_CCIPMessageReceived')

struct CCIPMessageReceived {
message: Any2TVMMessage
}

type Msg = Receiver_CCIPReceive;
struct (0x00000001) SetRejectAll {
rejectAll: bool;
}

type Msg = Receiver_CCIPReceive | SetRejectAll;

fun onInternalMessage(in: InMessage) {
val msg = lazy Msg.fromSlice(in.body);
Expand All @@ -27,8 +31,9 @@ fun onInternalMessage(in: InMessage) {
// Standard for every receiver to implement:
// - Check CCIPReceive only comes from offRamp/ router
// - Send CCIPReceiveConfirm to msg.callback
assert(in.senderAddress == Storage.load().offRamp, Error.Unauthorized);

val st = Storage.load();
assert(in.senderAddress == st.offRamp, Error.Unauthorized);
assert(!st.rejectAll, Error.Rejected);
val receiveConfirm = createMessage({
bounce: true,
value: ton("0.05"), //TODO how much do we need to send
Expand All @@ -43,6 +48,11 @@ fun onInternalMessage(in: InMessage) {
message: msg.message,
});
}
SetRejectAll => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolasgnr like Solana the new receiver supports toggling a receiver to deliberately fail. I didn't add any access control checks since this shouldn't be used outside of testnet?

var st = Storage.load();
st.rejectAll = msg.rejectAll;
st.store();
}
else => {
// ignore empty messages, "wrong opcode" for others
assert (in.body.isEmpty()) throw 0xFFFF
Expand Down
1 change: 1 addition & 0 deletions contracts/contracts/ccip/test/receiver/errors.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ import "../../../lib/utils"

enum Error {
Unauthorized = 34600; // Facility ID * 100
Rejected;
}

1 change: 1 addition & 0 deletions contracts/contracts/ccip/test/receiver/storage.tolk
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
struct Storage {
id: uint32;
offRamp: address; // TODO: should be router?
rejectAll: bool;
}

fun Storage.load(): Storage {
Expand Down
85 changes: 82 additions & 3 deletions contracts/tests/ccip/OffRamp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,24 @@ describe('OffRamp', () => {
return result
}

const manualExecuteReport = async (
report: ExecutionReport,
gasOverride: bigint | undefined = undefined,
expectSuccess = true,
) => {
const result = await offRamp.sendManualExecute(transmitters[0].getSender(), {
value: toNano('0.5'),
report,
gasOverride,
})

if (expectSuccess) {
expectSuccessfulTransaction(result, transmitters[0].address, offRamp.address)
}

return result
}

const executeReportExpectingFailure = async (
report: ExecutionReport,
expectedErrorCode: number,
Expand Down Expand Up @@ -397,7 +415,7 @@ describe('OffRamp', () => {
{
let code = await compile('ccip.test.receiver')
receiver = blockchain.openContract(
Receiver.createFromConfig({ id: 1, offramp: offRamp.address }, code),
Receiver.createFromConfig({ id: 1, offramp: offRamp.address, rejectAll: false }, code),
)
const result = await receiver.sendDeploy(deployer.getSender(), toNano('10'))
expect(result.transactions).toHaveTransaction({
Expand Down Expand Up @@ -689,7 +707,7 @@ describe('OffRamp', () => {
// There should be a failed transaction with the specific error code from offRamp to MerkleRoot
expect(secondExecuteResult.transactions).toHaveTransaction({
from: offRamp.address,
exitCode: MerkleRootError.StateIsNotUntouched,
exitCode: MerkleRootError.SkippedAlreadyExecutedMessage,
success: false,
})
})
Expand Down Expand Up @@ -1047,7 +1065,7 @@ describe('OffRamp', () => {
let code = await compile('ccip.test.receiver')
const wrongOffRampAddress = generateMockTonAddress() // Use a different address
const badReceiver = blockchain.openContract(
Receiver.createFromConfig({ id: 1, offramp: wrongOffRampAddress }, code),
Receiver.createFromConfig({ id: 1, offramp: wrongOffRampAddress, rejectAll: false }, code),
)
const result = await badReceiver.sendDeploy(deployer.getSender(), toNano('10'))
expect(result.transactions).toHaveTransaction({
Expand Down Expand Up @@ -1104,6 +1122,67 @@ describe('OffRamp', () => {
)
})

it('Manual execute: receiver fails, then succeeds', async () => {
const message = createTestMessage(1n, 1n, receiver.address) // empty data (Cell.EMPTY)
await setupAndCommitMessage(message)
const report = createExecuteReport([message])

const result = await receiver.sendSetRejectAll(deployer.getSender(), toNano('0.1'), true)
expect(result.transactions).toHaveTransaction({
from: deployer.address,
to: receiver.address,
success: true,
})

const result2 = await executeReport(report)

// TODO: expect fail

const result3 = await receiver.sendSetRejectAll(deployer.getSender(), toNano('0.1'), false)
expect(result.transactions).toHaveTransaction({
from: deployer.address,
to: receiver.address,
success: true,
})

//
const result4 = await manualExecuteReport(report, undefined, true)

expect(result4.transactions).toHaveTransaction({
from: offRamp.address,
to: receiver.address,
success: true,
})

assertLog(result4.transactions, offRamp.address, CCIPLogs.LogTypes.ExecutionStateChanged, {
sourceChainSelector: CHAINSEL_EVM_TEST_90000001,
sequenceNumber: 1n,
messageId: 1n,
state: EXECUTION_STATE_IN_PROGRESS,
})

assertLog(result4.transactions, offRamp.address, CCIPLogs.LogTypes.ExecutionStateChanged, {
sourceChainSelector: CHAINSEL_EVM_TEST_90000001,
sequenceNumber: 1n,
messageId: 1n,
state: EXECUTION_STATE_SUCCESS,
})

assertLog(
result4.transactions,
receiver.address,
CCIPLogs.LogTypes.ReceiverCCIPMessageReceived,
{
message: {
messageId: message.header.messageId,
sourceChainSelector: CHAINSEL_EVM_TEST_90000001,
sender: message.sender,
data: message.data,
},
},
)
})

it('Test facilityId matches facility name', () => {
expect(MERKLE_ROOT_FACILITY_ID).toEqual(facilityId(crc32(MERKLE_ROOT_FACILITY_NAME)))
expect(OFFRAMP_FACILITY_ID).toEqual(facilityId(crc32(OFFRAMP_FACILITY_NAME)))
Expand Down
1 change: 1 addition & 0 deletions contracts/tests/ccip/Receiver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ describe('Receiver', () => {
let data: ReceiverStorage = {
id: generateSecureRandomId(),
offramp: deployer.address,
rejectAll: false,
}

receiver = blockchain.openContract(Receiver.createFromConfig(data, code))
Expand Down
Loading
Loading