Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
20 changes: 19 additions & 1 deletion contracts/contracts/ccip/ccipsend_executor/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ fun onBouncedMessage(in: InMessageBounced) {
match (msg) {
FeeQuoter_GetValidatedFee<RemainingBitsAndRefs> => {
var this = CCIPSendExecutor<CCIPSendExecutor_State_OnGoingFeeValidation>.load();
this.exitWithError(CCIPSendExecutor_Error.InsufficientFunds as int);
this.exitWithError(CCIPSendExecutor_Error.FeeQuoterBounce as int);
}
}
}
Expand Down Expand Up @@ -101,6 +101,15 @@ fun CCIPSendExecutor<CCIPSendExecutor_State_OnGoingFeeValidation>.onMessageValid
}

fun CCIPSendExecutor<T>.exitSuccessfully(self, fee: Fee) {
val newState = CCIPSendExecutor<CCIPSendExecutor_State_Finalized> {
id: self.id,
onrampSend: self.onrampSend,
addresses: self.addresses,
state: CCIPSendExecutor_State_Finalized {
}.toCell(),
};
newState.store();

val sendMsg = createMessage({
bounce: true,
value: 0,
Expand All @@ -120,6 +129,15 @@ fun CCIPSendExecutor<CCIPSendExecutor_State_OnGoingFeeValidation>.onMessageValid
}

fun CCIPSendExecutor<T>.exitWithError(self, error: uint256) {
val newState = CCIPSendExecutor<CCIPSendExecutor_State_Finalized> {
id: self.id,
onrampSend: self.onrampSend,
addresses: self.addresses,
state: CCIPSendExecutor_State_Finalized {
}.toCell(),
};
newState.store();

val sendMsg = createMessage({
bounce: true,
value: 0,
Expand Down
1 change: 1 addition & 0 deletions contracts/contracts/ccip/ccipsend_executor/errors.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ enum CCIPSendExecutor_Error {
Unauthorized
InsufficientFunds
InsufficientFee
FeeQuoterBounce
}
4 changes: 4 additions & 0 deletions contracts/contracts/ccip/ccipsend_executor/types.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ struct CCIPSendExecutor_Addresses {
type CCIPSendExecutor_State =
| Cell<CCIPSendExecutor_State_Initialized>
| Cell<CCIPSendExecutor_State_OnGoingFeeValidation>
| Cell<CCIPSendExecutor_State_Finalized>

struct CCIPSendExecutor_State_Initialized {
}

struct CCIPSendExecutor_State_OnGoingFeeValidation {
}

struct CCIPSendExecutor_State_Finalized {
}

struct CCIPSendExecutor_Config {
feeQuoter: address,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,32 @@ tolk 1.2
const EVM_PRECOMPILE_SPACE = 1024;
const APTOS_PRECOMPILE_SPACE = 0x0b;

const ERROR_INVALID_EVM_ADDRESS = 5001;

struct EVMAddress {
value: uint256;
}

const TYPE_UINT160_MAX = (1 << 160) - 1; /* type(uint160).max */

fun CrossChainAddress.validateEVMAddress(self) {
fun CrossChainAddress.validateEVMAddress(self, error: int) {
var evmAddr: EVMAddress;
try {
evmAddr = EVMAddress.fromSlice(self);
} catch {
throw ERROR_INVALID_EVM_ADDRESS;
throw error;
}
assert (evmAddr.value <= TYPE_UINT160_MAX) throw ERROR_INVALID_EVM_ADDRESS;
assert (evmAddr.value >= EVM_PRECOMPILE_SPACE) throw ERROR_INVALID_EVM_ADDRESS;
assert (evmAddr.value <= TYPE_UINT160_MAX) throw error;
assert (evmAddr.value >= EVM_PRECOMPILE_SPACE) throw error;
}

struct a32ByteAddress {
value: uint256;
}

fun CrossChainAddress.validate32ByteAddress(self, minValue: uint256) {
fun CrossChainAddress.validate32ByteAddress(self, minValue: uint256, error: int) {
val addr = a32ByteAddress.fromSlice(self, {
throwIfOpcodeDoesNotMatch: ERROR_INVALID_EVM_ADDRESS,
throwIfOpcodeDoesNotMatch: error,
});
if (minValue > 0) {
assert (addr.value >= minValue) throw ERROR_INVALID_EVM_ADDRESS;
assert (addr.value >= minValue) throw error;
}
}
16 changes: 8 additions & 8 deletions contracts/contracts/ccip/fee_quoter/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -415,11 +415,11 @@ fun validateMessageAndResolveGasLimitForDestination(extraArgs: cell, config: Fee
if (a32ByteAddress.fromSlice(message.receiver).value == 0) {
// When message receiver is zero, CCIP receiver is not invoked on SVM.
// There should not be additional accounts specified for the receiver.
assert (accountsLength == 0) throw FeeQuoter_Error.InvalidSuiReceiverAddress;
assert (accountsLength == 0) throw FeeQuoter_Error.InvalidSVMReceiverAddress;
} else {
// The messaging accounts needed for CCIP receiver on SUI are:
// message receiver,
// plus remaining accounts specified in Sui extraArgs. Each account is 32 bytes.
// The messaging accounts needed for CCIP receiver on SVM are:
// message receiver, offRamp PDA signer,
// plus remaining accounts specified in SVM extraArgs. Each account is 32 bytes.
svmExpandedDataLength += ((accountsLength + SVM_MESSAGING_ACCOUNTS_OVERHEAD) * SVM_ACCOUNT_BYTE_SIZE);
}

Expand Down Expand Up @@ -479,15 +479,15 @@ fun parseSuiExtraArgs(extraArgs: cell, maxPerMsgGasLimit: uint32): SuiExtraArgsV

fun validateDestFamilyAddress(chainFamilySelector: uint32, receiver: CrossChainAddress, gasLimit: int) {
if (chainFamilySelector == CHAIN_FAMILY_SELECTOR_EVM) {
return receiver.validateEVMAddress();
return receiver.validateEVMAddress(FeeQuoter_Error.InvalidEVMReceiverAddress as int);
} else if (chainFamilySelector == CHAIN_FAMILY_SELECTOR_SVM) {
// SVM addresses don't have a precompile space at the first X addresses, instead we validate that if the gasLimit
// is non-zero, the address must not be 0x0.
return receiver.validate32ByteAddress(gasLimit > 0 ? 1 : 0);
return receiver.validate32ByteAddress(gasLimit > 0 ? 1 : 0, FeeQuoter_Error.Invalid32ByteReceiverAddress as int);
} else if (chainFamilySelector == CHAIN_FAMILY_SELECTOR_APTOS) {
return receiver.validate32ByteAddress(APTOS_PRECOMPILE_SPACE);
return receiver.validate32ByteAddress(APTOS_PRECOMPILE_SPACE, FeeQuoter_Error.Invalid32ByteReceiverAddress as int);
} else if (chainFamilySelector == CHAIN_FAMILY_SELECTOR_SUI) {
return receiver.validate32ByteAddress(gasLimit > 0 ? APTOS_PRECOMPILE_SPACE : 0);
return receiver.validate32ByteAddress(gasLimit > 0 ? APTOS_PRECOMPILE_SPACE : 0, FeeQuoter_Error.Invalid32ByteReceiverAddress as int);
} else {
throw FeeQuoter_Error.UnsupportedChainFamilySelector;
}
Expand Down
3 changes: 3 additions & 0 deletions contracts/contracts/ccip/fee_quoter/errors.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ enum FeeQuoter_Error {
ExtraArgOutOfOrderExecutionMustBeTrue
InvalidExtraArgsData
UnsupportedNumberOfTokens
InvalidEVMReceiverAddress
Invalid32ByteReceiverAddress
InvalidSuiReceiverAddress
InvalidSVMReceiverAddress
InvalidTokenReceiver
TooManySuiExtraArgsReceiverObjectIds
MsgDataTooLarge
Expand Down
6 changes: 4 additions & 2 deletions contracts/contracts/ccip/offramp/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,10 @@ fun onCommit(msg: OffRamp_Commit, sender: address, value: coins) {
var root: MerkleRoot? = null;

if (!priceUpdatesOnlyReport) {
// no batching of roots supported for now, so no need for an iterator
root = MerkleRoot.fromCell(report.merkleRoots);
// no batching of roots supported for now
var iter = report.merkleRoots.iter();
root = iter.next();
assert(iter.empty(), Error.BatchingNotSupported);
}

// check that the amount attached is enough to cover message execution and delivery off outgoing messages
Expand Down
1 change: 1 addition & 0 deletions contracts/contracts/ccip/offramp/errors.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ enum Error {
TooManyMessagesInReport
SignatureVerificationRequiredInCommitPlugin
SignatureVerificationNotAllowedInExecutionPlugin
BatchingNotSupported
}
33 changes: 22 additions & 11 deletions contracts/contracts/ccip/onramp/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ fun onInternalMessage(in: InMessage) {
var st = lazy OnRamp_Storage.load();
val ccipSend = msg.msg.load();
// validate allowlist
val destChainConfig = st.destChainConfigs.mustGet(ccipSend.destChainSelector, Error.UnknownDestChainSelector as int);
val destChainConfigResult = st.destChainConfigs.get(ccipSend.destChainSelector);
if (!destChainConfigResult.isFound) {
// This is a config error. The Router should not be sending messages to unknown chains.
return rejectMessage(Error.UnknownDestChainSelector as int, in.senderAddress, ccipSend, msg.metadata.sender);
}
val destChainConfig = destChainConfigResult.loadValue();
// ccipSend must be forwarded from the router
assert(destChainConfig.router == in.senderAddress, Error.Unauthorized);
onSend(st, destChainConfig, msg)
Expand Down Expand Up @@ -223,9 +228,11 @@ destination chain configuration is enabled, enforces the optional allowlist,
and deploys a CCIPSendExecutor to perform the send, carrying remaining value.
*/
fun onSend(st: OnRamp_Storage, destChainConfig: OnRamp_DestChainConfig, payload: OnRamp_Send) {
if (destChainConfig.allowlistEnabled) {
if (destChainConfig.allowlistEnabled) {
val entry = destChainConfig.allowedSenders.get(payload.metadata.sender);
assert(entry.isFound && entry.loadValue(), Error.SenderNotAllowed);
if (!entry.isFound || !entry.loadValue()) {
return rejectMessage(Error.SenderNotAllowed as int, destChainConfig.router, payload.msg.load(), payload.metadata.sender);
}
}

val config = st.config.load();
Expand Down Expand Up @@ -334,18 +341,22 @@ Router_MessageRejected notification to the Router on the destination chain
with error details for tracking and recovery.
*/
fun onExecutorFinishedWithError(st: OnRamp_Storage, msg: OnRamp_ExecutorFinishedWithError) {
val ccipsend: Router_CCIPSend = msg.msg.load();
val ccipSend: Router_CCIPSend = msg.msg.load();
val metadata = msg.metadata;
val router = st.destChainConfigs.mustGet(ccipSend.destChainSelector, Error.UnknownDestChainSelector as int).router;
rejectMessage(msg.error, router, ccipSend, metadata.sender);
}

fun rejectMessage(error: int, router: address, ccipSend: Router_CCIPSend, sender: address) {
val errorMsg = createMessage({
bounce: false,
value: 0,
dest: st.destChainConfigs.mustGet(ccipsend.destChainSelector, Error.UnknownDestChainSelector as int).router,
dest: router,
body: Router_MessageRejected {
queryID: ccipsend.queryID,
destChainSelector: ccipsend.destChainSelector,
sender: metadata.sender,
error: msg.error,
queryID: ccipSend.queryID,
destChainSelector: ccipSend.destChainSelector,
sender,
error,
},
});
errorMsg.send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE);
Expand Down Expand Up @@ -511,8 +522,8 @@ get fun sendExecutorCode(): cell {
val st = lazy OnRamp_Storage.load();
return st.executor.executorCode;
}

get fun getCCIPSendExecutorCodeHash(): uint256 {
get fun sendExecutorCodeHash(): uint256 {
val st = lazy OnRamp_Storage.load();
return st.executor.executorCode.hash();
}
Expand Down
6 changes: 3 additions & 3 deletions contracts/contracts/ccip/onramp/messages.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type OnRamp_NotificationInMessage =
| OnRamp_Send
;

struct (0x10000002) OnRamp_Send {
struct (0xdcf993c2) OnRamp_Send {
// queryId is embedded in the first element of CCIPSend
msg: Cell<Router_CCIPSend>;
metadata: Metadata
Expand All @@ -45,7 +45,7 @@ struct OnRamp_GetValidatedFeeContext {
userContext: RemainingBitsOrRef<RemainingBitsAndRefs>,
}

struct (0x10000003) OnRamp_SetDynamicConfig {
struct (0xa178c62e) OnRamp_SetDynamicConfig {
config: OnRamp_DynamicConfig
}

Expand All @@ -63,7 +63,7 @@ struct (0xC4068E21) OnRamp_ExecutorFinishedWithError {
metadata: Metadata,
}

struct (0x10000004) OnRamp_UpdateDestChainConfigs {
struct (0x1a246b6c) OnRamp_UpdateDestChainConfigs {
updates: SnakedCell<OnRampUpdateDestChainConfig>;
}

Expand Down
14 changes: 13 additions & 1 deletion contracts/contracts/ccip/router/contract.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ fun onInternalMessage(in: InMessage) {
Router_RouteMessage => {
val st = lazy Storage.load();
val message: Any2TVMMessage = msg.message.load();
val expectedOffRamp = st.offRamps.mustGet(message.sourceChainSelector, Router_Error.DestChainNotEnabled as int);
val expectedOffRamp = st.offRamps.mustGet(message.sourceChainSelector, Router_Error.SourceChainNotEnabled as int);
assert(in.senderAddress == expectedOffRamp, Router_Error.SenderIsNotOffRamp);
onRouteMessage(msg, message);
}
Expand Down Expand Up @@ -599,6 +599,18 @@ get fun pendingOwner(): address? {
return st.ownable.get_pendingOwner();
}

/// Gets the current owner of the contract.
get fun rmn_owner(): address {
val st: Storage = lazy Storage.load();
return st.rmnRemote.load().admin.get_owner();
}

/// Gets the pending owner of the contract, if any.
get fun rmn_pendingOwner(): address? {
val st = lazy Storage.load();
return st.rmnRemote.load().admin.get_pendingOwner();
}

get fun typeAndVersion(): (slice, slice) {
return ("com.chainlink.ton.ccip.Router", CONTRACT_VERSION);
}
Expand Down
12 changes: 6 additions & 6 deletions contracts/contracts/ccip/router/messages.tolk
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type Router_InMsg =
type Router_BouncedMessage = Receiver_CCIPReceive;

// update multiple chain selectors in batch to the same onramp/offramp address
struct (0xf6b0a5ca) Router_ApplyRampUpdates {
struct (0x7db6745d) Router_ApplyRampUpdates {
queryId: uint64;
onRampUpdates: OnRamps?;
offRampAdds: OffRamps?;
Expand All @@ -40,7 +40,7 @@ struct (0xf6b0a5ca) Router_ApplyRampUpdates {
// TODO should separate CCIPSend msg (with opcode) from CCIPSend data

// Answers with Router_CCIPReceive
struct (0x31768d95) Router_CCIPSend {
struct (0x38a69e3b) Router_CCIPSend {
queryID: uint64;
destChainSelector: uint64;
receiver: CrossChainAddress;
Expand All @@ -57,23 +57,23 @@ struct (0xfc69c50b) Router_RouteMessage {
gasLimit: coins;
}

struct (0x1e55bbf6) Router_CCIPReceiveConfirm {
struct (0xaf0cccef) Router_CCIPReceiveConfirm {
execId: ReceiveExecutorId;
}

// curse/uncurse/verify uncursed (also as getter)
struct (0x41e8c1dc) Router_RMNRemoteCurse {
struct (0xe6bf1813) Router_RMNRemoteCurse {
queryId: uint64
subjects: SnakedCell<uint128>
}

struct (0x3c3f5e73) Router_RMNRemoteUncurse {
struct (0x060d9dd1) Router_RMNRemoteUncurse {
queryId: uint64
subjects: SnakedCell<uint128>
}

// Internal contracts can have their own cache, but external contracts will need to fetch the curse status via this method.
struct (0xa6e4b7e1) Router_RMNRemoteVerifyNotCursed {
struct (0x49fd38ce) Router_RMNRemoteVerifyNotCursed {
queryId: uint64
subject: uint128
}
Expand Down
5 changes: 5 additions & 0 deletions contracts/contracts/test/mock/bouncer.tolk
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Small contract that bounces all incoming messages to test onBouncedMessage handler

fun onInternalMessage(in: InMessage) {
throw 0xFFFF;
}
22 changes: 21 additions & 1 deletion contracts/tests/ccip/OffRamp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ describe('OffRamp - Unit Tests', () => {

it('supports ownable messages', async () => {
const other = await blockchain.treasury('other')
await ownable2StepSpec.ownable2StepSpec(deployer, other, offRamp)
await ownable2StepSpec.ownable2StepSpec(deployer, other, offRamp, {})
})

it('should deploy', async () => {
Expand Down Expand Up @@ -874,6 +874,26 @@ describe('OffRamp - Unit Tests', () => {
})
})

it('Test commit report fails if more than one merkle root', async () => {
const message = createTestMessage()
const metadataHash = uint8ArrayToBigInt(getMetadataHash(CHAINSEL_EVM_TEST_90000001))
const rootBytes = uint8ArrayToBigInt(generateMessageId(message, metadataHash))
const root1 = createMerkleRoot(1n, 1n, rootBytes)
const root2 = createMerkleRoot(2n, 2n, rootBytes)

await setupOCRConfig()
await setupSourceChainConfig()

await commitReport(
[root1, root2],
toNano('0.5'),
0x01,
undefined,
false,
OffRampError.BatchingNotSupported,
)
})

it('Test commit report fails if source chain is not enabled', async () => {
const message = createTestMessage()
const metadataHash = uint8ArrayToBigInt(getMetadataHash(CHAINSEL_EVM_TEST_90000001))
Expand Down
2 changes: 1 addition & 1 deletion contracts/tests/ccip/Receiver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe('Receiver', () => {
success: true,
deploy: false,
body: rt.builder.message.in.ccipReceiveConfirm
.encode({ rootId: ccipReceiveSampleMessage.rootId })
.encode({ execID: ccipReceiveSampleMessage.rootId })
.endCell(),
})

Expand Down
Loading
Loading