From f18b5d4e7aabc0b8ab09959674666a493f04d8e2 Mon Sep 17 00:00:00 2001 From: Turan Ege Caner Date: Mon, 16 Dec 2024 17:16:19 +0700 Subject: [PATCH 1/3] xerc20 implementation --- xerc20/.gitignore | 5 + xerc20/.tool-versions | 2 + xerc20/Scarb.lock | 77 ++ xerc20/Scarb.toml | 34 + xerc20/scripts/.env.example | 4 + xerc20/scripts/declare.sh | 66 ++ xerc20/scripts/deploy_factory.sh | 95 +++ xerc20/snfoundry.toml | 11 + xerc20/src/factory/contract.cairo | 302 +++++++ xerc20/src/factory/interface.cairo | 27 + xerc20/src/lib.cairo | 26 + xerc20/src/lockbox/component.cairo | 139 ++++ xerc20/src/lockbox/contract.cairo | 66 ++ xerc20/src/lockbox/interface.cairo | 27 + xerc20/src/mocks/mock_account.cairo | 46 ++ xerc20/src/mocks/mock_erc20_token.cairo | 76 ++ xerc20/src/mocks/mock_xerc20_token.cairo | 115 +++ xerc20/src/utils/enumerable_address_set.cairo | 79 ++ xerc20/src/xerc20/component.cairo | 455 +++++++++++ xerc20/src/xerc20/contract.cairo | 112 +++ xerc20/src/xerc20/interface.cairo | 55 ++ xerc20/tests/common.cairo | 67 ++ xerc20/tests/e2e/common.cairo | 160 ++++ xerc20/tests/e2e/xerc20_factory_test.cairo | 44 ++ xerc20/tests/e2e/xerc20_lockbox_test.cairo | 162 ++++ xerc20/tests/e2e/xerc20_test.cairo | 627 +++++++++++++++ xerc20/tests/lib.cairo | 13 + xerc20/tests/unit/xerc20_factory_test.cairo | 333 ++++++++ xerc20/tests/unit/xerc20_lockbox_test.cairo | 324 ++++++++ xerc20/tests/unit/xerc20_test.cairo | 741 ++++++++++++++++++ 30 files changed, 4290 insertions(+) create mode 100644 xerc20/.gitignore create mode 100644 xerc20/.tool-versions create mode 100644 xerc20/Scarb.lock create mode 100644 xerc20/Scarb.toml create mode 100644 xerc20/scripts/.env.example create mode 100644 xerc20/scripts/declare.sh create mode 100644 xerc20/scripts/deploy_factory.sh create mode 100644 xerc20/snfoundry.toml create mode 100644 xerc20/src/factory/contract.cairo create mode 100644 xerc20/src/factory/interface.cairo create mode 100644 xerc20/src/lib.cairo create mode 100644 xerc20/src/lockbox/component.cairo create mode 100644 xerc20/src/lockbox/contract.cairo create mode 100644 xerc20/src/lockbox/interface.cairo create mode 100644 xerc20/src/mocks/mock_account.cairo create mode 100644 xerc20/src/mocks/mock_erc20_token.cairo create mode 100644 xerc20/src/mocks/mock_xerc20_token.cairo create mode 100644 xerc20/src/utils/enumerable_address_set.cairo create mode 100644 xerc20/src/xerc20/component.cairo create mode 100644 xerc20/src/xerc20/contract.cairo create mode 100644 xerc20/src/xerc20/interface.cairo create mode 100644 xerc20/tests/common.cairo create mode 100644 xerc20/tests/e2e/common.cairo create mode 100644 xerc20/tests/e2e/xerc20_factory_test.cairo create mode 100644 xerc20/tests/e2e/xerc20_lockbox_test.cairo create mode 100644 xerc20/tests/e2e/xerc20_test.cairo create mode 100644 xerc20/tests/lib.cairo create mode 100644 xerc20/tests/unit/xerc20_factory_test.cairo create mode 100644 xerc20/tests/unit/xerc20_lockbox_test.cairo create mode 100644 xerc20/tests/unit/xerc20_test.cairo diff --git a/xerc20/.gitignore b/xerc20/.gitignore new file mode 100644 index 0000000..c86fa4f --- /dev/null +++ b/xerc20/.gitignore @@ -0,0 +1,5 @@ +target +.snfoundry_cache/ + +.env +deployments/ diff --git a/xerc20/.tool-versions b/xerc20/.tool-versions new file mode 100644 index 0000000..e8f4cf5 --- /dev/null +++ b/xerc20/.tool-versions @@ -0,0 +1,2 @@ +scarb 2.9.2 +starknet-foundry 0.34.0 diff --git a/xerc20/Scarb.lock b/xerc20/Scarb.lock new file mode 100644 index 0000000..1ecd251 --- /dev/null +++ b/xerc20/Scarb.lock @@ -0,0 +1,77 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "openzeppelin_access" +version = "0.20.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.20.0#7756fd1de2b4ebd239fa6e372d75535cea02e5e5" +dependencies = [ + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_account" +version = "0.20.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.20.0#7756fd1de2b4ebd239fa6e372d75535cea02e5e5" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_introspection" +version = "0.20.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.20.0#7756fd1de2b4ebd239fa6e372d75535cea02e5e5" + +[[package]] +name = "openzeppelin_security" +version = "0.20.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.20.0#7756fd1de2b4ebd239fa6e372d75535cea02e5e5" + +[[package]] +name = "openzeppelin_token" +version = "0.20.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.20.0#7756fd1de2b4ebd239fa6e372d75535cea02e5e5" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_upgrades" +version = "0.20.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.20.0#7756fd1de2b4ebd239fa6e372d75535cea02e5e5" + +[[package]] +name = "openzeppelin_utils" +version = "0.20.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.20.0#7756fd1de2b4ebd239fa6e372d75535cea02e5e5" + +[[package]] +name = "snforge_scarb_plugin" +version = "0.34.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.34.0#d6976d4635cbe69bd199fd502788c469d408ed2d" + +[[package]] +name = "snforge_std" +version = "0.34.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.34.0#d6976d4635cbe69bd199fd502788c469d408ed2d" +dependencies = [ + "snforge_scarb_plugin", +] + +[[package]] +name = "xerc20" +version = "0.1.0" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_introspection", + "openzeppelin_security", + "openzeppelin_token", + "openzeppelin_upgrades", + "openzeppelin_utils", + "snforge_std", +] diff --git a/xerc20/Scarb.toml b/xerc20/Scarb.toml new file mode 100644 index 0000000..192a6e1 --- /dev/null +++ b/xerc20/Scarb.toml @@ -0,0 +1,34 @@ +[package] +name = "xerc20" +version = "0.1.0" +edition = "2024_07" +cairo-version = "2.9.2" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +starknet = "2.9.2" +openzeppelin_access = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" } +openzeppelin_token = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" } +openzeppelin_utils = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" } +openzeppelin_security = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" } +openzeppelin_account = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" } +openzeppelin_upgrades = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" } +openzeppelin_introspection = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" } + +[dev-dependencies] +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.34.0" } + +[[target.starknet-contract]] +sierra = true + +[tool.fmt] +sort-module-level-items = true + +[scripts] +test = "snforge test" + +[[tool.snforge.fork]] +name = "mainnet" +url = "https://free-rpc.nethermind.io/mainnet-juno" +block_id.tag = "latest" \ No newline at end of file diff --git a/xerc20/scripts/.env.example b/xerc20/scripts/.env.example new file mode 100644 index 0000000..a0996c1 --- /dev/null +++ b/xerc20/scripts/.env.example @@ -0,0 +1,4 @@ +FEE_TOKEN="strk" +SALT=0xff00ff +STARKNET_NETWORK="dev" +PROFILE="default" \ No newline at end of file diff --git a/xerc20/scripts/declare.sh b/xerc20/scripts/declare.sh new file mode 100644 index 0000000..fc8557f --- /dev/null +++ b/xerc20/scripts/declare.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Function to declare the contract and extract hashes +declare_contract() { + local contract_name="$1" # Take the contract name as a parameter + local profile="$2" + local fee_token="$3" + + echo "Executing sncast command to declare the contract '$contract_name'..." + + # Run the sncast command and capture the output + output=$(sncast --profile $profile --wait declare \ + --fee-token $fee_token \ + --contract-name "$contract_name") + + echo "Command executed successfully." + + # Check if output is not empty + if [[ -z "$output" ]]; then + echo "Error: No output received from sncast command." + echo "Output: $output" # Show output + return 1 # Changed from exit to return + fi + + # Extract class_hash + class_hash=$(echo "$output" | grep -E "class_hash[:=]" | awk -F '[:=]' '{gsub(/ /, "", $2); print $2}') + + # Validate extraction + if [[ -z "$class_hash" ]]; then + echo "Error: class_hash not found in the output." + echo "Output: $output" # Show output + return 1 # Changed from exit to return + fi + # Output the extracted hashes + echo "$contract_name: $class_hash" >> "deployments/$STARKNET_NETWORK/declared-classes.txt" + echo "Class hash for $contract_name saved to deployments/$STARKNET_NETWORK/declared-classes.txt" +} + +if [[ -z "$STARKNET_NETWORK" ]]; then + echo "Error: STARKNET_NETWORK is not set." + return 1 # Changed from exit to return +fi +# Determine the file path based on the STARKNET_NETWORK environment variable +output_dir="deployments/$STARKNET_NETWORK" +output_file="$output_dir/declared-classes.txt" + +local profile=$PROFILE +if [[ -z "$profile" ]]; then + profile="default" +fi + +local fee_token=$FEE_TOKEN +if [[ -z "$fee_token" ]]; then + fee_token="strk" +fi + +# Create the directory if it doesn't exist +mkdir -p "$output_dir" + +# Remove existing declared-classes.txt file if it exists +rm -f "$output_file" + +# Call the function with the provided contract name +declare_contract "XERC20Factory" $profile $fee_token +declare_contract "XERC20" $profile $fee_token +declare_contract "XERC20Lockbox" $profile $fee_token \ No newline at end of file diff --git a/xerc20/scripts/deploy_factory.sh b/xerc20/scripts/deploy_factory.sh new file mode 100644 index 0000000..3e59690 --- /dev/null +++ b/xerc20/scripts/deploy_factory.sh @@ -0,0 +1,95 @@ +# Read class hashes from deployments/declared-classes.txt and store them in a local mapping + +deploy_factory() { + local factory_class_hash="$1" + local xerc20_class_hash="$2" + local lockbox_class_hash="$3" + local profile="$4" + local fee_token="$5" + local salt="$6" + + echo "Deploying contract factory with class hash '$factory_class_hash'..." + # Capture the output of the deployment command + output=$(sncast --profile $profile --wait \ + deploy \ + --fee-token $fee_token \ + --class-hash $factory_class_hash\ + --constructor-calldata $xerc20_class_hash $lockbox_class_hash\ + --salt $SALT\ + --unique + ) + + echo "Deployment command executed." + + # Check if output is not empty + if [[ -z "$output" ]]; then + echo "Error: No output received from deployment command." + return 1 + fi + + # Extract transaction_hash and contract_address + transaction_hash=$(echo "$output" | grep -E "transaction_hash[:=]" | awk -F '[:=]' '{gsub(/ /, "", $2); print $2}') + contract_address=$(echo "$output" | grep -E "contract_address[:=]" | awk -F '[:=]' '{gsub(/ /, "", $2); print $2}') + + # Validate extraction + if [[ -z "$transaction_hash" ]]; then + echo "Error: transaction_hash not found in the output." + return 1 + fi + + if [[ -z "$contract_address" ]]; then + echo "Error: contract_address not found in the output." + return 1 + fi + + # Output the extracted hashes + echo "Transaction Hash: $transaction_hash" + echo "Contract Address: $contract_address" + + echo "XERC20Factory: $contract_address" >> "deployments/$STARKNET_NETWORK/deployed-contracts.txt" + echo "XERC20Factory deployed to $contract_address in tx_hash: $transaction_hash" +} + +declare -A class_hashes + +if [[ -z "$STARKNET_NETWORK" ]]; then + echo "Error: STARKNET_NETWORK is not set." + return 1 # Changed from exit to return +fi + +if [[ -z "$SALT" ]]; then + echo "Error: SALT is not set." + return 1 # Changed from exit to return +fi + +local profile=$PROFILE +if [[ -z "$profile" ]]; then + profile="default" +fi + +local fee_token=$FEE_TOKEN +if [[ -z "$fee_token" ]]; then + fee_token="strk" +fi + +input_file="deployments/$STARKNET_NETWORK/declared-classes.txt" + +if [[ -f "$input_file" ]]; then + while IFS= read -r line; do + # Extract the class name and hash + class_name=$(echo "$line" | awk -F ': ' '{print $1}') # Remove trailing colon and whitespace + class_hash=$(echo "$line" | awk -F ': ' '{print $2}') # Remove leading/trailing whitespace + class_hashes[$class_name]=$class_hash + done < "$input_file" +else + echo "Input file not found: $input_file" + return 1 +fi + +deploy_factory \ + "${class_hashes[XERC20Factory]}" \ + "${class_hashes[XERC20]}" \ + "${class_hashes[XERC20Lockbox]}" \ + $profile \ + $fee_token \ + $SALT diff --git a/xerc20/snfoundry.toml b/xerc20/snfoundry.toml new file mode 100644 index 0000000..e856a19 --- /dev/null +++ b/xerc20/snfoundry.toml @@ -0,0 +1,11 @@ +[sncast.default] +# change here with the account name you use +account = "user" + +# change this line with the path to your accounts.json +# see how to import your account if you already have one +# {https://foundry-rs.github.io/starknet-foundry/appendix/sncast/account/import.html} +accounts-file = ".starknet_accounts/starknet_open_zeppelin_accounts.json" + +# node url +url = "http://127.0.0.1:5050" diff --git a/xerc20/src/factory/contract.cairo b/xerc20/src/factory/contract.cairo new file mode 100644 index 0000000..6301c62 --- /dev/null +++ b/xerc20/src/factory/contract.cairo @@ -0,0 +1,302 @@ +#[starknet::contract] +pub mod XERC20Factory { + use core::num::traits::Zero; + use crate::{ + factory::interface::IXERC20Factory, + utils::enumerable_address_set::{ + EnumerableAddressSet, EnumerableAddressSetTrait, MutableEnumerableAddressSetTrait, + }, + xerc20::interface::{IXERC20Dispatcher, IXERC20DispatcherTrait}, + }; + use openzeppelin_access::ownable::{ + interface::{IOwnableDispatcher, IOwnableDispatcherTrait}, ownable::OwnableComponent, + }; + use starknet::{ + ClassHash, ContractAddress, SyscallResultTrait, + storage::{Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess}, + }; + + // Ownable Component + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + #[storage] + struct Storage { + xerc20_class_hash: ClassHash, + lockbox_class_hash: ClassHash, + xerc20_to_lockbox: Map, + erc20_to_lockbox: Map, + lockbox_registry: EnumerableAddressSet, + xerc20_registry: EnumerableAddressSet, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + XERC20Deployed: XERC20Deployed, + LockboxDeployed: LockboxDeployed, + XERC20ImplementationUpdated: XERC20ImplementationUpdated, + LockboxImplementationUpdated: LockboxImplementationUpdated, + #[flat] + OwnableEvent: OwnableComponent::Event, + } + + #[derive(Drop, starknet::Event)] + pub struct XERC20Deployed { + pub xerc20: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + pub struct LockboxDeployed { + pub lockbox: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + pub struct XERC20ImplementationUpdated { + pub class_hash: ClassHash, + } + + #[derive(Drop, starknet::Event)] + pub struct LockboxImplementationUpdated { + pub class_hash: ClassHash, + } + + pub mod Errors { + pub const CALLER_NOT_OWNER: felt252 = 'Caller is not the owner'; + pub const TOKEN_ADDRESS_ZERO: felt252 = 'Token address zero'; + pub const LOCKBOX_ALREADY_DEPLOYED: felt252 = 'Lockbox alread deployed'; + pub const INVALID_LENGTH: felt252 = 'Invalid length'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + xerc20_class_hash: ClassHash, + lockbox_class_hash: ClassHash, + owner: ContractAddress, + ) { + self.ownable.initializer(owner); + self.xerc20_class_hash.write(xerc20_class_hash); + self.lockbox_class_hash.write(lockbox_class_hash); + } + + #[abi(embed_v0)] + impl XERC20FactoryImpl of IXERC20Factory { + /// Deploys a XERC20 contract and set bridges and their limits. + /// + /// # Arguments + /// + /// - `name` a `ByteArray` representing the name of xerc20 token. + /// - `symbol` a `ByteArray` representing the symbol of xerc20 token. + /// - `minter_limits` a `Span` representing list of minting limits + /// - `burner_limits` a `Span` representing list of burning limits. + /// - `bridges` a `Span` representing list of bridges. + /// + /// # Requirements + /// + /// - `minter_limits`, `burner_limits` and `bridges`` needs to be paralel arrays that i-th + /// index represents limits for the bridge at `bridges[i]`. + /// + /// # Returns + /// + /// A `ContractAddress` representing the deployed xerc20. + fn deploy_xerc20( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + minter_limits: Span, + burner_limits: Span, + bridges: Span, + ) -> ContractAddress { + let deployed_address = self + ._deploy_xerc20(name, symbol, minter_limits, burner_limits, bridges); + self.emit(XERC20Deployed { xerc20: deployed_address }); + deployed_address + } + + /// Deploys a XERC20 lockbox and sets lockbox of given xerc20 token. + /// + /// # Arguments + /// + /// - `xerc20` A `ContractAddress` representing the xerc20 token we want to deploy a + /// lockbox for. + /// - `base_token` A `ContractAddress` representing the base token that we want to lock. + /// + /// # Requirementes + /// + /// - `base_token` should be non-zero. + /// - Caller must be the owner of xerc20 token. + /// - Lockbox must not already deployed. + /// + /// # Returns + /// + /// A `ContractAddress` representing the deployed lockbox. + fn deploy_lockbox( + ref self: ContractState, xerc20: ContractAddress, base_token: ContractAddress, + ) -> ContractAddress { + assert(base_token.is_non_zero(), Errors::TOKEN_ADDRESS_ZERO); + + let xerc20_token_owner = IOwnableDispatcher { contract_address: xerc20 }.owner(); + assert(xerc20_token_owner == starknet::get_caller_address(), Errors::CALLER_NOT_OWNER); + assert( + self.xerc20_to_lockbox.entry(xerc20).read().is_zero(), + Errors::LOCKBOX_ALREADY_DEPLOYED, + ); + + let deployed_address = self._deploy_lockbox(xerc20, base_token); + self.emit(LockboxDeployed { lockbox: deployed_address }); + deployed_address + } + + /// Updates the xerc20 class hash with `new_class_hash`. + /// + /// # Arguments + /// + /// - `new_class_hash` A `ClassHash` representing the implementation to update to. + /// + /// # Requirements + /// + /// - This function can only be called by the owner. + fn set_xerc20_class_hash(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.xerc20_class_hash.write(new_class_hash); + self.emit(XERC20ImplementationUpdated { class_hash: new_class_hash }); + } + + /// Updates the lockbox class hash with `new_class_hash`. + /// + /// # Arguments + /// + /// - `new_class_hash` A `ClassHash` representing the implementation to update to. + /// + /// # Requirements + /// + /// - This function can only be called by the owner. + fn set_lockbox_class_hash(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable.assert_only_owner(); + self.lockbox_class_hash.write(new_class_hash); + self.emit(LockboxImplementationUpdated { class_hash: new_class_hash }); + } + + /// Returns `ClassHash` of xerc20 implementation used by this contract + fn get_xerc20_class_hash(self: @ContractState) -> ClassHash { + self.xerc20_class_hash.read() + } + + /// Returns `ClassHash` of lockbox implementation used by this contract + fn get_lockbox_class_hash(self: @ContractState) -> ClassHash { + self.lockbox_class_hash.read() + } + + /// Determines if a given `ContractAddress` is a xerc20 deployed by this factory or not. + fn is_xerc20(self: @ContractState, xerc20: ContractAddress) -> bool { + self.xerc20_registry.deref().contains(xerc20) + } + + /// Determines if a given `ContractAddress` is a lockbox deployed by this factory or not. + fn is_lockbox(self: @ContractState, lockbox: ContractAddress) -> bool { + self.lockbox_registry.deref().contains(lockbox) + } + + /// Returns all xerc20 tokens deployed by this factory. + fn get_xerc20s(self: @ContractState) -> Array { + self.xerc20_registry.deref().values() + } + + /// Returns all lockboxes deployed by this factory. + fn get_lockboxes(self: @ContractState) -> Array { + self.lockbox_registry.deref().values() + } + + /// Returns lockbox for given erc20 token. + /// + /// # Arguments + /// + /// - `erc20` A `ContractAddress` representing the token to query for lookbox. + /// + /// # Returns + /// + /// A `ContractAddress` representing the lockbox for given erc20 token. Returns address zero + /// if no lockbox for the token. + fn get_lockbox_for_erc20(self: @ContractState, erc20: ContractAddress) -> ContractAddress { + self.erc20_to_lockbox.entry(erc20).read() + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + /// Internal function that deploys xerc20 tokens and set limits for the given bridges. + fn _deploy_xerc20( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + minter_limits: Span, + burner_limits: Span, + bridges: Span, + ) -> ContractAddress { + assert( + minter_limits.len() == bridges.len() && bridges.len() == burner_limits.len(), + Errors::INVALID_LENGTH, + ); + // calculate the salt + let mut serialized_data: Array = array![]; + name.serialize(ref serialized_data); + symbol.serialize(ref serialized_data); + starknet::get_caller_address().serialize(ref serialized_data); + let salt = core::poseidon::poseidon_hash_span(serialized_data.span()); + // prepare the constructor calldata + let mut serialized_ctor_data: Array = array![]; + name.serialize(ref serialized_ctor_data); + symbol.serialize(ref serialized_ctor_data); + starknet::get_contract_address().serialize(ref serialized_ctor_data); + // deploy the xerc20 contract + let (deployed_address, _) = starknet::syscalls::deploy_syscall( + self.xerc20_class_hash.read(), salt, serialized_ctor_data.span(), false, + ) + .unwrap_syscall(); + // update storage + self.xerc20_registry.deref().add(deployed_address); + // set the limits for given bridges + let xerc20_dispatcher = IXERC20Dispatcher { contract_address: deployed_address }; + for i in 0..bridges.len() { + xerc20_dispatcher + .set_limits(*bridges.at(i), *minter_limits.at(i), *burner_limits.at(i)); + }; + // transfer ownership to caller + IOwnableDispatcher { contract_address: deployed_address } + .transfer_ownership(starknet::get_caller_address()); + deployed_address + } + + /// Internal function that deploys lockbox for given xerc20 token and base token. + fn _deploy_lockbox( + ref self: ContractState, xerc20: ContractAddress, base_token: ContractAddress, + ) -> ContractAddress { + // calculate the salt + let salt = core::poseidon::poseidon_hash_span( + array![xerc20.into(), base_token.into(), starknet::get_caller_address().into()] + .span(), + ); + // deploy lockbox + let (deployed_address, _) = starknet::syscalls::deploy_syscall( + self.lockbox_class_hash.read(), + salt, + array![xerc20.into(), base_token.into()].span(), + false, + ) + .unwrap_syscall(); + // set lockbox on xerc20 contract + IXERC20Dispatcher { contract_address: xerc20 }.set_lockbox(deployed_address); + // update storage + self.lockbox_registry.deref().add(deployed_address); + self.xerc20_to_lockbox.entry(xerc20).write(deployed_address); + self.erc20_to_lockbox.entry(base_token).write(deployed_address); + deployed_address + } + } +} diff --git a/xerc20/src/factory/interface.cairo b/xerc20/src/factory/interface.cairo new file mode 100644 index 0000000..98d6fb2 --- /dev/null +++ b/xerc20/src/factory/interface.cairo @@ -0,0 +1,27 @@ +use starknet::{ClassHash, ContractAddress}; + +#[starknet::interface] +pub trait IXERC20Factory { + fn deploy_xerc20( + ref self: TState, + name: ByteArray, + symbol: ByteArray, + minter_limits: Span, + burner_limits: Span, + bridges: Span, + ) -> ContractAddress; + fn deploy_lockbox( + ref self: TState, xerc20: ContractAddress, base_token: ContractAddress, + ) -> ContractAddress; + // Setters + fn set_xerc20_class_hash(ref self: TState, new_class_hash: ClassHash); + fn set_lockbox_class_hash(ref self: TState, new_class_hash: ClassHash); + // Getters + fn get_xerc20_class_hash(self: @TState) -> ClassHash; + fn get_lockbox_class_hash(self: @TState) -> ClassHash; + fn get_xerc20s(self: @TState) -> Array; + fn get_lockboxes(self: @TState) -> Array; + fn get_lockbox_for_erc20(self: @TState, erc20: ContractAddress) -> ContractAddress; + fn is_lockbox(self: @TState, lockbox: ContractAddress) -> bool; + fn is_xerc20(self: @TState, xerc20: ContractAddress) -> bool; +} diff --git a/xerc20/src/lib.cairo b/xerc20/src/lib.cairo new file mode 100644 index 0000000..dac656c --- /dev/null +++ b/xerc20/src/lib.cairo @@ -0,0 +1,26 @@ +pub mod xerc20 { + pub mod component; + pub mod contract; + pub mod interface; +} + +pub mod factory { + pub mod contract; + pub mod interface; +} + +pub mod lockbox { + pub mod component; + pub mod contract; + pub mod interface; +} + +pub mod mocks { + pub mod mock_account; + pub mod mock_erc20_token; + pub mod mock_xerc20_token; +} + +pub mod utils { + pub mod enumerable_address_set; +} diff --git a/xerc20/src/lockbox/component.cairo b/xerc20/src/lockbox/component.cairo new file mode 100644 index 0000000..4074135 --- /dev/null +++ b/xerc20/src/lockbox/component.cairo @@ -0,0 +1,139 @@ +#[starknet::component] +pub mod XERC20LockboxComponent { + use crate::{ + lockbox::interface::{IXERC20Lockbox, IXERC20LockboxGetters}, + xerc20::interface::{IXERC20Dispatcher, IXERC20DispatcherTrait}, + }; + use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use starknet::ContractAddress; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + #[storage] + pub struct Storage { + xerc20: IXERC20Dispatcher, + erc20: ERC20ABIDispatcher, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + Deposit: Deposit, + Withdraw: Withdraw, + } + + #[derive(Drop, starknet::Event)] + pub struct Deposit { + pub sender: ContractAddress, + pub amount: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct Withdraw { + pub sender: ContractAddress, + pub amount: u256, + } + + pub mod Errors { + pub const ERC20_TRANSFER_FAILED: felt252 = 'ERC20 transfer failed'; + pub const ERC20_TRANSFER_FROM_FAILED: felt252 = 'ERC20 transfer_from failed'; + } + + #[embeddable_as(XERC20Lockbox)] + pub impl XERC20LockboxImpl< + TContractState, +HasComponent, +Drop, + > of IXERC20Lockbox> { + /// Deposit ERC20 tokens into the lockbox. + /// + /// # Arguments + /// + /// - `amount` An `u256` representing the amount of tokens to deposit. + fn deposit(ref self: ComponentState, amount: u256) { + self._deposit(starknet::get_caller_address(), amount); + } + + /// Deposit ERC20 tokens into the lockbox, and send the XERC20 to a user. + /// + /// # Arguments + /// + /// - `user` A `ContractAddress` representing the address to send the XERC20 to. + /// - `amount` An `u256` representing the amount of tokens to deposit. + fn deposit_to(ref self: ComponentState, to: ContractAddress, amount: u256) { + self._deposit(to, amount); + } + + /// Withdraw ERC20 tokens from the lockbox. + /// + /// # Arguments + /// + /// - `amount` An `u256` representing the amount of tokens to withdraw. + fn withdraw(ref self: ComponentState, amount: u256) { + self._withdraw(starknet::get_caller_address(), amount); + } + + /// Withdraw ERC20 tokens from the lockbox, and sends it to user. + /// + /// # Arguments + /// + /// - `user` A `ContractAddress` to send the ERC20 tokens to. + /// - `amount` An `u256` representing the amount of tokens to withdraw. + fn withdraw_to( + ref self: ComponentState, to: ContractAddress, amount: u256, + ) { + self._withdraw(to, amount); + } + } + + #[embeddable_as(XERC20LockboxGettersImpl)] + pub impl XERC20LockboxGetters< + TContractState, +HasComponent, +Drop, + > of IXERC20LockboxGetters> { + /// Returns `ContractAddress` representing the XERC20 token of this contract. + fn xerc20(self: @ComponentState) -> ContractAddress { + self.xerc20.read().contract_address + } + + /// Returns `ContractAddress` representing the ERC20 token of this contract. + fn erc20(self: @ComponentState) -> ContractAddress { + self.erc20.read().contract_address + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, +HasComponent, +Drop, + > of InternalTrait { + /// Initializer of this component. + fn initialize( + ref self: ComponentState, + xerc20: ContractAddress, + erc20: ContractAddress, + ) { + self.xerc20.write(IXERC20Dispatcher { contract_address: xerc20 }); + self.erc20.write(ERC20ABIDispatcher { contract_address: erc20 }); + } + + /// Internal function that burns the xerc20 tokens then transfer the erc20 token to + /// recipient. + fn _withdraw(ref self: ComponentState, to: ContractAddress, amount: u256) { + self.emit(Withdraw { sender: to, amount }); + self.xerc20.read().burn(starknet::get_caller_address(), amount); + assert(self.erc20.read().transfer(to, amount), Errors::ERC20_TRANSFER_FAILED); + } + + /// Internal function that locks erc20 token in lockbox then mints xerc20 tokens. + fn _deposit(ref self: ComponentState, to: ContractAddress, amount: u256) { + assert( + self + .erc20 + .read() + .transfer_from( + starknet::get_caller_address(), starknet::get_contract_address(), amount, + ), + Errors::ERC20_TRANSFER_FROM_FAILED, + ); + self.xerc20.read().mint(to, amount); + self.emit(Deposit { sender: to, amount: amount }); + } + } +} + diff --git a/xerc20/src/lockbox/contract.cairo b/xerc20/src/lockbox/contract.cairo new file mode 100644 index 0000000..2c1e941 --- /dev/null +++ b/xerc20/src/lockbox/contract.cairo @@ -0,0 +1,66 @@ +#[starknet::contract] +pub mod XERC20Lockbox { + use crate::lockbox::component::XERC20LockboxComponent; + use openzeppelin_access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; + use openzeppelin_upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use starknet::{ClassHash, ContractAddress}; + + /// XERC20Lockbox Component + component!(path: XERC20LockboxComponent, storage: lockbox, event: XERC20LockboxEvent); + + #[abi(embed_v0)] + impl XERC20LockboxImpl = XERC20LockboxComponent::XERC20Lockbox; + #[abi(embed_v0)] + impl XERC20LockboxGettersImpl = + XERC20LockboxComponent::XERC20LockboxGettersImpl; + impl XERC20LockboxInternalImpl = XERC20LockboxComponent::InternalImpl; + + // UpgradeableComponent + component!(path: UpgradeableComponent, storage: upgrades, event: UpgradeableEvent); + + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + lockbox: XERC20LockboxComponent::Storage, + #[substorage(v0)] + upgrades: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + XERC20LockboxEvent: XERC20LockboxComponent::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, xerc20: ContractAddress, erc20: ContractAddress) { + self.lockbox.initialize(xerc20, erc20); + } + + #[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) { + let ownable_dispatcher = IOwnableDispatcher { contract_address: self.lockbox.xerc20() }; + assert( + ownable_dispatcher.owner() == starknet::get_caller_address(), + 'Caller not XERC20 owner', + ); + self.upgrades.upgrade(new_class_hash); + } + } +} diff --git a/xerc20/src/lockbox/interface.cairo b/xerc20/src/lockbox/interface.cairo new file mode 100644 index 0000000..a5a2651 --- /dev/null +++ b/xerc20/src/lockbox/interface.cairo @@ -0,0 +1,27 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IXERC20Lockbox { + fn deposit(ref self: TState, amount: u256); + fn deposit_to(ref self: TState, to: ContractAddress, amount: u256); + fn withdraw(ref self: TState, amount: u256); + fn withdraw_to(ref self: TState, to: ContractAddress, amount: u256); +} + +#[starknet::interface] +pub trait IXERC20LockboxGetters { + fn xerc20(self: @TState) -> ContractAddress; + fn erc20(self: @TState) -> ContractAddress; +} + +#[starknet::interface] +pub trait XERC20LockboxABI { + /// IXERC20Lockbox + fn deposit(ref self: TState, amount: u256); + fn deposit_to(ref self: TState, to: ContractAddress, amount: u256); + fn withdraw(ref self: TState, amount: u256); + fn withdraw_to(ref self: TState, to: ContractAddress, amount: u256); + /// IXERC20LockboxGetters + fn xerc20(self: @TState) -> ContractAddress; + fn erc20(self: @TState) -> ContractAddress; +} diff --git a/xerc20/src/mocks/mock_account.cairo b/xerc20/src/mocks/mock_account.cairo new file mode 100644 index 0000000..4ffbc89 --- /dev/null +++ b/xerc20/src/mocks/mock_account.cairo @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts for Cairo ^0.18.0 + +#[starknet::contract(account)] +mod MockAccount { + use openzeppelin_account::AccountComponent; + use openzeppelin_introspection::src5::SRC5Component; + + component!(path: AccountComponent, storage: account, event: AccountEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + #[abi(embed_v0)] + impl SRC6Impl = AccountComponent::SRC6Impl; + #[abi(embed_v0)] + impl SRC6CamelOnlyImpl = AccountComponent::SRC6CamelOnlyImpl; + #[abi(embed_v0)] + impl PublicKeyImpl = AccountComponent::PublicKeyImpl; + #[abi(embed_v0)] + impl PublicKeyCamelImpl = AccountComponent::PublicKeyCamelImpl; + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + impl AccountInternalImpl = AccountComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + account: AccountComponent::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AccountEvent: AccountComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, public_key: felt252) { + self.account.initializer(public_key); + } +} diff --git a/xerc20/src/mocks/mock_erc20_token.cairo b/xerc20/src/mocks/mock_erc20_token.cairo new file mode 100644 index 0000000..62bc829 --- /dev/null +++ b/xerc20/src/mocks/mock_erc20_token.cairo @@ -0,0 +1,76 @@ +///! Mock ERC20 token that emits events on transfers and approvals. +///! Used for checking call has been made or not. +///! Returns 0 for balance_of, allowance, total_supply. +#[starknet::contract] +mod MockErc20 { + use openzeppelin_token::erc20::interface::IERC20; + use starknet::ContractAddress; + + #[storage] + pub struct Storage {} + + #[event] + #[derive(Drop, PartialEq, starknet::Event)] + pub enum Event { + Transfer: Transfer, + Approval: Approval, + } + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct Transfer { + #[key] + pub from: ContractAddress, + #[key] + pub to: ContractAddress, + pub value: u256, + } + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct Approval { + #[key] + pub owner: ContractAddress, + #[key] + pub spender: ContractAddress, + pub value: u256, + } + + #[abi(embed_v0)] + impl ERC20Impl of IERC20 { + fn total_supply(self: @ContractState) -> u256 { + 0 + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + 0 + } + + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, + ) -> u256 { + 0 + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + self + .emit( + Transfer { from: starknet::get_caller_address(), to: recipient, value: amount }, + ); + true + } + + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256, + ) -> bool { + self.emit(Transfer { from: sender, to: recipient, value: amount }); + true + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + self.emit(Approval { owner: starknet::get_caller_address(), spender, value: amount }); + true + } + } +} diff --git a/xerc20/src/mocks/mock_xerc20_token.cairo b/xerc20/src/mocks/mock_xerc20_token.cairo new file mode 100644 index 0000000..8482563 --- /dev/null +++ b/xerc20/src/mocks/mock_xerc20_token.cairo @@ -0,0 +1,115 @@ +///! Mock XERC20 token that emit events to check has been made or not. +///! Emit events for the following functions. +///! - mint +///! - burn +///! - set_limits +///! - set_lockbox +#[starknet::contract] +pub mod MockXERC20 { + use crate::xerc20::interface::{Bridge, BridgeParameters, XERC20ABI}; + use openzeppelin_token::erc20::erc20::ERC20Component; + use starknet::ContractAddress; + + pub const U256MAX_DIV_2: u256 = core::num::traits::Bounded::MAX / 2; + + #[storage] + pub struct Storage {} + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + LockboxSet: LockboxSet, + BridgeLimitsSet: BridgeLimitsSet, + Transfer: ERC20Component::Transfer, + } + + #[derive(Drop, starknet::Event)] + pub struct LockboxSet { + pub lockbox: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + pub struct BridgeLimitsSet { + pub minting_limit: u256, + pub burning_limit: u256, + #[key] + pub bridge: ContractAddress, + } + + #[abi(embed_v0)] + pub impl XERC20ABIImpl of XERC20ABI { + fn set_lockbox(ref self: ContractState, lockbox: ContractAddress) { + self.emit(LockboxSet { lockbox }); + } + + fn set_limits( + ref self: ContractState, + bridge: ContractAddress, + minting_limit: u256, + burning_limit: u256, + ) { + self.emit(BridgeLimitsSet { minting_limit, burning_limit, bridge }); + } + + fn mint(ref self: ContractState, user: ContractAddress, amount: u256) { + self + .emit( + ERC20Component::Transfer { + from: starknet::contract_address_const::<0>(), to: user, value: amount, + }, + ); + } + + + fn burn(ref self: ContractState, user: ContractAddress, amount: u256) { + self + .emit( + ERC20Component::Transfer { + from: user, to: starknet::contract_address_const::<0>(), value: amount, + }, + ); + } + + fn minting_max_limit_of(self: @ContractState, minter: ContractAddress) -> u256 { + U256MAX_DIV_2 + } + + fn burning_max_limit_of(self: @ContractState, bridge: ContractAddress) -> u256 { + U256MAX_DIV_2 + } + + fn minting_current_limit_of(self: @ContractState, minter: ContractAddress) -> u256 { + U256MAX_DIV_2 + } + + fn burning_current_limit_of(self: @ContractState, bridge: ContractAddress) -> u256 { + U256MAX_DIV_2 + } + + fn lockbox(self: @ContractState) -> ContractAddress { + starknet::contract_address_const::<'LOCKBOX'>() + } + + fn factory(self: @ContractState) -> ContractAddress { + starknet::contract_address_const::<'FACTORY'>() + } + + fn get_bridge(self: @ContractState, bridge: ContractAddress) -> Bridge { + let minter_params = BridgeParameters { + max_limit: U256MAX_DIV_2, + current_limit: U256MAX_DIV_2, + timestamp: 0, + rate_per_second: U256MAX_DIV_2, + }; + let burner_params = BridgeParameters { + max_limit: U256MAX_DIV_2, + current_limit: U256MAX_DIV_2, + timestamp: 0, + rate_per_second: U256MAX_DIV_2, + }; + + Bridge { minter_params, burner_params } + } + } +} + diff --git a/xerc20/src/utils/enumerable_address_set.cairo b/xerc20/src/utils/enumerable_address_set.cairo new file mode 100644 index 0000000..04e2420 --- /dev/null +++ b/xerc20/src/utils/enumerable_address_set.cairo @@ -0,0 +1,79 @@ +use starknet::{ + ContractAddress, + storage::{ + Map, Mutable, MutableVecTrait, StoragePath, StoragePathEntry, StoragePointerReadAccess, + StoragePointerWriteAccess, Vec, VecTrait, + }, +}; + +///! Append-only set +#[starknet::storage_node] +pub struct EnumerableAddressSet { + values: Vec, + positions: Map, +} + +pub trait EnumerableAddressSetTrait { + fn contains(self: StoragePath, value: ContractAddress) -> bool; + fn len(self: StoragePath) -> u64; + fn at(self: StoragePath, index: u64) -> ContractAddress; + fn values(self: StoragePath) -> Array; +} + +impl EnumerableAddressSetImpl of EnumerableAddressSetTrait { + fn contains(self: StoragePath, value: ContractAddress) -> bool { + self.positions.entry(value).read() > 0 + } + fn len(self: StoragePath) -> u64 { + self.values.len() + } + fn at(self: StoragePath, index: u64) -> ContractAddress { + self.values.at(index).read() + } + fn values(self: StoragePath) -> Array { + let mut addresses = array![]; + let length = self.values.len(); + for i in 0..length { + addresses.append(self.values.at(i).read()); + }; + addresses + } +} + +pub trait MutableEnumerableAddressSetTrait { + fn add(self: StoragePath>, value: ContractAddress); + fn contains(self: StoragePath>, value: ContractAddress) -> bool; + fn len(self: StoragePath>) -> u64; + fn at(self: StoragePath>, index: u64) -> ContractAddress; + fn values(self: StoragePath>) -> Array; +} + +impl MutableEnumerableAddressSetTraitImpl of MutableEnumerableAddressSetTrait { + fn add(self: StoragePath>, value: ContractAddress) { + let position_storage_path = self.positions.entry(value); + assert(position_storage_path.read() == 0, 'Value already member of the set'); + self.values.append().write(value); + position_storage_path.write(self.values.len()); + } + + fn contains(self: StoragePath>, value: ContractAddress) -> bool { + self.positions.entry(value).read() > 0 + } + + fn len(self: StoragePath>) -> u64 { + self.values.len() + } + + fn at(self: StoragePath>, index: u64) -> ContractAddress { + self.values.at(index).read() + } + + fn values(self: StoragePath>) -> Array { + let mut addresses = array![]; + let length = self.values.len(); + for i in 0..length { + addresses.append(self.values.at(i).read()); + }; + addresses + } +} diff --git a/xerc20/src/xerc20/component.cairo b/xerc20/src/xerc20/component.cairo new file mode 100644 index 0000000..912499d --- /dev/null +++ b/xerc20/src/xerc20/component.cairo @@ -0,0 +1,455 @@ +#[starknet::component] +pub mod XERC20Component { + use crate::xerc20::interface::{Bridge, BridgeParameters, IXERC20, IXERC20Getters}; + use openzeppelin_access::ownable::ownable::{ + OwnableComponent, OwnableComponent::InternalTrait as OwnableInternalTrait, + }; + use openzeppelin_token::erc20::{ + erc20::{ERC20Component, ERC20Component::InternalTrait as ERC20InternalTrait}, + }; + use starknet::ContractAddress; + use starknet::storage::{ + Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess, + }; + + /// The duration it takes for limits to fully replenish. A day. + pub const DURATION: u64 = 60 * 60 * 24; + pub const U256MAX_DIV_2: u256 = core::num::traits::Bounded::MAX / 2; + + #[storage] + pub struct Storage { + pub XERC20_factory: ContractAddress, + pub XERC20_lockbox: ContractAddress, + pub XERC20_bridges: Map, + } + + #[starknet::storage_node] + pub struct BridgeNode { + pub minter_params: BridgeParametersNode, + pub burner_params: BridgeParametersNode, + } + + #[starknet::storage_node] + pub struct BridgeParametersNode { + pub max_limit: u256, + pub current_limit: u256, + pub timestamp: u64, + pub rate_per_second: u256, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + LockboxSet: LockboxSet, + BridgeLimitsSet: BridgeLimitsSet, + } + + #[derive(Drop, starknet::Event)] + pub struct LockboxSet { + pub lockbox: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + pub struct BridgeLimitsSet { + pub minting_limit: u256, + pub burning_limit: u256, + #[key] + pub bridge: ContractAddress, + } + + pub mod Errors { + pub const NOT_HIGH_ENOUGH_LIMITS: felt252 = 'User does not have enough limit'; + pub const CALLER_NOT_FACTORY: felt252 = 'Caller is not the factory'; + pub const LIMITS_TO_HIGH: felt252 = 'Limits too high'; + } + + #[embeddable_as(XERC20Impl)] + pub impl XERC20< + TContractState, + +HasComponent, + impl Ownable: OwnableComponent::HasComponent, + impl ERC20: ERC20Component::HasComponent, + +ERC20Component::ERC20HooksTrait, + +Drop, + > of IXERC20> { + /// Sets the lockbox. + /// + /// # Arguments + /// + /// - `lockbox` A `ContractAddress` representing the address of lockbox to set. + /// + /// # Requirements + /// + /// -Only callable by the factory of the token. + fn set_lockbox(ref self: ComponentState, lockbox: ContractAddress) { + assert( + self.XERC20_factory.read() == starknet::get_caller_address(), + Errors::CALLER_NOT_FACTORY, + ); + self.XERC20_lockbox.write(lockbox); + self.emit(LockboxSet { lockbox }); + } + + /// Sets burning & minting limit for a given bridge. + /// + /// # Arguments + /// + /// - `bridge` A `ContractAddress` representing the bridge to set limits for. + /// - `minting_limit` A `u256` representing the minting limits for the bridge. + /// - `burning_limit` A `u256` representing the burning limits for the bridge. + /// + /// # Requirements + /// + /// - Only callable by the owner of the token. + /// - `minting_limit` and `burning_limit` should be lte to given treshold. + fn set_limits( + ref self: ComponentState, + bridge: ContractAddress, + minting_limit: u256, + burning_limit: u256, + ) { + let ownable_comp = get_dep_component!(@self, Ownable); + ownable_comp.assert_only_owner(); + assert( + minting_limit <= U256MAX_DIV_2 && burning_limit <= U256MAX_DIV_2, + Errors::LIMITS_TO_HIGH, + ); + + self.change_minter_limit(bridge, minting_limit); + self.change_burner_limit(bridge, burning_limit); + self.emit(BridgeLimitsSet { minting_limit, burning_limit, bridge }); + } + + /// Mints tokens for a user. + /// + /// # Arguments + /// + /// - `user` A `ContractAddress` representing the address who needs tokens minted. + /// - `amount` A `u256` representing the amount of tokens being minted. + fn mint(ref self: ComponentState, user: ContractAddress, amount: u256) { + self.mint_with_caller(starknet::get_caller_address(), user, amount); + } + + /// Burns tokens for a user. + /// + /// # Arguments + /// + /// - `user` A `ContractAddress` representing the address who needs tokens burned. + /// - `amount` A `u256` representing the amount of tokens being burned. + fn burn(ref self: ComponentState, user: ContractAddress, amount: u256) { + let caller = starknet::get_caller_address(); + if caller != user { + let mut erc20_comp = get_dep_component_mut!(ref self, ERC20); + erc20_comp._spend_allowance(user, caller, amount); + } + self.burn_with_caller(caller, user, amount); + } + + /// Returns the minting max limit for the bridge. + /// + /// # Arguments + /// + /// - `minter` A `ContractAddress` representing the bridge we are querying the limits of. + fn minting_max_limit_of( + self: @ComponentState, minter: ContractAddress, + ) -> u256 { + self.XERC20_bridges.entry(minter).minter_params.max_limit.read() + } + + /// Returns the burning max limit for the bridge. + /// + /// # Arguments + /// + /// - `bridge` A `ContractAddress` representing the bridge we are querying the limits of. + fn burning_max_limit_of( + self: @ComponentState, bridge: ContractAddress, + ) -> u256 { + self.XERC20_bridges.entry(bridge).burner_params.max_limit.read() + } + + /// Determines the minting current limit of given bridge. + /// + /// # Arguments + /// + /// - `minter` A `ContractAddress` representing the bridge we are querying the limits of. + fn minting_current_limit_of( + self: @ComponentState, minter: ContractAddress, + ) -> u256 { + let minter_params_storage_path = self + .XERC20_bridges + .entry(minter) + .minter_params + .deref(); + PureImpl::get_current_limit( + minter_params_storage_path.current_limit.read(), + minter_params_storage_path.max_limit.read(), + minter_params_storage_path.rate_per_second.read(), + minter_params_storage_path.timestamp.read(), + ) + } + + /// Determines the burning current limit of given bridge. + /// + /// # Arguments + /// + /// - `bridge` A `ContractAddress` representing the bridge we are querying the limits of. + fn burning_current_limit_of( + self: @ComponentState, bridge: ContractAddress, + ) -> u256 { + let burner_params_storage_path = self + .XERC20_bridges + .entry(bridge) + .burner_params + .deref(); + PureImpl::get_current_limit( + burner_params_storage_path.current_limit.read(), + burner_params_storage_path.max_limit.read(), + burner_params_storage_path.rate_per_second.read(), + burner_params_storage_path.timestamp.read(), + ) + } + } + + #[embeddable_as(XERC20GettersImpl)] + pub impl XERC20Getters< + TContractState, +HasComponent, +Drop, + > of IXERC20Getters> { + /// Returns `ContractAddress` representing the address of the lockbox that is used by this + /// xerc20 token. + fn lockbox(self: @ComponentState) -> ContractAddress { + self.XERC20_lockbox.read() + } + + /// Returns `ContractAddress` representing the address of the factory that is used by this + /// xerc20 token. + fn factory(self: @ComponentState) -> ContractAddress { + self.XERC20_factory.read() + } + + /// Returns `Bridge` struct respresenting the parameters for given `bridge`. + fn get_bridge(self: @ComponentState, bridge: ContractAddress) -> Bridge { + let bridge_storage_path = self.XERC20_bridges.entry(bridge).deref(); + let minter_params_storage_path = bridge_storage_path.minter_params.deref(); + let burner_params_storage_path = bridge_storage_path.burner_params.deref(); + let minter_params = BridgeParameters { + max_limit: minter_params_storage_path.max_limit.read(), + current_limit: minter_params_storage_path.current_limit.read(), + timestamp: minter_params_storage_path.timestamp.read(), + rate_per_second: minter_params_storage_path.rate_per_second.read(), + }; + let burner_params = BridgeParameters { + max_limit: burner_params_storage_path.max_limit.read(), + current_limit: burner_params_storage_path.current_limit.read(), + timestamp: burner_params_storage_path.timestamp.read(), + rate_per_second: burner_params_storage_path.rate_per_second.read(), + }; + + Bridge { minter_params, burner_params } + } + } + + #[generate_trait] + pub impl PureImpl of PureTrait { + /// Determines the new current limit. + /// + /// # Arguments + /// + /// - `limit` A `u256` representing the new limit. + /// - `old_limit` A `u256` representing the old limit. + /// - `current_limit` A `u256` representing the current limit. + /// + /// # Returns + /// + /// - A `u256` representing the new current limit. + fn calculate_new_current_limit(limit: u256, old_limit: u256, current_limit: u256) -> u256 { + if old_limit <= limit { + let difference = limit - old_limit; + return current_limit + difference; + } + + let difference = old_limit - limit; + if current_limit > difference { + current_limit - difference + } else { + 0 + } + } + + /// Determines the current_limit. + /// + /// # Arguments + /// + /// - `current_limit` - A `u256` representing the current limit. + /// - `max_limit` A `u256` representing the max limit. + /// - `is_minter` A `bool` flag representing the calculation is done for minter params or + /// burner. + /// + /// # Returns + /// + /// - A `u256`  representing the current limit + fn get_current_limit( + current_limit: u256, max_limit: u256, rate_per_second: u256, timestamp: u64, + ) -> u256 { + if current_limit == max_limit { + return current_limit; + } + + let current_timestamp = starknet::get_block_timestamp(); + if timestamp + DURATION <= current_timestamp { + return max_limit; + } + let time_delta = current_timestamp - timestamp; + let calculated_limit = current_limit + (time_delta.into() * rate_per_second); + if calculated_limit > max_limit { + max_limit + } else { + calculated_limit + } + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + +OwnableComponent::HasComponent, + impl ERC20: ERC20Component::HasComponent, + +ERC20Component::ERC20HooksTrait, + +Drop, + > of InternalTrait { + /// Internal function to initialize the component. + fn initialize(ref self: ComponentState, factory: ContractAddress) { + self.XERC20_factory.write(factory); + } + + /// Internal function for burning tokens. + fn burn_with_caller( + ref self: ComponentState, + caller: ContractAddress, + user: ContractAddress, + amount: u256, + ) { + if caller != self.XERC20_lockbox.read() { + let current_limit = self.burning_current_limit_of(caller); + assert(current_limit >= amount, Errors::NOT_HIGH_ENOUGH_LIMITS); + self.use_burner_limits(caller, amount); + } + let mut erc20_comp = get_dep_component_mut!(ref self, ERC20); + erc20_comp.burn(user, amount); + } + + /// Internal function for minting tokens. + fn mint_with_caller( + ref self: ComponentState, + caller: ContractAddress, + user: ContractAddress, + amount: u256, + ) { + if caller != self.XERC20_lockbox.read() { + let current_limit = self.minting_current_limit_of(caller); + assert(current_limit >= amount, Errors::NOT_HIGH_ENOUGH_LIMITS); + self.use_minter_limits(caller, amount); + } + let mut erc20_comp = get_dep_component_mut!(ref self, ERC20); + erc20_comp.mint(user, amount); + } + + /// Consumes the minter limits from `bridge` by `change` amount. + /// + /// # Arguments + /// + /// - `bridge` A `ContractAddress` representing the bridge to consume limits from. + /// - `change` A `u256` representing the change in the limit. + /// + /// # Requirements + /// + /// - `change` should be lte to actual limit. + fn use_minter_limits( + ref self: ComponentState, bridge: ContractAddress, change: u256, + ) { + let current_limit = self.minting_current_limit_of(bridge); + let minter_params_storage_path = self + .XERC20_bridges + .entry(bridge) + .minter_params + .deref(); + minter_params_storage_path.current_limit.write(current_limit - change); + minter_params_storage_path.timestamp.write(starknet::get_block_timestamp()); + } + + /// Consumes the burner limits from `bridge` by `change` amount. + /// + /// # Arguments + /// + /// - `bridge` A `ContractAddress` representing the bridge to consume limits from. + /// - `change` A `u256` representing the change in the limit. + /// + /// # Requirements + /// + /// - `change` should be lte to actual limit. + fn use_burner_limits( + ref self: ComponentState, bridge: ContractAddress, change: u256, + ) { + let current_limit = self.burning_current_limit_of(bridge); + let burner_params_storage_path = self + .XERC20_bridges + .entry(bridge) + .burner_params + .deref(); + burner_params_storage_path.current_limit.write(current_limit - change); + burner_params_storage_path.timestamp.write(starknet::get_block_timestamp()); + } + + /// Updates the minting limits of given bridge. + /// + /// # Arguments + /// + /// - `bridge` A `ContractAddress` representing the bridge whos limit is being changed. + /// - `limit` A `u256` new limit value to set. + fn change_minter_limit( + ref self: ComponentState, bridge: ContractAddress, limit: u256, + ) { + let minter_params_storage_path = self + .XERC20_bridges + .entry(bridge) + .minter_params + .deref(); + let old_limit = minter_params_storage_path.max_limit.read(); + let current_limit = self.minting_current_limit_of(bridge); + minter_params_storage_path.max_limit.write(limit); + let new_current_limit = PureImpl::calculate_new_current_limit( + limit, old_limit, current_limit, + ); + minter_params_storage_path.current_limit.write(new_current_limit); + minter_params_storage_path.rate_per_second.write(limit / DURATION.into()); + minter_params_storage_path.timestamp.write(starknet::get_block_timestamp()); + } + + /// Updates the burning limits of given bridge. + /// + /// # Arguments + /// + /// - `bridge` A `ContractAddress` representing the bridge whos limit is being changed. + /// - `limit` A `u256` new limit value to set. + fn change_burner_limit( + ref self: ComponentState, bridge: ContractAddress, limit: u256, + ) { + let burner_params_storage_path = self + .XERC20_bridges + .entry(bridge) + .burner_params + .deref(); + let old_limit = burner_params_storage_path.max_limit.read(); + let current_limit = self.burning_current_limit_of(bridge); + + burner_params_storage_path.max_limit.write(limit); + let new_current_limit = PureImpl::calculate_new_current_limit( + limit, old_limit, current_limit, + ); + burner_params_storage_path.current_limit.write(new_current_limit); + burner_params_storage_path.rate_per_second.write(limit / DURATION.into()); + burner_params_storage_path.timestamp.write(starknet::get_block_timestamp()); + } + } +} + diff --git a/xerc20/src/xerc20/contract.cairo b/xerc20/src/xerc20/contract.cairo new file mode 100644 index 0000000..9e6315f --- /dev/null +++ b/xerc20/src/xerc20/contract.cairo @@ -0,0 +1,112 @@ +#[starknet::contract] +pub mod XERC20 { + use crate::xerc20::component::XERC20Component; + use openzeppelin_access::ownable::ownable::OwnableComponent; + use openzeppelin_token::erc20::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use openzeppelin_upgrades::{interface::IUpgradeable, upgradeable::UpgradeableComponent}; + use openzeppelin_utils::cryptography::{nonces::NoncesComponent, snip12::SNIP12Metadata}; + use starknet::{ClassHash, ContractAddress}; + + // Ownable Component + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + // Nonces Component for ERC20Permit + component!(path: NoncesComponent, storage: nonces, event: NoncesEvent); + + // ERC20Component with Permit + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + // ERC20Mixin + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + impl ERC20InternalImpl = ERC20Component::InternalImpl; + // ISNIP12Metadata + #[abi(embed_v0)] + impl SNIP12MetadataExternalImpl = + ERC20Component::SNIP12MetadataExternalImpl; + // IERC20Permit + #[abi(embed_v0)] + impl ERC20PermitImpl = ERC20Component::ERC20PermitImpl; + + // XERC20Component + component!(path: XERC20Component, storage: xerc20, event: XERC20Event); + + #[abi(embed_v0)] + impl XERC20Impl = XERC20Component::XERC20Impl; + #[abi(embed_v0)] + impl XERC20GettersImpl = XERC20Component::XERC20GettersImpl; + + impl XERC20InternalImpl = XERC20Component::InternalImpl; + + // UpgradeableComponent + component!(path: UpgradeableComponent, storage: upgrades, event: UpgradeableEvent); + + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage, + #[substorage(v0)] + nonces: NoncesComponent::Storage, + #[substorage(v0)] + xerc20: XERC20Component::Storage, + #[substorage(v0)] + upgrades: UpgradeableComponent::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + ERC20Event: ERC20Component::Event, + #[flat] + NoncesEvent: NoncesComponent::Event, + #[flat] + XERC20Event: XERC20Component::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + } + + pub impl SNIP12MetadataImpl of SNIP12Metadata { + fn name() -> felt252 { + 'XERC20_Starknet' + } + fn version() -> felt252 { + '0.1.0' + } + } + + #[constructor] + fn constructor( + ref self: ContractState, name: ByteArray, symbol: ByteArray, factory: ContractAddress, + ) { + self.ownable.initializer(factory); + self.xerc20.initialize(factory); + self.erc20.initializer(name, symbol); + } + + #[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 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); + } + } +} diff --git a/xerc20/src/xerc20/interface.cairo b/xerc20/src/xerc20/interface.cairo new file mode 100644 index 0000000..afc27e3 --- /dev/null +++ b/xerc20/src/xerc20/interface.cairo @@ -0,0 +1,55 @@ +use starknet::ContractAddress; + +#[derive(Drop, Serde, Copy)] +pub struct Bridge { + pub minter_params: BridgeParameters, + pub burner_params: BridgeParameters, +} + +#[derive(Drop, Serde, Copy)] +pub struct BridgeParameters { + pub max_limit: u256, + pub current_limit: u256, + pub timestamp: u64, + pub rate_per_second: u256, +} + +#[starknet::interface] +pub trait IXERC20 { + fn set_lockbox(ref self: TState, lockbox: ContractAddress); + fn set_limits( + ref self: TState, bridge: ContractAddress, minting_limit: u256, burning_limit: u256, + ); + fn mint(ref self: TState, user: ContractAddress, amount: u256); + fn burn(ref self: TState, user: ContractAddress, amount: u256); + fn minting_max_limit_of(self: @TState, minter: ContractAddress) -> u256; + fn burning_max_limit_of(self: @TState, bridge: ContractAddress) -> u256; + fn minting_current_limit_of(self: @TState, minter: ContractAddress) -> u256; + fn burning_current_limit_of(self: @TState, bridge: ContractAddress) -> u256; +} + +#[starknet::interface] +pub trait IXERC20Getters { + fn lockbox(self: @TState) -> ContractAddress; + fn factory(self: @TState) -> ContractAddress; + fn get_bridge(self: @TState, bridge: ContractAddress) -> Bridge; +} + +#[starknet::interface] +pub trait XERC20ABI { + /// IXERC20 + fn set_lockbox(ref self: TState, lockbox: ContractAddress); + fn set_limits( + ref self: TState, bridge: ContractAddress, minting_limit: u256, burning_limit: u256, + ); + fn mint(ref self: TState, user: ContractAddress, amount: u256); + fn burn(ref self: TState, user: ContractAddress, amount: u256); + fn minting_max_limit_of(self: @TState, minter: ContractAddress) -> u256; + fn burning_max_limit_of(self: @TState, bridge: ContractAddress) -> u256; + fn minting_current_limit_of(self: @TState, minter: ContractAddress) -> u256; + fn burning_current_limit_of(self: @TState, bridge: ContractAddress) -> u256; + /// IXERC20Getters + fn lockbox(self: @TState) -> ContractAddress; + fn factory(self: @TState) -> ContractAddress; + fn get_bridge(self: @TState, bridge: ContractAddress) -> Bridge; +} diff --git a/xerc20/tests/common.cairo b/xerc20/tests/common.cairo new file mode 100644 index 0000000..af1be7d --- /dev/null +++ b/xerc20/tests/common.cairo @@ -0,0 +1,67 @@ +use core::num::traits::{Bounded, One}; +use core::ops::RemAssign; + +pub const HOUR: u64 = 60 * 60; +pub const DAY: u64 = HOUR * 24; +pub const E18: u256 = 1_000_000_000_000_000_000; +pub const E40: u256 = 10_000_000_000_000_000_000_000_000_000_000_000_000_000; +pub const U256MAX_DIV_2: u256 = core::num::traits::Bounded::MAX / 2; + +/// Bounds given value within given range [lower, upper] +pub fn bound< + T, + +PartialOrd, + +PartialEq, + +Bounded, + +RemAssign, + +Add, + +One, + +Drop, + +Copy, +>( + mut value: T, lower: T, upper: T, +) -> T { + if upper == Bounded::::MAX { + if value < lower { + return lower; + } + return value; + } + + value %= upper + One::::one(); + if value < lower { + return lower; + } + value +} + +/// see +/// {https://github.com/foundry-rs/foundry/blob/e16a75b615f812db6127ea22e23c3ee65504c1f1/crates/cheatcodes/src/test/assert.rs#L533} +pub fn assert_approx_eq_rel(lhs: u256, rhs: u256, max_delta: u256) { + if lhs == 0 { + if rhs == 0 { + return; + } else { + panic!("eq_rel_assertion error lhs {}, rhs {}, max_delta {}", lhs, rhs, max_delta); + } + } + + let mut delta = if lhs > rhs { + lhs - rhs + } else { + rhs - lhs + }; + + delta *= E18; + delta /= rhs; + + if delta > max_delta { + panic!( + "eq_rel_assertion error lhs {}, rhs {}, max_delta {}, real_delta {}", + lhs, + rhs, + max_delta, + delta, + ); + } +} diff --git a/xerc20/tests/e2e/common.cairo b/xerc20/tests/e2e/common.cairo new file mode 100644 index 0000000..ee547a3 --- /dev/null +++ b/xerc20/tests/e2e/common.cairo @@ -0,0 +1,160 @@ +use core::hash::{HashStateExTrait, HashStateTrait}; +use core::poseidon::PoseidonTrait; +use crate::common::E18; +use openzeppelin_token::erc20::interface::ERC20ABIDispatcher; +use openzeppelin_token::erc20::snip12_utils::permit::Permit; +use openzeppelin_utils::cryptography::{ + interface::{ + INoncesDispatcher, INoncesDispatcherTrait, ISNIP12MetadataDispatcher, + ISNIP12MetadataDispatcherTrait, + }, + snip12::{StarknetDomain, StructHash}, +}; +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, declare, load, map_entry_address, + signature::{ + KeyPair, KeyPairTrait, SignerTrait, + stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl, StarkCurveVerifierImpl}, + }, + start_cheat_caller_address, stop_cheat_caller_address, store, +}; +use starknet::ContractAddress; +use starknet::account::AccountContractDispatcher; +use xerc20::{ + factory::interface::{IXERC20FactoryDispatcher, IXERC20FactoryDispatcherTrait}, + lockbox::interface::XERC20LockboxABIDispatcher, xerc20::interface::XERC20ABIDispatcher, +}; + +pub fn DAI() -> ContractAddress { + starknet::contract_address_const::< + 0x05574eb6b8789a91466f902c380d978e472db68170ff82a5b650b95a58ddf4ad, + >() +} + +pub fn DAI_NAME() -> ByteArray { + "Dai Stablecoin" +} + +pub fn DAI_SYMBOL() -> ByteArray { + "DAI" +} + +#[derive(Drop)] +pub struct Setup { + pub factory: IXERC20FactoryDispatcher, + pub xerc20: XERC20ABIDispatcher, + pub lockbox: XERC20LockboxABIDispatcher, + pub owner: ContractAddress, + pub user: ContractAddress, + pub user_account: AccountContractDispatcher, + pub user_key_pair: KeyPair, + pub dai: ERC20ABIDispatcher, + pub test_minter: ContractAddress, +} + +pub fn setup_base() -> Setup { + let owner = starknet::contract_address_const::<1>(); + let user_account_contract = declare("MockAccount").unwrap().contract_class(); + // set deployer key and account + let user_key_pair = KeyPairTrait::::generate(); + let (user_account_address, _) = user_account_contract + .deploy(@array![user_key_pair.public_key]) + .unwrap(); + let test_minter = starknet::contract_address_const::<123_456_789>(); + let dai_address = DAI(); + let dai_dispatcher = ERC20ABIDispatcher { contract_address: dai_address }; + /// Declare implementations + let xerc20_class_hash = declare("XERC20").unwrap().contract_class().class_hash; + let xerc20_lockbox_class_hash = declare("XERC20Lockbox").unwrap().contract_class().class_hash; + /// Declare and deploy factory + let factory_contract = declare("XERC20Factory").unwrap().contract_class(); + let mut ctor_calldata: Array = array![]; + xerc20_class_hash.serialize(ref ctor_calldata); + xerc20_lockbox_class_hash.serialize(ref ctor_calldata); + owner.serialize(ref ctor_calldata); + let (factory_address, _) = factory_contract.deploy(@ctor_calldata).unwrap(); + let factory_dispatcher = IXERC20FactoryDispatcher { contract_address: factory_address }; + /// Deploy xerc20 token + let minter_limits = array![100 * E18].span(); + let burner_limits = array![50 * E18].span(); + let bridges = array![test_minter].span(); + + start_cheat_caller_address(factory_address, owner); + let xerc20_address = factory_dispatcher + .deploy_xerc20(DAI_NAME(), DAI_SYMBOL(), minter_limits, burner_limits, bridges); + let xerc20 = XERC20ABIDispatcher { contract_address: xerc20_address }; + /// Deploy lockbox + let lockbox_address = factory_dispatcher.deploy_lockbox(xerc20_address, dai_address); + let lockbox = XERC20LockboxABIDispatcher { contract_address: lockbox_address }; + stop_cheat_caller_address(factory_address); + + Setup { + factory: factory_dispatcher, + xerc20, + lockbox, + owner, + user: user_account_address, + user_account: AccountContractDispatcher { contract_address: user_account_address }, + user_key_pair, + dai: dai_dispatcher, + test_minter, + } +} + +pub fn mint_dai(to: ContractAddress, amount: u256) { + let dai_address = DAI(); + // Increment balance + let mut loaded_balance = load( + dai_address, map_entry_address(selector!("ERC20_balances"), array![to.into()].span()), 2, + ) + .span(); + let balance = Serde::::deserialize(ref loaded_balance).unwrap(); + + let mut serialized_new_balance: Array = array![]; + (balance + amount).serialize(ref serialized_new_balance); + store( + dai_address, + map_entry_address(selector!("ERC20_balances"), array![to.into()].span()), + serialized_new_balance.span(), + ); + // Increment total supply + let mut loaded_total_supply = load(dai_address, selector!("ERC20_total_supply"), 2).span(); + let total_supply = Serde::::deserialize(ref loaded_total_supply).unwrap(); + + let mut serialized_new_total_supply: Array = array![]; + (total_supply + amount).serialize(ref serialized_new_total_supply); + store(dai_address, selector!("ERC20_total_supply"), serialized_new_total_supply.span()); +} + +pub fn prepare_permit_signature( + setup: @Setup, spender: ContractAddress, amount: u256, deadline: u64, +) -> Span { + let snip12_metadata_dispatcher = ISNIP12MetadataDispatcher { + contract_address: *setup.xerc20.contract_address, + }; + let (name, version) = snip12_metadata_dispatcher.snip12_metadata(); + let sn_domain = StarknetDomain { + name: name, + version: version, + chain_id: starknet::get_tx_info().unbox().chain_id, + revision: 1, + }; + let nonces_dispatcher = INoncesDispatcher { contract_address: *setup.xerc20.contract_address }; + let nonce = nonces_dispatcher.nonces(*setup.user); + let permit = Permit { + token: *setup.xerc20.contract_address, + spender: *setup.owner, + amount: amount, + nonce, + deadline, + }; + let msg_hash = PoseidonTrait::new() + .update_with('StarkNet Message') + .update_with(sn_domain.hash_struct()) + .update_with(*setup.user) + .update_with(permit.hash_struct()) + .finalize(); + + let (r, s) = (*setup.user_key_pair).sign(msg_hash).unwrap(); + array![r, s].span() +} diff --git a/xerc20/tests/e2e/xerc20_factory_test.cairo b/xerc20/tests/e2e/xerc20_factory_test.cairo new file mode 100644 index 0000000..0395a9e --- /dev/null +++ b/xerc20/tests/e2e/xerc20_factory_test.cairo @@ -0,0 +1,44 @@ +use crate::{common::E18, e2e::common::{DAI_NAME, DAI_SYMBOL, setup_base}}; +use openzeppelin_access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use xerc20::{ + factory::interface::IXERC20FactoryDispatcherTrait, + lockbox::interface::{XERC20LockboxABIDispatcher, XERC20LockboxABIDispatcherTrait}, + xerc20::interface::XERC20ABIDispatcherTrait, +}; + +#[test] +//#[fork("mainnet")] +#[ignore] +fn test_deploy() { + let setup = setup_base(); + + let erc20_dispatcher = ERC20ABIDispatcher { contract_address: setup.xerc20.contract_address }; + let ownable_dispatcher = IOwnableDispatcher { contract_address: setup.xerc20.contract_address }; + + assert!(ownable_dispatcher.owner() == setup.owner); + assert!(erc20_dispatcher.name() == DAI_NAME()); + assert!(erc20_dispatcher.symbol() == DAI_SYMBOL()); + assert!(setup.xerc20.factory() == setup.factory.contract_address); + assert!(setup.lockbox.xerc20() == setup.xerc20.contract_address); + assert!(setup.lockbox.erc20() == setup.dai.contract_address); + assert!(setup.xerc20.burning_max_limit_of(setup.test_minter) == 50 * E18); + assert!(setup.xerc20.minting_max_limit_of(setup.test_minter) == 100 * E18); +} + +#[test] +//#[fork("mainnet")] +#[ignore] +fn test_deploy_lockbox() { + let setup = setup_base(); + + let limits = array![].span(); + let minters = array![].span(); + + let xerc20_token = setup.factory.deploy_xerc20("Test", "TST", limits, limits, minters); + let lockbox = setup.factory.deploy_lockbox(xerc20_token, setup.dai.contract_address); + let lockbox_dispatcher = XERC20LockboxABIDispatcher { contract_address: lockbox }; + + assert!(lockbox_dispatcher.xerc20() == xerc20_token); + assert!(lockbox_dispatcher.erc20() == setup.dai.contract_address); +} diff --git a/xerc20/tests/e2e/xerc20_lockbox_test.cairo b/xerc20/tests/e2e/xerc20_lockbox_test.cairo new file mode 100644 index 0000000..0617a40 --- /dev/null +++ b/xerc20/tests/e2e/xerc20_lockbox_test.cairo @@ -0,0 +1,162 @@ +use crate::{common::E18, e2e::common::{mint_dai, setup_base}}; +use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use openzeppelin_upgrades::interface::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; +use snforge_std::{get_class_hash, start_cheat_caller_address, stop_cheat_caller_address}; +use xerc20::lockbox::interface::XERC20LockboxABIDispatcherTrait; + +#[test] +//#[fork("mainnet")] +#[ignore] +fn test_lockbox() { + let setup = setup_base(); + + assert!(setup.lockbox.xerc20() == setup.xerc20.contract_address); + assert!(setup.lockbox.erc20() == setup.dai.contract_address); +} + +#[test] +//#[fork("mainnet")] +#[ignore] +fn test_deposit() { + let setup = setup_base(); + + mint_dai(setup.user, 100 * E18); + + start_cheat_caller_address(setup.dai.contract_address, setup.user); + setup.dai.approve(setup.lockbox.contract_address, 100 * E18); + stop_cheat_caller_address(setup.dai.contract_address); + + start_cheat_caller_address(setup.lockbox.contract_address, setup.user); + setup.lockbox.deposit(100 * E18); + stop_cheat_caller_address(setup.lockbox.contract_address); + + assert!( + ERC20ABIDispatcher { contract_address: setup.xerc20.contract_address } + .balance_of(setup.user) == 100 + * E18, + ); + assert!(setup.dai.balance_of(setup.user) == 0); + assert!(setup.dai.balance_of(setup.lockbox.contract_address) == 100 * E18); +} + +#[test] +//#[fork("mainnet")] +#[ignore] +fn test_deposit_to() { + let setup = setup_base(); + + mint_dai(setup.user, 100 * E18); + + start_cheat_caller_address(setup.dai.contract_address, setup.user); + setup.dai.approve(setup.lockbox.contract_address, 100 * E18); + stop_cheat_caller_address(setup.dai.contract_address); + + start_cheat_caller_address(setup.lockbox.contract_address, setup.user); + setup.lockbox.deposit_to(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.lockbox.contract_address); + + assert!( + ERC20ABIDispatcher { contract_address: setup.xerc20.contract_address } + .balance_of(setup.owner) == 100 + * E18, + ); + assert!(setup.dai.balance_of(setup.user) == 0); + assert!(setup.dai.balance_of(setup.lockbox.contract_address) == 100 * E18); +} + +#[test] +//#[fork("mainnet")] +#[ignore] +fn test_withdraw() { + let setup = setup_base(); + + mint_dai(setup.user, 100 * E18); + + start_cheat_caller_address(setup.dai.contract_address, setup.user); + setup.dai.approve(setup.lockbox.contract_address, 100 * E18); + stop_cheat_caller_address(setup.dai.contract_address); + + start_cheat_caller_address(setup.lockbox.contract_address, setup.user); + setup.lockbox.deposit(100 * E18); + let xerc20_erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + xerc20_erc20_dispatcher.approve(setup.lockbox.contract_address, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + setup.lockbox.withdraw(100 * E18); + stop_cheat_caller_address(setup.lockbox.contract_address); + + assert!( + ERC20ABIDispatcher { contract_address: setup.xerc20.contract_address } + .balance_of(setup.user) == 0, + ); + assert!(setup.dai.balance_of(setup.user) == 100 * E18); + assert!(setup.dai.balance_of(setup.lockbox.contract_address) == 0); +} + +#[test] +//#[fork("mainnet")] +#[ignore] +fn test_withdraw_to() { + let setup = setup_base(); + + mint_dai(setup.user, 100 * E18); + + start_cheat_caller_address(setup.dai.contract_address, setup.user); + setup.dai.approve(setup.lockbox.contract_address, 100 * E18); + stop_cheat_caller_address(setup.dai.contract_address); + + start_cheat_caller_address(setup.lockbox.contract_address, setup.user); + setup.lockbox.deposit(100 * E18); + let xerc20_erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + xerc20_erc20_dispatcher.approve(setup.lockbox.contract_address, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + setup.lockbox.withdraw_to(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.lockbox.contract_address); + + assert!( + ERC20ABIDispatcher { contract_address: setup.xerc20.contract_address } + .balance_of(setup.user) == 0, + ); + assert!(setup.dai.balance_of(setup.owner) == 100 * E18); + assert!(setup.dai.balance_of(setup.lockbox.contract_address) == 0); +} + +#[test] +//#[fork("mainnet")] +#[should_panic(expected: 'Caller not XERC20 owner')] +#[ignore] +fn test_should_panic_when_upgrade_when_caller_not_xerc20_owner() { + let setup = setup_base(); + let upgradeable_dispatcher = IUpgradeableDispatcher { + contract_address: setup.lockbox.contract_address, + }; + let new_class_hash = starknet::class_hash::class_hash_const::<'NEW_CLASS_HASH'>(); + start_cheat_caller_address(upgradeable_dispatcher.contract_address, setup.user); + upgradeable_dispatcher.upgrade(new_class_hash); + stop_cheat_caller_address(upgradeable_dispatcher.contract_address); +} + +#[test] +//#[fork("mainnet")] +#[ignore] +fn test_should_upgrade_implementation() { + let setup = setup_base(); + let upgradeable_dispatcher = IUpgradeableDispatcher { + contract_address: setup.lockbox.contract_address, + }; + let new_class_hash = get_class_hash(setup.factory.contract_address); + start_cheat_caller_address(upgradeable_dispatcher.contract_address, setup.owner); + upgradeable_dispatcher.upgrade(new_class_hash); + stop_cheat_caller_address(upgradeable_dispatcher.contract_address); + assert!( + get_class_hash(upgradeable_dispatcher.contract_address) == new_class_hash, + "Class Hash does not match!", + ); +} diff --git a/xerc20/tests/e2e/xerc20_test.cairo b/xerc20/tests/e2e/xerc20_test.cairo new file mode 100644 index 0000000..0ba66e1 --- /dev/null +++ b/xerc20/tests/e2e/xerc20_test.cairo @@ -0,0 +1,627 @@ +pub mod e2e_mint_and_burn { + use crate::{common::{DAY, E18}, e2e::common::{prepare_permit_signature, setup_base}}; + use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use snforge_std::{start_cheat_caller_address, stop_cheat_caller_address}; + use xerc20::xerc20::interface::XERC20ABIDispatcherTrait; + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_mint() { + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 0); + setup.xerc20.mint(setup.user, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + assert!(erc20_dispatcher.balance_of(setup.user) == 100 * E18, "Balance mismatch!"); + assert!(erc20_dispatcher.total_supply() == 100 * E18, "Total supply mismatch!"); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_burn() { + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 100 * E18); + setup.xerc20.mint(setup.user, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.burn(setup.user, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!(erc20_dispatcher.balance_of(setup.user) == 0, "Balance mismatch!"); + assert!(erc20_dispatcher.total_supply() == 0, "Total supply mismatch!"); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_burn_w_permit() { + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 100 * E18); + setup.xerc20.mint(setup.user, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + + let deadline = starknet::get_block_timestamp() + DAY; + let user_permit_sig = prepare_permit_signature(@setup, setup.owner, 100 * E18, deadline); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + erc20_dispatcher.permit(setup.user, setup.owner, 100 * E18, deadline, user_permit_sig); + assert!(erc20_dispatcher.allowance(setup.user, setup.owner) == 100 * E18); + setup.xerc20.burn(setup.user, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!(erc20_dispatcher.balance_of(setup.user) == 0, "Balance mismatch!"); + assert!(erc20_dispatcher.total_supply() == 0, "Total supply mismatch!"); + } +} + +pub mod e2e_parameter_math { + use crate::{common::{E18, HOUR, assert_approx_eq_rel}, e2e::common::setup_base}; + use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use snforge_std::{ + start_cheat_block_timestamp_global, start_cheat_caller_address, + stop_cheat_block_timestamp_global, stop_cheat_caller_address, + }; + use xerc20::xerc20::interface::XERC20ABIDispatcherTrait; + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_should_change_limit() { + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!(setup.xerc20.minting_current_limit_of(setup.owner) == 100 * E18); + assert!(setup.xerc20.burning_max_limit_of(setup.owner) == 100 * E18); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_adding_minters_and_limits() { + let setup = setup_base(); + + let limits = array![100 * E18, 100 * E18, 100 * E18]; + let minters = array![ + starknet::contract_address_const::<'minter_1'>(), + starknet::contract_address_const::<'minter_2'>(), + starknet::contract_address_const::<'minter_3'>(), + ]; + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + for i in 0..limits.len() { + setup.xerc20.set_limits(*minters[i], *limits[i], *limits[i]); + }; + stop_cheat_caller_address(setup.xerc20.contract_address); + for i in 0..limits.len() { + assert!(setup.xerc20.minting_max_limit_of(*minters[i]) == *limits[i]); + }; + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_should_use_limits_updates_limits() { + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.mint(setup.user, 100 * E18); + setup.xerc20.burn(setup.user, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!(setup.xerc20.minting_current_limit_of(setup.owner) == 0); + assert!(setup.xerc20.burning_current_limit_of(setup.owner) == 0); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_should_changing_max_limit_updates_current_limit() { + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 100 * E18); + + setup.xerc20.set_limits(setup.owner, 50 * E18, 50 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!(setup.xerc20.minting_current_limit_of(setup.owner) == 50 * E18); + assert!(setup.xerc20.burning_current_limit_of(setup.owner) == 50 * E18); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_changing_max_limit_when_limit_is_used_updates_current_limit() { + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.mint(setup.user, 100 * E18); + setup.xerc20.burn(setup.user, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 50 * E18, 50 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!(setup.xerc20.minting_current_limit_of(setup.owner) == 0); + assert!(setup.xerc20.burning_current_limit_of(setup.owner) == 0); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_changing_partial_max_limit_updates_current_limit_when_used() { + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.mint(setup.user, 10 * E18); + setup.xerc20.burn(setup.user, 10 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 50 * E18, 50 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!(setup.xerc20.minting_current_limit_of(setup.owner) == 40 * E18); + assert!(setup.xerc20.burning_current_limit_of(setup.owner) == 40 * E18); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_changing_partial_max_limit_updates_current_limit_with_increase() { + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.mint(setup.user, 10 * E18); + setup.xerc20.burn(setup.user, 10 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 120 * E18, 120 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!(setup.xerc20.minting_current_limit_of(setup.owner) == 110 * E18); + assert!(setup.xerc20.burning_current_limit_of(setup.owner) == 110 * E18); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_current_limit_is_updated_with_time() { + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.mint(setup.user, 100 * E18); + setup.xerc20.burn(setup.user, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let current_timestamp = starknet::get_block_timestamp(); + start_cheat_block_timestamp_global(current_timestamp + 12 * HOUR); + + assert_approx_eq_rel( + setup.xerc20.minting_current_limit_of(setup.owner), 100 * E18 / 2, E18 / 10, + ); + assert_approx_eq_rel( + setup.xerc20.burning_current_limit_of(setup.owner), 100 * E18 / 2, E18 / 10, + ); + + stop_cheat_block_timestamp_global(); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_current_limit_is_max_after_max_duration() { + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.mint(setup.user, 100 * E18); + setup.xerc20.burn(setup.user, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let current_timestamp = starknet::get_block_timestamp(); + start_cheat_block_timestamp_global(current_timestamp + 25 * HOUR); + + assert!(setup.xerc20.minting_current_limit_of(setup.owner) == 100 * E18); + assert!(setup.xerc20.burning_current_limit_of(setup.owner) == 100 * E18); + + stop_cheat_block_timestamp_global(); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_limit_is_same_if_unused() { + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let current_timestamp = starknet::get_block_timestamp(); + start_cheat_block_timestamp_global(current_timestamp + 12 * HOUR); + + assert!(setup.xerc20.minting_current_limit_of(setup.owner) == 100 * E18); + assert!(setup.xerc20.burning_current_limit_of(setup.owner) == 100 * E18); + + stop_cheat_block_timestamp_global(); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_multiple_users_use_bridge() { + let user_0 = starknet::contract_address_const::<'user_0'>(); + let user_1 = starknet::contract_address_const::<'user_1'>(); + let user_2 = starknet::contract_address_const::<'user_2'>(); + let user_3 = starknet::contract_address_const::<'user_3'>(); + let user_4 = starknet::contract_address_const::<'user_4'>(); + + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 100 * E18); + + setup.xerc20.mint(user_0, 10 * E18); + setup.xerc20.mint(user_1, 10 * E18); + setup.xerc20.mint(user_2, 10 * E18); + setup.xerc20.mint(user_3, 10 * E18); + setup.xerc20.mint(user_4, 10 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!(setup.xerc20.minting_current_limit_of(setup.owner) == 50 * E18); + + let current_timestamp = starknet::get_block_timestamp(); + start_cheat_block_timestamp_global(current_timestamp + 12 * HOUR); + + assert_approx_eq_rel( + setup.xerc20.minting_current_limit_of(setup.owner), 50 * E18 + 100 * E18 / 2, E18 / 10, + ); + stop_cheat_block_timestamp_global(); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_multiple_mints_and_burns() { + let user_0 = starknet::contract_address_const::<'user_0'>(); + let user_1 = starknet::contract_address_const::<'user_1'>(); + let user_2 = starknet::contract_address_const::<'user_2'>(); + let user_3 = starknet::contract_address_const::<'user_3'>(); + let user_4 = starknet::contract_address_const::<'user_4'>(); + + let setup = setup_base(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, 100 * E18, 100 * E18); + + setup.xerc20.mint(user_0, 20 * E18); + setup.xerc20.mint(user_1, 10 * E18); + setup.xerc20.mint(user_2, 20 * E18); + setup.xerc20.mint(user_3, 10 * E18); + setup.xerc20.mint(user_4, 20 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!(setup.xerc20.minting_current_limit_of(setup.owner) == 20 * E18); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + start_cheat_caller_address(setup.xerc20.contract_address, user_0); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, user_1); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, user_2); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, user_3); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, user_4); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.burn(user_0, 5 * E18); + setup.xerc20.burn(user_1, 5 * E18); + setup.xerc20.burn(user_2, 5 * E18); + setup.xerc20.burn(user_3, 5 * E18); + setup.xerc20.burn(user_4, 5 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!(setup.xerc20.burning_current_limit_of(setup.owner) == 75 * E18); + + let current_timestamp = starknet::get_block_timestamp(); + start_cheat_block_timestamp_global(current_timestamp + 12 * HOUR); + + assert_approx_eq_rel( + setup.xerc20.minting_current_limit_of(setup.owner), 20 * E18 + 100 * E18 / 2, E18 / 10, + ); + assert!(setup.xerc20.burning_current_limit_of(setup.owner) == 100 * E18); + stop_cheat_block_timestamp_global(); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_multiple_bridges_has_different_value() { + let setup = setup_base(); + + let owner_limit = 100 * E18; + let user_limit = 50 * E18; + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, owner_limit, owner_limit); + setup.xerc20.set_limits(setup.user, user_limit, user_limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + erc20_dispatcher.approve(setup.user, 100 * E18); + + setup.xerc20.mint(setup.user, 90 * E18); + setup.xerc20.burn(setup.user, 90 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + setup.xerc20.mint(setup.owner, 40 * E18); + setup.xerc20.burn(setup.owner, 40 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let minting_max_limit_of_owner = setup.xerc20.minting_max_limit_of(setup.owner); + let minting_max_limit_of_user = setup.xerc20.minting_max_limit_of(setup.user); + + let minting_current_limit_of_owner = setup.xerc20.minting_current_limit_of(setup.owner); + let minting_current_limit_of_user = setup.xerc20.minting_current_limit_of(setup.user); + + assert!(minting_max_limit_of_owner == owner_limit); + assert!(minting_current_limit_of_owner == owner_limit - 90 * E18); + + assert!(minting_max_limit_of_user == user_limit); + assert!(minting_current_limit_of_user == user_limit - 40 * E18); + + let current_timestamp = starknet::get_block_timestamp(); + start_cheat_block_timestamp_global(current_timestamp + 12 * HOUR); + + assert_approx_eq_rel( + setup.xerc20.minting_current_limit_of(setup.owner), + owner_limit - 90 * E18 + owner_limit / 2, + E18 / 10, + ); + assert_approx_eq_rel( + setup.xerc20.minting_current_limit_of(setup.user), + user_limit - 40 * E18 + user_limit / 2, + E18 / 10, + ); + + assert_approx_eq_rel( + setup.xerc20.burning_current_limit_of(setup.owner), + owner_limit - 90 * E18 + owner_limit / 2, + E18 / 10, + ); + assert_approx_eq_rel( + setup.xerc20.burning_current_limit_of(setup.user), + user_limit - 40 * E18 + user_limit / 2, + E18 / 10, + ); + + stop_cheat_block_timestamp_global(); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_multiple_bridges_burns_have_different_values() { + let setup = setup_base(); + + let owner_limit = 100 * E18; + let user_limit = 50 * E18; + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.owner, owner_limit, owner_limit); + setup.xerc20.set_limits(setup.user, user_limit, user_limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + erc20_dispatcher.approve(setup.owner, 100 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + erc20_dispatcher.approve(setup.user, 100 * E18); + + setup.xerc20.mint(setup.user, 90 * E18); + setup.xerc20.burn(setup.user, 50 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + setup.xerc20.mint(setup.owner, 40 * E18); + setup.xerc20.burn(setup.owner, 25 * E18); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let burning_max_limit_of_owner = setup.xerc20.burning_max_limit_of(setup.owner); + let burning_max_limit_of_user = setup.xerc20.burning_max_limit_of(setup.user); + + let burning_current_limit_of_owner = setup.xerc20.burning_current_limit_of(setup.owner); + let burning_current_limit_of_user = setup.xerc20.burning_current_limit_of(setup.user); + + assert!(burning_max_limit_of_owner == owner_limit); + assert!(burning_current_limit_of_owner == owner_limit - 50 * E18); + + assert!(burning_max_limit_of_user == user_limit); + assert!(burning_current_limit_of_user == user_limit - 25 * E18); + + let current_timestamp = starknet::get_block_timestamp(); + start_cheat_block_timestamp_global(current_timestamp + 12 * HOUR); + + assert_approx_eq_rel( + setup.xerc20.minting_current_limit_of(setup.owner), + owner_limit - 90 * E18 + owner_limit / 2, + E18 / 10, + ); + assert_approx_eq_rel( + setup.xerc20.minting_current_limit_of(setup.user), + user_limit - 40 * E18 + user_limit / 2, + E18 / 10, + ); + + assert_approx_eq_rel( + setup.xerc20.burning_current_limit_of(setup.owner), + owner_limit - 50 * E18 + owner_limit / 2, + E18 / 10, + ); + assert_approx_eq_rel( + setup.xerc20.burning_current_limit_of(setup.user), + user_limit - 25 * E18 + user_limit / 2, + E18 / 10, + ); + + stop_cheat_block_timestamp_global(); + } +} + +pub mod upgrade { + use crate::e2e::common::setup_base; + use openzeppelin_upgrades::interface::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; + use snforge_std::{get_class_hash, start_cheat_caller_address, stop_cheat_caller_address}; + + #[test] + //#[fork("mainnet")] + #[should_panic(expected: 'Caller is not the owner')] + #[ignore] + fn test_should_panic_when_upgrade_when_caller_not_owner() { + let setup = setup_base(); + let upgradeable_dispatcher = IUpgradeableDispatcher { + contract_address: setup.xerc20.contract_address, + }; + let new_class_hash = starknet::class_hash::class_hash_const::<'NEW_CLASS_HASH'>(); + start_cheat_caller_address(upgradeable_dispatcher.contract_address, setup.user); + upgradeable_dispatcher.upgrade(new_class_hash); + stop_cheat_caller_address(upgradeable_dispatcher.contract_address); + } + + #[test] + //#[fork("mainnet")] + #[ignore] + fn test_should_upgrade_implementation() { + let setup = setup_base(); + let upgradeable_dispatcher = IUpgradeableDispatcher { + contract_address: setup.xerc20.contract_address, + }; + let new_class_hash = get_class_hash(setup.factory.contract_address); + start_cheat_caller_address(upgradeable_dispatcher.contract_address, setup.owner); + upgradeable_dispatcher.upgrade(new_class_hash); + stop_cheat_caller_address(upgradeable_dispatcher.contract_address); + assert!( + get_class_hash(upgradeable_dispatcher.contract_address) == new_class_hash, + "Class Hash does not match!", + ); + } +} diff --git a/xerc20/tests/lib.cairo b/xerc20/tests/lib.cairo new file mode 100644 index 0000000..cf62763 --- /dev/null +++ b/xerc20/tests/lib.cairo @@ -0,0 +1,13 @@ +pub mod common; +pub mod e2e { + pub mod common; + pub mod xerc20_factory_test; + pub mod xerc20_lockbox_test; + pub mod xerc20_test; +} + +pub mod unit { + pub mod xerc20_factory_test; + pub mod xerc20_lockbox_test; + pub mod xerc20_test; +} diff --git a/xerc20/tests/unit/xerc20_factory_test.cairo b/xerc20/tests/unit/xerc20_factory_test.cairo new file mode 100644 index 0000000..5ceaf84 --- /dev/null +++ b/xerc20/tests/unit/xerc20_factory_test.cairo @@ -0,0 +1,333 @@ +use core::num::traits::Zero; +use core::poseidon::poseidon_hash_span; +use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; +use openzeppelin_utils::deployments::calculate_contract_address_from_deploy_syscall; +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, EventSpyAssertionsTrait, declare, spy_events, + start_cheat_caller_address, stop_cheat_caller_address, +}; +use starknet::ClassHash; +use starknet::ContractAddress; +use xerc20::{ + factory::{ + contract::XERC20Factory, + interface::{IXERC20FactoryDispatcher, IXERC20FactoryDispatcherTrait}, + }, + lockbox::interface::{XERC20LockboxABIDispatcher, XERC20LockboxABIDispatcherTrait}, +}; + +#[derive(Drop)] +pub struct Setup { + owner: ContractAddress, + user: ContractAddress, + erc20: ContractAddress, + xerc20_factory: IXERC20FactoryDispatcher, + xerc20_class_hash: ClassHash, + lockbox_class_hash: ClassHash, +} + +pub fn setup() -> Setup { + let owner = starknet::contract_address_const::<1>(); + let user = starknet::contract_address_const::<2>(); + let erc20 = starknet::contract_address_const::<3>(); + + let xerc20_class_hash = declare("XERC20").unwrap().contract_class().class_hash; + let lockbox_class_hash = declare("XERC20Lockbox").unwrap().contract_class().class_hash; + let factory_contract = declare("XERC20Factory").unwrap().contract_class(); + let mut ctor_calldata: Array = array![]; + xerc20_class_hash.serialize(ref ctor_calldata); + lockbox_class_hash.serialize(ref ctor_calldata); + owner.serialize(ref ctor_calldata); + let (factory_address, _) = factory_contract.deploy(@ctor_calldata).unwrap(); + + Setup { + owner, + user, + erc20, + xerc20_factory: IXERC20FactoryDispatcher { contract_address: factory_address }, + xerc20_class_hash: *xerc20_class_hash, + lockbox_class_hash: *lockbox_class_hash, + } +} + +#[test] +fn test_deployment() { + let setup = setup(); + + let limits = array![].span(); + let minters = array![].span(); + + let xerc20 = setup.xerc20_factory.deploy_xerc20("Test", "TST", limits, limits, minters); + let erc20_dispatcher = ERC20ABIDispatcher { contract_address: xerc20 }; + assert!(erc20_dispatcher.name() == "Test", "Name does not match!"); + assert!(erc20_dispatcher.symbol() == "TST", "Symbol does not match!"); +} + +// NOTE: this test should panic and panicing but fails +//#[test] +//#[should_panic] +//fn test_should_panic_when_address_is_taken() { +// let setup = setup(); +// let limits = array![].span(); +// let minters = array![].span(); +// setup.xerc20_factory.deploy_xerc20("Test", "TST", limits, limits, minters); +// // second time deploying to same address should fail +// setup.xerc20_factory.deploy_xerc20("Test", "TST", limits, limits, minters); +//} + +#[test] +fn test_xerc20_pre_computed_address() { + let setup = setup(); + + let limits = array![].span(); + let minters = array![].span(); + + let name: ByteArray = "Test"; + let symbol: ByteArray = "TST"; + let mut serialized_data: Array = array![]; + name.serialize(ref serialized_data); + symbol.serialize(ref serialized_data); + starknet::get_contract_address().serialize(ref serialized_data); + let salt = poseidon_hash_span(serialized_data.span()); + let mut serialized_ctor_data: Array = array![]; + name.serialize(ref serialized_ctor_data); + symbol.serialize(ref serialized_ctor_data); + setup.xerc20_factory.contract_address.serialize(ref serialized_ctor_data); + + let actual_address = setup.xerc20_factory.deploy_xerc20("Test", "TST", limits, limits, minters); + let expected_address = calculate_contract_address_from_deploy_syscall( + salt, + setup.xerc20_class_hash, + serialized_ctor_data.span(), + setup.xerc20_factory.contract_address, + ); + assert!(expected_address == actual_address, "Addresses does not match!"); +} + +#[test] +fn test_xerc20_lockbox_pre_computed_address() { + let setup = setup(); + + let limits = array![].span(); + let minters = array![].span(); + + let xerc20_address = setup.xerc20_factory.deploy_xerc20("Test", "TST", limits, limits, minters); + + let salt = poseidon_hash_span( + array![xerc20_address.into(), setup.erc20.into(), starknet::get_contract_address().into()] + .span(), + ); + let mut serialized_ctor_data: Array = array![]; + xerc20_address.serialize(ref serialized_ctor_data); + setup.erc20.serialize(ref serialized_ctor_data); + let expected_address = calculate_contract_address_from_deploy_syscall( + salt, + setup.lockbox_class_hash, + serialized_ctor_data.span(), + setup.xerc20_factory.contract_address, + ); + + let actual_address = setup.xerc20_factory.deploy_lockbox(xerc20_address, setup.erc20); + assert!(expected_address == actual_address, "Addresses does not match!"); +} + +#[test] +fn test_lockbox_single_deployment() { + let setup = setup(); + + let limits = array![].span(); + let minters = array![].span(); + + let xerc20_address = setup.xerc20_factory.deploy_xerc20("Test", "TST", limits, limits, minters); + + let lockbox_address = setup.xerc20_factory.deploy_lockbox(xerc20_address, setup.erc20); + let lockbox_dispatcher = XERC20LockboxABIDispatcher { contract_address: lockbox_address }; + assert!(lockbox_dispatcher.erc20() == setup.erc20, "ERC20 address does not match!"); + assert!(lockbox_dispatcher.xerc20() == xerc20_address, "XERC20 address does not match!"); +} + +#[test] +#[should_panic(expected: 'Caller is not the owner')] +fn test_should_panic_when_lockbox_single_deployment_when_caller_not_owner() { + let setup = setup(); + + let limits = array![].span(); + let minters = array![].span(); + + start_cheat_caller_address( + setup.xerc20_factory.contract_address, starknet::contract_address_const::<'not_owner'>(), + ); + let xerc20_address = setup.xerc20_factory.deploy_xerc20("Test", "TST", limits, limits, minters); + stop_cheat_caller_address(setup.xerc20_factory.contract_address); + + setup.xerc20_factory.deploy_lockbox(xerc20_address, setup.erc20); +} + +#[test] +#[should_panic(expected: 'Token address zero')] +fn test_should_panic_when_lockbox_deployment_when_base_token_adress_zero() { + let setup = setup(); + + let limits = array![].span(); + let minters = array![].span(); + + let xerc20_address = setup.xerc20_factory.deploy_xerc20("Test", "TST", limits, limits, minters); + setup.xerc20_factory.deploy_lockbox(xerc20_address, Zero::zero()); +} + +#[test] +#[should_panic(expected: 'Lockbox alread deployed')] +fn test_should_panic_when_lockbox_deployment_twice() { + let setup = setup(); + + let limits = array![].span(); + let minters = array![].span(); + + let xerc20_address = setup.xerc20_factory.deploy_xerc20("Test", "TST", limits, limits, minters); + + setup.xerc20_factory.deploy_lockbox(xerc20_address, setup.erc20); + setup.xerc20_factory.deploy_lockbox(xerc20_address, setup.erc20); +} + +#[test] +#[should_panic(expected: 'Invalid length')] +fn test_should_panic_when_arrays_len_does_not_match() { + let setup = setup(); + + let limits = array![1].span(); + let minters = array![].span(); + + setup.xerc20_factory.deploy_xerc20("Test", "TST", limits, limits, minters); +} + +#[test] +fn test_deploy_xerc20_should_emit_events() { + let setup = setup(); + + let limits = array![].span(); + let minters = array![].span(); + + let mut spy = spy_events(); + let xerc20_address = setup.xerc20_factory.deploy_xerc20("Test", "TST", limits, limits, minters); + + spy + .assert_emitted( + @array![ + ( + setup.xerc20_factory.contract_address, + XERC20Factory::Event::XERC20Deployed( + XERC20Factory::XERC20Deployed { xerc20: xerc20_address }, + ), + ), + ], + ); +} + +#[test] +fn test_deploy_lockbox_should_emit_events() { + let setup = setup(); + + let limits = array![].span(); + let minters = array![].span(); + + let xerc20_address = setup.xerc20_factory.deploy_xerc20("Test", "TST", limits, limits, minters); + + let mut spy = spy_events(); + + let lockbox_address = setup.xerc20_factory.deploy_lockbox(xerc20_address, setup.erc20); + spy + .assert_emitted( + @array![ + ( + setup.xerc20_factory.contract_address, + XERC20Factory::Event::LockboxDeployed( + XERC20Factory::LockboxDeployed { lockbox: lockbox_address }, + ), + ), + ], + ); +} + +#[test] +#[should_panic(expected: 'Caller is not the owner')] +fn test_should_panic_when_set_xerc20_implementation_when_caller_not_owner() { + let setup = setup(); + + let new_class_hash = starknet::class_hash::class_hash_const::<'NEW_CLASS_HASH'>(); + + start_cheat_caller_address(setup.xerc20_factory.contract_address, setup.user); + setup.xerc20_factory.set_xerc20_class_hash(new_class_hash); + stop_cheat_caller_address(setup.xerc20_factory.contract_address); +} + +#[test] +fn test_should_set_xerc20_implementation() { + let setup = setup(); + + let class_hash_before = setup.xerc20_factory.get_xerc20_class_hash(); + let new_class_hash = starknet::class_hash::class_hash_const::<'NEW_CLASS_HASH'>(); + + let mut spy = spy_events(); + + start_cheat_caller_address(setup.xerc20_factory.contract_address, setup.owner); + setup.xerc20_factory.set_xerc20_class_hash(new_class_hash); + stop_cheat_caller_address(setup.xerc20_factory.contract_address); + + let class_hash_after = setup.xerc20_factory.get_xerc20_class_hash(); + assert!(class_hash_after != class_hash_before); + assert!(class_hash_after == new_class_hash); + + spy + .assert_emitted( + @array![ + ( + setup.xerc20_factory.contract_address, + XERC20Factory::Event::XERC20ImplementationUpdated( + XERC20Factory::XERC20ImplementationUpdated { class_hash: new_class_hash }, + ), + ), + ], + ); +} + +#[test] +#[should_panic(expected: 'Caller is not the owner')] +fn test_should_panic_when_set_lockbox_implementation_when_caller_not_owner() { + let setup = setup(); + + let new_class_hash = starknet::class_hash::class_hash_const::<'NEW_CLASS_HASH'>(); + + start_cheat_caller_address(setup.xerc20_factory.contract_address, setup.user); + setup.xerc20_factory.set_lockbox_class_hash(new_class_hash); + stop_cheat_caller_address(setup.xerc20_factory.contract_address); +} + +#[test] +fn test_should_set_lockbox_implementation() { + let setup = setup(); + + let class_hash_before = setup.xerc20_factory.get_lockbox_class_hash(); + let new_class_hash = starknet::class_hash::class_hash_const::<'NEW_CLASS_HASH'>(); + + let mut spy = spy_events(); + + start_cheat_caller_address(setup.xerc20_factory.contract_address, setup.owner); + setup.xerc20_factory.set_lockbox_class_hash(new_class_hash); + stop_cheat_caller_address(setup.xerc20_factory.contract_address); + + let class_hash_after = setup.xerc20_factory.get_lockbox_class_hash(); + assert!(class_hash_after != class_hash_before); + assert!(class_hash_after == new_class_hash); + + spy + .assert_emitted( + @array![ + ( + setup.xerc20_factory.contract_address, + XERC20Factory::Event::LockboxImplementationUpdated( + XERC20Factory::LockboxImplementationUpdated { class_hash: new_class_hash }, + ), + ), + ], + ); +} diff --git a/xerc20/tests/unit/xerc20_lockbox_test.cairo b/xerc20/tests/unit/xerc20_lockbox_test.cairo new file mode 100644 index 0000000..75dd171 --- /dev/null +++ b/xerc20/tests/unit/xerc20_lockbox_test.cairo @@ -0,0 +1,324 @@ +use openzeppelin_token::erc20::interface::ERC20ABIDispatcher; +use snforge_std::{ContractClassTrait, DeclareResultTrait, declare}; +use starknet::ContractAddress; +use xerc20::{lockbox::interface::IXERC20LockboxDispatcher, xerc20::interface::IXERC20Dispatcher}; + +#[derive(Drop)] +pub struct Setup { + owner: ContractAddress, + user: ContractAddress, + minter: ContractAddress, + xerc20: IXERC20Dispatcher, + erc20: ERC20ABIDispatcher, + lockbox: IXERC20LockboxDispatcher, +} + +pub fn setup() -> Setup { + let owner = starknet::contract_address_const::<1>(); + let user = starknet::contract_address_const::<2>(); + let minter = starknet::contract_address_const::<3>(); + + let mock_erc20_contract = declare("MockErc20").unwrap().contract_class(); + let (erc20_address, _) = mock_erc20_contract.deploy(@array![]).unwrap(); + let erc20_dispatcher = ERC20ABIDispatcher { contract_address: erc20_address }; + + let xerc20_contract = declare("MockXERC20").unwrap().contract_class(); + let (xerc20_address, _) = xerc20_contract.deploy(@array![]).unwrap(); + let xerc20_dispatcher = IXERC20Dispatcher { contract_address: xerc20_address }; + + let xerc20_lockbox_contract = declare("XERC20Lockbox").unwrap().contract_class(); + let (xerc20_lockbox_address, _) = xerc20_lockbox_contract + .deploy(@array![xerc20_address.into(), erc20_address.into()]) + .unwrap(); + + Setup { + owner, + user, + minter, + xerc20: xerc20_dispatcher, + erc20: erc20_dispatcher, + lockbox: IXERC20LockboxDispatcher { contract_address: xerc20_lockbox_address }, + } +} + +mod unit_deposit { + use core::num::traits::Bounded; + use core::num::traits::Zero; + use crate::common::bound; + use openzeppelin_token::erc20::erc20::ERC20Component; + use snforge_std::{ + EventSpyAssertionsTrait, mock_call, spy_events, start_cheat_caller_address, + stop_cheat_caller_address, + }; + use super::setup; + use xerc20::lockbox::{ + component::XERC20LockboxComponent, interface::IXERC20LockboxDispatcherTrait, + }; + + #[test] + fn test_deposit(mut amount: u256) { + let setup = setup(); + + amount = bound(amount, 1, Bounded::MAX); + + let mut spy = spy_events(); + + start_cheat_caller_address(setup.lockbox.contract_address, setup.owner); + setup.lockbox.deposit(amount); + stop_cheat_caller_address(setup.lockbox.contract_address); + + spy + .assert_emitted( + @array![ + ( + setup.erc20.contract_address, + ERC20Component::Event::Transfer( + ERC20Component::Transfer { + from: setup.owner, + to: setup.lockbox.contract_address, + value: amount, + }, + ), + ), + ( + setup.xerc20.contract_address, + ERC20Component::Event::Transfer( + ERC20Component::Transfer { + from: Zero::zero(), to: setup.owner, value: amount, + }, + ), + ), + ], + ); + } + + #[test] + #[should_panic(expected: 'ERC20 transfer_from failed')] + fn test_deposit_should_panic_when_transfer_from_returns_false(mut amount: u256) { + let setup = setup(); + + amount = bound(amount, 1, Bounded::MAX); + mock_call(setup.erc20.contract_address, selector!("transfer_from"), false, 1); + mock_call(setup.xerc20.contract_address, selector!("mint"), (), 1); + + start_cheat_caller_address(setup.lockbox.contract_address, setup.owner); + setup.lockbox.deposit(amount); + stop_cheat_caller_address(setup.lockbox.contract_address); + } + + #[test] + fn test_deposit_to(mut amount: u256) { + let setup = setup(); + + amount = bound(amount, 1, Bounded::MAX); + + let mut spy = spy_events(); + + start_cheat_caller_address(setup.lockbox.contract_address, setup.owner); + setup.lockbox.deposit_to(setup.user, amount); + stop_cheat_caller_address(setup.lockbox.contract_address); + + spy + .assert_emitted( + @array![ + ( + setup.erc20.contract_address, + ERC20Component::Event::Transfer( + ERC20Component::Transfer { + from: setup.owner, + to: setup.lockbox.contract_address, + value: amount, + }, + ), + ), + ( + setup.xerc20.contract_address, + ERC20Component::Event::Transfer( + ERC20Component::Transfer { + from: Zero::zero(), to: setup.user, value: amount, + }, + ), + ), + ], + ); + } + + #[test] + #[should_panic(expected: 'ERC20 transfer_from failed')] + fn test_deposit_to_should_panic_when_transfer_from_returns_false(mut amount: u256) { + let setup = setup(); + + amount = bound(amount, 1, Bounded::MAX); + mock_call(setup.erc20.contract_address, selector!("transfer_from"), false, 1); + mock_call(setup.xerc20.contract_address, selector!("mint"), (), 1); + + start_cheat_caller_address(setup.lockbox.contract_address, setup.owner); + setup.lockbox.deposit_to(setup.user, amount); + stop_cheat_caller_address(setup.lockbox.contract_address); + } + + #[test] + fn test_deposit_emits_event(mut amount: u256) { + let setup = setup(); + + amount = bound(amount, 1, Bounded::MAX); + mock_call(setup.erc20.contract_address, selector!("transfer_from"), true, 1); + mock_call(setup.xerc20.contract_address, selector!("mint"), (), 1); + + let mut spy = spy_events(); + start_cheat_caller_address(setup.lockbox.contract_address, setup.owner); + setup.lockbox.deposit(amount); + stop_cheat_caller_address(setup.lockbox.contract_address); + spy + .assert_emitted( + @array![ + ( + setup.lockbox.contract_address, + XERC20LockboxComponent::Event::Deposit( + XERC20LockboxComponent::Deposit { sender: setup.owner, amount }, + ), + ), + ], + ); + } +} + + +pub mod unit_withdraw { + use core::num::traits::{Bounded, Zero}; + use crate::common::bound; + use openzeppelin_token::erc20::erc20::ERC20Component; + use snforge_std::{ + EventSpyAssertionsTrait, mock_call, spy_events, start_cheat_caller_address, + stop_cheat_caller_address, + }; + use super::setup; + use xerc20::lockbox::{ + component::XERC20LockboxComponent, interface::IXERC20LockboxDispatcherTrait, + }; + + #[test] + fn test_withdraw(mut amount: u256) { + let setup = setup(); + + amount = bound(amount, 1, Bounded::MAX); + + let mut spy = spy_events(); + + start_cheat_caller_address(setup.lockbox.contract_address, setup.owner); + setup.lockbox.withdraw(amount); + stop_cheat_caller_address(setup.lockbox.contract_address); + + spy + .assert_emitted( + @array![ + ( + setup.erc20.contract_address, + ERC20Component::Event::Transfer( + ERC20Component::Transfer { + from: setup.lockbox.contract_address, + to: setup.owner, + value: amount, + }, + ), + ), + ( + setup.xerc20.contract_address, + ERC20Component::Event::Transfer( + ERC20Component::Transfer { + from: setup.owner, to: Zero::zero(), value: amount, + }, + ), + ), + ], + ); + } + + #[test] + #[should_panic(expected: 'ERC20 transfer failed')] + fn test_withdraw_should_panic_when_transfer_returns_false(mut amount: u256) { + let setup = setup(); + + amount = bound(amount, 1, Bounded::MAX); + mock_call(setup.erc20.contract_address, selector!("transfer"), false, 1); + mock_call(setup.xerc20.contract_address, selector!("burn"), (), 1); + + start_cheat_caller_address(setup.lockbox.contract_address, setup.owner); + setup.lockbox.withdraw(amount); + stop_cheat_caller_address(setup.lockbox.contract_address); + } + + #[test] + fn test_withdraw_to(mut amount: u256) { + let setup = setup(); + + amount = bound(amount, 1, Bounded::MAX); + + let mut spy = spy_events(); + + start_cheat_caller_address(setup.lockbox.contract_address, setup.owner); + setup.lockbox.withdraw_to(setup.user, amount); + stop_cheat_caller_address(setup.lockbox.contract_address); + + spy + .assert_emitted( + @array![ + ( + setup.erc20.contract_address, + ERC20Component::Event::Transfer( + ERC20Component::Transfer { + from: setup.lockbox.contract_address, to: setup.user, value: amount, + }, + ), + ), + ( + setup.xerc20.contract_address, + ERC20Component::Event::Transfer( + ERC20Component::Transfer { + from: setup.owner, to: Zero::zero(), value: amount, + }, + ), + ), + ], + ); + } + + #[test] + #[should_panic(expected: 'ERC20 transfer failed')] + fn test_withdraw_to_should_panic_when_transfer_returns_false(mut amount: u256) { + let setup = setup(); + + amount = bound(amount, 1, Bounded::MAX); + mock_call(setup.erc20.contract_address, selector!("transfer"), false, 1); + mock_call(setup.xerc20.contract_address, selector!("burn"), (), 1); + + start_cheat_caller_address(setup.lockbox.contract_address, setup.owner); + setup.lockbox.withdraw_to(setup.user, amount); + stop_cheat_caller_address(setup.lockbox.contract_address); + } + + #[test] + fn test_withdraw_emit_events(mut amount: u256) { + let setup = setup(); + + amount = bound(amount, 1, Bounded::MAX); + + let mut spy = spy_events(); + + start_cheat_caller_address(setup.lockbox.contract_address, setup.owner); + setup.lockbox.withdraw(amount); + stop_cheat_caller_address(setup.lockbox.contract_address); + + spy + .assert_emitted( + @array![ + ( + setup.lockbox.contract_address, + XERC20LockboxComponent::Event::Withdraw( + XERC20LockboxComponent::Withdraw { sender: setup.owner, amount }, + ), + ), + ], + ); + } +} diff --git a/xerc20/tests/unit/xerc20_test.cairo b/xerc20/tests/unit/xerc20_test.cairo new file mode 100644 index 0000000..74b6cc3 --- /dev/null +++ b/xerc20/tests/unit/xerc20_test.cairo @@ -0,0 +1,741 @@ +use snforge_std::{ContractClassTrait, DeclareResultTrait, declare}; +use starknet::ContractAddress; +use xerc20::xerc20::interface::XERC20ABIDispatcher; + +#[derive(Drop)] +pub struct Setup { + owner: ContractAddress, + user: ContractAddress, + minter: ContractAddress, + xerc20: XERC20ABIDispatcher, + token_name: ByteArray, + token_symbol: ByteArray, +} + +pub fn setup() -> Setup { + let owner = starknet::contract_address_const::<1>(); + let user = starknet::contract_address_const::<2>(); + let minter = starknet::contract_address_const::<3>(); + let token_name = "Test"; + let token_symbol = "TST"; + + let xerc20_contract = declare("XERC20").unwrap().contract_class(); + let mut ctor_calldata: Array = array![]; + token_name.serialize(ref ctor_calldata); + token_symbol.serialize(ref ctor_calldata); + owner.serialize(ref ctor_calldata); + let (xerc20_address, _) = xerc20_contract.deploy(@ctor_calldata).unwrap(); + + Setup { + owner, + user, + minter, + xerc20: XERC20ABIDispatcher { contract_address: xerc20_address }, + token_name, + token_symbol, + } +} + +pub mod unit_names { + use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use super::setup; + + #[test] + fn test_name() { + let setup = setup(); + let xerc20 = ERC20ABIDispatcher { contract_address: setup.xerc20.contract_address }; + assert!(xerc20.name() == setup.token_name, "Token name does not match!"); + } + + #[test] + fn test_symbol() { + let setup = setup(); + let xerc20 = ERC20ABIDispatcher { contract_address: setup.xerc20.contract_address }; + assert!(xerc20.symbol() == setup.token_symbol, "Token symbol does not match!"); + } +} + +pub mod unit_mint_burn { + use core::num::traits::Bounded; + use crate::common::{E40, U256MAX_DIV_2, bound}; + use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use snforge_std::{start_cheat_caller_address, stop_cheat_caller_address}; + use super::setup; + use xerc20::xerc20::interface::XERC20ABIDispatcherTrait; + + #[test] + #[should_panic(expected: 'User does not have enough limit')] + fn test_mint_should_panic_when_not_approve(mut amount: u256) { + let setup = setup(); + amount = bound(amount, 1, Bounded::MAX); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + setup.xerc20.mint(setup.user, amount); + stop_cheat_caller_address(setup.xerc20.contract_address); + } + + #[test] + #[should_panic(expected: 'User does not have enough limit')] + fn test_burn_should_panic_when_limit_is_too_low(mut amount_0: u256, mut amount_1: u256) { + let setup = setup(); + amount_0 = bound(amount_0, 1, E40); + amount_1 = bound(amount_1, amount_0 + 1, E40); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.user, amount_0, 0); + stop_cheat_caller_address(setup.xerc20.contract_address); + + // should revert since burning_limit eq zero + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + setup.xerc20.burn(setup.user, amount_1); + stop_cheat_caller_address(setup.xerc20.contract_address); + } + + #[test] + #[should_panic(expected: 'Limits too high')] + fn test_set_limit_should_panic_when_limit_is_too_high_after(mut limit: u256) { + let setup = setup(); + limit = bound(limit, U256MAX_DIV_2 + 1, Bounded::MAX); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.user, limit, limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + } + + #[test] + fn test_mint(mut amount: u256) { + let setup = setup(); + amount = bound(amount, 1, U256MAX_DIV_2); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.user, amount, 0); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + let balance_prev = erc20_dispatcher.balance_of(setup.minter); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + setup.xerc20.mint(setup.minter, amount); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let balance_after = erc20_dispatcher.balance_of(setup.minter); + assert(balance_prev + amount == balance_after, 'Balances does not match!'); + } + + #[test] + fn test_burn(mut amount: u256) { + let setup = setup(); + amount = bound(amount, 1, E40); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.user, amount, amount); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + let balance_prev = erc20_dispatcher.balance_of(setup.user); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + setup.xerc20.mint(setup.user, amount); + let balance_mid = erc20_dispatcher.balance_of(setup.user); + assert(balance_prev + amount == balance_mid, 'Balances does not match!'); + setup.xerc20.burn(setup.user, amount); + stop_cheat_caller_address(setup.xerc20.contract_address); + let balance_after = erc20_dispatcher.balance_of(setup.user); + assert(balance_prev == balance_after, 'Balances does not match!'); + } + + #[test] + #[should_panic(expected: 'ERC20: insufficient allowance')] + fn test_burn_should_panic_when_not_have_approval(mut amount: u256) { + let setup = setup(); + amount = bound(amount, 1, E40); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.user, amount, amount); + setup.xerc20.burn(setup.user, amount); + stop_cheat_caller_address(setup.xerc20.contract_address); + } + + #[test] + fn test_burn_should_reduces_allowance(mut amount: u256, mut approval_amount: u256) { + let setup = setup(); + amount = bound(amount, 1, E40); + approval_amount = bound(approval_amount, amount, 100_000 * E40); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.minter, amount, amount); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + start_cheat_caller_address(setup.xerc20.contract_address, setup.user); + erc20_dispatcher.approve(setup.minter, approval_amount); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.minter); + setup.xerc20.mint(setup.user, amount); + setup.xerc20.burn(setup.user, amount); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert( + erc20_dispatcher.allowance(setup.user, setup.minter) == approval_amount - amount, + 'Allowance not reduced!', + ); + } +} + +pub mod unit_create_params { + use core::num::traits::Zero; + use crate::common::{E18, E40, HOUR, U256MAX_DIV_2, assert_approx_eq_rel, bound}; + use openzeppelin_token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use snforge_std::{ + EventSpyAssertionsTrait, spy_events, start_cheat_block_timestamp_global, + start_cheat_caller_address, stop_cheat_block_timestamp_global, stop_cheat_caller_address, + }; + use starknet::ContractAddress; + use super::setup; + use xerc20::xerc20::{component::XERC20Component as XERC20, interface::XERC20ABIDispatcherTrait}; + + #[test] + fn test_should_change_limit(mut amount: u256, mut random_address_u128: u128) { + let setup = setup(); + + if random_address_u128.is_zero() { + random_address_u128 = 0xBadCafe; + } + let mut random_address: ContractAddress = Into::::into(random_address_u128) + .try_into() + .unwrap(); + amount = bound(amount, 0, U256MAX_DIV_2); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(random_address, amount, amount); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!( + setup.xerc20.minting_max_limit_of(random_address) == amount, + "MintingMaxLimitOf does not match!", + ); + assert!( + setup.xerc20.burning_max_limit_of(random_address) == amount, + "BurningMaxLimitOf does not match!", + ); + } + + #[test] + #[should_panic(expected: 'Caller is not the owner')] + fn test_should_panic_when_caller_is_not_owner() { + let setup = setup(); + setup.xerc20.set_limits(setup.minter, U256MAX_DIV_2, U256MAX_DIV_2); + } + + #[test] + fn test_should_add_minters_and_limits( + mut amount_0: u256, + mut amount_1: u256, + mut amount_2: u256, + mut user_0: u128, + mut user_1: u128, + mut user_2: u128, + ) { + let setup = setup(); + + amount_0 = bound(amount_0, 1, U256MAX_DIV_2); + amount_1 = bound(amount_1, 1, U256MAX_DIV_2); + amount_2 = bound(amount_2, 1, U256MAX_DIV_2); + if user_0 == user_1 { + user_1 += 1; + } + if user_2 == user_1 || user_2 == user_0 { + user_2 += 2 + } + let user_0_address: ContractAddress = Into::::into(user_0) + .try_into() + .unwrap(); + let user_1_address: ContractAddress = Into::::into(user_1) + .try_into() + .unwrap(); + let user_2_address: ContractAddress = Into::::into(user_2) + .try_into() + .unwrap(); + let limits = array![amount_0, amount_1, amount_2]; + let minters = array![user_0_address, user_1_address, user_2_address]; + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + for i in 0..limits.len() { + setup.xerc20.set_limits(*minters[i], *limits[i], *limits[i]); + }; + stop_cheat_caller_address(setup.xerc20.contract_address); + + for i in 0..limits.len() { + assert!( + setup.xerc20.minting_max_limit_of(*minters[i]) == *limits[i], + "MintingMaxLimitOf does not match", + ); + assert!( + setup.xerc20.burning_max_limit_of(*minters[i]) == *limits[i], + "BurningMaxLimitOf does not match", + ); + }; + } + + #[test] + fn test_change_bridge_minting_limit_emits_event(mut limit: u256, minter: u128) { + let setup = setup(); + let minter_address: ContractAddress = Into::::into(minter) + .try_into() + .unwrap(); + limit = bound(limit, 0, U256MAX_DIV_2); + let mut spy = spy_events(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(minter_address, limit, 0); + stop_cheat_caller_address(setup.xerc20.contract_address); + + spy + .assert_emitted( + @array![ + ( + setup.xerc20.contract_address, + XERC20::Event::BridgeLimitsSet( + XERC20::BridgeLimitsSet { + minting_limit: limit, burning_limit: 0, bridge: minter_address, + }, + ), + ), + ], + ); + } + + #[test] + fn test_change_bridge_burning_limit_emits_event(mut limit: u256, minter: u128) { + let setup = setup(); + let minter_address: ContractAddress = Into::::into(minter) + .try_into() + .unwrap(); + limit = bound(limit, 0, U256MAX_DIV_2); + let mut spy = spy_events(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(minter_address, 0, limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + spy + .assert_emitted( + @array![ + ( + setup.xerc20.contract_address, + XERC20::Event::BridgeLimitsSet( + XERC20::BridgeLimitsSet { + minting_limit: 0, burning_limit: limit, bridge: minter_address, + }, + ), + ), + ], + ); + } + + #[test] + fn test_setting_limits_to_unapproved_user(mut amount: u256) { + let setup = setup(); + amount = bound(amount, 1, U256MAX_DIV_2); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.minter, amount, amount); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!( + setup.xerc20.minting_max_limit_of(setup.minter) == amount, + "Minting limit not setted correctly", + ); + assert!( + setup.xerc20.burning_max_limit_of(setup.minter) == amount, + "Burning limit not setted correctly", + ); + } + + #[test] + fn test_use_limit_updates_limit(mut limit: u256, mut minter_u128: u128) { + let setup = setup(); + + if minter_u128.is_zero() { + minter_u128 = 0xBadCafe; + } + let mut minter_address: ContractAddress = Into::::into(minter_u128) + .try_into() + .unwrap(); + + limit = bound(limit, 1, U256MAX_DIV_2); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(minter_address, limit, limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, minter_address); + setup.xerc20.mint(minter_address, limit); + setup.xerc20.burn(minter_address, limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!( + setup.xerc20.minting_max_limit_of(minter_address) == limit, + "Minting limit not setted correctly", + ); + assert!( + setup.xerc20.minting_current_limit_of(minter_address) == 0, + "Minting current limit not setted correctly", + ); + assert!( + setup.xerc20.burning_max_limit_of(minter_address) == limit, + "Burning limit not setted correctly", + ); + assert!( + setup.xerc20.burning_current_limit_of(minter_address) == 0, + "Burning current limit not setted correctly", + ); + } + + #[test] + fn test_current_limit_is_max_limit_if_unused(mut limit: u256, mut minter_u128: u128) { + let setup = setup(); + + if minter_u128.is_zero() { + minter_u128 = 0xBadCafe; + } + let mut minter_address: ContractAddress = Into::::into(minter_u128) + .try_into() + .unwrap(); + + limit = bound(limit, 1, U256MAX_DIV_2); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(minter_address, limit, limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let current_timestamp = starknet::get_block_timestamp(); + start_cheat_block_timestamp_global(current_timestamp + 12 * HOUR); + + assert!( + setup.xerc20.minting_current_limit_of(minter_address) == limit, + "Minting current limit does not match", + ); + assert!( + setup.xerc20.burning_current_limit_of(minter_address) == limit, + "Burning current limit does not match", + ); + stop_cheat_block_timestamp_global(); + } + + #[test] + fn test_current_limit_is_max_limit_if_over_24_hours(mut limit: u256, mut minter_u128: u128) { + let setup = setup(); + + if minter_u128.is_zero() { + minter_u128 = 0xBadCafe; + } + let mut minter_address: ContractAddress = Into::::into(minter_u128) + .try_into() + .unwrap(); + + limit = bound(limit, 1, U256MAX_DIV_2); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(minter_address, limit, limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, minter_address); + setup.xerc20.mint(minter_address, limit); + setup.xerc20.burn(minter_address, limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let current_timestamp = starknet::get_block_timestamp(); + start_cheat_block_timestamp_global(current_timestamp + 30 * HOUR); + + assert!( + setup.xerc20.minting_current_limit_of(minter_address) == limit, + "Minting current limit does not match", + ); + assert!( + setup.xerc20.burning_current_limit_of(minter_address) == limit, + "Burning current limit does not match", + ); + + stop_cheat_block_timestamp_global(); + } + + #[test] + fn test_limit_vests_linearly(mut limit: u256, mut minter_u128: u128) { + let setup = setup(); + + if minter_u128.is_zero() { + minter_u128 = 0xBadCafe; + } + let mut minter_address: ContractAddress = Into::::into(minter_u128) + .try_into() + .unwrap(); + limit = bound(limit, 1_000_000, U256MAX_DIV_2); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(minter_address, limit, limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, minter_address); + setup.xerc20.mint(minter_address, limit); + setup.xerc20.burn(minter_address, limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let current_timestamp = starknet::get_block_timestamp(); + start_cheat_block_timestamp_global(current_timestamp + 12 * HOUR); + + assert_approx_eq_rel( + setup.xerc20.minting_current_limit_of(minter_address), limit / 2, E18 / 10, + ); + assert_approx_eq_rel( + setup.xerc20.burning_current_limit_of(minter_address), limit / 2, E18 / 10, + ); + + stop_cheat_block_timestamp_global(); + } + + #[test] + fn test_overflow_limit_makes_it_max( + mut limit: u256, mut minter_u128: u128, mut used_limit: u256, + ) { + let setup = setup(); + limit = bound(limit, 1_000_000, 100_000_000_000_000 * E18); + used_limit = bound(used_limit, 0, 1_000); + + if minter_u128.is_zero() { + minter_u128 = 0xBadCafe; + } + let mut minter_address: ContractAddress = Into::::into(minter_u128) + .try_into() + .unwrap(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(minter_address, limit, limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, minter_address); + setup.xerc20.mint(minter_address, used_limit); + setup.xerc20.burn(minter_address, used_limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let current_timestamp = starknet::get_block_timestamp(); + start_cheat_block_timestamp_global(current_timestamp + 20 * HOUR); + + assert!( + setup.xerc20.minting_current_limit_of(minter_address) == limit, + "Minting current limit does not match!", + ); + assert!( + setup.xerc20.burning_current_limit_of(minter_address) == limit, + "Burning current limit does not match!", + ); + + stop_cheat_block_timestamp_global(); + } + + #[test] + fn test_change_bridge_minting_limit_increase_current_limit_by_the_difference_it_was_changed( + mut limit: u256, mut minter_u128: u128, mut used_limit: u256, + ) { + let setup = setup(); + used_limit = bound(used_limit, 0, 1_000); + limit = bound(limit, used_limit, E40); + + if minter_u128.is_zero() { + minter_u128 = 0xBadCafe; + } + let mut minter_address: ContractAddress = Into::::into(minter_u128) + .try_into() + .unwrap(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(minter_address, limit, limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, minter_address); + setup.xerc20.mint(minter_address, used_limit); + setup.xerc20.burn(minter_address, used_limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(minter_address, limit + 100_000, limit + 100_000); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!( + setup.xerc20.minting_current_limit_of(minter_address) == (limit - used_limit) + 100_000, + "Minting current limit does not match!", + ); + } + + #[test] + fn test_change_bridge_minting_limit_decrease_current_limit_by_the_difference_it_was_changed( + mut limit: u256, mut minter_u128: u128, mut used_limit: u256, + ) { + let setup = setup(); + used_limit = bound(used_limit, 100_000, 1_000_000_000); + limit = bound(limit, E18 / 1_000, E40); + + if minter_u128.is_zero() { + minter_u128 = 0xBadCafe; + } + let mut minter_address: ContractAddress = Into::::into(minter_u128) + .try_into() + .unwrap(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(minter_address, limit, limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, minter_address); + setup.xerc20.mint(minter_address, used_limit); + setup.xerc20.burn(minter_address, used_limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(minter_address, limit - 100_000, limit - 100_000); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!( + setup.xerc20.minting_current_limit_of(minter_address) == (limit - used_limit) - 100_000, + "Minting current limit does not match!", + ); + assert!( + setup.xerc20.burning_current_limit_of(minter_address) == (limit - used_limit) - 100_000, + "Burning current limit does not match!", + ); + } + + #[test] + fn test_changing_used_limits_to_zero(mut limit: u256, mut amount: u256) { + let setup = setup(); + limit = bound(limit, 1, E40); + amount = bound(amount, 0, limit); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.minter, limit, limit); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.minter); + setup.xerc20.mint(setup.minter, amount); + setup.xerc20.burn(setup.minter, amount); + stop_cheat_caller_address(setup.xerc20.contract_address); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.minter, 0, 0); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!( + setup.xerc20.minting_max_limit_of(setup.minter) == 0, "Minting limit does not match!", + ); + assert!( + setup.xerc20.minting_current_limit_of(setup.minter) == 0, + "Minting current limit does not match!", + ); + assert!( + setup.xerc20.burning_max_limit_of(setup.minter) == 0, "Burning limit does not match!", + ); + assert!( + setup.xerc20.burning_current_limit_of(setup.minter) == 0, + "Burning current limit does not match!", + ); + } + + #[test] + fn test_set_lockbox(mut lockbox: u128) { + let setup = setup(); + let lockbox_address: ContractAddress = Into::::into(lockbox) + .try_into() + .unwrap(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_lockbox(lockbox_address); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!(setup.xerc20.lockbox() == lockbox_address, "Lockbox addresses does not match!"); + } + + #[test] + fn test_set_lockbox_emits_events(mut lockbox: u128) { + let setup = setup(); + let lockbox_address: ContractAddress = Into::::into(lockbox) + .try_into() + .unwrap(); + + let mut spy = spy_events(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_lockbox(lockbox_address); + stop_cheat_caller_address(setup.xerc20.contract_address); + + spy + .assert_emitted( + @array![ + ( + setup.xerc20.contract_address, + XERC20::Event::LockboxSet(XERC20::LockboxSet { lockbox: lockbox_address }), + ), + ], + ); + } + + #[test] + fn test_lockbox_doesnt_need_minter_rights(mut lockbox_u128: u128) { + let setup = setup(); + + if lockbox_u128.is_zero() { + lockbox_u128 = 0xBadCafe; + } + let mut lockbox_address: ContractAddress = Into::::into(lockbox_u128) + .try_into() + .unwrap(); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_lockbox(lockbox_address); + stop_cheat_caller_address(setup.xerc20.contract_address); + + let erc20_dispatcher = ERC20ABIDispatcher { + contract_address: setup.xerc20.contract_address, + }; + start_cheat_caller_address(setup.xerc20.contract_address, lockbox_address); + setup.xerc20.mint(lockbox_address, 10); + assert!(erc20_dispatcher.balance_of(lockbox_address) == 10, "Balances does not match!"); + setup.xerc20.burn(lockbox_address, 10); + assert!(erc20_dispatcher.balance_of(lockbox_address) == 0, "Balances does not match!"); + stop_cheat_caller_address(setup.xerc20.contract_address); + } + + #[test] + fn test_remove_bridge(mut limit: u256) { + let setup = setup(); + limit = bound(limit, 1, U256MAX_DIV_2); + + start_cheat_caller_address(setup.xerc20.contract_address, setup.owner); + setup.xerc20.set_limits(setup.minter, limit, limit); + + assert!( + setup.xerc20.minting_max_limit_of(setup.minter) == limit, + "Minting limit does not match!", + ); + assert!( + setup.xerc20.burning_max_limit_of(setup.minter) == limit, + "Burning limit does not match!", + ); + + setup.xerc20.set_limits(setup.minter, 0, 0); + stop_cheat_caller_address(setup.xerc20.contract_address); + + assert!( + setup.xerc20.minting_max_limit_of(setup.minter) == 0, "Minting limit does not match!", + ); + assert!( + setup.xerc20.burning_max_limit_of(setup.minter) == 0, "Burning limit does not match!", + ); + } +} From 717c87d83a87aff3e4fc058970bbe3d4d4432634 Mon Sep 17 00:00:00 2001 From: Turan Ege Caner Date: Mon, 16 Dec 2024 17:34:54 +0700 Subject: [PATCH 2/3] XERC20 CI --- .github/workflows/test.yml | 61 +++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fb6a4c3..41b6304 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,33 +1,78 @@ name: contracts-tests + on: push: pull_request: jobs: - contracts: + contracts-cairo: runs-on: ubuntu-latest env: working-directory: ./cairo + SCARB_VERSION: "2.6.5" + SNFOUNDRY_VERSION: "0.22.0" steps: - uses: actions/checkout@v3 - uses: software-mansion/setup-scarb@v1 with: - scarb-version: "2.6.5" + scarb-version: ${{ env.SCARB_VERSION }} + - uses: foundry-rs/setup-snfoundry@v3 with: - starknet-foundry-version: "0.22.0" - - working-directory: ${{ env.working-directory}} + starknet-foundry-version: ${{ env.SNFOUNDRY_VERSION }} + + - name: Format check + working-directory: ${{ env.working-directory }} run: scarb fmt --check - name: Cache contracts id: cache-contracts uses: actions/cache@v3 with: - path: ./target - key: ${{ runner.os }}-contracts-${{ hashFiles('./src', 'Scarb.lock') }} + path: ${{ env.working-directory }}/target + key: ${{ runner.os }}-contracts-${{ env.working-directory }}-${{ hashFiles(format('{0}/src/**', env.working-directory), format('{0}/Scarb.lock', env.working-directory)) }} - - working-directory: ${{ env.working-directory}} + - name: Build + working-directory: ${{ env.working-directory }} run: scarb build - - working-directory: ${{ env.working-directory}} + + - name: Test + working-directory: ${{ env.working-directory }} run: snforge test + + contracts-xerc20: + runs-on: ubuntu-latest + env: + working-directory: ./xerc20 + SCARB_VERSION: "2.9.2" + SNFOUNDRY_VERSION: "0.34.0" + steps: + - uses: actions/checkout@v3 + + - uses: software-mansion/setup-scarb@v1 + with: + scarb-version: ${{ env.SCARB_VERSION }} + + - uses: foundry-rs/setup-snfoundry@v3 + with: + starknet-foundry-version: ${{ env.SNFOUNDRY_VERSION }} + + - name: Format check + working-directory: ${{ env.working-directory }} + run: scarb fmt --check + + - name: Cache contracts + id: cache-contracts + uses: actions/cache@v3 + with: + path: ${{ env.working-directory }}/target + key: ${{ runner.os }}-contracts-${{ env.working-directory }}-${{ hashFiles(format('{0}/src/**', env.working-directory), format('{0}/Scarb.lock', env.working-directory)) }} + + - name: Build + working-directory: ${{ env.working-directory }} + run: scarb build + + - name: Test + working-directory: ${{ env.working-directory }} + run: snforge test \ No newline at end of file From 4d6f89e6a868a3b3963ef410b9d2452ea563aad5 Mon Sep 17 00:00:00 2001 From: Turan Ege Caner Date: Mon, 16 Dec 2024 19:28:32 +0700 Subject: [PATCH 3/3] unused dependency removed --- xerc20/Scarb.lock | 6 ------ xerc20/Scarb.toml | 1 - 2 files changed, 7 deletions(-) diff --git a/xerc20/Scarb.lock b/xerc20/Scarb.lock index 1ecd251..a7db191 100644 --- a/xerc20/Scarb.lock +++ b/xerc20/Scarb.lock @@ -23,11 +23,6 @@ name = "openzeppelin_introspection" version = "0.20.0" source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.20.0#7756fd1de2b4ebd239fa6e372d75535cea02e5e5" -[[package]] -name = "openzeppelin_security" -version = "0.20.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.20.0#7756fd1de2b4ebd239fa6e372d75535cea02e5e5" - [[package]] name = "openzeppelin_token" version = "0.20.0" @@ -69,7 +64,6 @@ dependencies = [ "openzeppelin_access", "openzeppelin_account", "openzeppelin_introspection", - "openzeppelin_security", "openzeppelin_token", "openzeppelin_upgrades", "openzeppelin_utils", diff --git a/xerc20/Scarb.toml b/xerc20/Scarb.toml index 192a6e1..c90f8d7 100644 --- a/xerc20/Scarb.toml +++ b/xerc20/Scarb.toml @@ -11,7 +11,6 @@ starknet = "2.9.2" openzeppelin_access = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" } openzeppelin_token = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" } openzeppelin_utils = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" } -openzeppelin_security = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" } openzeppelin_account = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" } openzeppelin_upgrades = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" } openzeppelin_introspection = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.20.0" }