From 6b5875e580d72b539fceabb13ff413d9bc76fd92 Mon Sep 17 00:00:00 2001 From: Turan Ege Caner Date: Fri, 6 Dec 2024 13:45:10 +0700 Subject: [PATCH] ModularCompliance --- Scarb.lock | 2 + crates/compliance/Scarb.toml | 2 + .../compliance/src/imodular_compliance.cairo | 50 +--- crates/compliance/src/lib.cairo | 1 - .../compliance/src/modular_compliance.cairo | 244 +++++++++++++++--- 5 files changed, 227 insertions(+), 72 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index bc61a2a..c97b8d2 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -7,7 +7,9 @@ version = "0.1.0" dependencies = [ "openzeppelin_access", "openzeppelin_upgrades", + "registry", "roles", + "storage", "token", ] diff --git a/crates/compliance/Scarb.toml b/crates/compliance/Scarb.toml index c31ca82..32e7c73 100644 --- a/crates/compliance/Scarb.toml +++ b/crates/compliance/Scarb.toml @@ -7,6 +7,8 @@ edition = "2024_07" starknet.workspace = true token = { path = "../token"} roles = { path = "../roles"} +registry = { path = "../registry"} +storage = { path = "../storage"} openzeppelin_access.workspace = true openzeppelin_upgrades.workspace = true diff --git a/crates/compliance/src/imodular_compliance.cairo b/crates/compliance/src/imodular_compliance.cairo index 1fb2649..3b78817 100644 --- a/crates/compliance/src/imodular_compliance.cairo +++ b/crates/compliance/src/imodular_compliance.cairo @@ -1,44 +1,5 @@ use starknet::ContractAddress; -#[event] -#[derive(Drop, starknet::Event)] -pub enum ModularComplianceEvent { - ModuleInteraction: ModuleInteraction, - TokenBound: TokenBound, - TokenUnbound: TokenUnbound, - ModuleAdded: ModuleAdded, - ModuleRemoved: ModuleRemoved, -} - -#[derive(Drop, starknet::Event)] -pub struct ModuleInteraction { - #[key] - target: ContractAddress, - selector: u32 -} - -#[derive(Drop, starknet::Event)] -pub struct TokenBound { - token: ContractAddress, -} - -#[derive(Drop, starknet::Event)] -pub struct TokenUnbound { - token: ContractAddress, -} - -#[derive(Drop, starknet::Event)] -pub struct ModuleAdded { - #[key] - module: ContractAddress, -} - -#[derive(Drop, starknet::Event)] -pub struct ModuleRemoved { - #[key] - module: ContractAddress, -} - #[starknet::interface] pub trait IModularCompliance { /// @dev binds a token to the compliance contract @@ -73,7 +34,12 @@ pub trait IModularCompliance { /// @param _module The address of the module /// This function can be called only by the modular compliance owner /// emits a `ModuleInteraction` event - fn call_module_function(ref self: TContractState, calldata: ByteArray, module: ContractAddress); + fn call_module_function( + ref self: TContractState, + selector: felt252, + calldata: Span, + module: ContractAddress, + ); /// @dev function called whenever tokens are transferred /// from one wallet to another @@ -87,7 +53,7 @@ pub trait IModularCompliance { /// @param _amount The amount of tokens involved in the transfer /// This function calls moduleTransferAction() on each module bound to the compliance contract fn transferred( - ref self: TContractState, from: ContractAddress, to: ContractAddress, amount: u256 + ref self: TContractState, from: ContractAddress, to: ContractAddress, amount: u256, ); /// @dev function called whenever tokens are created on a wallet @@ -123,7 +89,7 @@ pub trait IModularCompliance { /// If each of the module checks return TRUE, this function will return TRUE as well /// returns FALSE otherwise fn can_transfer( - self: @TContractState, from: ContractAddress, to: ContractAddress, amount: u256 + self: @TContractState, from: ContractAddress, to: ContractAddress, amount: u256, ) -> bool; /// @dev getter for the modules bound to the compliance contract diff --git a/crates/compliance/src/lib.cairo b/crates/compliance/src/lib.cairo index 43bb07e..79df6c7 100644 --- a/crates/compliance/src/lib.cairo +++ b/crates/compliance/src/lib.cairo @@ -1,6 +1,5 @@ pub mod imodular_compliance; pub mod modular_compliance; -//pub mod removable_vec; pub mod modules { pub mod abstract_module; pub mod conditional_transfer_module; diff --git a/crates/compliance/src/modular_compliance.cairo b/crates/compliance/src/modular_compliance.cairo index 70d1d44..4a325d0 100644 --- a/crates/compliance/src/modular_compliance.cairo +++ b/crates/compliance/src/modular_compliance.cairo @@ -1,21 +1,43 @@ #[starknet::contract] mod ModularCompliance { - //use compliance::modular::imodular_compliance::IModularCompliance; - use starknet::ContractAddress; - use starknet::storage::{ - Vec, //VecTrait, MutableVecTrait, - Map, //StoragePathEntry, StorageMapReadAccess, - //StorageMapWriteAccess + use core::num::traits::Zero; + use crate::{ + imodular_compliance::IModularCompliance, + modules::imodule::{IModuleDispatcher, IModuleDispatcherTrait}, }; + use openzeppelin_access::ownable::OwnableComponent; + use openzeppelin_upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ + ClassHash, ContractAddress, + storage::{Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess}, + }; + use storage::storage_array::{ + ContractAddressVecToContractAddressArray, MutableStorageArrayTrait, + StorageArrayContractAddress, StorageArrayTrait, + }; + + component!(path: UpgradeableComponent, storage: upgrades, event: UpgradeableEvent); + + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; #[storage] struct Storage { /// token linked to the compliance contract token_bound: ContractAddress, /// Array of modules bound to the compliance - modules: Vec, + modules: StorageArrayContractAddress, /// Mapping of module binding status module_bound: Map, + #[substorage(v0)] + upgrades: UpgradeableComponent::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, } #[event] @@ -25,54 +47,218 @@ mod ModularCompliance { TokenBound: TokenBound, TokenUnbound: TokenUnbound, ModuleAdded: ModuleAdded, - ModuleRemoved: ModuleRemoved + ModuleRemoved: ModuleRemoved, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, } - /// @dev Event emitted for each executed interaction with a module contract. - /// For gas efficiency, only the interaction calldata selector (first 4 - /// bytes) is included in the event. For interactions without calldata or - /// whose calldata is shorter than 4 bytes, the selector will be `0`. + #[derive(Drop, starknet::Event)] struct ModuleInteraction { #[key] target: ContractAddress, - selector: i32 + selector: felt252, } - /// this event is emitted when a token has been bound to the compliance contract - /// the event is emitted by the bind_token function - /// `token` is the address of the token to bind #[derive(Drop, starknet::Event)] struct TokenBound { token: ContractAddress, } - /// this event is emitted when a token has been unbound from the compliance contract - /// the event is emitted by the unbind_token function - /// `token` is the address of the token to unbind #[derive(Drop, starknet::Event)] struct TokenUnbound { token: ContractAddress, } - /// this event is emitted when a module has been added to the list of modules bound to the - /// compliance contract the event is emitted by the add_module function - /// `module` is the address of the compliance module #[derive(Drop, starknet::Event)] struct ModuleAdded { #[key] module: ContractAddress, } - /// this event is emitted when a module has been removed from the list of modules bound to the - /// compliance contract the event is emitted by the remove_module function - /// `module` is the address of the compliance module #[derive(Drop, starknet::Event)] struct ModuleRemoved { #[key] module: ContractAddress, } - //#[abi(embed_v0)] -//impl ModularComplianceImpl of IModularCompliance{ -// -//} + + #[abi(embed_v0)] + impl UpgradeableImpl of IUpgradeable { + /// Upgrades the implementation used by this contract. + /// + /// # Arguments + /// + /// - `new_class_hash` A `ClassHash` representing the implementation to update to. + /// + /// # Requirements + /// + /// - This function can only be called by the xerc20 owner. + /// - The `ClassHash` should already have been declared. + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.upgrades.upgrade(new_class_hash); + } + } + + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress) { + self.ownable.initializer(owner); + } + + #[abi(embed_v0)] + impl ModularComplianceImpl of IModularCompliance { + fn bind_token(ref self: ContractState, token: ContractAddress) { + assert(token.is_non_zero(), 'Token zero address'); + let caller = starknet::get_caller_address(); + assert( + self.ownable.owner() == caller + || (self.token_bound.read().is_zero() && caller == token), + 'Only owner or token can call', + ); + self.token_bound.write(token); + self.emit(TokenBound { token }); + } + + fn unbind_token(ref self: ContractState, token: ContractAddress) { + assert(token.is_non_zero(), 'Token zero address'); + let caller = starknet::get_caller_address(); + assert( + self.ownable.owner() == caller + || (self.token_bound.read().is_zero() && caller == token), + 'Only owner or token can call', + ); + assert(self.token_bound.read() == token, 'This token is not bound'); + self.token_bound.write(Zero::zero()); + self.emit(TokenUnbound { token }); + } + + fn add_module(ref self: ContractState, module: ContractAddress) { + self.ownable.assert_only_owner(); + assert(module.is_non_zero(), 'Module address zero'); + assert(!self.module_bound.entry(module).read(), 'module already bound'); + let modules_storage_path = self.modules.deref(); + assert(modules_storage_path.len() < 25, 'Cannot add more than 25 modules'); + let module_dispatcher = IModuleDispatcher { contract_address: module }; + if !module_dispatcher.is_plug_and_play() { + assert!( + module_dispatcher.can_compliance_bind(starknet::get_contract_address()), + "Compliance is not suitable for binding to the module", + ); + } + + module_dispatcher.bind_compliance(starknet::get_contract_address()); + modules_storage_path.append().write(module); + self.module_bound.entry(module).write(true); + self.emit(ModuleAdded { module }); + } + + fn remove_module(ref self: ContractState, module: ContractAddress) { + self.ownable.assert_only_owner(); + assert(module.is_non_zero(), 'Module address zero'); + assert(self.module_bound.entry(module).read(), 'module not bound'); + self.module_bound.entry(module).write(false); + IModuleDispatcher { contract_address: module } + .unbind_compliance(starknet::get_contract_address()); + + let modules_storage_path = self.modules.deref(); + for i in 0..modules_storage_path.len() { + if modules_storage_path.at(i).read() == module { + modules_storage_path.delete(i); + self.emit(ModuleRemoved { module }); + break; + } + }; + } + + fn call_module_function( + ref self: ContractState, + selector: felt252, + calldata: Span, + module: ContractAddress, + ) { + self.ownable.assert_only_owner(); + assert(self.module_bound.entry(module).read(), 'Can only call bound module'); + starknet::syscalls::call_contract_syscall(module, selector, calldata).unwrap(); + self.emit(ModuleInteraction { target: module, selector }); + } + + fn transferred( + ref self: ContractState, from: ContractAddress, to: ContractAddress, amount: u256, + ) { + self.assert_only_token(); + assert(from.is_non_zero() && to.is_non_zero(), 'Zero address'); + assert(amount.is_non_zero(), 'No value transfer'); + + let modules_storage_path = self.modules.deref(); + for i in 0..modules_storage_path.len() { + IModuleDispatcher { contract_address: modules_storage_path.at(i).read() } + .module_transfer_action(from, to, amount); + }; + } + + fn created(ref self: ContractState, to: ContractAddress, amount: u256) { + self.assert_only_token(); + assert(to.is_non_zero(), 'Zero address'); + assert(amount.is_non_zero(), 'No value transfer'); + + let modules_storage_path = self.modules.deref(); + for i in 0..modules_storage_path.len() { + IModuleDispatcher { contract_address: modules_storage_path.at(i).read() } + .module_mint_action(to, amount); + }; + } + + fn destroyed(ref self: ContractState, from: ContractAddress, amount: u256) { + self.assert_only_token(); + assert(from.is_non_zero(), 'Zero address'); + assert(amount.is_non_zero(), 'No value transfer'); + + let modules_storage_path = self.modules.deref(); + for i in 0..modules_storage_path.len() { + IModuleDispatcher { contract_address: modules_storage_path.at(i).read() } + .module_burn_action(from, amount); + }; + } + + fn can_transfer( + self: @ContractState, from: ContractAddress, to: ContractAddress, amount: u256, + ) -> bool { + let mut can_transfer = true; + let modules_storage_path = self.modules.deref(); + for i in 0..modules_storage_path.len() { + let check_result = IModuleDispatcher { + contract_address: modules_storage_path.at(i).read(), + } + .module_check(from, to, amount, starknet::get_contract_address()); + if !check_result { + can_transfer = false; + break; + } + }; + can_transfer + } + + fn get_modules(self: @ContractState) -> Array { + self.modules.deref().into() + } + + fn get_token_bound(self: @ContractState) -> ContractAddress { + self.token_bound.read() + } + + fn is_module_bound(self: @ContractState, module: ContractAddress) -> bool { + self.module_bound.entry(module).read() + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn assert_only_token(self: @ContractState) { + assert!( + starknet::get_caller_address() == self.token_bound.read(), + "This address is not a token bound to the compliance contract", + ); + } + } }