Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add domain routing hook #38

Merged
merged 15 commits into from
Dec 17, 2024
108 changes: 108 additions & 0 deletions contracts/src/contracts/hooks/domain_routing_hook.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#[starknet::contract]
pub mod domain_routing_hook {
use alexandria_bytes::{Bytes, BytesTrait, BytesStore};
use hyperlane_starknet::contracts::client::mailboxclient_component::{
MailboxclientComponent, MailboxclientComponent::MailboxClientInternalImpl
};
use hyperlane_starknet::contracts::client::{mailboxclient};
use hyperlane_starknet::contracts::libs::message::Message;
use hyperlane_starknet::interfaces::{
IPostDispatchHook, IPostDispatchHookDispatcher, IPostDispatchHookDispatcherTrait,
DomainRoutingHookConfig, IDomainRoutingHook, Types
};
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent};
use starknet::{ContractAddress, contract_address_const};
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
component!(path: MailboxclientComponent, storage: mailboxclient, event: MailboxclientEvent);
component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);
#[abi(embed_v0)]
impl OwnableImpl = OwnableComponent::OwnableImpl<ContractState>;
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;
impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl<ContractState>;


#[storage]
struct Storage {
hooks: LegacyMap<u32, IPostDispatchHookDispatcher>,
#[substorage(v0)]
mailboxclient: MailboxclientComponent::Storage,
#[substorage(v0)]
ownable: OwnableComponent::Storage,
#[substorage(v0)]
upgradeable: UpgradeableComponent::Storage,
}


mod Errors {
pub const INVALID_DESTINATION: felt252 = 'Destination has no hooks';
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
OwnableEvent: OwnableComponent::Event,
#[flat]
UpgradeableEvent: UpgradeableComponent::Event,
#[flat]
MailboxclientEvent: MailboxclientComponent::Event,
}

#[constructor]
fn constructor(ref self: ContractState, _mailbox: ContractAddress, _owner: ContractAddress) {
self.mailboxclient.initialize(_mailbox);
self.ownable.initializer(_owner);
}

#[abi(embed_v0)]
impl IPostDispatchHookImpl of IPostDispatchHook<ContractState> {
fn hook_type(self: @ContractState) -> Types {
Types::ROUTING(())
}

fn supports_metadata(self: @ContractState, _metadata: Bytes) -> bool {
true
}

fn post_dispatch(ref self: ContractState, _metadata: Bytes, _message: Message) {
self._get_configured_hook(_message.clone()).post_dispatch(_metadata, _message);
EvolveArt marked this conversation as resolved.
Show resolved Hide resolved
}

fn quote_dispatch(ref self: ContractState, _metadata: Bytes, _message: Message) -> u256 {
self._get_configured_hook(_message.clone()).quote_dispatch(_metadata, _message)
}
}

#[abi(embed_v0)]
impl IDomainRoutingHookImpl of IDomainRoutingHook<ContractState> {
fn set_hook(ref self: ContractState, _destination: u32, _hook: ContractAddress) {
self.ownable.assert_only_owner();
self.hooks.write(_destination, IPostDispatchHookDispatcher { contract_address: _hook });
}
fn set_hooks(ref self: ContractState, configs: Array<DomainRoutingHookConfig>) {
self.ownable.assert_only_owner();
let mut configs_span = configs.span();
loop {
match configs_span.pop_front() {
Option::Some(config) => { self.set_hook(*config.destination, *config.hook) },
Option::None(_) => { break; },
};
};
}
}

