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

✨ ISM #7

Merged
merged 27 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,29 @@ jobs:
pull-requests: write
name: artifact
runs-on: ubuntu-latest
env:
working-directory: ./contracts
steps:
- uses: actions/checkout@v4
- uses: software-mansion/setup-scarb@v1
- name: Build contracts
working-directory: ${{ env.working-directory}}
run: scarb build
- name: Archive contracts
working-directory: ${{ env.working-directory}}
run: |
mkdir -p filtered_artifacts
find ./target/dev -type f -name '*.contract_class.json' -exec cp {} filtered_artifacts/ \;
find ./contracts/target/dev -type f -name '*.contract_class.json' -exec cp {} filtered_artifacts/ \;
- name: Generate checksums
working-directory: ${{ env.working-directory}}
run: |
cd filtered_artifacts
for file in *; do
sha256sum "$file" > "$file.sha256"
md5sum "$file" > "$file.md5"
done
- name: Build artifact zip
working-directory: ${{ env.working-directory}}
run: |
cd filtered_artifacts
zip -r ../hyperlane-starknet-${{ github.ref_name }}.zip .
Expand All @@ -37,6 +43,7 @@ jobs:
md5sum hyperlane-starknet-${{ github.ref_name }}.zip > hyperlane-starknet-${{ github.ref_name }}.CHECKSUM.MD5

- name: Find zip files
working-directory: ${{ env.working-directory}}
run: |
find ./filtered_artifacts -type f -name '*.zip' -exec echo "::set-output name=zip_files::{}" \;
id: find_zip_files
Expand All @@ -45,8 +52,8 @@ jobs:
uses: softprops/action-gh-release@v1
with:
files: |
hyperlane-starknet-${{ github.ref_name }}.zip
hyperlane-starknet-${{ github.ref_name }}.CHECKSUM
hyperlane-starknet-${{ github.ref_name }}.CHECKSUM.MD5
./contracts/hyperlane-starknet-${{ github.ref_name }}.zip
./contracts/hyperlane-starknet-${{ github.ref_name }}.CHECKSUM
./contracts/hyperlane-starknet-${{ github.ref_name }}.CHECKSUM.MD5


11 changes: 8 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ on:
jobs:
check:
runs-on: ubuntu-latest
env:
working-directory: ./contracts
steps:
- uses: actions/checkout@v3
- uses: software-mansion/setup-scarb@v1
- uses: foundry-rs/setup-snfoundry@v3
- run: scarb fmt --check
- run: scarb build
- run: snforge test
- working-directory: ${{ env.working-directory}}
run: scarb fmt --check
- working-directory: ${{ env.working-directory}}
run: scarb build
- working-directory: ${{ env.working-directory}}
run: snforge test
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ mod mailboxclient {
self.interchain_security_module.write(_module);
}

fn get_local_domain(self: @ContractState) -> u32 {
self.local_domain.read()
}

fn get_hook(self: @ContractState) -> ContractAddress {
self.hook.read()
}

fn get_interchain_security_module(self: @ContractState) -> ContractAddress {
self.interchain_security_module.read()
}


