diff --git a/src/account.cairo b/src/account.cairo index e29e379f2..e5fb810fc 100644 --- a/src/account.cairo +++ b/src/account.cairo @@ -3,6 +3,7 @@ mod dual_account; mod dual_eth_account; mod eth_account; mod interface; +mod multisig_account; mod utils; use account::AccountComponent; diff --git a/src/account/interface.cairo b/src/account/interface.cairo index 7ba173a8d..aac873dd8 100644 --- a/src/account/interface.cairo +++ b/src/account/interface.cairo @@ -146,3 +146,21 @@ trait EthAccountABI { fn getPublicKey(self: @TState) -> EthPublicKey; fn setPublicKey(ref self: TState, newPublicKey: EthPublicKey); } + +// +// Multisig Account +// + +#[starknet::interface] +trait IPublicKeys { + fn get_public_keys(self: @TState) -> Span; + fn add_public_key(ref self: TState, new_public_key: felt252); + fn remove_public_key(ref self: TState, public_key: felt252); +} + +#[starknet::interface] +trait IPublicKeysCamel { + fn getPublicKeys(self: @TState) -> Span; + fn addPublicKey(ref self: TState, newPublicKey: felt252); + fn removePublicKey(ref self: TState, newPublicKey: felt252); +} diff --git a/src/account/multisig_account.cairo b/src/account/multisig_account.cairo new file mode 100644 index 000000000..f47aa3460 --- /dev/null +++ b/src/account/multisig_account.cairo @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.9.0 (account/account.cairo) + +/// # Multisig Account Component +/// +/// The Multisig Account component enables contracts to behave as accounts with multiple signers. +#[starknet::component] +mod MultisigAccountComponent { + use openzeppelin::account::interface::IPublicKeys; + use openzeppelin::account::interface; + use openzeppelin::account::utils::{MIN_TRANSACTION_VERSION, QUERY_VERSION, QUERY_OFFSET}; + use openzeppelin::account::utils::{execute_calls, is_valid_stark_signature}; + use openzeppelin::introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait; + use openzeppelin::introspection::src5::SRC5Component; + use starknet::account::Call; + use starknet::get_caller_address; + use starknet::get_contract_address; + use starknet::get_tx_info; + + #[storage] + struct Storage { + account_public_keys: LegacyMap, + number_of_signers: usize, + threshold: usize, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + NewSignerAdded: NewSignerAdded, + SignerRemoved: SignerRemoved, + ThresholdUpdated: ThresholdUpdated, + } + + #[derive(Drop, starknet::Event)] + struct NewSignerAdded { + #[key] + new_signer_public_key: felt252 + } + + #[derive(Drop, starknet::Event)] + struct SignerRemoved { + #[key] + removed_signer_public_key: felt252 + } + + #[derive(Drop, starknet::Event)] + struct ThresholdUpdated { + #[key] + old_threshold: usize, + #[key] + new_threshold: usize + } + + mod Errors { + const INVALID_CALLER: felt252 = 'Account: invalid caller'; + const INVALID_SIGNATURE: felt252 = 'Account: invalid signature'; + const INVALID_TX_VERSION: felt252 = 'Account: invalid tx version'; + const UNAUTHORIZED: felt252 = 'Account: unauthorized'; + } + + #[embeddable_as(SRC6Impl)] + impl SRC6< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +Drop + > of interface::ISRC6> { + /// Executes a list of calls from the account. + /// + /// Requirements: + /// + /// - The transaction version must be greater than or equal to `MIN_TRANSACTION_VERSION`. + /// - If the transaction is a simulation (version than `QUERY_OFFSET`), it must be + /// greater than or equal to `QUERY_OFFSET` + `MIN_TRANSACTION_VERSION`. + fn __execute__( + self: @ComponentState, mut calls: Array + ) -> Array> { + // Avoid calls from other contracts + // https://github.com/OpenZeppelin/cairo-contracts/issues/344 + let sender = get_caller_address(); + assert(sender.is_zero(), Errors::INVALID_CALLER); + + // Check tx version + let tx_info = get_tx_info().unbox(); + let tx_version: u256 = tx_info.version.into(); + // Check if tx is a query + if (tx_version >= QUERY_OFFSET) { + assert( + QUERY_OFFSET + MIN_TRANSACTION_VERSION <= tx_version, Errors::INVALID_TX_VERSION + ); + } else { + assert(MIN_TRANSACTION_VERSION <= tx_version, Errors::INVALID_TX_VERSION); + } + + execute_calls(calls) + } + + /// Verifies the validity of the signature for the current transaction. + /// This function is used by the protocol to verify `invoke` transactions. + fn __validate__(self: @ComponentState, mut calls: Array) -> felt252 { + self.validate_transaction() + } + + /// Verifies that the given signature is valid for the given hash. + fn is_valid_signature( + self: @ComponentState, hash: felt252, signature: Array + ) -> felt252 { + if self._is_valid_signature(hash, signature.span()) { + starknet::VALIDATED + } else { + 0 + } + } + } + + #[embeddable_as(DeclarerImpl)] + impl Declarer< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +Drop + > of interface::IDeclarer> { + /// Verifies the validity of the signature for the current transaction. + /// This function is used by the protocol to verify `declare` transactions. + fn __validate_declare__( + self: @ComponentState, class_hash: felt252 + ) -> felt252 { + self.validate_transaction() + } + } + + #[embeddable_as(DeployableImpl)] + impl Deployable< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +Drop + > of interface::IDeployable> { + /// Verifies the validity of the signature for the current transaction. + /// This function is used by the protocol to verify `deploy_account` transactions. + fn __validate_deploy__( + self: @ComponentState, + class_hash: felt252, + contract_address_salt: felt252, + public_key: felt252 + ) -> felt252 { + self.validate_transaction() + } + } + + #[embeddable_as(PublicKeysImpl)] + impl PublicKeys< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +Drop + > of interface::IPublicKeys> { + /// Returns the current public keys associated to the account. + fn get_public_keys(self: @ComponentState) -> Span { + let mut result: Array = array![]; + let mut i: usize = 0; + while i != self.number_of_signers.read() { + let public_key: felt252 = self.account_public_keys.read(i); + result.append(public_key); + i += 1; + }; + + result.span() + } + + /// Adds a public key of the multisig account. + fn add_public_key(ref self: ComponentState, new_public_key: felt252) { + self.assert_only_self(); + self.emit(NewSignerAdded { new_signer_public_key: new_public_key }); + self._add_public_key(new_public_key); + } + + /// Removes a public key of the multisig account. + fn remove_public_key(ref self: ComponentState, public_key: felt252) { + self.assert_only_self(); + // Check missing to make sure the public_key passed as argument is indeed a public key of this account + self.emit(SignerRemoved { removed_signer_public_key: public_key }); + + let mut i = 0; + while i != self.number_of_signers.read() { + if self.account_public_keys.read(i) == public_key { + let mut j = i; + while j != self.number_of_signers.read() { + self.account_public_keys.write(j, self.account_public_keys.read(j + 1)); + j += 1; + } + } + + i += 1; + } + } + } + + /// Adds camelCase support for `ISRC6`. + #[embeddable_as(SRC6CamelOnlyImpl)] + impl SRC6CamelOnly< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +Drop + > of interface::ISRC6CamelOnly> { + fn isValidSignature( + self: @ComponentState, hash: felt252, signature: Array + ) -> felt252 { + self.is_valid_signature(hash, signature) + } + } + + /// Adds camelCase support for `PublicKeyTrait`. + #[embeddable_as(PublicKeysCamelImpl)] + impl PublicKeysCamel< + TContractState, + +HasComponent, + +SRC5Component::HasComponent, + +Drop + > of interface::IPublicKeysCamel> { + fn getPublicKeys(self: @ComponentState) -> Span { + self.get_public_keys() + } + + fn addPublicKey(ref self: ComponentState, newPublicKey: felt252) { + self.add_public_key(newPublicKey); + } + + fn removePublicKey(ref self: ComponentState, newPublicKey: felt252) { + self.remove_public_key(newPublicKey); + } + } + + #[generate_trait] + impl InternalImpl< + TContractState, + +HasComponent, + impl SRC5: SRC5Component::HasComponent, + +Drop + > of InternalTrait { + /// Initializes the account by setting the initial public key + /// and registering the ISRC6 interface Id. + fn initializer(ref self: ComponentState, public_key: felt252) { + let mut src5_component = get_dep_component_mut!(ref self, SRC5); + src5_component.register_interface(interface::ISRC6_ID); + self._add_public_key(public_key); + } + + /// Validates that the caller is the account itself. Otherwise it reverts. + fn assert_only_self(self: @ComponentState) { + let caller = get_caller_address(); + let self = get_contract_address(); + assert(self == caller, Errors::UNAUTHORIZED); + } + + /// Validates the signature for the current transaction. + /// Returns the short string `VALID` if valid, otherwise it reverts. + fn validate_transaction(self: @ComponentState) -> felt252 { + let tx_info = get_tx_info().unbox(); + let tx_hash = tx_info.transaction_hash; + let signature = tx_info.signature; + assert(self._is_valid_signature(tx_hash, signature), Errors::INVALID_SIGNATURE); + starknet::VALIDATED + } + + /// Sets the public key without validating the caller. + /// The usage of this method outside the `set_public_key` function is discouraged. + /// + /// Emits an `OwnerAdded` event. + fn _add_public_key(ref self: ComponentState, new_public_key: felt252) { + let position = self.number_of_signers.read(); + self.account_public_keys.write(position, new_public_key); + self.emit(NewSignerAdded { new_signer_public_key: new_public_key }); + } + + /// Returns whether the given signature is valid for the given hash + /// using the account's current public key. + fn _is_valid_signature( + self: @ComponentState, hash: felt252, signature: Span + ) -> bool { + let threshold = self.threshold.read(); + // assert there is 1 signature per required signer + assert(signature.len() == threshold * 2, 'invalid signature length'); + + let mut final_result: bool = false; + let mut signer_signatures = signature; + loop { + match signer_signatures.pop_front() { + Option::Some(r) => { + match signer_signatures.pop_front() { + Option::Some(s) => { + let sig = array![*r, *s].span(); + let mut result: bool = false; + let mut i: usize = 0; + while i != self.number_of_signers.read() { + result = + is_valid_stark_signature( + hash, self.account_public_keys.read(i), sig + ); + + if result { + final_result = true; + break; + } + + i += 1; + }; + + if !result { + final_result = false; + break; + } + }, + Option::None => { break; } + } + }, + Option::None => { break; } + }; + }; + + final_result + } + } +} diff --git a/src/presets.cairo b/src/presets.cairo index 1a0804706..d231e0f16 100644 --- a/src/presets.cairo +++ b/src/presets.cairo @@ -2,6 +2,7 @@ mod account; mod erc20; mod erc721; mod eth_account; +mod multisig_account; use account::Account; use erc20::ERC20; diff --git a/src/presets/multisig_account.cairo b/src/presets/multisig_account.cairo new file mode 100644 index 000000000..df7481adc --- /dev/null +++ b/src/presets/multisig_account.cairo @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts for Cairo v0.9.0 (presets/account.cairo) + +/// # Account Preset +/// +/// OpenZeppelin's basic account which can change its public key and declare, deploy, or call contracts. +#[starknet::contract(multisig_account)] +mod MultisigAccount { + use openzeppelin::account::multisig_account::MultisigAccountComponent; + use openzeppelin::introspection::src5::SRC5Component; + + component!(path: MultisigAccountComponent, storage: account, event: AccountEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // Account + #[abi(embed_v0)] + impl SRC6Impl = MultisigAccountComponent::SRC6Impl; + #[abi(embed_v0)] + impl SRC6CamelOnlyImpl = + MultisigAccountComponent::SRC6CamelOnlyImpl; + #[abi(embed_v0)] + impl PublicKeysImpl = MultisigAccountComponent::PublicKeysImpl; + #[abi(embed_v0)] + impl PublicKeysCamelImpl = + MultisigAccountComponent::PublicKeysCamelImpl; + #[abi(embed_v0)] + impl DeclarerImpl = MultisigAccountComponent::DeclarerImpl; + #[abi(embed_v0)] + impl DeployableImpl = MultisigAccountComponent::DeployableImpl; + impl AccountInternalImpl = MultisigAccountComponent::InternalImpl; + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + struct Storage { + #[substorage(v0)] + account: MultisigAccountComponent::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AccountEvent: MultisigAccountComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, public_key: felt252) { + self.account.initializer(public_key); + } +}