diff --git a/src/components.cairo b/src/components.cairo index 18d258c..7554689 100644 --- a/src/components.cairo +++ b/src/components.cairo @@ -3,4 +3,4 @@ pub mod lockable; pub mod permissionable; pub mod upgradeable; pub mod presets; -pub mod signatory; \ No newline at end of file +pub mod signatory; diff --git a/src/components/lockable/lockable.cairo b/src/components/lockable/lockable.cairo index ff05b27..6fbac1e 100644 --- a/src/components/lockable/lockable.cairo +++ b/src/components/lockable/lockable.cairo @@ -1,4 +1,3 @@ -// lockable component // ************************************************************************* // LOCKABLE COMPONENT // ************************************************************************* @@ -72,6 +71,8 @@ pub mod LockableComponent { +Drop, impl Account: AccountComponent::HasComponent > of ILockable> { + // @notice locks an account + // @param lock_until duration for which account should be locked fn lock(ref self: ComponentState, lock_until: u64) { let current_timestamp = get_block_timestamp(); assert( @@ -98,6 +99,7 @@ pub mod LockableComponent { ); } + // @notice returns the lock status of an account fn is_locked(self: @ComponentState) -> (bool, u64) { let unlock_timestamp = self.lock_until.read(); let current_time = get_block_timestamp(); diff --git a/src/components/permissionable/permissionable.cairo b/src/components/permissionable/permissionable.cairo index b111e5e..795a3b1 100644 --- a/src/components/permissionable/permissionable.cairo +++ b/src/components/permissionable/permissionable.cairo @@ -1,3 +1,112 @@ -// permissionable component +// ************************************************************************* +// PERMISSIONABLE COMPONENT +// ************************************************************************* +#[starknet::component] +pub mod PermissionableComponent { + // ************************************************************************* + // IMPORTS + // ************************************************************************* + use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; + use starknet::{ContractAddress, get_caller_address, get_block_timestamp}; + use token_bound_accounts::components::account::account::AccountComponent; + use token_bound_accounts::interfaces::IAccount::{IAccount, IAccountDispatcherTrait}; + use token_bound_accounts::components::account::account::AccountComponent::InternalImpl; + use token_bound_accounts::interfaces::IPermissionable::{ + IPermissionable, IPermissionableDispatcher, IPermissionableDispatcherTrait + }; + // ************************************************************************* + // STORAGE + // ************************************************************************* + #[storage] + pub struct Storage { + permissions: Map< + (ContractAddress, ContractAddress), bool + > // <, bool> + } + // ************************************************************************* + // EVENTS + // ************************************************************************* + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + PermissionUpdated: PermissionUpdated + } + + // @notice emitted when permissions are updated for an account + // @param owner tokenbound account owner + // @param permissioned_address address to be given/revoked permission + // @param has_permission returns true if user has permission else false + #[derive(Drop, starknet::Event)] + pub struct PermissionUpdated { + #[key] + pub owner: ContractAddress, + pub permissioned_address: ContractAddress, + pub has_permission: bool, + } + + // ************************************************************************* + // ERRORS + // ************************************************************************* + pub mod Errors { + pub const UNAUTHORIZED: felt252 = 'Permission: unauthorized'; + pub const NOT_OWNER: felt252 = 'Permission: not account owner'; + pub const INVALID_LENGTH: felt252 = 'Permission: invalid length'; + pub const NOT_PERMITTED: felt252 = 'Permisson: not permitted'; + } + + + // ************************************************************************* + // EXTERNAL FUNCTIONS + // ************************************************************************* + #[embeddable_as(PermissionableImpl)] + pub impl Permissionable< + TContractState, + +HasComponent, + +Drop, + impl Account: AccountComponent::HasComponent + > of IPermissionable> { + // @notice sets permission for an account + // @permissioned_addresses array of addresses who's permission is to be updated + // @param permssions permission value + fn set_permission( + ref self: ComponentState, + permissioned_addresses: Array, + permissions: Array + ) { + assert(permissioned_addresses.len() == permissions.len(), Errors::INVALID_LENGTH); + + let account_comp = get_dep_component!(@self, Account); + let owner = account_comp.owner(); + let length = permissioned_addresses.len(); + let mut index: u32 = 0; + while index < length { + self + .permissions + .write((owner, *permissioned_addresses[index]), *permissions[index]); + self + .emit( + PermissionUpdated { + owner: owner, + permissioned_address: *permissioned_addresses[index], + has_permission: *permissions[index] + } + ); + index += 1 + } + } + + // @notice returns if a user has permission or not + // @param owner tokenbound account owner + // @param permissioned_address address to check permission for + fn has_permission( + self: @ComponentState, + owner: ContractAddress, + permissioned_address: ContractAddress + ) -> bool { + let permission = self.permissions.read((owner, permissioned_address)); + permission + } + } +} diff --git a/src/components/presets/account_preset.cairo b/src/components/presets/account_preset.cairo index 875b292..e472756 100644 --- a/src/components/presets/account_preset.cairo +++ b/src/components/presets/account_preset.cairo @@ -8,14 +8,17 @@ pub mod AccountPreset { 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::components::permissionable::permissionable::PermissionableComponent; use token_bound_accounts::interfaces::{ - IUpgradeable::IUpgradeable, IExecutable::IExecutable, ILockable::ILockable, ISignatory::ISignatory + IUpgradeable::IUpgradeable, IExecutable::IExecutable, ILockable::ILockable, + ISignatory::ISignatory, IPermissionable::IPermissionable }; 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); + component!(path: PermissionableComponent, storage: permissionable, event: PermissionableEvent); // Account #[abi(embed_v0)] @@ -38,7 +41,9 @@ pub mod AccountPreset { #[substorage(v0)] lockable: LockableComponent::Storage, #[substorage(v0)] - signatory: SignatoryComponent::Storage + signatory: SignatoryComponent::Storage, + #[substorage(v0)] + permissionable: PermissionableComponent::Storage, } // ************************************************************************* @@ -54,7 +59,9 @@ pub mod AccountPreset { #[flat] LockableEvent: LockableComponent::Event, #[flat] - SignatoryEvent: SignatoryComponent::Event + SignatoryEvent: SignatoryComponent::Event, + #[flat] + PermissionableEvent: PermissionableComponent::Event } // ************************************************************************* @@ -65,7 +72,7 @@ pub mod AccountPreset { self.account.initializer(token_contract, token_id); } - // ************************************************************************* + // ************************************************************************* // SIGNATORY IMPL // ************************************************************************* #[abi(embed_v0)] @@ -126,8 +133,34 @@ pub mod AccountPreset { // lock account self.lockable.lock(lock_until); } + fn is_locked(self: @ContractState) -> (bool, u64) { self.lockable.is_locked() } } + + // ************************************************************************* + // PERMISSIONABLE IMPL + // ************************************************************************* + #[abi(embed_v0)] + impl Permissionable of IPermissionable { + fn set_permission( + ref self: ContractState, + permissioned_addresses: Array, + permissions: Array + ) { + // validate signer + let caller = get_caller_address(); + assert(self.is_valid_signer(caller), 'Account: unauthorized'); + + // set permissions + self.permissionable.set_permission(permissioned_addresses, permissions) + } + + fn has_permission( + self: @ContractState, owner: ContractAddress, permissioned_address: ContractAddress + ) -> bool { + self.permissionable.has_permission(owner, permissioned_address) + } + } } diff --git a/src/components/signatory.cairo b/src/components/signatory.cairo index 5c7b7a2..fa38d7b 100644 --- a/src/components/signatory.cairo +++ b/src/components/signatory.cairo @@ -1 +1 @@ -pub mod signatory; \ No newline at end of file +pub mod signatory; diff --git a/src/components/signatory/signatory.cairo b/src/components/signatory/signatory.cairo index cdb53c0..efdcee5 100644 --- a/src/components/signatory/signatory.cairo +++ b/src/components/signatory/signatory.cairo @@ -6,11 +6,11 @@ pub mod SignatoryComponent { // ************************************************************************* // IMPORTS // ************************************************************************* - use starknet::{ - get_caller_address, get_contract_address, ContractAddress - }; + 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; + use token_bound_accounts::components::permissionable::permissionable::PermissionableComponent; + use token_bound_accounts::components::permissionable::permissionable::PermissionableComponent::PermissionableImpl; // ************************************************************************* // STORAGE @@ -26,17 +26,19 @@ pub mod SignatoryComponent { TContractState, +HasComponent, +Drop, - impl Account: AccountComponent::HasComponent + impl Account: AccountComponent::HasComponent, + impl Permissionable: PermissionableComponent::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 { + 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); + let owner = account._get_owner(contract_address, token_id); // validate if (signer == owner) { @@ -46,35 +48,58 @@ pub mod SignatoryComponent { } } - /// @notice implements a signer validation where both NFT owner and the root owner (for nested accounts) are valid signers. + /// @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 { + 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); + let owner = account._get_owner(contract_address, token_id); // get root owner - let root_owner = account - ._get_root_owner(contract_address, token_id); + let root_owner = account._get_root_owner(contract_address, token_id); // validate if (signer == owner) { return true; - } - else if(signer == root_owner) { + } else if (signer == root_owner) { return true; - } - else { + } else { return false; } } - /// @notice implements a more complex signer validation where NFT owner, root owner, and permissioned addresses are valid signers. + /// @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 + fn _permissioned_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); + + // check if signer has permissions + let permission = get_dep_component!(self, Permissionable); + let is_permissioned = permission.has_permission(owner, signer); + + // validate + if (signer == owner) { + return true; + } else if (signer == root_owner) { + return true; + } else if (is_permissioned) { + return true; + } else { + return false; + } } } } diff --git a/src/interfaces.cairo b/src/interfaces.cairo index de01f5a..8048787 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -4,4 +4,5 @@ pub mod IRegistry; pub mod IUpgradeable; pub mod IExecutable; pub mod ILockable; -pub mod ISignatory; \ No newline at end of file +pub mod ISignatory; +pub mod IPermissionable; diff --git a/src/interfaces/ILockable.cairo b/src/interfaces/ILockable.cairo index d075dd2..d893e39 100644 --- a/src/interfaces/ILockable.cairo +++ b/src/interfaces/ILockable.cairo @@ -1,3 +1,6 @@ +// ************************************************************************* +// LOCKABLE INTERFACE +// ************************************************************************* use starknet::ContractAddress; #[starknet::interface] diff --git a/src/interfaces/IPermissionable.cairo b/src/interfaces/IPermissionable.cairo new file mode 100644 index 0000000..438a477 --- /dev/null +++ b/src/interfaces/IPermissionable.cairo @@ -0,0 +1,16 @@ +// ************************************************************************* +// PERMISSIONABLE INTERFACE +// ************************************************************************* +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IPermissionable { + fn set_permission( + ref self: TContractState, + permissioned_addresses: Array, + permissions: Array + ); + fn has_permission( + self: @TContractState, owner: ContractAddress, permissioned_address: ContractAddress + ) -> bool; +} diff --git a/tests/test_account_component.cairo b/tests/test_account_component.cairo index 33c217c..58c84b0 100644 --- a/tests/test_account_component.cairo +++ b/tests/test_account_component.cairo @@ -159,20 +159,6 @@ fn test_is_valid_signature() { stop_cheat_caller_address(contract_address); } -#[test] -fn test_is_valid_signer() { - let (contract_address, erc721_contract_address) = __setup__(); - let dispatcher = IAccountDispatcher { contract_address }; - let token_dispatcher = IERC721Dispatcher { contract_address: erc721_contract_address }; - let token_owner = token_dispatcher.ownerOf(1.try_into().unwrap()); - - // check for valid signer - let valid_signer = dispatcher.is_valid_signer(token_owner); - let invalid_signer = dispatcher.is_valid_signer(ACCOUNT.try_into().unwrap()); - assert(valid_signer == true, 'signer is meant to be valid!'); - assert(invalid_signer == false, 'signer is meant to be invalid!'); -} - #[test] fn test_execute() { let (contract_address, erc721_contract_address) = __setup__();