fn _MailboxClient_initialize(
ref self: ContractState,
_hook: ContractAddress,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#[starknet::contract]
pub mod merkleroot_multisig_ism {
use alexandria_bytes::{Bytes, BytesTrait, BytesStore};


use core::ecdsa::check_ecdsa_signature;
use hyperlane_starknet::contracts::libs::message::{Message, MessageTrait};
use hyperlane_starknet::interfaces::{
IMultisigIsm, IMultisigIsmDispatcher, IMultisigIsmDispatcherTrait, ModuleType,
IInterchainSecurityModule, IInterchainSecurityModuleDispatcher,
IInterchainSecurityModuleDispatcherTrait,
};

use starknet::ContractAddress;
#[storage]
struct Storage {}

mod Errors {
pub const NO_MULTISIG_THRESHOLD_FOR_MESSAGE: felt252 = 'No MultisigISM treshold present';
pub const VERIFICATION_FAILED_THRESHOLD_NOT_REACHED: felt252 = 'Verify failed, < threshold';
}

#[abi(embed_v0)]
impl IMerklerootMultisigIsmImpl of IInterchainSecurityModule<ContractState> {
fn module_type(self: @ContractState) -> ModuleType {
ModuleType::MERKLE_ROOT_MULTISIG(starknet::get_contract_address())
}

fn verify(
self: @ContractState,
_metadata: Bytes,
_message: Message,
_validator_configuration: ContractAddress
) -> bool {
let digest = digest(_metadata.clone(), _message.clone());
let validator_configuration = IMultisigIsmDispatcher {
contract_address: _validator_configuration
};
let (validators, threshold) = validator_configuration
.validators_and_threshold(_message);
assert(threshold > 0, Errors::NO_MULTISIG_THRESHOLD_FOR_MESSAGE);
let validator_count = validators.len();
let mut unmatched_signatures = 0;
let mut matched_signatures = 0;
let mut i = 0;

// for each couple (sig_s, sig_r) extracted from the metadata
loop {
if (i == threshold) {
break ();
}
let (signature_r, signature_s) = get_signature_at(_metadata.clone(), i);

// we loop on the validators list public key in order to find a match
let mut cur_idx = 0;
let is_signer_in_list = loop {
if (cur_idx == validators.len()) {
break false;
}
let signer = *validators.at(cur_idx);
if check_ecdsa_signature(
digest, signer.try_into().unwrap(), signature_r, signature_s
) {
// we found a match
break true;
}
cur_idx += 1;
};
if (!is_signer_in_list) {
unmatched_signatures += 1;
} else {
matched_signatures += 1;
}
assert(
unmatched_signatures < validator_count - threshold,
Errors::VERIFICATION_FAILED_THRESHOLD_NOT_REACHED
);
i += 1;
};
assert(
matched_signatures >= threshold, Errors::VERIFICATION_FAILED_THRESHOLD_NOT_REACHED
);
true
}
}

fn digest(_metadata: Bytes, _message: Message) -> felt252 {
return 0;
}

fn get_signature_at(_metadata: Bytes, index: u32) -> (felt252, felt252) {
(0, 0)
}
}
176 changes: 176 additions & 0 deletions contracts/src/contracts/isms/multisig/messageid_multisig_ism.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#[starknet::contract]
pub mod messageid_multisig_ism {
use alexandria_bytes::{Bytes, BytesTrait, BytesStore};
use core::ecdsa::check_ecdsa_signature;
use hyperlane_starknet::contracts::libs::checkpoint_lib::checkpoint_lib::CheckpointLib;
use hyperlane_starknet::contracts::libs::message::{Message, MessageTrait};
use hyperlane_starknet::contracts::libs::multisig::message_id_ism_metadata::message_id_ism_metadata::MessageIdIsmMetadata;
use hyperlane_starknet::interfaces::{
ModuleType, IInterchainSecurityModule, IInterchainSecurityModuleDispatcher,
IInterchainSecurityModuleDispatcherTrait,
};
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent};
use starknet::ContractAddress;
use starknet::EthAddress;
use starknet::eth_signature::is_eth_signature_valid;
use starknet::secp256_trait::{Signature, signature_from_vrs};
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
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 {
validators: LegacyMap<u32, EthAddress>,
threshold: u32,
#[substorage(v0)]
ownable: OwnableComponent::Storage,
#[substorage(v0)]
upgradeable: UpgradeableComponent::Storage,
}

mod Errors {
pub const NO_MULTISIG_THRESHOLD_FOR_MESSAGE: felt252 = 'No MultisigISM treshold present';
pub const NO_MATCH_FOR_SIGNATURE: felt252 = 'No match for given signature';
pub const EMPTY_METADATA: felt252 = 'Empty metadata';
pub const VALIDATOR_ADDRESS_CANNOT_BE_NULL: felt252 = 'Validator address cannot be 0';
}

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


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

#[abi(embed_v0)]
impl IMessageidMultisigIsmImpl of IInterchainSecurityModule<ContractState> {
fn module_type(self: @ContractState) -> ModuleType {
ModuleType::MESSAGE_ID_MULTISIG(starknet::get_contract_address())
}

fn verify(self: @ContractState, _metadata: Bytes, _message: Message,) -> bool {
assert(_metadata.clone().data().len() > 0, Errors::EMPTY_METADATA);
let digest = digest(_metadata.clone(), _message.clone());
let (validators, threshold) = self.validators_and_threshold(_message);
assert(threshold > 0, Errors::NO_MULTISIG_THRESHOLD_FOR_MESSAGE);
let mut matched_signatures = 0;
let mut i = 0;

// for each couple (sig_s, sig_r) extracted from the metadata
loop {
if (i == threshold) {
break ();
}
let signature = get_signature_at(_metadata.clone(), i);
// we loop on the validators list public key in order to find a match
let mut cur_idx = 0;
let is_signer_in_list = loop {
if (cur_idx == validators.len()) {
break false;
}
let signer = *validators.at(cur_idx);
if bool_is_eth_signature_valid(digest, signature, signer) {
// we found a match
break true;
}
cur_idx += 1;
};
assert(is_signer_in_list, Errors::NO_MATCH_FOR_SIGNATURE);
i += 1;
};
true
}
fn get_validators(self: @ContractState) -> Span<EthAddress> {
build_validators_span(self)
}

fn get_threshold(self: @ContractState) -> u32 {
self.threshold.read()
}

fn set_validators(ref self: ContractState, _validators: Span<EthAddress>) {
self.ownable.assert_only_owner();
let mut cur_idx = 0;

loop {
if (cur_idx == _validators.len()) {
break ();
}
let validator = *_validators.at(cur_idx);
assert(
validator != 0.try_into().unwrap(), Errors::VALIDATOR_ADDRESS_CANNOT_BE_NULL
);
self.validators.write(cur_idx.into(), validator);
cur_idx += 1;
}
}

fn set_threshold(ref self: ContractState, _threshold: u32) {
self.ownable.assert_only_owner();
self.threshold.write(_threshold);
}

fn validators_and_threshold(
self: @ContractState, _message: Message
) -> (Span<EthAddress>, u32) {
// USER CONTRACT DEFINITION HERE
// USER CAN SPECIFY VALIDATORS SELECTION CONDITIONS
let threshold = self.threshold.read();
(build_validators_span(self), threshold)
}
}

fn digest(_metadata: Bytes, _message: Message) -> u256 {
let origin_merkle_tree_hook = MessageIdIsmMetadata::origin_merkle_tree_hook(
_metadata.clone()
);
let root = MessageIdIsmMetadata::root(_metadata.clone());
let index = MessageIdIsmMetadata::index(_metadata.clone());
CheckpointLib::digest(
_message.origin,
origin_merkle_tree_hook.into(),
root.into(),
index,
MessageTrait::format_message(_message)
)
}

fn get_signature_at(_metadata: Bytes, _index: u32) -> Signature {
let (v, r, s) = MessageIdIsmMetadata::signature_at(_metadata, _index);
signature_from_vrs(v.into(), r, s)
}

fn bool_is_eth_signature_valid(
msg_hash: u256, signature: Signature, signer: EthAddress
) -> bool {
match is_eth_signature_valid(msg_hash, signature, signer) {
Result::Ok(()) => true,
Result::Err(_) => false
}
}

fn build_validators_span(self: @ContractState) -> Span<EthAddress> {
let mut validators = ArrayTrait::new();
let mut cur_idx = 0;
loop {
let validator = self.validators.read(cur_idx);
if (validator == 0.try_into().unwrap()) {
break ();
}
validators.append(validator);
cur_idx += 1;
};
validators.span()
}
}
Loading
Loading