From 6f1d4fd7c6276a781b84a9972e1186b48358318f Mon Sep 17 00:00:00 2001 From: Abdulkadir Date: Tue, 29 Apr 2025 15:50:09 +0100 Subject: [PATCH 1/2] Create functionality for delegating account permissions --- .gitignore | 1 - .tool-versions | 2 +- Scarb.toml | 3 + src/base/errors.cairo | 35 ++ src/chainlib/AccountDelegation.cairo | 295 ++++++++++++++++ src/chainlib/ChainLib.cairo | 20 +- src/events/AccountDelegationEvent.cairo | 39 ++ src/interfaces/IAccountDelegation.cairo | 26 ++ src/interfaces/IChainLib.cairo | 6 +- src/lib.cairo | 6 +- tests/lib.cairo | 1 + tests/test_ChainLib.cairo | 6 +- tests/test_account_delegation.cairo | 450 ++++++++++++++++++++++++ 13 files changed, 869 insertions(+), 21 deletions(-) create mode 100644 src/chainlib/AccountDelegation.cairo create mode 100644 src/events/AccountDelegationEvent.cairo create mode 100644 src/interfaces/IAccountDelegation.cairo create mode 100644 tests/test_account_delegation.cairo diff --git a/.gitignore b/.gitignore index 3542ed2..73aa31e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ target .snfoundry_cache/ -.tool-versions diff --git a/.tool-versions b/.tool-versions index ddfaa0e..e8b3e21 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ scarb 2.8.4 -starknet-foundry 0.39.0 \ No newline at end of file +starknet-foundry 0.39.0 diff --git a/Scarb.toml b/Scarb.toml index 6203031..2e59346 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -15,6 +15,9 @@ assert_macros = "2.8.4" [[target.starknet-contract]] sierra = true + + + [scripts] test = "snforge test" diff --git a/src/base/errors.cairo b/src/base/errors.cairo index 8b13789..4a703e3 100644 --- a/src/base/errors.cairo +++ b/src/base/errors.cairo @@ -1 +1,36 @@ +pub mod Error { + pub const NOT_AUTHORIZED: felt252 = 'NotAuthorized'; + pub const DELEGATION_EXPIRED: felt252 = 'DelegationExpired'; + pub const ACTION_LIMIT_REACHED: felt252 = 'ActionLimitReached'; + pub const DELEGATION_NOT_ACTIVE: felt252 = 'DelegationNotActive'; + pub const INVALID_DELEGATION: felt252 = 'InvalidDelegate'; + pub const INVALID_EXPIRY: felt252 = 'InvalidExpiry'; + pub const INVALID_PERMISSION: felt252 = 'InvalidPermission'; +} +// // Errors +// #[derive(Drop, PartialEq)] +// pub enum Error { +// NotAuthorized, +// DelegationExpired, +// ActionLimitReached, +// DelegationNotActive, +// InvalidDelegate, +// InvalidExpiry, +// InvalidPermission, +// } + +// impl ImplError of ErrorImpl { +// fn into_felt252(self) -> felt252 { +// match self { +// Error::NotAuthorized => 0, +// Error::DelegationExpired => 1, +// Error::ActionLimitReached => 2, +// Error::DelegationNotActive => 3, +// Error::InvalidDelegate => 4, +// Error::InvalidExpiry => 5, +// Error::InvalidPermission => 6, +// } +// } +// } + diff --git a/src/chainlib/AccountDelegation.cairo b/src/chainlib/AccountDelegation.cairo new file mode 100644 index 0000000..b22dfce --- /dev/null +++ b/src/chainlib/AccountDelegation.cairo @@ -0,0 +1,295 @@ +#[starknet::contract] +pub mod AccountDelegation { + use chain_lib::base::errors::Error; + use chain_lib::events::AccountDelegationEvent::*; + use chain_lib::interfaces::IAccountDelegation::IAccountDelegation; + use core::num::traits::zero::Zero; + use core::traits::{Into, TryInto}; + use starknet::contract_address::ContractAddress; + use starknet::event::EventEmitter; + use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; + use starknet::{get_block_timestamp, get_caller_address}; + + + // Structure to represent delegation permissions + #[derive(Copy, Drop, Serde, starknet::Store)] + pub struct DelegationInfo { + pub delegate: ContractAddress, // Delegate account address + pub expiry: u64, // Unix timestamp when delegation expires (0 for no expiration) + pub max_actions: u64, // Maximum number of actions allowed (0 for unlimited) + pub action_count: u64, // Current count of actions performed + pub active: bool // Whether this delegation is active + } + + // Permission constants + const PERMISSION_TRANSFER: u8 = 1; + const PERMISSION_SIGN: u8 = 2; + const PERMISSION_CALL: u8 = 3; + const PERMISSION_ADMIN: u8 = 4; + + #[storage] + pub struct Storage { + owner_namespaces: Map< + ContractAddress, felt252, + >, // Maps from owner address to a namespace identifier + delegations: Map< + (felt252, u8), DelegationInfo, + >, // Maps from (namespace, permission_id) to delegation info + owners: Map, // Mapping to store if an address is the owner + next_namespace: felt252 // Counter for generating unique namespaces + } + + // Events for delegation activities + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + DelegationCreated: DelegationCreated, + DelegationRevoked: DelegationRevoked, + DelegationUsed: DelegationUsed, + DelegationExpire: DelegationExpire, + } + + + #[constructor] + fn constructor(ref self: ContractState, initial_owner: ContractAddress) { + self.owners.write(initial_owner, true); + // Initialize namespace counter + self.next_namespace.write(1); + } + + #[external(v0)] + impl AccountDelegationImpl of IAccountDelegation { + // Function to add a new owner + fn add_owner(ref self: ContractState, new_owner: ContractAddress) { + let caller = get_caller_address(); + self.assert_is_owner(caller); + self.owners.write(new_owner, true); + } + + // Function to delegate permission to another account + fn delegate_permission( + ref self: ContractState, + delegate: ContractAddress, + permission_id: u8, + expiry: u64, + max_actions: u64, + ) { + // Validate inputs + assert(!delegate.is_zero(), Error::INVALID_DELEGATION); + assert(self.is_valid_permission(permission_id), Error::INVALID_PERMISSION); + + // Check that caller is the owner + let owner = get_caller_address(); + self.assert_is_owner(owner); + + // Check that expiry is in the future if provided + let current_time = get_block_timestamp(); + if expiry != 0 { + assert(expiry > current_time, Error::INVALID_EXPIRY); + } + + // Create delegation info + let delegation = DelegationInfo { + delegate, expiry, max_actions, action_count: 0, active: true, + }; + + // Store delegation using the nested maps approach + let namespace = self.get_or_create_namespace(owner); + self.delegations.write((namespace, permission_id), delegation); + + // Emit event + self + .emit( + Event::DelegationCreated( + DelegationCreated { owner, delegate, permission_id, expiry, max_actions }, + ), + ); + } + + // Function to revoke delegation + fn revoke_delegation( + ref self: ContractState, permission_id: u8, delegate: ContractAddress, + ) { + // Validate permission + assert(self.is_valid_permission(permission_id), Error::INVALID_PERMISSION); + + let owner = get_caller_address(); + self.assert_is_owner(owner); + + // Get current delegation info + let namespace = self.owner_namespaces.read(owner); + let mut delegation_info = self.delegations.read((namespace, permission_id)); + + // Ensure delegate matches + assert(delegation_info.delegate == delegate, Error::NOT_AUTHORIZED); + + // Deactivate delegation + delegation_info.active = false; + self.delegations.write((namespace, permission_id), delegation_info); + + // Emit event + self + .emit( + Event::DelegationRevoked(DelegationRevoked { owner, delegate, permission_id }), + ); + } + + // Function to check if a caller has delegated permission + fn has_delegation( + self: @ContractState, + owner: ContractAddress, + caller: ContractAddress, + permission_id: u8, + ) -> bool { + // Validate permission + if !self.is_valid_permission(permission_id) { + return false; + } + + let namespace = self.owner_namespaces.read(owner); + if namespace == 0 { + return false; // No delegations exist for this owner + } + + let delegation = self.delegations.read((namespace, permission_id)); + + if delegation.delegate != caller || !delegation.active { + return false; + } + + // Check expiry + if delegation.expiry != 0 && delegation.expiry <= get_block_timestamp() { + return false; + } + + // Check action limit + if delegation.max_actions != 0 && delegation.action_count >= delegation.max_actions { + return false; + } + + true + } + + // Function to use a delegation (increment action count) + fn use_delegation(ref self: ContractState, owner: ContractAddress, permission_id: u8) { + // Validate permission + assert(self.is_valid_permission(permission_id), Error::INVALID_PERMISSION); + + let caller = get_caller_address(); + let namespace = self.owner_namespaces.read(owner); + assert(namespace != 0, Error::NOT_AUTHORIZED); + + // Get delegation info + let mut delegation = self.delegations.read((namespace, permission_id)); + + // Verify delegation + assert(delegation.delegate == caller, Error::NOT_AUTHORIZED); + assert(delegation.active, Error::DELEGATION_NOT_ACTIVE); + + // Check expiry + let current_time = get_block_timestamp(); + if delegation.expiry != 0 && delegation.expiry <= current_time { + // Mark as inactive since it's expired + delegation.active = false; + self.delegations.write((namespace, permission_id), delegation); + + // Emit expiry event + self + .emit( + Event::DelegationExpire( + DelegationExpire { owner, delegate: caller, permission_id }, + ), + ); + + // panic!("Delegation Expired"); + assert(delegation.expiry > current_time, Error::DELEGATION_EXPIRED); + } + + // Check action limit + if delegation.max_actions != 0 { + assert( + delegation.action_count < delegation.max_actions, Error::ACTION_LIMIT_REACHED, + ); + + // Increment action count + delegation.action_count += 1; + + // Update if this was the last allowed action + if delegation.action_count == delegation.max_actions { + delegation.active = false; + } + + // Update storage + self.delegations.write((namespace, permission_id), delegation); + } + + // Emit usage event + self + .emit( + Event::DelegationUsed( + DelegationUsed { + owner, + delegate: caller, + permission_id, + action_count: delegation.action_count, + }, + ), + ); + } + + // Function to get delegation details + fn get_delegation_info( + self: @ContractState, owner: ContractAddress, permission_id: u8, + ) -> DelegationInfo { + assert(self.is_valid_permission(permission_id), Error::INVALID_PERMISSION); + + let namespace = self.owner_namespaces.read(owner); + if namespace == 0 { + // Return empty delegation if namespace doesn't exist + // let zero_address: ContractAddress = 0.try_into(); + return DelegationInfo { + delegate: 0.try_into().unwrap(), + expiry: 0, + max_actions: 0, + action_count: 0, + active: false, + }; + } + + self.delegations.read((namespace, permission_id)) + } + + // Function to check if an address is an owner + fn is_owner(self: @ContractState, address: ContractAddress) -> bool { + self.owners.read(address) + } + + // Function to check if a permission ID is valid + fn is_valid_permission(self: @ContractState, permission_id: u8) -> bool { + permission_id == PERMISSION_TRANSFER + || permission_id == PERMISSION_SIGN + || permission_id == PERMISSION_CALL + || permission_id == PERMISSION_ADMIN + } + } + + // Internal helper functions + #[generate_trait] + impl HelperImpl of HelperTrait { + fn assert_is_owner(self: @ContractState, address: ContractAddress) { + assert(self.owners.read(address), Error::NOT_AUTHORIZED); + } + + fn get_or_create_namespace(ref self: ContractState, owner: ContractAddress) -> felt252 { + let namespace = self.owner_namespaces.read(owner); + if namespace == 0 { + // Create new namespace if it doesn't exist + let new_namespace = self.next_namespace.read(); + self.next_namespace.write(new_namespace + 1); + self.owner_namespaces.write(owner, new_namespace); + return new_namespace; + } + namespace + } + } +} diff --git a/src/chainlib/ChainLib.cairo b/src/chainlib/ChainLib.cairo index a84186f..60df1f3 100644 --- a/src/chainlib/ChainLib.cairo +++ b/src/chainlib/ChainLib.cairo @@ -5,8 +5,8 @@ pub mod ChainLib { StoragePointerWriteAccess, }; use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; + use crate::base::types::{Rank, Role, TokenBoundAccount, User}; use crate::interfaces::IChainLib::IChainLib; - use crate::base::types::{TokenBoundAccount, User, Role, Rank}; #[derive(Copy, Drop, Serde, starknet::Store, PartialEq, Debug)] pub enum ContentType { @@ -23,7 +23,7 @@ pub mod ChainLib { #[default] Education, Literature, - Art + Art, } #[derive(Copy, Drop, Serde, starknet::Store, Debug)] @@ -33,7 +33,7 @@ pub mod ChainLib { pub description: felt252, pub content_type: ContentType, pub creator: ContractAddress, - pub category: Category + pub category: Category, } #[storage] @@ -46,9 +46,9 @@ pub mod ChainLib { next_course_id: u256, user_id: u256, users: Map, - creators_content: Map::, - content: Map::, - content_tags: Map::> + creators_content: Map, + content: Map, + content_tags: Map>, } @@ -85,7 +85,7 @@ pub mod ChainLib { /// @param init_param2 An additional initialization parameter. /// @return account_id The unique identifier assigned to the token-bound account. fn create_token_account( - ref self: ContractState, user_name: felt252, init_param1: felt252, init_param2: felt252 + ref self: ContractState, user_name: felt252, init_param1: felt252, init_param2: felt252, ) -> u256 { // Ensure that the username is not empty. assert!(user_name != 0, "User name cannot be empty"); @@ -126,7 +126,7 @@ pub mod ChainLib { token_bound_account } fn get_token_bound_account_by_owner( - ref self: ContractState, address: ContractAddress + ref self: ContractState, address: ContractAddress, ) -> TokenBoundAccount { let token_bound_account = self.accountsaddr.read(address); token_bound_account @@ -144,7 +144,7 @@ pub mod ChainLib { /// @param metadata Additional metadata associated with the user. /// @return user_id The unique identifier assigned to the user. fn register_user( - ref self: ContractState, username: felt252, role: Role, rank: Rank, metadata: felt252 + ref self: ContractState, username: felt252, role: Role, rank: Rank, metadata: felt252, ) -> u256 { // Ensure that the username is not empty. assert!(username != 0, "User name cannot be empty"); @@ -160,7 +160,7 @@ pub mod ChainLib { role: role, rank: rank, verified: false, // Default verification status is false. - metadata: metadata + metadata: metadata, }; // Store the new user in the users mapping. diff --git a/src/events/AccountDelegationEvent.cairo b/src/events/AccountDelegationEvent.cairo new file mode 100644 index 0000000..cde34fc --- /dev/null +++ b/src/events/AccountDelegationEvent.cairo @@ -0,0 +1,39 @@ +// use chain_lib::chainlib::AccountDelegation::AccountDelegation::{ Event}; + +use starknet::ContractAddress; + + +// Event emitted when delegation is created +#[derive(Drop, starknet::Event)] +pub struct DelegationCreated { + pub owner: ContractAddress, + pub delegate: ContractAddress, + pub permission_id: u8, + pub expiry: u64, + pub max_actions: u64, +} + +// Event emitted when delegation is revoked +#[derive(Drop, starknet::Event)] +pub struct DelegationRevoked { + pub owner: ContractAddress, + pub delegate: ContractAddress, + pub permission_id: u8, +} + +// Event emitted when delegation is used +#[derive(Drop, starknet::Event)] +pub struct DelegationUsed { + pub owner: ContractAddress, + pub delegate: ContractAddress, + pub permission_id: u8, + pub action_count: u64, +} + +// Event emitted when delegation expires +#[derive(Drop, starknet::Event)] +pub struct DelegationExpire { + pub owner: ContractAddress, + pub delegate: ContractAddress, + pub permission_id: u8, +} diff --git a/src/interfaces/IAccountDelegation.cairo b/src/interfaces/IAccountDelegation.cairo new file mode 100644 index 0000000..4021230 --- /dev/null +++ b/src/interfaces/IAccountDelegation.cairo @@ -0,0 +1,26 @@ +use chain_lib::chainlib::AccountDelegation::AccountDelegation::DelegationInfo; +use starknet::ContractAddress; + + +#[starknet::interface] +pub trait IAccountDelegation { + fn add_owner(ref self: TContractState, new_owner: ContractAddress); + fn delegate_permission( + ref self: TContractState, + delegate: ContractAddress, + permission_id: u8, + expiry: u64, + max_actions: u64, + ); + fn revoke_delegation(ref self: TContractState, permission_id: u8, delegate: ContractAddress); + fn has_delegation( + self: @TContractState, owner: ContractAddress, caller: ContractAddress, permission_id: u8, + ) -> bool; + fn use_delegation(ref self: TContractState, owner: ContractAddress, permission_id: u8); + fn get_delegation_info( + self: @TContractState, owner: ContractAddress, permission_id: u8, + ) -> DelegationInfo; + fn is_owner(self: @TContractState, address: ContractAddress) -> bool; + fn is_valid_permission(self: @TContractState, permission_id: u8) -> bool; +} + diff --git a/src/interfaces/IChainLib.cairo b/src/interfaces/IChainLib.cairo index 4f90038..966ff70 100644 --- a/src/interfaces/IChainLib.cairo +++ b/src/interfaces/IChainLib.cairo @@ -1,5 +1,5 @@ use starknet::ContractAddress; -use crate::base::types::{TokenBoundAccount, User, Role, Rank}; +use crate::base::types::{Rank, Role, TokenBoundAccount, User}; #[starknet::interface] pub trait IChainLib { @@ -10,11 +10,11 @@ pub trait IChainLib { fn get_token_bound_account(ref self: TContractState, id: u256) -> TokenBoundAccount; fn get_token_bound_account_by_owner( - ref self: TContractState, address: ContractAddress + ref self: TContractState, address: ContractAddress, ) -> TokenBoundAccount; fn register_user( - ref self: TContractState, username: felt252, role: Role, rank: Rank, metadata: felt252 + ref self: TContractState, username: felt252, role: Role, rank: Rank, metadata: felt252, ) -> u256; fn verify_user(ref self: TContractState, user_id: u256) -> bool; fn retrieve_user_profile(ref self: TContractState, user_id: u256) -> User; diff --git a/src/lib.cairo b/src/lib.cairo index f727319..fe156a4 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -3,9 +3,13 @@ pub mod base { pub mod types; } pub mod chainlib { + pub mod AccountDelegation; pub mod ChainLib; } pub mod interfaces { + pub mod IAccountDelegation; pub mod IChainLib; } - +pub mod events { + pub mod AccountDelegationEvent; +} diff --git a/tests/lib.cairo b/tests/lib.cairo index a0a3575..db8d904 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -1,3 +1,4 @@ #[cfg(test)] pub mod test_ChainLib; +pub mod test_account_delegation; diff --git a/tests/test_ChainLib.cairo b/tests/test_ChainLib.cairo index 5fbbb1f..2a7149a 100644 --- a/tests/test_ChainLib.cairo +++ b/tests/test_ChainLib.cairo @@ -1,14 +1,12 @@ // Import the contract modules +use chain_lib::base::types::{Rank, Role}; use chain_lib::chainlib::ChainLib; - use chain_lib::interfaces::IChainLib::{IChainLib, IChainLibDispatcher, IChainLibDispatcherTrait}; use snforge_std::{CheatSpan, ContractClassTrait, DeclareResultTrait, cheat_caller_address, declare}; use starknet::ContractAddress; use starknet::class_hash::ClassHash; use starknet::contract_address::contract_address_const; use starknet::testing::{set_caller_address, set_contract_address}; -use chain_lib::base::types::{Role, Rank}; - fn setup() -> (ContractAddress, ContractAddress) { let declare_result = declare("ChainLib"); @@ -38,7 +36,6 @@ fn test_initial_data() { assert(admin == admin_address, 'deployment failed'); } - #[test] fn test_create_token_bount_account() { let (contract_address, _) = setup(); @@ -62,7 +59,6 @@ fn test_create_token_bount_account() { assert(token_bound_account.init_param2 == init_param2, 'init_param2 mismatch'); } - #[test] fn test_create_user() { let (contract_address, _) = setup(); diff --git a/tests/test_account_delegation.cairo b/tests/test_account_delegation.cairo new file mode 100644 index 0000000..639c4de --- /dev/null +++ b/tests/test_account_delegation.cairo @@ -0,0 +1,450 @@ +// Permission constants +const PERMISSION_TRANSFER: u8 = 1; +const PERMISSION_SIGN: u8 = 2; +const PERMISSION_CALL: u8 = 3; +const PERMISSION_ADMIN: u8 = 4; +use chain_lib::chainlib::AccountDelegation::AccountDelegation::*; +use chain_lib::interfaces::IAccountDelegation::{ + IAccountDelegationDispatcher, IAccountDelegationDispatcherTrait, +}; +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, EventSpyAssertionsTrait, declare, spy_events, + start_cheat_caller_address, stop_cheat_caller_address, +}; +use starknet::{ContractAddress, contract_address_const, get_block_timestamp}; + + +fn setup() -> ContractAddress { + let contract_class = declare("AccountDelegation").unwrap().contract_class(); + + // prepare constructor argument + let next_namespace: felt252 = 1; + let account: ContractAddress = contract_address_const::<'owner'>(); + + let calldata = array![next_namespace.into(), account.into()]; + + let (contract_address, _) = contract_class.deploy(@calldata).unwrap(); + contract_address +} + + +#[test] +fn test_constructor() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<0xabc>(); + let non_owner = contract_address_const::<0xadb>(); + + let namespace = 1; + + // start_cheat_caller_address(contract_address, owner); + + // Check if the initial owner is correctly set + assert(contract_instance.is_owner(owner), 'Owner should be set'); + + // Verify non-owner + assert(!contract_instance.is_owner(non_owner), 'Non-owner should not be set'); + // stop_cheat_caller_address(owner); +} + + +#[test] +fn test_add_owner() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<'OWNER'>(); + let new_owner = contract_address_const::<'ANOTHER_USER'>(); + + // Set caller as owner + start_cheat_caller_address(contract_address, owner); + + // Add new owner + contract_instance.add_owner(new_owner); + + // Verify new owner was added + assert(contract_instance.is_owner(new_owner), 'New owner should be set'); + stop_cheat_caller_address(owner); +} + +#[test] +#[should_panic(expected: NOT_AUTHORIZED)] +fn test_add_owner_unauthorized() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let non_owner = contract_address_const::<'ANOTHER_USER'>(); + let new_address = contract_address_const::<'DELEGATE'>(); + + // Set caller as non-owner + start_cheat_caller_address(contract_address, non_owner); + + // Attempt to add new owner, should fail + contract_instance.add_owner(new_address); + + stop_cheat_caller_address(non_owner); +} + + +#[test] +fn test_delegate_permission() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<'OWNER'>(); + let delegate = contract_address_const::<'DELEGATE'>(); + + // Set caller as owner + start_cheat_caller_address(contract_address, owner); + + // Set current time + let current_time: u64 = 1000; + // set_block_timestamp(current_time); + // let future_time = current_time + 3600; + + // Delegate permission with expiry and max actions + let expiry: u64 = current_time + 3600; // 1 hour in the future + let max_actions: u64 = 5; + contract_instance.delegate_permission(delegate, PERMISSION_TRANSFER, expiry, max_actions); + + // Check delegation info + let delegation = contract_instance.get_delegation_info(owner, PERMISSION_TRANSFER); + assert(delegation.delegate == delegate, 'Delegate address mismatch'); + assert(delegation.expiry == expiry, 'Expiry time mismatch'); + assert(delegation.max_actions == max_actions, 'Max actions mismatch'); + assert(delegation.action_count == 0, 'Action count should be 0'); + assert(delegation.active == true, 'Delegation should be active'); + + // Verify has_delegation returns true + assert( + contract_instance.has_delegation(owner, delegate, PERMISSION_TRANSFER) == true, + 'has_delegation is true', + ); +} + +#[test] +fn test_delegate_permission_unlimited() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<'OWNER'>(); + let delegate = contract_address_const::<'DELEGATE'>(); + + // Set caller as owner + start_cheat_caller_address(contract_address, owner); + + // Delegate permission with no expiry and unlimited actions + contract_instance.delegate_permission(delegate, PERMISSION_SIGN, 0, 0); + + // Check delegation info + let delegation = contract_instance.get_delegation_info(owner, PERMISSION_SIGN); + assert(delegation.expiry == 0, 'Expiry should be 0'); + assert(delegation.max_actions == 0, 'Max actions should be 0'); + assert(delegation.active == true, 'Delegation should be active'); +} + +#[test] +#[should_panic(expected: INVALID_DELEGATION)] +fn test_delegate_permission_zero_address() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<'OWNER'>(); + + let current_time = 1000; + let FUTURE_TIME = current_time + 3600; + + let delegate: ContractAddress = 0.try_into().unwrap(); + // Set caller as owner + start_cheat_caller_address(contract_address, owner); + + // Try to delegate to zero address + contract_instance.delegate_permission(delegate, PERMISSION_TRANSFER, FUTURE_TIME, 0); +} + +#[test] +#[should_panic(expected: INVALID_EXPIRY)] +fn test_delegate_permission_past_expiry() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<'OWNER'>(); + let delegate = contract_address_const::<'DELEGATE'>(); + + // Set caller as owner + start_cheat_caller_address(contract_address, owner); + + // Set current time + let current_time: u64 = 1000; + // set_block_timestamp(current_time); + + // Try to delegate with expiry in the past + let past_expiry: u64 = current_time - 100; + contract_instance.delegate_permission(delegate, PERMISSION_TRANSFER, past_expiry, 0); +} + + +#[test] +#[should_panic(expected: INVALID_PERMISSION)] +fn test_delegate_invalid_permission() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<'OWNER'>(); + let delegate = contract_address_const::<'DELEGATE'>(); + + let current_time = 1000; + let FUTURE_TIME = current_time + 3600; + + // Set caller as owner + start_cheat_caller_address(contract_address, owner); + + // Try to use an invalid permission ID + let invalid_permission: u8 = 99; + contract_instance.delegate_permission(delegate, invalid_permission, FUTURE_TIME, 0); +} + + +#[test] +fn test_revoke_delegation() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<'OWNER'>(); + let delegate = contract_address_const::<'DELEGATE'>(); + + let current_time = 1000; + let FUTURE_TIME = current_time + 3600; + + // Set caller as owner + start_cheat_caller_address(contract_address, owner); + + // First delegate permission + contract_instance.delegate_permission(delegate, PERMISSION_TRANSFER, FUTURE_TIME, 0); + + // Then revoke it + contract_instance.revoke_delegation(PERMISSION_TRANSFER, delegate); + + // Check delegation info is updated + let delegation = contract_instance.get_delegation_info(owner, PERMISSION_TRANSFER); + assert(delegation.active == false, 'Delegation should be inactive'); + + // Verify has_delegation returns false + assert( + !contract_instance.has_delegation(owner, delegate, PERMISSION_TRANSFER), + 'has_delegation is false', + ); +} + +#[test] +#[should_panic(expected: NOT_AUTHORIZED)] +fn test_revoke_delegation_wrong_delegate() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<'OWNER'>(); + let delegate = contract_address_const::<'DELEGATE'>(); + let wrong_delegate = contract_address_const::<'ANOTHER_USER'>(); + + let current_time = 1000; + let FUTURE_TIME = current_time + 3600; + + // Set caller as owner + start_cheat_caller_address(contract_address, owner); + + // First delegate permission + contract_instance.delegate_permission(delegate, PERMISSION_TRANSFER, FUTURE_TIME, 0); + + // Try to revoke for wrong delegate + contract_instance.revoke_delegation(PERMISSION_TRANSFER, wrong_delegate); +} + +#[test] +fn test_use_delegation() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<'OWNER'>(); + let delegate = contract_address_const::<'DELEGATE'>(); + + // Set owner as caller to create delegation + start_cheat_caller_address(contract_address, owner); + + // Set current time + let current_time = 1000; + let FUTURE_TIME = current_time + 3600; + + // Create delegation with max actions + let max_actions: u64 = 3; + contract_instance.delegate_permission(delegate, PERMISSION_CALL, FUTURE_TIME, max_actions); + + // Switch caller to delegate + start_cheat_caller_address(contract_address, delegate); + + // Use delegation + contract_instance.use_delegation(owner, PERMISSION_CALL); + + // Verify action count increased + let delegation = contract_instance.get_delegation_info(owner, PERMISSION_CALL); + assert(delegation.action_count == 1, 'Action count should increase'); + assert(delegation.active == true, 'Delegation should still active'); + + // Use delegation two more times + contract_instance.use_delegation(owner, PERMISSION_CALL); + contract_instance.use_delegation(owner, PERMISSION_CALL); + + // Verify delegation is now inactive due to reaching max actions + let final_delegation = contract_instance.get_delegation_info(owner, PERMISSION_CALL); + assert(final_delegation.action_count == 3, 'Action count should be max'); + assert(final_delegation.active == false, 'Delegation should be inactive'); +} + +#[test] +#[should_panic(expected: ACTION_LIMIT_REACHED)] +fn test_use_delegation_exceed_max_actions() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<'OWNER'>(); + let delegate = contract_address_const::<'DELEGATE'>(); + + let current_time = 1000; + let FUTURE_TIME = current_time + 3600; + + // Set owner as caller to create delegation + start_cheat_caller_address(contract_address, owner); + + // Create delegation with max actions + contract_instance.delegate_permission(delegate, PERMISSION_TRANSFER, FUTURE_TIME, 1); + + // Switch caller to delegate + start_cheat_caller_address(contract_address, delegate); + + // Use delegation once (should succeed) + contract_instance.use_delegation(owner, PERMISSION_TRANSFER); + + // Try to use it again (should fail) + contract_instance.use_delegation(owner, PERMISSION_TRANSFER); +} + + +#[test] +#[should_panic(expected: ('NotAuthorized',))] +fn test_use_delegation_wrong_delegate() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<'OWNER'>(); + let delegate = contract_address_const::<'DELEGATE'>(); + let wrong_delegate = contract_address_const::<'ANOTHER_USER'>(); + + let current_time = 1000; + let FUTURE_TIME = current_time + 3600; + + // Set owner as caller to create delegation + start_cheat_caller_address(contract_address, owner); + + // Create delegation + contract_instance.delegate_permission(delegate, PERMISSION_ADMIN, FUTURE_TIME, 0); + + // Set wrong delegate as caller + start_cheat_caller_address(contract_address, wrong_delegate); + + // Try to use delegation as wrong delegate + contract_instance.use_delegation(owner, PERMISSION_ADMIN); +} + + +#[test] +#[should_panic(expected: ('DelegationNotActive',))] +fn test_use_delegation_inactive() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<'OWNER'>(); + let delegate = contract_address_const::<'DELEGATE'>(); + + let current_time = 1000; + let FUTURE_TIME = current_time + 3600; + + // Set owner as caller to create delegation + start_cheat_caller_address(contract_address, owner); + + // Create delegation + contract_instance.delegate_permission(delegate, PERMISSION_SIGN, FUTURE_TIME, 0); + + // Revoke delegation + contract_instance.revoke_delegation(PERMISSION_SIGN, delegate); + + // Set delegate as caller + start_cheat_caller_address(contract_address, delegate); + + // Try to use inactive delegation + contract_instance.use_delegation(owner, PERMISSION_SIGN); +} + + +#[test] +fn test_has_delegation_multiple_permissions() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner = contract_address_const::<'OWNER'>(); + let delegate = contract_address_const::<'DELEGATE'>(); + + let current_time = 1000; + let FUTURE_TIME = current_time + 3600; + + // Set owner as caller + start_cheat_caller_address(contract_address, owner); + + // Create delegations for different permissions + contract_instance.delegate_permission(delegate, PERMISSION_TRANSFER, FUTURE_TIME, 0); + contract_instance.delegate_permission(delegate, PERMISSION_SIGN, FUTURE_TIME, 0); + + // Check has_delegation for both permissions + assert( + contract_instance.has_delegation(owner, delegate, PERMISSION_TRANSFER), + 'Should have Transfer permission', + ); + assert( + contract_instance.has_delegation(owner, delegate, PERMISSION_SIGN), + 'Should have Sign permission', + ); + + // Should not have Call permission + assert( + !contract_instance.has_delegation(owner, delegate, PERMISSION_CALL), + 'Should not have Call permission', + ); +} + + +#[test] +fn test_multiple_owners() { + let contract_address = setup(); + let contract_instance = IAccountDelegationDispatcher { contract_address }; + + let owner1 = contract_address_const::<'OWNER'>(); + let owner2 = contract_address_const::<'ANOTHER_USER'>(); + let delegate = contract_address_const::<'DELEGATE'>(); + + let current_time = 1000; + let FUTURE_TIME = current_time + 3600; + + // Set first owner as caller + start_cheat_caller_address(contract_address, owner1); + + // Add second owner + contract_instance.add_owner(owner2); + + // Second owner should be able to delegate permissions + start_cheat_caller_address(contract_address, owner2); + contract_instance.delegate_permission(delegate, PERMISSION_ADMIN, FUTURE_TIME, 0); + + // Check delegation info + let delegation = contract_instance.get_delegation_info(owner2, PERMISSION_ADMIN); + assert(delegation.delegate == delegate, 'Delegate address mismatch'); + assert(delegation.active == true, 'Delegation should be active'); +} From 04bc4742fc1751388e218b0b5bb4404c22ee4fad Mon Sep 17 00:00:00 2001 From: Abdulkadir Date: Tue, 29 Apr 2025 16:14:18 +0100 Subject: [PATCH 2/2] Format --- src/base/errors.cairo | 27 --------------------------- tests/test_account_delegation.cairo | 16 ++++++++-------- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/src/base/errors.cairo b/src/base/errors.cairo index 4a703e3..ad76373 100644 --- a/src/base/errors.cairo +++ b/src/base/errors.cairo @@ -7,30 +7,3 @@ pub mod Error { pub const INVALID_EXPIRY: felt252 = 'InvalidExpiry'; pub const INVALID_PERMISSION: felt252 = 'InvalidPermission'; } -// // Errors -// #[derive(Drop, PartialEq)] -// pub enum Error { -// NotAuthorized, -// DelegationExpired, -// ActionLimitReached, -// DelegationNotActive, -// InvalidDelegate, -// InvalidExpiry, -// InvalidPermission, -// } - -// impl ImplError of ErrorImpl { -// fn into_felt252(self) -> felt252 { -// match self { -// Error::NotAuthorized => 0, -// Error::DelegationExpired => 1, -// Error::ActionLimitReached => 2, -// Error::DelegationNotActive => 3, -// Error::InvalidDelegate => 4, -// Error::InvalidExpiry => 5, -// Error::InvalidPermission => 6, -// } -// } -// } - - diff --git a/tests/test_account_delegation.cairo b/tests/test_account_delegation.cairo index 639c4de..aa8300a 100644 --- a/tests/test_account_delegation.cairo +++ b/tests/test_account_delegation.cairo @@ -69,7 +69,7 @@ fn test_add_owner() { } #[test] -#[should_panic(expected: NOT_AUTHORIZED)] +#[should_panic(expected: 'NotAuthorized')] fn test_add_owner_unauthorized() { let contract_address = setup(); let contract_instance = IAccountDelegationDispatcher { contract_address }; @@ -145,7 +145,7 @@ fn test_delegate_permission_unlimited() { } #[test] -#[should_panic(expected: INVALID_DELEGATION)] +#[should_panic(expected: 'InvalidDelegate')] fn test_delegate_permission_zero_address() { let contract_address = setup(); let contract_instance = IAccountDelegationDispatcher { contract_address }; @@ -164,7 +164,7 @@ fn test_delegate_permission_zero_address() { } #[test] -#[should_panic(expected: INVALID_EXPIRY)] +#[should_panic(expected: 'InvalidExpiry')] fn test_delegate_permission_past_expiry() { let contract_address = setup(); let contract_instance = IAccountDelegationDispatcher { contract_address }; @@ -186,7 +186,7 @@ fn test_delegate_permission_past_expiry() { #[test] -#[should_panic(expected: INVALID_PERMISSION)] +#[should_panic(expected: 'InvalidPermission')] fn test_delegate_invalid_permission() { let contract_address = setup(); let contract_instance = IAccountDelegationDispatcher { contract_address }; @@ -238,7 +238,7 @@ fn test_revoke_delegation() { } #[test] -#[should_panic(expected: NOT_AUTHORIZED)] +#[should_panic(expected: 'NotAuthorized')] fn test_revoke_delegation_wrong_delegate() { let contract_address = setup(); let contract_instance = IAccountDelegationDispatcher { contract_address }; @@ -301,7 +301,7 @@ fn test_use_delegation() { } #[test] -#[should_panic(expected: ACTION_LIMIT_REACHED)] +#[should_panic(expected: 'ActionLimitReached')] fn test_use_delegation_exceed_max_actions() { let contract_address = setup(); let contract_instance = IAccountDelegationDispatcher { contract_address }; @@ -330,7 +330,7 @@ fn test_use_delegation_exceed_max_actions() { #[test] -#[should_panic(expected: ('NotAuthorized',))] +#[should_panic(expected: 'NotAuthorized')] fn test_use_delegation_wrong_delegate() { let contract_address = setup(); let contract_instance = IAccountDelegationDispatcher { contract_address }; @@ -357,7 +357,7 @@ fn test_use_delegation_wrong_delegate() { #[test] -#[should_panic(expected: ('DelegationNotActive',))] +#[should_panic(expected: 'DelegationNotActive')] fn test_use_delegation_inactive() { let contract_address = setup(); let contract_instance = IAccountDelegationDispatcher { contract_address };