diff --git a/Scarb.lock b/Scarb.lock index e9f77ba..7389f62 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -54,8 +54,20 @@ dependencies = [ ] [[package]] -name = "hyperlane_interfaces" +name = "hyperlane_starknet" version = "0.1.0" dependencies = [ "alexandria_bytes", + "openzeppelin", + "snforge_std", ] + +[[package]] +name = "openzeppelin" +version = "0.10.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?rev=f22438c#f22438cca0e81c807d7c1742e83050ebd0ffcb3b" + +[[package]] +name = "snforge_std" +version = "0.22.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.22.0#9b215944c6c5871c738381b4ded61bbf06e7ba35" diff --git a/Scarb.toml b/Scarb.toml index 197dc49..b3b678a 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -1,5 +1,4 @@ -[workspace] -members = ["crates/hyperlane_interfaces"] +[package] name = "hyperlane_starknet" description = "Implementation of the Hyperlane protocol on Starknet." version = "0.1.0" @@ -8,14 +7,16 @@ cairo-version = "2.6.3" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html -[workspace.dependencies] +[dependencies] starknet = "2.6.3" alexandria_bytes = { git = "https://github.com/keep-starknet-strange/alexandria.git" } +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", rev = "f22438c" } -[workspace.dev-dependencies] + +[dev-dependencies] snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.22.0" } -[workspace.tool.fmt] +[tool.fmt] sort-module-level-items = true [[target.starknet-contract]] diff --git a/crates/hyperlane_interfaces/Scarb.toml b/crates/hyperlane_interfaces/Scarb.toml deleted file mode 100644 index 2ef1c20..0000000 --- a/crates/hyperlane_interfaces/Scarb.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "hyperlane_interfaces" -version = "0.1.0" -description = "Interfaces to interact with the hyperlane protocol." -homepage = "https://github.com/astraly-labs/hyperlane-starknet/tree/main/crates/hyperlane_interfaces" -edition = "2023_11" - -[tool] -fmt.workspace = true - -[dependencies] -starknet.workspace = true -alexandria_bytes.workspace = true diff --git a/crates/hyperlane_interfaces/src/lib.cairo b/crates/hyperlane_interfaces/src/lib.cairo deleted file mode 100644 index b5d5572..0000000 --- a/crates/hyperlane_interfaces/src/lib.cairo +++ /dev/null @@ -1,72 +0,0 @@ -use alexandria_bytes::Bytes; -use starknet::ContractAddress; - -#[starknet::interface] -trait IMailbox { - fn local_domain(self: @TContractState) -> u32; - - fn delivered(self: @TContractState, message_id: Bytes) -> bool; - - fn default_ism(self: @TContractState) -> ContractAddress; - - fn default_hook(self: @TContractState) -> ContractAddress; - - fn required_hook(self: @TContractState) -> ContractAddress; - - fn latest_dispatched_id(self: @TContractState) -> Bytes; - - fn dispatch( - ref self: TContractState, - destination_domain: u32, - recipient_address: Bytes, - message_body: Bytes, - custom_hook_metadata: Option, - custom_hook: Option, - ) -> Bytes; - - fn quote_dispatch( - ref self: TContractState, - destination_domain: u32, - recipient_address: Bytes, - message_body: Bytes, - custom_hook_metadata: Option, - custom_hook: Option, - ) -> u256; - - fn process(ref self: TContractState, metadata: Bytes, message: Bytes,); - - fn recipient_ism(ref self: TContractState, recipient: ContractAddress) -> ContractAddress; -} - -#[derive(Serde)] -pub enum ModuleType { - UNUSED, - ROUTING, - AGGREGATION, - LEGACY_MULTISIG, - MERKLE_ROOT_MULTISIG, - MESSAGE_ID_MULTISIG, - NULL, // used with relayer carrying no metadata - CCIP_READ, -} - -#[starknet::interface] -trait IInterchainSecurityModule { - /// Returns an enum that represents the type of security model encoded by this ISM. - /// Relayers infer how to fetch and format metadata. - fn module_type(self: @TContractState) -> ModuleType; - - /// Defines a security model responsible for verifying interchain messages based on the provided metadata. - /// Returns true if the message was verified. - /// - /// # Arguments - /// * `_metadata` - Off-chain metadata provided by a relayer, specific to the security model encoded by - /// the module (e.g. validator signatures) - /// * `_message` - Hyperlane encoded interchain message - fn verify(self: @TContractState, metadata: Bytes, message: Bytes) -> bool; -} - -#[starknet::interface] -trait ISpecifiesInterchainSecurityModule { - fn interchain_security_module(self: @TContractState) -> ContractAddress; -} diff --git a/src/contracts/client/mailboxclient.cairo b/src/contracts/client/mailboxclient.cairo new file mode 100644 index 0000000..1d4f671 --- /dev/null +++ b/src/contracts/client/mailboxclient.cairo @@ -0,0 +1,122 @@ +#[starknet::contract] +mod mailboxclient { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + use hyperlane_starknet::interfaces::{ + IMailbox, IMailboxDispatcher, IMailboxDispatcherTrait, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait, IMailboxClient, + }; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ContractAddress, contract_address_const, ClassHash}; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + + #[storage] + struct Storage { + mailbox: ContractAddress, + local_domain: u32, + hook: ContractAddress, + interchain_security_module: ContractAddress, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + + #[constructor] + fn constructor(ref self: ContractState, _mailbox: ContractAddress, _owner: ContractAddress) { + self.mailbox.write(_mailbox); + let mailbox = IMailboxDispatcher { contract_address: _mailbox }; + let local_domain = mailbox.get_local_domain(); + self.local_domain.write(local_domain); + self.ownable.initializer(_owner); + } + #[abi(embed_v0)] + impl Upgradeable of IUpgradeable { + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable._upgrade(new_class_hash); + } + } + + + #[abi(embed_v0)] + impl IMailboxClientImpl of IMailboxClient { + fn set_hook(ref self: ContractState, _hook: ContractAddress) { + self.ownable.assert_only_owner(); + self.hook.write(_hook); + } + + fn set_interchain_security_module(ref self: ContractState, _module: ContractAddress) { + self.ownable.assert_only_owner(); + self.interchain_security_module.write(_module); + } + + fn _MailboxClient_initialize( + ref self: ContractState, + _hook: ContractAddress, + _interchain_security_module: ContractAddress, + ) { + self.ownable.assert_only_owner(); + self.set_hook(_hook); + self.set_interchain_security_module(_interchain_security_module); + } + + fn _is_latest_dispatched(self: @ContractState, _id: u256) -> bool { + let mailbox_address = self.mailbox.read(); + let mailbox = IMailboxDispatcher { contract_address: mailbox_address }; + mailbox.get_latest_dispatched_id() == _id + } + + fn _is_delivered(self: @ContractState, _id: u256) -> bool { + let mailbox_address = self.mailbox.read(); + let mailbox = IMailboxDispatcher { contract_address: mailbox_address }; + mailbox.delivered(_id) + } + + fn _dispatch( + self: @ContractState, + _destination_domain: u32, + _recipient: ContractAddress, + _message_body: Bytes, + _hook_metadata: Option, + _hook: Option + ) -> u256 { + let mailbox_address = self.mailbox.read(); + let mailbox = IMailboxDispatcher { contract_address: mailbox_address }; + mailbox.dispatch(_destination_domain, _recipient, _message_body, _hook_metadata, _hook) + } + + fn quote_dispatch( + self: @ContractState, + _destination_domain: u32, + _recipient: ContractAddress, + _message_body: Bytes, + _hook_metadata: Option, + _hook: Option + ) -> u256 { + let mailbox_address = self.mailbox.read(); + let mailbox = IMailboxDispatcher { contract_address: mailbox_address }; + mailbox + .quote_dispatch( + _destination_domain, _recipient, _message_body, _hook_metadata, _hook + ) + } + } +} diff --git a/src/contracts/client/router.cairo b/src/contracts/client/router.cairo new file mode 100644 index 0000000..0bd031e --- /dev/null +++ b/src/contracts/client/router.cairo @@ -0,0 +1,131 @@ +#[starknet::contract] +mod router { + use hyperlane_starknet::contracts::libs::message::Message; + use hyperlane_starknet::interfaces::IRouter; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + + use starknet::{ContractAddress, get_caller_address, ClassHash, contract_address_const}; + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + type Domain = u32; + #[storage] + struct Storage { + routers: LegacyMap, + mailbox: ContractAddress, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + + mod Errors { + pub const LENGTH_MISMATCH: felt252 = 'Domains and Router len mismatch'; + pub const CALLER_IS_NOT_MAILBOX: felt252 = 'Caller is not mailbox'; + pub const NO_ROUTER_FOR_DOMAIN: felt252 = 'No router for domain'; + pub const ENROLLED_ROUTER_AND_SENDER_MISMATCH: felt252 = 'Enrolled router/sender mismatch'; + } + + #[constructor] + fn constructor(ref self: ContractState, _mailbox: ContractAddress) { + self.mailbox.write(_mailbox); + } + + #[abi(embed_v0)] + impl Upgradeable of IUpgradeable { + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable._upgrade(new_class_hash); + } + } + + #[abi(embed_v0)] + impl IRouterImpl of IRouter { + fn routers(self: @ContractState, _domain: u32) -> ContractAddress { + self.routers.read(_domain) + } + + fn unenroll_remote_router(ref self: ContractState, _domain: u32) { + self.ownable.assert_only_owner(); + _unenroll_remote_router(ref self, _domain); + } + + fn enroll_remote_router(ref self: ContractState, _domain: u32, _router: ContractAddress) { + self.ownable.assert_only_owner(); + _enroll_remote_router(ref self, _domain, _router); + } + + fn enroll_remote_routers( + ref self: ContractState, _domains: Span, _routers: Span + ) { + self.ownable.assert_only_owner(); + assert(_domains.len() == _routers.len(), Errors::LENGTH_MISMATCH); + let length = _domains.len(); + let mut cur_idx = 0; + loop { + if (cur_idx == length) { + break (); + } + _enroll_remote_router(ref self, *_domains.at(cur_idx), *_routers.at(cur_idx)); + cur_idx += 1; + } + } + + fn unenroll_remote_routers(ref self: ContractState, _domains: Span) { + self.ownable.assert_only_owner(); + let length = _domains.len(); + let mut cur_idx = 0; + loop { + if (cur_idx == length) { + break (); + } + _unenroll_remote_router(ref self, *_domains.at(cur_idx)); + cur_idx += 1; + } + } + + fn handle(self: @ContractState, _origin: u32, _sender: ContractAddress, _message: Message) { + let caller = get_caller_address(); + assert(caller == self.mailbox.read(), Errors::CALLER_IS_NOT_MAILBOX); + let router = _must_have_remote_router(self, _origin); + assert(router == _sender, Errors::ENROLLED_ROUTER_AND_SENDER_MISMATCH); + _handle(_origin, _sender, _message); + } + } + + fn _unenroll_remote_router(ref self: ContractState, _domain: u32) { + self.routers.write(_domain, contract_address_const::<0>()); + } + + fn _enroll_remote_router(ref self: ContractState, _domain: u32, _address: ContractAddress) { + self.routers.write(_domain, _address); + } + + fn _must_have_remote_router(self: @ContractState, _domain: u32) -> ContractAddress { + let router = self.routers.read(_domain); + assert(router != 0.try_into().unwrap(), Errors::NO_ROUTER_FOR_DOMAIN); + router + } + + fn _is_remote_Router(self: @ContractState, _domain: u32, _address: ContractAddress) -> bool { + let router = self.routers.read(_domain); + router == _address + } + + fn _handle(_origin: u32, _sender: ContractAddress, _message: Message) {} +} diff --git a/src/contracts/libs/message.cairo b/src/contracts/libs/message.cairo new file mode 100644 index 0000000..06fe24f --- /dev/null +++ b/src/contracts/libs/message.cairo @@ -0,0 +1,72 @@ +use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; +use core::keccak::keccak_u256s_be_inputs; +use core::poseidon::poseidon_hash_span; +use hyperlane_starknet::utils::keccak256::reverse_endianness; +use starknet::{ContractAddress, contract_address_const}; + +pub const HYPERLANE_VERSION: u8 = 3; + + +#[derive(Serde, starknet::Store, Drop, Clone)] +pub struct Message { + pub version: u8, + pub nonce: u32, + pub origin: u32, + pub sender: ContractAddress, + pub destination: u32, + pub recipient: ContractAddress, + pub body: Bytes, +} + + +#[generate_trait] +pub impl MessageImpl of MessageTrait { + /// Generate a default empty message + /// + /// # Returns + /// + /// * An empty message structure + fn default() -> Message { + Message { + version: 3_u8, + nonce: 0_u32, + origin: 0_u32, + sender: contract_address_const::<0>(), + destination: 0_u32, + recipient: contract_address_const::<0>(), + body: BytesTrait::new_empty(), + } + } + + /// Format an input message, using reverse keccak big endian + /// + /// # Arguments + /// + /// * `_message` - Message to hash + /// + /// # Returns + /// + /// * u256 representing the hash of the message + fn format_message(_message: Message) -> u256 { + let sender: felt252 = _message.sender.into(); + let recipient: felt252 = _message.recipient.into(); + + let mut input: Array = array![ + _message.version.into(), + _message.origin.into(), + sender.into(), + _message.destination.into(), + recipient.into(), + _message.body.size().into() + ]; + let mut message_data = _message.body.data(); + loop { + match message_data.pop_front() { + Option::Some(data) => { input.append(data.into()); }, + Option::None(_) => { break (); } + }; + }; + let hash = keccak_u256s_be_inputs(input.span()); + reverse_endianness(hash) + } +} diff --git a/src/contracts/mailbox.cairo b/src/contracts/mailbox.cairo new file mode 100644 index 0000000..d703c2e --- /dev/null +++ b/src/contracts/mailbox.cairo @@ -0,0 +1,476 @@ +#[starknet::contract] +pub mod mailbox { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + use core::starknet::SyscallResultTrait; + use core::starknet::event::EventEmitter; + use hyperlane_starknet::contracts::libs::message::{Message, MessageTrait, HYPERLANE_VERSION}; + use hyperlane_starknet::interfaces::{ + IMailbox, IMailboxDispatcher, IMailboxDispatcherTrait, IInterchainSecurityModuleDispatcher, + IInterchainSecurityModuleDispatcherTrait, IPostDispatchHookDispatcher, + ISpecifiesInterchainSecurityModuleDispatcher, + ISpecifiesInterchainSecurityModuleDispatcherTrait, IPostDispatchHookDispatcherTrait, + IMessageRecipientDispatcher, IMessageRecipientDispatcherTrait, + }; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ + ContractAddress, ClassHash, get_caller_address, get_block_number, contract_address_const + }; + + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[derive(Drop, Serde, starknet::Store)] + struct Delivery { + processor: ContractAddress, + block_number: u64, + } + + + #[storage] + struct Storage { + // Domain of chain on which the contract is deployed + local_domain: u32, + // A monotonically increasing nonce for outbound unique message IDs. + nonce: u32, + // The latest dispatched message ID used for auth in post-dispatch hooks. + latest_dispatched_id: u256, + // The default ISM, used if the recipient fails to specify one. + default_ism: ContractAddress, + // The default post dispatch hook, used for post processing of opting-in dispatches. + default_hook: ContractAddress, + // The required post dispatch hook, used for post processing of ALL dispatches. + required_hook: ContractAddress, + // Mapping of message ID to delivery context that processed the message. + deliveries: LegacyMap::, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + DefaultIsmSet: DefaultIsmSet, + DefaultHookSet: DefaultHookSet, + RequiredHookSet: RequiredHookSet, + Process: Process, + ProcessId: ProcessId, + Dispatch: Dispatch, + DispatchId: DispatchId, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + #[derive(starknet::Event, Drop)] + pub struct DefaultIsmSet { + pub module: ContractAddress + } + + #[derive(starknet::Event, Drop)] + pub struct DefaultHookSet { + pub hook: ContractAddress + } + + #[derive(starknet::Event, Drop)] + pub struct RequiredHookSet { + pub hook: ContractAddress + } + + #[derive(starknet::Event, Drop)] + pub struct Process { + pub origin: u32, + pub sender: ContractAddress, + pub recipient: ContractAddress + } + + #[derive(starknet::Event, Drop)] + pub struct ProcessId { + pub id: u256 + } + + #[derive(starknet::Event, Drop)] + pub struct Dispatch { + pub sender: ContractAddress, + pub destination_domain: u32, + pub recipient_address: ContractAddress, + pub message: u256 + } + + #[derive(starknet::Event, Drop)] + pub struct DispatchId { + pub id: u256 + } + + + mod Errors { + pub const WRONG_HYPERLANE_VERSION: felt252 = 'Wrong hyperlane version'; + pub const UNEXPECTED_DESTINATION: felt252 = 'Unexpected destination'; + pub const ALREADY_DELIVERED: felt252 = 'Mailbox: already delivered'; + pub const ISM_VERIFICATION_FAILED: felt252 = 'Mailbox:ism verification failed'; + pub const NO_ISM_FOUND: felt252 = 'ISM: no ISM found'; + pub const NEW_OWNER_IS_ZERO: felt252 = 'Ownable: new owner cannot be 0'; + pub const ALREADY_OWNER: felt252 = 'Ownable: already owner'; + } + + #[constructor] + fn constructor(ref self: ContractState, _local_domain: u32, owner: ContractAddress) { + self.local_domain.write(_local_domain); + self.ownable.initializer(owner); + } + + #[abi(embed_v0)] + impl Upgradeable of IUpgradeable { + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgradeable._upgrade(new_class_hash); + } + } + + + #[abi(embed_v0)] + impl IMailboxImpl of IMailbox { + fn initializer( + ref self: ContractState, + _default_ism: ContractAddress, + _default_hook: ContractAddress, + _required_hook: ContractAddress + ) { + self.set_default_ism(_default_ism); + self.set_default_hook(_default_hook); + self.set_required_hook(_required_hook); + } + + + fn get_local_domain(self: @ContractState) -> u32 { + self.local_domain.read() + } + + fn get_default_ism(self: @ContractState) -> ContractAddress { + self.default_ism.read() + } + + fn get_default_hook(self: @ContractState) -> ContractAddress { + self.default_hook.read() + } + + fn get_required_hook(self: @ContractState) -> ContractAddress { + self.required_hook.read() + } + + fn get_latest_dispatched_id(self: @ContractState) -> u256 { + self.latest_dispatched_id.read() + } + + /// Sets the default ISM for the Mailbox. + /// Callable only by the admin + /// + /// # Arguments + /// + /// * `_hook` - The new default ISM + fn set_default_ism(ref self: ContractState, _module: ContractAddress) { + self.ownable.assert_only_owner(); + self.default_ism.write(_module); + self.emit(DefaultIsmSet { module: _module }); + } + + /// Sets the default post dispatch hook for the Mailbox. + /// Callable only by the admin + /// + /// # Arguments + /// + /// * `_hook` - The new default post dispatch hook. + fn set_default_hook(ref self: ContractState, _hook: ContractAddress) { + self.ownable.assert_only_owner(); + self.default_hook.write(_hook); + self.emit(DefaultHookSet { hook: _hook }); + } + + /// Sets the required post dispatch hook for the Mailbox. + /// Callable only by the admin + /// + /// # Arguments + /// + /// * `_hook` - The new required post dispatch hook. + fn set_required_hook(ref self: ContractState, _hook: ContractAddress) { + self.ownable.assert_only_owner(); + self.required_hook.write(_hook); + self.emit(RequiredHookSet { hook: _hook }); + } + + /// Sets the domain of chain for the mailbox + /// Callable only by the admin + /// + /// # Arguments + /// + /// * `_local_domain` - The new local domain + fn set_local_domain(ref self: ContractState, _local_domain: u32) { + self.ownable.assert_only_owner(); + self.local_domain.write(_local_domain); + } + + /// Dispatches a message to the destination domain & recipient using the default hook and empty metadata. + /// + /// # Arguments + /// + /// * `_destination_domain` - Domain of destination chain + /// * `_recipient_address` - Address of recipient on destination chain + /// * `_message_body` - Raw bytes content of message body + /// * `_custom_hook_metadata` - Metadata used by the post dispatch hook + /// * `_custom_hook` - Custom hook to use instead of the default + /// + /// # Returns + /// + /// * The message ID inserted into the Mailbox's merkle tree + fn dispatch( + ref self: ContractState, + _destination_domain: u32, + _recipient_address: ContractAddress, + _message_body: Bytes, + _custom_hook_metadata: Option, + _custom_hook: Option + ) -> u256 { + let hook = match _custom_hook { + Option::Some(hook) => hook, + Option::None(()) => self.default_hook.read(), + }; + let hook_metadata = match _custom_hook_metadata { + Option::Some(hook_metadata) => hook_metadata, + Option::None(()) => BytesTrait::new_empty() + }; + + let message = build_message( + @self, _destination_domain, _recipient_address, _message_body + ); + let id = message; + self.latest_dispatched_id.write(id); + let current_nonce = self.nonce.read(); + self.nonce.write(current_nonce + 1); + let caller = get_caller_address(); + self + .emit( + Dispatch { + sender: caller, + destination_domain: _destination_domain, + recipient_address: _recipient_address, + message: message + } + ); + self.emit(DispatchId { id: id }); + + // + // HOOKS + // + + // let required_hook_address = self.required_hook.read(); + // let required_hook = IPostDispatchHookDispatcher { + // contract_address: required_hook_address + // }; + // if (hook != contract_address_const::<0>() ){ + // let hook = IPostDispatchHookDispatcher { contract_address: hook }; + // hook.post_dispatch(hook_metadata.clone(), message); + // } + // required_hook.post_dispatch(hook_metadata, message); + + id + } + + /// Returns true if the message has been processed. + /// + /// # Arguments + /// + /// * `_message_id` - The message ID to check. + /// + /// # Returns + /// + /// * True if the message has been delivered. + fn delivered(self: @ContractState, _message_id: u256) -> bool { + self.deliveries.read(_message_id).block_number > 0 + } + + fn nonce(self: @ContractState) -> u32 { + self.nonce.read() + } + + /// Attempts to deliver `_message` to its recipient. Verifies `_message` via the recipient's ISM using the provided `_metadata` + /// + /// # Arguments + /// + /// * `_metadata` - Metadata used by the ISM to verify `_message`. + /// * `_message` - Formatted Hyperlane message (ref: message.cairo) + fn process(ref self: ContractState, _metadata: Bytes, _message: Message) { + assert(_message.version == HYPERLANE_VERSION, Errors::WRONG_HYPERLANE_VERSION); + assert( + _message.destination == self.local_domain.read(), Errors::UNEXPECTED_DESTINATION + ); + let id = MessageTrait::format_message(_message.clone()); + let caller = get_caller_address(); + let block_number = get_block_number(); + assert(!self.delivered(id), Errors::ALREADY_DELIVERED); + + // + // ISM + // + + // let recipient_ism = self.recipient_ism(_message.recipient); + // let ism = IInterchainSecurityModuleDispatcher { contract_address: recipient_ism }; + + // + // + // + + self.deliveries.write(id, Delivery { processor: caller, block_number: block_number }); + self + .emit( + Process { + origin: _message.origin, + sender: _message.sender, + recipient: _message.recipient + } + ); + self.emit(ProcessId { id: id }); + + // + // ISM + // + + // assert(ism.verify(_metadata, _message.clone()), Errors::ISM_VERIFICATION_FAILED); + + // + // + // + let message_recipient = IMessageRecipientDispatcher { + contract_address: _message.recipient + }; + message_recipient.handle(_message.origin, _message.sender, _message.body); + } + + /// Computes quote for dispatching a message to the destination domain & recipient. + /// + /// # Arguments + /// + /// * `_destination_domain` - Domain of destination chain + /// * `_recipient_address` - Address of recipient on destination chain + /// * `_message_body` - Raw bytes content of message body + /// * `_custom_hook_metadata` - Metadata used by the post dispatch hook + /// * `_custom_hook` - Custom hook to use instead of the default + /// + /// # Returns + /// + /// * The message ID inserted into the Mailbox's merkle tree + fn quote_dispatch( + self: @ContractState, + _destination_domain: u32, + _recipient_address: ContractAddress, + _message_body: Bytes, + _custom_hook_metadata: Option, + _custom_hook: Option, + ) -> u256 { + let hook_address = match _custom_hook { + Option::Some(hook) => hook, + Option::None(()) => self.default_hook.read() + }; + let hook_metadata = match _custom_hook_metadata { + Option::Some(hook_metadata) => hook_metadata, + Option::None(()) => BytesTrait::new_empty(), + }; + let message = build_message( + self, _destination_domain, _recipient_address, _message_body.clone() + ); + let required_hook_address = self.required_hook.read(); + let required_hook = IPostDispatchHookDispatcher { + contract_address: required_hook_address + }; + let hook = IPostDispatchHookDispatcher { contract_address: hook_address }; + required_hook.quote_dispatch(hook_metadata.clone(), message.clone()) + + hook.quote_dispatch(hook_metadata, message) + } + + /// Returns the ISM to use for the recipient, defaulting to the default ISM if none is specified. + /// + /// # Arguments + /// + /// * `_recipient` - The message recipient whose ISM should be returned. + /// + /// # Returns + /// + /// * The ISM to use for `_recipient` + fn recipient_ism(self: @ContractState, _recipient: ContractAddress) -> ContractAddress { + let mut call_data: Array = ArrayTrait::new(); + let mut res = starknet::syscalls::call_contract_syscall( + _recipient, selector!("interchain_security_module"), call_data.span() + ); + let mut ism_res = match res { + Result::Ok(ism) => ism, + Result::Err(revert_reason) => { + assert(revert_reason == array!['ENTRYPOINT_FAILED'], Errors::NO_ISM_FOUND); + array![].span() + } + }; + if (ism_res.len() != 0) { + let ism_address = Serde::::deserialize(ref ism_res).unwrap(); + if (ism_address != contract_address_const::<0>()) { + return ism_address; + } + } + self.default_ism.read() + } + + /// Returns the account that processed the message. + /// + /// # Arguments + /// + /// * `_id` - The message ID to check. + /// + /// # Returns + /// + /// * The account that processed the message. + fn processor(self: @ContractState, _id: u256) -> ContractAddress { + self.deliveries.read(_id).processor + } + + /// Returns the account that processed the message. + /// + /// # Arguments + /// + /// * `_id` - The message ID to check. + /// + /// # Returns + /// + /// * The number of the block that the message was processed at. + fn processed_at(self: @ContractState, _id: u256) -> u64 { + self.deliveries.read(_id).block_number + } + } + + fn build_message( + self: @ContractState, + _destination_domain: u32, + _recipient_address: ContractAddress, + _message_body: Bytes + ) -> u256 { + let nonce = self.nonce.read(); + let local_domain = self.local_domain.read(); + let caller = get_caller_address(); + MessageTrait::format_message( + Message { + version: HYPERLANE_VERSION, + nonce: nonce, + origin: local_domain, + sender: caller, + destination: _destination_domain, + recipient: _recipient_address, + body: _message_body + } + ) + } +} + diff --git a/src/contracts/mocks/message_recipient.cairo b/src/contracts/mocks/message_recipient.cairo new file mode 100644 index 0000000..e2e7e93 --- /dev/null +++ b/src/contracts/mocks/message_recipient.cairo @@ -0,0 +1,39 @@ +#[starknet::contract] +pub mod message_recipient { + use alexandria_bytes::{Bytes, BytesTrait, BytesStore}; + use hyperlane_starknet::interfaces::{ + IMessageRecipient, IMessageRecipientDispatcher, IMessageRecipientDispatcherTrait + }; + use starknet::ContractAddress; + + + #[storage] + struct Storage { + origin: u32, + sender: ContractAddress, + message: Bytes + } + + #[abi(embed_v0)] + impl IMessageRecipientImpl of IMessageRecipient { + fn handle( + ref self: ContractState, _origin: u32, _sender: ContractAddress, _message: Bytes + ) { + self.message.write(_message); + self.origin.write(_origin); + self.sender.write(_sender); + } + + fn get_origin(self: @ContractState) -> u32 { + self.origin.read() + } + + fn get_sender(self: @ContractState) -> ContractAddress { + self.sender.read() + } + + fn get_message(self: @ContractState) -> Bytes { + self.message.read() + } + } +} diff --git a/src/interfaces.cairo b/src/interfaces.cairo new file mode 100644 index 0000000..12ed7ab --- /dev/null +++ b/src/interfaces.cairo @@ -0,0 +1,206 @@ +use alexandria_bytes::Bytes; +use hyperlane_starknet::contracts::libs::message::Message; +use starknet::ContractAddress; + +#[derive(Serde)] +pub enum Types { + UNUSED, + ROUTING, + AGGREGATION, + MERKLE_TREE, + INTERCHAIN_GAS_PAYMASTER, + FALLBACK_ROUTING, + ID_AUTH_ISM, + PAUSABLE, + PROTOCOL_FEE, + LAYER_ZERO_V1, + Rate_Limited_Hook +} + + +#[derive(Serde)] +pub enum ModuleType { + UNUSED, + ROUTING, + AGGREGATION, + LEGACY_MULTISIG, + MERKLE_ROOT_MULTISIG, + MESSAGE_ID_MULTISIG, + NULL, // used with relayer carrying no metadata + CCIP_READ, +} + +#[starknet::interface] +pub trait IMailbox { + fn initializer( + ref self: TContractState, + _default_ism: ContractAddress, + _default_hook: ContractAddress, + _required_hook: ContractAddress + ); + + fn get_local_domain(self: @TContractState) -> u32; + + fn delivered(self: @TContractState, _message_id: u256) -> bool; + + fn nonce(self: @TContractState) -> u32; + + fn get_default_ism(self: @TContractState) -> ContractAddress; + + fn get_default_hook(self: @TContractState) -> ContractAddress; + + fn get_required_hook(self: @TContractState) -> ContractAddress; + + fn get_latest_dispatched_id(self: @TContractState) -> u256; + + fn dispatch( + ref self: TContractState, + _destination_domain: u32, + _recipient_address: ContractAddress, + _message_body: Bytes, + _custom_hook_metadata: Option, + _custom_hook: Option, + ) -> u256; + + fn quote_dispatch( + self: @TContractState, + _destination_domain: u32, + _recipient_address: ContractAddress, + _message_body: Bytes, + _custom_hook_metadata: Option, + _custom_hook: Option, + ) -> u256; + + fn process(ref self: TContractState, _metadata: Bytes, _message: Message,); + + fn recipient_ism(self: @TContractState, _recipient: ContractAddress) -> ContractAddress; + + fn set_default_ism(ref self: TContractState, _module: ContractAddress); + + fn set_default_hook(ref self: TContractState, _hook: ContractAddress); + + fn set_required_hook(ref self: TContractState, _hook: ContractAddress); + + fn set_local_domain(ref self: TContractState, _local_domain: u32); + + fn processor(self: @TContractState, _id: u256) -> ContractAddress; + + fn processed_at(self: @TContractState, _id: u256) -> u64; +} + + +#[starknet::interface] +pub trait IInterchainSecurityModule { + /// Returns an enum that represents the type of security model encoded by this ISM. + /// Relayers infer how to fetch and format metadata. + fn module_type(self: @TContractState) -> ModuleType; + + /// Defines a security model responsible for verifying interchain messages based on the provided metadata. + /// Returns true if the message was verified. + /// + /// # Arguments + /// * `_metadata` - Off-chain metadata provided by a relayer, specific to the security model encoded by + /// the module (e.g. validator signatures) + /// * `_message` - Hyperlane encoded interchain message + fn verify(self: @TContractState, _metadata: Bytes, _message: Message) -> bool; +} + +#[starknet::interface] +pub trait ISpecifiesInterchainSecurityModule { + fn interchain_security_module(self: @TContractState) -> ContractAddress; +} + + +#[starknet::interface] +pub trait IPostDispatchHook { + fn get_hook_type(self: @TContractState) -> Types; + + fn supports_metadata(self: @TContractState, _metadata: Bytes) -> bool; + + fn post_dispatch(ref self: TContractState, _metadata: Bytes, _message: u256); + + fn quote_dispatch(ref self: TContractState, _metadata: Bytes, _message: u256) -> u256; +} + + +#[starknet::interface] +pub trait IMessageRecipient { + fn handle(ref self: TContractState, _origin: u32, _sender: ContractAddress, _message: Bytes); + + fn get_origin(self: @TContractState) -> u32; + + fn get_sender(self: @TContractState) -> ContractAddress; + + fn get_message(self: @TContractState) -> Bytes; +} + + +#[starknet::interface] +pub trait IMailboxClient { + fn set_hook(ref self: TContractState, _hook: ContractAddress); + + fn set_interchain_security_module(ref self: TContractState, _module: ContractAddress); + + fn _MailboxClient_initialize( + ref self: TContractState, + _hook: ContractAddress, + _interchain_security_module: ContractAddress, + ); + + fn _is_latest_dispatched(self: @TContractState, _id: u256) -> bool; + + fn _is_delivered(self: @TContractState, _id: u256) -> bool; + + fn _dispatch( + self: @TContractState, + _destination_domain: u32, + _recipient: ContractAddress, + _message_body: Bytes, + _hook_metadata: Option, + _hook: Option + ) -> u256; + + fn quote_dispatch( + self: @TContractState, + _destination_domain: u32, + _recipient: ContractAddress, + _message_body: Bytes, + _hook_metadata: Option, + _hook: Option + ) -> u256; +} + + +#[starknet::interface] +pub trait IInterchainGasPaymaster { + fn pay_for_gas( + ref self: TContractState, + _message_id: u256, + _destination_domain: u32, + _gas_amount: u256, + _payment: u256 + ); + + fn quote_gas_payment( + ref self: TContractState, _destination_domain: u32, _gas_amount: u256 + ) -> u256; +} + + +#[starknet::interface] +pub trait IRouter { + fn routers(self: @TContractState, _domain: u32) -> ContractAddress; + + fn unenroll_remote_router(ref self: TContractState, _domain: u32); + + fn enroll_remote_router(ref self: TContractState, _domain: u32, _router: ContractAddress); + + fn enroll_remote_routers( + ref self: TContractState, _domains: Span, _routers: Span + ); + + fn unenroll_remote_routers(ref self: TContractState, _domains: Span); + + fn handle(self: @TContractState, _origin: u32, _sender: ContractAddress, _message: Message); +} + diff --git a/src/lib.cairo b/src/lib.cairo new file mode 100644 index 0000000..71d0b62 --- /dev/null +++ b/src/lib.cairo @@ -0,0 +1,23 @@ +mod interfaces; +mod contracts { + pub mod mailbox; + pub mod libs { + pub mod message; + } + pub mod client { + pub mod mailboxclient; + pub mod router; + } + pub mod mocks { + pub mod message_recipient; + } +} +mod utils { + pub mod keccak256; +} + +#[cfg(test)] +mod tests { + pub mod setup; + pub mod test_mailbox; +} diff --git a/src/tests/setup.cairo b/src/tests/setup.cairo new file mode 100644 index 0000000..0ef29fd --- /dev/null +++ b/src/tests/setup.cairo @@ -0,0 +1,66 @@ +use core::result::ResultTrait; +use hyperlane_starknet::contracts::mocks::message_recipient::message_recipient; +use hyperlane_starknet::interfaces::{ + IMailboxDispatcher, IMailboxDispatcherTrait, IMessageRecipientDispatcher, + IMessageRecipientDispatcherTrait +}; +use snforge_std::{ + declare, ContractClassTrait, CheatTarget, EventSpy, EventAssertions, spy_events, SpyOn +}; + +use starknet::{ContractAddress, contract_address_const}; + +pub const LOCAL_DOMAIN: u32 = 534352; +pub const DESTINATION_DOMAIN: u32 = 9841001; + +pub fn OWNER() -> ContractAddress { + contract_address_const::<'OWNER'>() +} + +pub fn NEW_OWNER() -> ContractAddress { + contract_address_const::<'NEW_OWNER'>() +} + +pub fn DEFAULT_ISM() -> ContractAddress { + contract_address_const::<'DEFAULT_ISM'>() +} + +pub fn DEFAULT_HOOK() -> ContractAddress { + contract_address_const::<'DEFAULT_HOOK'>() +} + +pub fn REQUIRED_HOOK() -> ContractAddress { + contract_address_const::<'REQUIRED_HOOK'>() +} + +pub fn NEW_DEFAULT_ISM() -> ContractAddress { + contract_address_const::<'NEW_DEFAULT_ISM'>() +} + +pub fn NEW_DEFAULT_HOOK() -> ContractAddress { + contract_address_const::<'NEW_DEFAULT_HOOK'>() +} + +pub fn NEW_REQUIRED_HOOK() -> ContractAddress { + contract_address_const::<'NEW_REQUIRED_HOOK'>() +} + +pub fn RECIPIENT_ADDRESS() -> ContractAddress { + contract_address_const::<'RECIPIENT_ADDRESS'>() +} + +pub fn setup() -> (IMailboxDispatcher, EventSpy) { + let mailbox_class = declare("mailbox").unwrap(); + let (mailbox_addr, _) = mailbox_class + .deploy(@array![LOCAL_DOMAIN.into(), OWNER().into()]) + .unwrap(); + let mut spy = spy_events(SpyOn::One(mailbox_addr)); + (IMailboxDispatcher { contract_address: mailbox_addr }, spy) +} + +pub fn mock_setup() -> IMessageRecipientDispatcher { + let message_recipient_class = declare("message_recipient").unwrap(); + + let (message_recipient_addr, _) = message_recipient_class.deploy(@array![]).unwrap(); + IMessageRecipientDispatcher { contract_address: message_recipient_addr } +} diff --git a/src/tests/test_mailbox.cairo b/src/tests/test_mailbox.cairo new file mode 100644 index 0000000..024cbbb --- /dev/null +++ b/src/tests/test_mailbox.cairo @@ -0,0 +1,312 @@ +use alexandria_bytes::{Bytes, BytesTrait}; +use hyperlane_starknet::contracts::libs::message::{Message, MessageTrait, HYPERLANE_VERSION}; +use hyperlane_starknet::contracts::mailbox::mailbox; +use hyperlane_starknet::interfaces::IMessageRecipientDispatcherTrait; +use hyperlane_starknet::interfaces::{IMailbox, IMailboxDispatcher, IMailboxDispatcherTrait}; +use hyperlane_starknet::tests::setup::{ + setup, mock_setup, OWNER, LOCAL_DOMAIN, NEW_OWNER, DEFAULT_ISM, DEFAULT_HOOK, REQUIRED_HOOK, + NEW_DEFAULT_ISM, NEW_DEFAULT_HOOK, NEW_REQUIRED_HOOK, DESTINATION_DOMAIN, RECIPIENT_ADDRESS +}; +use openzeppelin::access::ownable::OwnableComponent; +use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use snforge_std::cheatcodes::events::EventAssertions; +use snforge_std::{start_prank, CheatTarget, stop_prank}; + +#[test] +fn test_local_domain() { + let (mailbox, _) = setup(); + assert(mailbox.get_local_domain() == LOCAL_DOMAIN, 'Wrong local domain'); +} + +#[test] +fn test_owner() { + let (mailbox, _) = setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + assert(ownable.owner() == OWNER(), 'Wrong contract owner'); +} + +#[test] +fn test_transfer_ownership() { + let (mailbox, mut spy) = setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER()); + ownable.transfer_ownership(NEW_OWNER()); + stop_prank(CheatTarget::One(ownable.contract_address)); + assert(ownable.owner() == NEW_OWNER(), 'Failed transfer ownership'); + + let expected_event = OwnableComponent::OwnershipTransferred { + previous_owner: OWNER(), new_owner: NEW_OWNER() + }; + spy + .assert_emitted( + @array![ + ( + ownable.contract_address, + OwnableComponent::Event::OwnershipTransferred(expected_event) + ) + ] + ); +} + +#[test] +fn test_initializer() { + let (mailbox, _) = setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER()); + mailbox.initializer(DEFAULT_ISM(), DEFAULT_HOOK(), REQUIRED_HOOK()); + assert(mailbox.get_default_hook() == DEFAULT_HOOK(), 'Failed to set default hook'); + assert(mailbox.get_required_hook() == REQUIRED_HOOK(), 'Failed to set required hook'); + assert(mailbox.get_default_ism() == DEFAULT_ISM(), 'Failed to set default ism'); +} + +#[test] +fn test_set_default_hook() { + let (mailbox, mut spy) = setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER()); + mailbox.set_default_hook(NEW_DEFAULT_HOOK()); + assert(mailbox.get_default_hook() == NEW_DEFAULT_HOOK(), 'Failed to set default hook'); + let expected_event = mailbox::Event::DefaultHookSet( + mailbox::DefaultHookSet { hook: NEW_DEFAULT_HOOK() } + ); + spy.assert_emitted(@array![(mailbox.contract_address, expected_event)]); +} + +#[test] +fn test_set_required_hook() { + let (mailbox, mut spy) = setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER()); + mailbox.set_required_hook(NEW_REQUIRED_HOOK()); + assert(mailbox.get_required_hook() == NEW_REQUIRED_HOOK(), 'Failed to set required hook'); + let expected_event = mailbox::Event::RequiredHookSet( + mailbox::RequiredHookSet { hook: NEW_REQUIRED_HOOK() } + ); + spy.assert_emitted(@array![(mailbox.contract_address, expected_event)]); +} + +#[test] +fn test_set_default_ism() { + let (mailbox, mut spy) = setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER()); + mailbox.set_default_ism(NEW_DEFAULT_ISM()); + assert(mailbox.get_default_ism() == NEW_DEFAULT_ISM(), 'Failed to set default ism'); + let expected_event = mailbox::Event::DefaultIsmSet( + mailbox::DefaultIsmSet { module: NEW_DEFAULT_ISM() } + ); + spy.assert_emitted(@array![(mailbox.contract_address, expected_event)]); +} +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_set_default_hook_fails_if_not_owner() { + let (mailbox, _) = setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), NEW_OWNER()); + mailbox.set_default_hook(NEW_DEFAULT_HOOK()); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_set_required_hook_fails_if_not_owner() { + let (mailbox, _) = setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), NEW_OWNER()); + mailbox.set_required_hook(NEW_REQUIRED_HOOK()); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_set_default_ism_fails_if_not_owner() { + let (mailbox, _) = setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), NEW_OWNER()); + mailbox.set_default_ism(NEW_DEFAULT_ISM()); +} + +#[test] +fn test_dispatch() { + let (mailbox, mut spy) = setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER()); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN, + recipient: RECIPIENT_ADDRESS(), + body: message_body.clone() + }; + let message_id = MessageTrait::format_message(message.clone()); + mailbox + .dispatch( + DESTINATION_DOMAIN, RECIPIENT_ADDRESS(), message_body, Option::None, Option::None + ); + let expected_event = mailbox::Event::Dispatch( + mailbox::Dispatch { + sender: OWNER(), + destination_domain: DESTINATION_DOMAIN, + recipient_address: RECIPIENT_ADDRESS(), + message: message_id + } + ); + let expected_event_id = mailbox::Event::DispatchId(mailbox::DispatchId { id: message_id }); + + spy + .assert_emitted( + @array![ + (mailbox.contract_address, expected_event), + (mailbox.contract_address, expected_event_id) + ] + ); + + assert(mailbox.get_latest_dispatched_id() == message_id, 'Failed to fetch latest id'); +} + + +#[test] +fn test_process() { + let (mailbox, mut spy) = setup(); + let mock_recipient = mock_setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER()); + mailbox.set_local_domain(DESTINATION_DOMAIN); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN, + recipient: mock_recipient.contract_address, + body: message_body.clone() + }; + let message_id = MessageTrait::format_message(message.clone()); + let metadata = message_body; + mailbox.process(metadata.clone(), message); + let expected_event = mailbox::Event::Process( + mailbox::Process { + origin: LOCAL_DOMAIN, sender: OWNER(), recipient: mock_recipient.contract_address, + } + ); + let expected_event_id = mailbox::Event::ProcessId(mailbox::ProcessId { id: message_id }); + + spy + .assert_emitted( + @array![ + (mailbox.contract_address, expected_event), + (mailbox.contract_address, expected_event_id) + ] + ); + let block_number = starknet::get_block_number(); + assert(mailbox.delivered(message_id), 'Failed to delivered(id)'); + assert(mailbox.processor(message_id) == OWNER(), 'Wrong processor'); + assert(mailbox.processed_at(message_id) == block_number, 'Wrong processed block number'); + assert(mock_recipient.get_origin() == LOCAL_DOMAIN, 'Failed to retrieve origin'); + assert(mock_recipient.get_sender() == OWNER(), 'Failed to retrieve sender'); + assert(mock_recipient.get_message() == metadata, 'Failed to retrieve metadata'); +} + + +#[test] +#[should_panic(expected: ('Wrong hyperlane version',))] +fn test_process_fails_if_version_mismatch() { + let (mailbox, _) = setup(); + let mock_recipient = mock_setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER()); + mailbox.set_local_domain(DESTINATION_DOMAIN); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION + 1, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN, + recipient: mock_recipient.contract_address, + body: message_body.clone() + }; + let metadata = message_body; + mailbox.process(metadata.clone(), message); +} + +#[test] +#[should_panic(expected: ('Unexpected destination',))] +fn test_process_fails_if_destination_domain_does_not_match_local_domain() { + let (mailbox, _) = setup(); + let mock_recipient = mock_setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER()); + mailbox.set_local_domain(DESTINATION_DOMAIN); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN + 1, + recipient: mock_recipient.contract_address, + body: message_body.clone() + }; + let metadata = message_body; + mailbox.process(metadata.clone(), message); +} + + +#[test] +#[should_panic(expected: ('Mailbox: already delivered',))] +fn test_process_fails_if_already_delivered() { + let (mailbox, _) = setup(); + let mock_recipient = mock_setup(); + let ownable = IOwnableDispatcher { contract_address: mailbox.contract_address }; + start_prank(CheatTarget::One(ownable.contract_address), OWNER()); + mailbox.set_local_domain(DESTINATION_DOMAIN); + let array = array![ + 0x01020304050607080910111213141516, + 0x01020304050607080910111213141516, + 0x01020304050607080910000000000000 + ]; + + let message_body = BytesTrait::new(42, array); + let message = Message { + version: HYPERLANE_VERSION, + nonce: 0, + origin: LOCAL_DOMAIN, + sender: OWNER(), + destination: DESTINATION_DOMAIN, + recipient: mock_recipient.contract_address, + body: message_body.clone() + }; + let metadata = message_body; + mailbox.process(metadata.clone(), message.clone()); + let message_id = MessageTrait::format_message(message.clone()); + assert(mailbox.delivered(message_id), 'Delivered status did not change'); + mailbox.process(metadata.clone(), message); +} + diff --git a/src/utils/keccak256.cairo b/src/utils/keccak256.cairo new file mode 100644 index 0000000..5665fdc --- /dev/null +++ b/src/utils/keccak256.cairo @@ -0,0 +1,21 @@ +use core::integer::u128_byte_reverse; +/// Reverse the endianness of an u256 +pub fn reverse_endianness(value: u256) -> u256 { + let new_low = u128_byte_reverse(value.high); + let new_high = u128_byte_reverse(value.low); + u256 { low: new_low, high: new_high } +} + + +#[cfg(test)] +mod tests { + use super::reverse_endianness; + #[test] + fn test_reverse_endianness() { + let big_endian_number: u256 = u256 { high: 0x12345678, low: 0 }; + let expected_result: u256 = u256 { high: 0, low: 0x78563412000000000000000000000000 }; + assert( + reverse_endianness(big_endian_number) == expected_result, 'Failed to realise reverse' + ); + } +}