#[generate_trait]
impl InternalImpl of InternalTrait {
fn _get_configured_hook(
self: @ContractState, _message: Message
) -> IPostDispatchHookDispatcher {
let dispatcher_instance = self.hooks.read(_message.destination);
assert(
dispatcher_instance.contract_address != contract_address_const::<0>(),
Errors::INVALID_DESTINATION
);
dispatcher_instance
}
}
}
12 changes: 12 additions & 0 deletions contracts/src/interfaces.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,15 @@ pub trait IProtocolFee<TContractState> {
pub trait IRoutingIsm<TContractState> {
fn route(self: @TContractState, _message: Message) -> ContractAddress;
}

#[derive(Drop, Serde)]
pub struct DomainRoutingHookConfig {
pub destination: u32,
pub hook: ContractAddress
}

#[starknet::interface]
pub trait IDomainRoutingHook<TContractState> {
fn set_hook(ref self: TContractState, _destination: u32, _hook: ContractAddress);
fn set_hooks(ref self: TContractState, configs: Array<DomainRoutingHookConfig>);
}
2 changes: 2 additions & 0 deletions contracts/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod contracts {
}
}
pub mod hooks {
pub mod domain_routing_hook;
pub mod merkle_tree_hook;
pub mod protocol_fee;
pub mod libs {
Expand Down Expand Up @@ -64,6 +65,7 @@ mod tests {
pub mod test_messageid_multisig;
}
pub mod hooks {
pub mod test_domain_routing_hook;
pub mod test_merkle_tree_hook;
pub mod test_protocol_fee;
}
Expand Down
145 changes: 145 additions & 0 deletions contracts/src/tests/hooks/test_domain_routing_hook.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use alexandria_bytes::{Bytes, BytesTrait, BytesStore};
use hyperlane_starknet::contracts::libs::message::{Message, MessageTrait, HYPERLANE_VERSION};
use hyperlane_starknet::interfaces::{
Types, IPostDispatchHookDispatcher, IPostDispatchHookDispatcherTrait,
IDomainRoutingHookDispatcher, IDomainRoutingHookDispatcherTrait, DomainRoutingHookConfig
};
use hyperlane_starknet::tests::setup::{setup_domain_routing_hook, setup_mock_hook, OWNER};
use openzeppelin::access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait};

use snforge_std::{start_prank, CheatTarget};
use starknet::{get_caller_address, contract_address_const, ContractAddress};


#[test]
fn test_domain_routing_hook_type() {
let (routing_hook_addrs, _) = setup_domain_routing_hook();
assert_eq!(routing_hook_addrs.hook_type(), Types::ROUTING(()));
}

#[test]
fn test_supports_metadata_for_domain_routing_hook() {
let (routing_hook_addrs, _) = setup_domain_routing_hook();

let metadata = BytesTrait::new_empty();
assert_eq!(routing_hook_addrs.supports_metadata(metadata), true);
}

#[test]
fn test_domain_rounting_set_hook() {
let (_, set_routing_hook_addrs) = setup_domain_routing_hook();
let ownable = IOwnableDispatcher { contract_address: set_routing_hook_addrs.contract_address };
start_prank(CheatTarget::One(ownable.contract_address), OWNER());
let destination: u32 = 12;
let hook: ContractAddress = contract_address_const::<1>();
set_routing_hook_addrs.set_hook(destination, hook);
}

#[test]
#[should_panic(expected: ('Caller is not the owner',))]
fn test_set_hook_fails_if_not_owner() {
let (_, set_routing_hook_addrs) = setup_domain_routing_hook();
let destination: u32 = 12;
let hook: ContractAddress = contract_address_const::<1>();
set_routing_hook_addrs.set_hook(destination, hook);
}

#[test]
fn test_domain_rounting_set_hook_array() {
let (_, set_routing_hook_addrs) = setup_domain_routing_hook();
let ownable = IOwnableDispatcher { contract_address: set_routing_hook_addrs.contract_address };
start_prank(CheatTarget::One(ownable.contract_address), OWNER());
let mut hook_config_arr = ArrayTrait::<DomainRoutingHookConfig>::new();
hook_config_arr
.append(DomainRoutingHookConfig { destination: 1, hook: contract_address_const::<2>() });
hook_config_arr
.append(DomainRoutingHookConfig { destination: 2, hook: contract_address_const::<3>() });
hook_config_arr
.append(DomainRoutingHookConfig { destination: 3, hook: contract_address_const::<4>() });
set_routing_hook_addrs.set_hooks(hook_config_arr);
}


#[test]
#[should_panic(expected: ('Caller is not the owner',))]
fn test_set_hook_array_fails_if_not_owner() {
let (_, set_routing_hook_addrs) = setup_domain_routing_hook();
let mut hook_config_arr = ArrayTrait::<DomainRoutingHookConfig>::new();
hook_config_arr
.append(DomainRoutingHookConfig { destination: 1, hook: contract_address_const::<2>() });
hook_config_arr
.append(DomainRoutingHookConfig { destination: 2, hook: contract_address_const::<3>() });
set_routing_hook_addrs.set_hooks(hook_config_arr);
}


#[test]
#[should_panic(expected: ('Destination has no hooks',))]
fn hook_not_set_for_destination_should_fail() {
let (routing_hook_addrs, set_routing_hook_addrs) = setup_domain_routing_hook();
let ownable = IOwnableDispatcher { contract_address: set_routing_hook_addrs.contract_address };
start_prank(CheatTarget::One(ownable.contract_address), OWNER());
let destination: u32 = 12;
let hook: ContractAddress = contract_address_const::<1>();
set_routing_hook_addrs.set_hook(destination, hook);

let message = Message {
version: HYPERLANE_VERSION,
nonce: 0_u32,
origin: 0_u32,
sender: contract_address_const::<0>(),
destination: destination - 1,
recipient: contract_address_const::<0>(),
body: BytesTrait::new_empty(),
};
let metadata = BytesTrait::new_empty();
routing_hook_addrs.post_dispatch(metadata, message);
}

// Note: Test fails with msg('Result::unwrap failed')
#[ignore]
#[test]
fn hook_set_for_destination_post_dispatch() {
let (routing_hook_addrs, set_routing_hook_addrs) = setup_domain_routing_hook();
let ownable = IOwnableDispatcher { contract_address: set_routing_hook_addrs.contract_address };
start_prank(CheatTarget::One(ownable.contract_address), OWNER());
let destination: u32 = 18;
let hook: ContractAddress = setup_mock_hook().contract_address;
set_routing_hook_addrs.set_hook(destination, hook);

let message = Message {
version: HYPERLANE_VERSION,
nonce: 0_u32,
origin: 0_u32,
sender: contract_address_const::<0>(),
destination: destination,
recipient: contract_address_const::<0>(),
body: BytesTrait::new_empty(),
};
let metadata = BytesTrait::new_empty();
routing_hook_addrs.post_dispatch(metadata, message);
}

// Note: Test fails with msg('Result::unwrap failed')
#[ignore]
#[test]
fn hook_set_for_destination_quote_dispatch() {
let (routing_hook_addrs, set_routing_hook_addrs) = setup_domain_routing_hook();
let ownable = IOwnableDispatcher { contract_address: set_routing_hook_addrs.contract_address };
start_prank(CheatTarget::One(ownable.contract_address), OWNER());
let destination: u32 = 18;
let hook: ContractAddress = setup_mock_hook().contract_address;
EvolveArt marked this conversation as resolved.
Show resolved Hide resolved
set_routing_hook_addrs.set_hook(destination, hook);

let message = Message {
version: HYPERLANE_VERSION,
nonce: 0_u32,
origin: 0_u32,
sender: contract_address_const::<0>(),
destination: destination,
recipient: contract_address_const::<0>(),
body: BytesTrait::new_empty(),
};
let metadata = BytesTrait::new_empty();
routing_hook_addrs.quote_dispatch(metadata, message);
}
17 changes: 16 additions & 1 deletion contracts/src/tests/setup.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use hyperlane_starknet::interfaces::{
IPostDispatchHookDispatcherTrait, IProtocolFeeDispatcherTrait, IMockValidatorAnnounceDispatcher,
ISpecifiesInterchainSecurityModuleDispatcher, ISpecifiesInterchainSecurityModuleDispatcherTrait,
IRoutingIsmDispatcher, IRoutingIsmDispatcherTrait, IDomainRoutingIsmDispatcher,
IDomainRoutingIsmDispatcherTrait
IDomainRoutingIsmDispatcherTrait, IDomainRoutingHookDispatcher,
IDomainRoutingHookDispatcherTrait
};
use openzeppelin::account::utils::signature::EthSignature;
use openzeppelin::token::erc20::interface::{IERC20, IERC20Dispatcher, IERC20DispatcherTrait};
Expand Down Expand Up @@ -439,3 +440,17 @@ pub fn setup_protocol_fee() -> (
IPostDispatchHookDispatcher { contract_address: protocol_fee_addr }
)
}

pub fn setup_domain_routing_hook() -> (IPostDispatchHookDispatcher, IDomainRoutingHookDispatcher) {
let (mailbox, _, _, _) = setup();

let domain_routing_hook_class = declare("domain_routing_hook").unwrap();
let (domain_routing_hook_addrs, _) = domain_routing_hook_class
.deploy(@array![mailbox.contract_address.into(), OWNER().into()])
.unwrap();
(
IPostDispatchHookDispatcher { contract_address: domain_routing_hook_addrs },
IDomainRoutingHookDispatcher { contract_address: domain_routing_hook_addrs }
)
}