From 89918ffe8837efa5eb9d19e330cfdfa3e264028f Mon Sep 17 00:00:00 2001 From: Darlington02 Date: Wed, 28 Aug 2024 22:25:19 +0100 Subject: [PATCH] chore: add signatory component --- src/components.cairo | 1 + src/components/account/account.cairo | 43 ++++------- src/components/lockable/lockable.cairo | 5 -- src/components/presets/account_preset.cairo | 38 +++++++++- src/components/signatory.cairo | 1 + src/components/signatory/signatory.cairo | 80 ++++++++++++++++++++ src/components/upgradeable/upgradeable.cairo | 6 -- src/interfaces.cairo | 1 + src/interfaces/IAccount.cairo | 2 - src/interfaces/ISignatory.cairo | 9 +++ 10 files changed, 142 insertions(+), 44 deletions(-) create mode 100644 src/components/signatory.cairo create mode 100644 src/components/signatory/signatory.cairo create mode 100644 src/interfaces/ISignatory.cairo diff --git a/src/components.cairo b/src/components.cairo index 54838ad..18d258c 100644 --- a/src/components.cairo +++ b/src/components.cairo @@ -3,3 +3,4 @@ pub mod lockable; pub mod permissionable; pub mod upgradeable; pub mod presets; +pub mod signatory; \ No newline at end of file diff --git a/src/components/account/account.cairo b/src/components/account/account.cairo index b7a123e..9d676fd 100644 --- a/src/components/account/account.cairo +++ b/src/components/account/account.cairo @@ -91,18 +91,6 @@ pub mod AccountComponent { self._is_valid_signature(hash, signature) } - /// @notice used to validate signer - /// @param signer address to be validated - fn is_valid_signer(self: @ComponentState, signer: ContractAddress) -> bool { - self._is_valid_signer(signer) - } - - fn __validate_declare__( - self: @ComponentState, class_hash: felt252 - ) -> felt252 { - self._validate_transaction() - } - /// @notice gets the NFT owner /// @param token_contract the contract address of the NFT /// @param token_id the token ID of the NFT @@ -170,10 +158,6 @@ pub mod AccountComponent { fn _execute( ref self: ComponentState, mut calls: Array ) -> Array> { - // validate signer - let caller = get_caller_address(); - assert(self._is_valid_signer(caller), Errors::UNAUTHORIZED); - // update state self._update_state(); @@ -225,6 +209,20 @@ pub mod AccountComponent { Serde::::deserialize(ref address).unwrap() } + /// @notice internal function for getting the root NFT owner + /// @param token_contract contract address of NFT + // @param token_id token ID of NFT + // NB: This function aims for compatibility with all contracts (snake or camel case) but do + // not work as expected on mainnet as low level calls do not return err at the moment. + // Should work for contracts which implements CamelCase but not snake_case until starknet + // v0.15. + fn _get_root_owner( + self: @ComponentState, token_contract: ContractAddress, token_id: u256 + ) -> ContractAddress { + // TODO: implement logic to get root owner + 123.try_into().unwrap() + } + /// @notice internal transaction for returning the contract address and token ID of the NFT fn _get_token(self: @ComponentState) -> (ContractAddress, u256, felt252) { let contract = self.account_token_contract.read(); @@ -234,19 +232,6 @@ pub mod AccountComponent { (contract, token_id, chain_id) } - // @notice internal function for validating signer - fn _is_valid_signer( - self: @ComponentState, signer: ContractAddress - ) -> bool { - let owner = self - ._get_owner(self.account_token_contract.read(), self.account_token_id.read()); - if (signer == owner) { - return true; - } else { - return false; - } - } - /// @notice internal function for signature validation fn _is_valid_signature( self: @ComponentState, hash: felt252, signature: Span diff --git a/src/components/lockable/lockable.cairo b/src/components/lockable/lockable.cairo index 2ffda9c..ff05b27 100644 --- a/src/components/lockable/lockable.cairo +++ b/src/components/lockable/lockable.cairo @@ -74,11 +74,6 @@ pub mod LockableComponent { > of ILockable> { fn lock(ref self: ComponentState, lock_until: u64) { let current_timestamp = get_block_timestamp(); - let account_comp = get_dep_component!(@self, Account); - - let is_valid = account_comp._is_valid_signer(get_caller_address()); - assert(is_valid, Errors::UNAUTHORIZED); - assert( lock_until <= current_timestamp + YEAR_DAYS_SECONDS, Errors::EXCEEDS_MAX_LOCK_TIME ); diff --git a/src/components/presets/account_preset.cairo b/src/components/presets/account_preset.cairo index 7a87a86..875b292 100644 --- a/src/components/presets/account_preset.cairo +++ b/src/components/presets/account_preset.cairo @@ -7,13 +7,15 @@ pub mod AccountPreset { use token_bound_accounts::components::account::account::AccountComponent; use token_bound_accounts::components::upgradeable::upgradeable::UpgradeableComponent; use token_bound_accounts::components::lockable::lockable::LockableComponent; + use token_bound_accounts::components::signatory::signatory::SignatoryComponent; use token_bound_accounts::interfaces::{ - IUpgradeable::IUpgradeable, IExecutable::IExecutable, ILockable::ILockable + IUpgradeable::IUpgradeable, IExecutable::IExecutable, ILockable::ILockable, ISignatory::ISignatory }; component!(path: AccountComponent, storage: account, event: AccountEvent); component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); component!(path: LockableComponent, storage: lockable, event: LockableEvent); + component!(path: SignatoryComponent, storage: signatory, event: SignatoryEvent); // Account #[abi(embed_v0)] @@ -22,6 +24,7 @@ pub mod AccountPreset { impl AccountInternalImpl = AccountComponent::InternalImpl; impl UpgradeableInternalImpl = UpgradeableComponent::Private; impl LockableImpl = LockableComponent::LockableImpl; + impl SignerImpl = SignatoryComponent::Private; // ************************************************************************* // STORAGE @@ -34,6 +37,8 @@ pub mod AccountPreset { upgradeable: UpgradeableComponent::Storage, #[substorage(v0)] lockable: LockableComponent::Storage, + #[substorage(v0)] + signatory: SignatoryComponent::Storage } // ************************************************************************* @@ -47,7 +52,9 @@ pub mod AccountPreset { #[flat] UpgradeableEvent: UpgradeableComponent::Event, #[flat] - LockableEvent: LockableComponent::Event + LockableEvent: LockableComponent::Event, + #[flat] + SignatoryEvent: SignatoryComponent::Event } // ************************************************************************* @@ -58,15 +65,31 @@ pub mod AccountPreset { self.account.initializer(token_contract, token_id); } + // ************************************************************************* + // SIGNATORY IMPL + // ************************************************************************* + #[abi(embed_v0)] + impl Signatory of ISignatory { + fn is_valid_signer(self: @ContractState, signer: ContractAddress) -> bool { + self.signatory._permissioned_signer_validation(signer) + } + } + // ************************************************************************* // EXECUTABLE IMPL // ************************************************************************* #[abi(embed_v0)] impl Executable of IExecutable { fn execute(ref self: ContractState, mut calls: Array) -> Array> { + // validate signer + let caller = get_caller_address(); + assert(self.is_valid_signer(caller), 'Account: unauthorized'); + // cannot make this call when the account is lock let (is_locked, _) = self.lockable.is_locked(); assert(is_locked != true, 'Account: locked'); + + // execute calls self.account._execute(calls) } } @@ -77,9 +100,15 @@ pub mod AccountPreset { #[abi(embed_v0)] impl Upgradeable of IUpgradeable { fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + // validate signer + let caller = get_caller_address(); + assert(self.is_valid_signer(caller), 'Account: unauthorized'); + // cannot make this call when the account is lock let (is_locked, _) = self.lockable.is_locked(); assert(is_locked != true, 'Account: locked'); + + // upgrade account self.upgradeable._upgrade(new_class_hash); } } @@ -90,6 +119,11 @@ pub mod AccountPreset { #[abi(embed_v0)] impl Lockable of ILockable { fn lock(ref self: ContractState, lock_until: u64) { + // validate signer + let caller = get_caller_address(); + assert(self.is_valid_signer(caller), 'Account: unauthorized'); + + // lock account self.lockable.lock(lock_until); } fn is_locked(self: @ContractState) -> (bool, u64) { diff --git a/src/components/signatory.cairo b/src/components/signatory.cairo new file mode 100644 index 0000000..5c7b7a2 --- /dev/null +++ b/src/components/signatory.cairo @@ -0,0 +1 @@ +pub mod signatory; \ No newline at end of file diff --git a/src/components/signatory/signatory.cairo b/src/components/signatory/signatory.cairo new file mode 100644 index 0000000..cdb53c0 --- /dev/null +++ b/src/components/signatory/signatory.cairo @@ -0,0 +1,80 @@ +// ************************************************************************* +// SIGNATORY COMPONENT +// ************************************************************************* +#[starknet::component] +pub mod SignatoryComponent { + // ************************************************************************* + // IMPORTS + // ************************************************************************* + use starknet::{ + get_caller_address, get_contract_address, ContractAddress + }; + use token_bound_accounts::components::account::account::AccountComponent; + use token_bound_accounts::components::account::account::AccountComponent::InternalImpl; + + // ************************************************************************* + // STORAGE + // ************************************************************************* + #[storage] + pub struct Storage {} + + // ************************************************************************* + // PRIVATE FUNCTIONS + // ************************************************************************* + #[generate_trait] + pub impl Private< + TContractState, + +HasComponent, + +Drop, + impl Account: AccountComponent::HasComponent + > of PrivateTrait { + /// @notice implements a simple signer validation where only NFT owner is a valid signer. + /// @param signer the address to be validated + fn _base_signer_validation(self: @ComponentState, signer: ContractAddress) -> bool { + let account = get_dep_component!(self, Account); + let (contract_address, token_id, _) = account._get_token(); + + // get owner + let owner = account + ._get_owner(contract_address, token_id); + + // validate + if (signer == owner) { + return true; + } else { + return false; + } + } + + /// @notice implements a signer validation where both NFT owner and the root owner (for nested accounts) are valid signers. + /// @param signer the address to be validated + fn _base_and_root_signer_validation(self: @ComponentState, signer: ContractAddress) -> bool { + let account = get_dep_component!(self, Account); + let (contract_address, token_id, _) = account._get_token(); + + // get owner + let owner = account + ._get_owner(contract_address, token_id); + // get root owner + let root_owner = account + ._get_root_owner(contract_address, token_id); + + // validate + if (signer == owner) { + return true; + } + else if(signer == root_owner) { + return true; + } + else { + return false; + } + } + + /// @notice implements a more complex signer validation where NFT owner, root owner, and permissioned addresses are valid signers. + /// @param signer the address to be validated + fn _permissioned_signer_validation(self: @ComponentState, signer: ContractAddress) -> bool { + true + } + } +} diff --git a/src/components/upgradeable/upgradeable.cairo b/src/components/upgradeable/upgradeable.cairo index acf5056..994e8bd 100644 --- a/src/components/upgradeable/upgradeable.cairo +++ b/src/components/upgradeable/upgradeable.cairo @@ -41,7 +41,6 @@ pub mod UpgradeableComponent { // ************************************************************************* pub mod Errors { pub const INVALID_CLASS: felt252 = 'Class hash cannot be zero'; - pub const UNAUTHORIZED: felt252 = 'Account: unauthorized'; } // ************************************************************************* @@ -57,11 +56,6 @@ pub mod UpgradeableComponent { /// @notice replaces the contract's class hash with `new_class_hash`. /// Emits an `Upgraded` event. fn _upgrade(ref self: ComponentState, new_class_hash: ClassHash) { - // validate new signer - let account_comp = get_dep_component!(@self, Account); - let is_valid = account_comp._is_valid_signer(get_caller_address()); - assert(is_valid, Errors::UNAUTHORIZED); - // update state let mut account_comp_mut = get_dep_component_mut!(ref self, Account); account_comp_mut._update_state(); diff --git a/src/interfaces.cairo b/src/interfaces.cairo index 56551f2..de01f5a 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -4,3 +4,4 @@ pub mod IRegistry; pub mod IUpgradeable; pub mod IExecutable; pub mod ILockable; +pub mod ISignatory; \ No newline at end of file diff --git a/src/interfaces/IAccount.cairo b/src/interfaces/IAccount.cairo index 6f6f7e0..cc31e14 100644 --- a/src/interfaces/IAccount.cairo +++ b/src/interfaces/IAccount.cairo @@ -14,8 +14,6 @@ pub trait IAccount { fn is_valid_signature( self: @TContractState, hash: felt252, signature: Span ) -> felt252; - fn is_valid_signer(self: @TContractState, signer: ContractAddress) -> bool; - fn __validate_declare__(self: @TContractState, class_hash: felt252) -> felt252; fn token(self: @TContractState) -> (ContractAddress, u256, felt252); fn owner(self: @TContractState) -> ContractAddress; fn state(self: @TContractState) -> u256; diff --git a/src/interfaces/ISignatory.cairo b/src/interfaces/ISignatory.cairo new file mode 100644 index 0000000..b41b27c --- /dev/null +++ b/src/interfaces/ISignatory.cairo @@ -0,0 +1,9 @@ +// ************************************************************************* +// SIGNER VALIDATION INTERFACE +// ************************************************************************* +use starknet::ContractAddress; + +#[starknet::interface] +pub trait ISignatory { + fn is_valid_signer(self: @TContractState, signer: ContractAddress) -> bool; +}