diff --git a/contracts/contracts/ccip/merkle_root/contract.tolk b/contracts/contracts/ccip/merkle_root/contract.tolk index d29a70064..46741c215 100644 --- a/contracts/contracts/ccip/merkle_root/contract.tolk +++ b/contracts/contracts/ccip/merkle_root/contract.tolk @@ -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); @@ -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; diff --git a/contracts/contracts/ccip/merkle_root/errors.tolk b/contracts/contracts/ccip/merkle_root/errors.tolk index 47de5ef9c..f877cee6f 100644 --- a/contracts/contracts/ccip/merkle_root/errors.tolk +++ b/contracts/contracts/ccip/merkle_root/errors.tolk @@ -1,10 +1,11 @@ import "../../lib/utils" enum Error { - StateIsNotUntouched = 47900; // Facility ID * 100 + AlreadyAttempted = 47900; // Facility ID * 100 UpdatingStateOfNonExecutedMessage; NotificationFromInvalidReceiver; NotOwner; + ManualExecutionNotYetEnabled; + SkippedAlreadyExecutedMessage; } - diff --git a/contracts/contracts/ccip/merkle_root/messages.tolk b/contracts/contracts/ccip/merkle_root/messages.tolk index eae3d8c5e..d807e1297 100644 --- a/contracts/contracts/ccip/merkle_root/messages.tolk +++ b/contracts/contracts/ccip/merkle_root/messages.tolk @@ -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') @@ -18,5 +20,3 @@ struct(0x845e303f) MerkleRoot_CCIPReceiveBounced { receiver: address } - - diff --git a/contracts/contracts/ccip/merkle_root/storage.tolk b/contracts/contracts/ccip/merkle_root/storage.tolk index 803cbd969..f6a197e74 100644 --- a/contracts/contracts/ccip/merkle_root/storage.tolk +++ b/contracts/contracts/ccip/merkle_root/storage.tolk @@ -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{}; diff --git a/contracts/contracts/ccip/offramp/contract.tolk b/contracts/contracts/ccip/offramp/contract.tolk index f5cc2090d..03bcd7c3f 100644 --- a/contracts/contracts/ccip/offramp/contract.tolk +++ b/contracts/contracts/ccip/offramp/contract.tolk @@ -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); @@ -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) } @@ -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{} @@ -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); @@ -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? diff --git a/contracts/contracts/ccip/offramp/errors.tolk b/contracts/contracts/ccip/offramp/errors.tolk index 9a9d4bacc..0aa9b9078 100644 --- a/contracts/contracts/ccip/offramp/errors.tolk +++ b/contracts/contracts/ccip/offramp/errors.tolk @@ -9,5 +9,3 @@ enum Error { InvalidOnRampUpdate } - - diff --git a/contracts/contracts/ccip/offramp/messages.tolk b/contracts/contracts/ccip/offramp/messages.tolk index 2c180a888..af3159907 100644 --- a/contracts/contracts/ccip/offramp/messages.tolk +++ b/contracts/contracts/ccip/offramp/messages.tolk @@ -7,6 +7,7 @@ import "../common/types.tolk" type OffRamp_InMessage = | OffRamp_Commit | OffRamp_Execute + | OffRamp_ManuallyExecute | OffRamp_DispatchValidated | OffRamp_UpdateSourceChainConfig | OCR3Base_SetOCR3Config @@ -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? +} + //crc32('OffRamp_UpdateSourceChainConfig') struct (0xb98c95e3) OffRamp_UpdateSourceChainConfig { queryId: uint64; diff --git a/contracts/contracts/ccip/test/receiver/contract.tolk b/contracts/contracts/ccip/test/receiver/contract.tolk index b039eaad7..94035b2e4 100644 --- a/contracts/contracts/ccip/test/receiver/contract.tolk +++ b/contracts/contracts/ccip/test/receiver/contract.tolk @@ -10,7 +10,7 @@ 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') @@ -18,7 +18,11 @@ 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); @@ -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 @@ -43,6 +48,11 @@ fun onInternalMessage(in: InMessage) { message: msg.message, }); } + SetRejectAll => { + var st = Storage.load(); + st.rejectAll = msg.rejectAll; + st.store(); + } else => { // ignore empty messages, "wrong opcode" for others assert (in.body.isEmpty()) throw 0xFFFF diff --git a/contracts/contracts/ccip/test/receiver/errors.tolk b/contracts/contracts/ccip/test/receiver/errors.tolk index e05e18f48..514ae1e2d 100644 --- a/contracts/contracts/ccip/test/receiver/errors.tolk +++ b/contracts/contracts/ccip/test/receiver/errors.tolk @@ -2,5 +2,6 @@ import "../../../lib/utils" enum Error { Unauthorized = 34600; // Facility ID * 100 + Rejected; } diff --git a/contracts/contracts/ccip/test/receiver/storage.tolk b/contracts/contracts/ccip/test/receiver/storage.tolk index 899af35f7..2b383cd27 100644 --- a/contracts/contracts/ccip/test/receiver/storage.tolk +++ b/contracts/contracts/ccip/test/receiver/storage.tolk @@ -1,6 +1,7 @@ struct Storage { id: uint32; offRamp: address; // TODO: should be router? + rejectAll: bool; } fun Storage.load(): Storage { diff --git a/contracts/tests/ccip/OffRamp.spec.ts b/contracts/tests/ccip/OffRamp.spec.ts index 8c5f3ad73..163f4f9d5 100644 --- a/contracts/tests/ccip/OffRamp.spec.ts +++ b/contracts/tests/ccip/OffRamp.spec.ts @@ -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, @@ -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({ @@ -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, }) }) @@ -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({ @@ -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))) diff --git a/contracts/tests/ccip/Receiver.spec.ts b/contracts/tests/ccip/Receiver.spec.ts index 560339698..5e30d88a6 100644 --- a/contracts/tests/ccip/Receiver.spec.ts +++ b/contracts/tests/ccip/Receiver.spec.ts @@ -41,6 +41,7 @@ describe('Receiver', () => { let data: ReceiverStorage = { id: generateSecureRandomId(), offramp: deployer.address, + rejectAll: false, } receiver = blockchain.openContract(Receiver.createFromConfig(data, code)) diff --git a/contracts/wrappers/ccip/OffRamp.ts b/contracts/wrappers/ccip/OffRamp.ts index bdf07e6b4..337ee4af4 100644 --- a/contracts/wrappers/ccip/OffRamp.ts +++ b/contracts/wrappers/ccip/OffRamp.ts @@ -2,7 +2,6 @@ import { Address, beginCell, Cell, - Contract, contractAddress, ContractProvider, Dictionary, @@ -16,8 +15,7 @@ import { OCR3Base, ReportContext, SignatureEd25519 } from '../libraries/ocr/Mult import { asSnakeData, fromSnakeData, bigIntToUint8Array } from '../../src/utils/types' import * as ownable2step from '../libraries/access/Ownable2Step' import { crc32 } from 'zlib' -import { CellCodec, facilityId } from '../utils' -import { CCIPReceive, ReceiverStorage } from './Receiver' +import { CellCodec } from '../utils' export type OffRampStorage = { id: bigint @@ -203,6 +201,7 @@ export abstract class Params {} export const Opcodes = { commit: crc32('OffRamp_Commit'), execute: crc32('OffRamp_Execute'), + manualExecute: crc32('OffRamp_ManuallyExecute'), updateSourceChainConfig: crc32('OffRamp_UpdateSourceChainConfig'), dispatchValidated: crc32('OffRamp_DispatchValidated'), ccipReceiveConfirm: crc32('OffRamp_CCIPReceiveConfirm'), @@ -226,10 +225,12 @@ export enum OffRampError { } export enum MerkleRootError { - StateIsNotUntouched = MERKLE_ROOT_ERROR_CODE, + AlreadyAttempted = MERKLE_ROOT_ERROR_CODE, UpdatingStateOfNonExecutedMessage, NotificationFromInvalidReceiver, NotOwner, + ManualExecutionNotYetEnabled, + SkippedAlreadyExecutedMessage, } export class OffRamp extends OCR3Base { @@ -320,6 +321,28 @@ export class OffRamp extends OCR3Base { }) } + async sendManualExecute( + provider: ContractProvider, + via: Sender, + opts: { + value: bigint + queryID?: number + report: ExecutionReport + gasOverride?: bigint + }, + ) { + await provider.internal(via, { + value: opts.value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell() + .storeUint(Opcodes.manualExecute, 32) + .storeUint(opts.queryID ?? 0, 64) + .storeBuilder(ExecutionReportToBuilder(opts.report)) + .storeCoins(opts.gasOverride ?? 0) + .endCell(), + }) + } + async sendUpdateSourceChainConfig( provider: ContractProvider, via: Sender, diff --git a/contracts/wrappers/ccip/Receiver.ts b/contracts/wrappers/ccip/Receiver.ts index 999a331a6..2913385c4 100644 --- a/contracts/wrappers/ccip/Receiver.ts +++ b/contracts/wrappers/ccip/Receiver.ts @@ -35,11 +35,13 @@ export enum ReceiverError { export type ReceiverStorage = { id: number offramp: Address + rejectAll: boolean } export abstract class Params {} export abstract class Opcodes { + static setRejectAll = 0x00000001 static ccipReceive = 0xb3126df1 static ccipReceiveConfirm = 0x28f4166f } @@ -81,6 +83,19 @@ export class Receiver implements Contract { }) } + async sendSetRejectAll( + provider: ContractProvider, + via: Sender, + value: bigint, + rejectAll: boolean, + ) { + await provider.internal(via, { + value: value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell().storeUint(Opcodes.setRejectAll, 32).storeBit(rejectAll).endCell(), + }) + } + async getId(provider: ContractProvider): Promise { const { stack } = await provider.get('getId', []) return stack.readNumber() @@ -109,13 +124,17 @@ export const builder = { data: (() => { const contractData: CellCodec = { encode: (config: ReceiverStorage): Builder => { - return beginCell().storeUint(config.id, 32).storeAddress(config.offramp) + return beginCell() + .storeUint(config.id, 32) + .storeAddress(config.offramp) + .storeBit(config.rejectAll) }, load: (src: Slice): ReceiverStorage => { return { id: src.loadUint(32), offramp: src.loadAddress(), + rejectAll: src.loadBoolean(), } }, } diff --git a/deployment/ccip/operation/receiver.go b/deployment/ccip/operation/receiver.go index 205a52f23..f773ceeca 100644 --- a/deployment/ccip/operation/receiver.go +++ b/deployment/ccip/operation/receiver.go @@ -43,8 +43,9 @@ func deployReceiver(b operations.Bundle, deps TonDeps, in DeployReceiverInput) ( conn := tracetracking.NewSignedAPIClient(deps.TonChain.Client, *deps.TonChain.Wallet) storage := receiver.Storage{ - ID: in.ID, - OffRamp: in.OffRampAddress, + ID: in.ID, + OffRamp: in.OffRampAddress, + RejectAll: false, } initData, err := tlb.ToCell(storage) diff --git a/pkg/ccip/bindings/receiver/receiver.go b/pkg/ccip/bindings/receiver/receiver.go index 341f755ab..27de45c17 100644 --- a/pkg/ccip/bindings/receiver/receiver.go +++ b/pkg/ccip/bindings/receiver/receiver.go @@ -6,6 +6,7 @@ import ( // Storage represents the storage structure for the CCIP receiver contract. type Storage struct { - ID uint32 `tlb:"## 32"` - OffRamp *address.Address `tlb:"addr"` + ID uint32 `tlb:"## 32"` + OffRamp *address.Address `tlb:"addr"` + RejectAll bool `tlb:"bool"` }