diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e9d4c2 --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +## Yield Dex + +L2 contracts for Yield Dex project. + +### Compilation + +We are using Scarb to compile our contracts, you need to install it to be able to use it. +For that, you can follow the steps [here](https://book.cairo-lang.org/ch01-01-installation.html). +Once Scarb is installed, you are able to compile the contracts. +Run `scarb build`, it will build Sierra code of this package which will be written to the `target/dev` directory. + +### Test + +You can find our tests under `src/tests`, these tests are written using snfoundry you can find more information [here](https://foundry-rs.github.io/starknet-foundry/getting-started/installation.html). +Follow the installation documentation to be able to run our tests. +To run the tests, just do `snforge test`. + +### Declare With Starkli + +For this, we used starkli, you need to install it to be able to declare and deploy the contracts. +Once starkli is installed, you will need to set some environment variables. +If you want to declare on goerli then do `export STARKNET_NETWORK="goerli"` (you replace it be mainnet or sepolia depending on the network you want to deploy to). +You will have to create a signers and account, just follow those [documentation](https://book.starkli.rs/signers). +Now that everything is set up, you are able to declare the contracts. +For that, we will use starkli declare command, for more info check the documentation [here](https://book.starkli.rs/declaring-classes). +Run `starkli declare ./target/dev/CONTRACT_NAME.contract_class.json --account PATH/account-store --network NETWORK`. +`CONTRACT_NAME `the name of the json file that can be found inside `./target/dev/ `folder. +`PATH/account-store` is the path to your account-store that has been previously created. +`NETWORK `the name of the network you want to declare to (goerli, sepolia or mainnet). +After running that, it will return to you the declared contract class hash. + +### Declare With Script + +Create a `.env` and add `ACCOUNT_ADDRESS` and `ACCOUNT_PK`. +Run `npm i` to install the package. +The script is deploying the contract on goerli if you want to deploy on another network then go inside `scripts/declareContracts` and change `const provider = new RpcProvider({ nodeUrl: constants.NetworkName.SN_MAIN });` with the correct network (ex: SN_GOERLI). +To run the script just do `npx ts-node scripts/declareContracts.ts --contract CONTRACT_NAME.` +`CONTRACT_NAME` must be replaced by : +- PoolingManager +- Factory +- Token +- TokenManager + +### Deploy With Starkli + +Now that our contracts are declared, the next step is to deploy them. For that, you can follow the documentation [here](https://book.starkli.rs/deploying-contracts). +Some of the contracts take parameters in their constructor, you will need to add them in your command. + +PoolingManager: `starkli deploy POOLING_MANAGER_CLASS_HASH OWNER_ACCOUNT_ADDRESS --account PATH/account-store --network NETWORK` +Factory: `starkli deploy FACTORY_CLASS_HASH POOLING_MANAGER_CONTRACT_ADDRESS TOKEN_CLASS_HASH TOKEN_MANAGER_CLASS_HASH --account PATH/account-store --network NETWORK` + +### Deploy With Script + +Create a `.env` and add `ACCOUNT_ADDRESS` and `ACCOUNT_PK`. +Run `npm i` to install the package. +The script is deploying the contract on goerli if you want to deploy on another network then go inside `scripts/deployContracts` and change `const provider = new RpcProvider({ nodeUrl: constants.NetworkName.SN_MAIN });` with the correct network (ex: `SN_GOERLI`). +To run the script just do `npx ts-node scripts/deployContracts.ts --contract CONTRACT_NAME`. +`CONTRACT_NAME` must be replaced by : +- PoolingManager +- Factory + +### Setup +Only the owner of the contract will be able to set up the contract. +You can do the setup through voyager or starkscan. + +PoolingManager: +Only Owner: +- set_fees_recipient: Address of the fees recipient. +- set_l1_pooling_manager: Address of the pooling manager on l1. +- set_factory: Address of the Factory contract previously deployed. + +Only Role, the owner has the correct role, but you can also give permission to other accounts: +- register_underlying: Registers an underlying asset, its corresponding bridge contract and the corresponding address of the l1bridge + +Factory: +Only Owner: +- `deploy_strategy`: Deploys a new strategy with specified parameters. +Parameters are: + - l1_strategy: The Ethereum address of the L1 strategy + - underlying: The contract address of the underlying asset + - token_name: The name for the new token + - token_symbol: The symbol for the new token + - performance_fees: The performance fees for the strategy + - min_deposit: The minimum deposit limit + - max_deposit: The maximum deposit limit + - min_withdrawal: The minimum withdrawal limit + - max_withdrawal: The maximum withdrawal limit + - withdrawal_epoch_delay: The delay in epochs for withdrawals + - dust_limit: The dust limit for the strategy +Deploy_strategy will deploy a new contract called token_manager and return you the address. We are going to use this contract to deposit some tokens. + +Go to the Token Manager contract address and call the deposit function, This function can be called by any user : + +TokenManager: +- `deposit`: Allows a user to deposit assets into the contract. +Parameters are: + - assets: The amount of assets to deposit. + - receiver: The address to receive the minted shares. + - referral: The referral address for the deposit. +Once users have deposited some assets, they can now request a withdrawal from the contract. +- `request_withdrawal`: Allows a user to request a withdrawal from the contract +Parameter is: + - shares: The amount of shares to withdraw. + +### Goerli Class Hash +You can declare a contract only once on each network. So if you don't do any modification into our current contract implementation you may face an error while declaring. Therefore here you can find the current class hash of each contract on Goerli. + +``` +POOLINGMANAGER_CLASS_HASH="0x05adb7661d0dcb3cc5fbe69380846fb7662c92f1943fcf609c51b756cae7d411" + +TOKENMANAGER_CLASS_HASH="0x03be98338455134abae1d830802a162cd81b24ddb38a868ec9c6a4341ecd7210" + +TOKENMOCK_CLASS_HASH="0x00da57dbb24ceb46a3901f148442e0d591528baba485ee84ed6d4948dedf12e5" + +TOKEN_CLASS_HASH="0x0720f601c0432ab03e12df99c2b215e7ab9a9c12e1b4d8b0473e18bbb3213bea" + +TOKENBRIDGE_CLASS_HASH="0x00de6d9bd84775dd221273e833dc44946da586483cf822e0021385de95964700" + +FACTORY_CLASS_HASH="0x0581277daf0e409c2537979108b7eb4a5cec3624db552c35f8f6acc9a3ac937b" +``` + +​ +### Mainnet Class Hash +You can declare a contract only once on each network. So if you don't do any modification into our current contract implementation you may face an error while declaring. Therefore here you can find the current class hash of each contract on Mainnet. + +``` +POOLINGMANAGER_CLASS_HASH=0x05adb7661d0dcb3cc5fbe69380846fb7662c92f1943fcf609c51b756cae7d411 + +FACTORY_CLASS_HASH=0x581277daf0e409c2537979108b7eb4a5cec3624db552c35f8f6acc9a3ac937b + +TOKENMANAGER_CLASS_HASH=0x3be98338455134abae1d830802a162cd81b24ddb38a868ec9c6a4341ecd7210 + +TOKEN_CLASS_HASH=0x720f601c0432ab03e12df99c2b215e7ab9a9c12e1b4d8b0473e18bbb3213bea +``` + +​ diff --git a/package.json b/package.json new file mode 100644 index 0000000..09c9734 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "dotenv": "^16.3.1", + "fs": "^0.0.1-security", + "starknet": "^5.24.3" + }, + "devDependencies": { + "@types/node": "^20.11.5" + } +} diff --git a/scripts/declareContracts.ts b/scripts/declareContracts.ts new file mode 100644 index 0000000..50a5a15 --- /dev/null +++ b/scripts/declareContracts.ts @@ -0,0 +1,60 @@ +import { Account, Provider, Contract, json, RpcProvider, constants } from 'starknet'; +import fs from 'fs'; +import dotenv from 'dotenv'; + +dotenv.config({ path: __dirname + '/../.env' }); + +const provider = new RpcProvider({ nodeUrl: constants.NetworkName.SN_MAIN }); + +const owner = new Account(provider, process.env.ACCOUNT_ADDRESS as string, process.env.ACCOUNT_PK as string, "1"); + +export async function declareContract(name: string) { + const compiledContract = await json.parse(fs.readFileSync(`./target/dev/nimbora_yields_${name}.contract_class.json`).toString('ascii')); + const compiledSierraCasm = await json.parse(fs.readFileSync(`./target/dev/nimbora_yields_${name}.compiled_contract_class.json`).toString('ascii')); + const declareResponse = await owner.declare({ + contract: compiledContract, + casm: compiledSierraCasm, + }); + + console.log('Contract classHash: ', declareResponse.class_hash); + fs.appendFile(__dirname + '/../.env', `\n${name.toUpperCase()}_CLASS_HASH=${declareResponse.class_hash}`, function (err) { + if (err) throw err; + }); +} + +async function main() { + if (!process.argv[2] || !process.argv[3]) { + throw new Error("Missing --contract "); + } + + switch (process.argv[3]) { + case "PoolingManager": + console.log("Declaring PoolingManager..."); + await declareContract('PoolingManager'); + + break; + + case "Factory": + console.log("Declaring Factory..."); + await declareContract('Factory'); + + break; + + case "TokenManager": + console.log("Declaring TokenManager..."); + await declareContract('TokenManager'); + + break; + + case "Token": + console.log("Declaring Token..."); + await declareContract('Token'); + + break; + + default: + throw new Error("Error: Unknown contract"); + } +} + +main(); \ No newline at end of file diff --git a/scripts/deployContracts.ts b/scripts/deployContracts.ts new file mode 100644 index 0000000..4ccdae6 --- /dev/null +++ b/scripts/deployContracts.ts @@ -0,0 +1,86 @@ +import { Account, Provider, Contract, json, uint256, RpcProvider, constants } from "starknet"; +import fs from 'fs'; +import dotenv from 'dotenv'; +import { appendToEnv } from "./utils"; + +dotenv.config({ path: __dirname + '/../.env' }) + +const provider = new RpcProvider({ nodeUrl: constants.NetworkName.SN_MAIN }); +const owner = new Account(provider, process.env.ACCOUNT_ADDRESS as string, process.env.ACCOUNT_PK as string, "1"); + +async function deployPoolingManagerContract(): Promise { + let contractAddress: any; + const compiledContract = await json.parse(fs.readFileSync(`./target/dev/nimbora_yields_PoolingManager.contract_class.json`).toString('ascii')); + + let { transaction_hash, contract_address } = await owner.deploy({ + classHash: process.env.POOLINGMANAGER_CLASS_HASH as string, + constructorCalldata: { + owner: owner.address, + }, + }); + [contractAddress] = contract_address; + // await provider.waitForTransaction(transaction_hash); + + const poolingManagerContract = new Contract(compiledContract.abi, contractAddress, owner); + console.log('✅ Test PoolingManager contract connected at =', poolingManagerContract.address); + + return poolingManagerContract; +} + +async function deployFactoryContract(): Promise { + let contractAddress: any; + const compiledContract = await json.parse(fs.readFileSync(`./target/dev/nimbora_yields_Factory.contract_class.json`).toString('ascii')); + + let { transaction_hash, contract_address } = await owner.deploy({ + classHash: process.env.FACTORY_CLASS_HASH as string, + constructorCalldata: { + pooling_manager: process.env.POOLINGMANAGER_ADDRESS as string, + token_class_hash: process.env.TOKEN_CLASS_HASH as string, + token_manager_class_hash: process.env.TOKENMANAGER_CLASS_HASH as string, + }, + }); + [contractAddress] = contract_address; + // await provider.waitForTransaction(transaction_hash); + + const factoryContract = new Contract(compiledContract.abi, contractAddress, owner); + console.log('✅ Test Factory contract connected at =', factoryContract.address); + + return factoryContract; +} + + + + +async function main() { + + const flag = process.argv[2]; + const action = process.argv[3]; + + if (!flag || !action) { + throw new Error("Missing --contract "); + } + + if (flag == "--contract") { + switch (action) { + case "PoolingManager": + console.log("Deploying PoolingManager..."); + + const poolingManagerContract = await deployPoolingManagerContract( + ); + break; + + case "Factory": + console.log("Deploying Factory..."); + const factoryContractAddress = await deployFactoryContract(); + + break; + } + } else if (flag == "--setup") { + const contract_address = process.argv[4]; + if (!contract_address) { + throw new Error("Error: Provide contract address"); + } + } +} + +main(); \ No newline at end of file diff --git a/scripts/utils.ts b/scripts/utils.ts new file mode 100644 index 0000000..4ccf3ab --- /dev/null +++ b/scripts/utils.ts @@ -0,0 +1,12 @@ +import fs from 'fs'; +import dotenv from 'dotenv'; + +dotenv.config({ path: __dirname + '/../.env' }) + +export async function appendToEnv(name: string, address: string) { + fs.appendFile(`${__dirname}/../.env`, `\n${name}_ADDRESS=${address}`, function ( + err, + ) { + if (err) throw err + }) +} \ No newline at end of file diff --git a/src/factory/factory.cairo b/src/factory/factory.cairo index 630737f..f3e6052 100644 --- a/src/factory/factory.cairo +++ b/src/factory/factory.cairo @@ -17,6 +17,7 @@ mod Factory { IAccessControlDispatcher, IAccessControlDispatcherTrait }; use openzeppelin::token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::upgrades::UpgradeableComponent; // Local imports. @@ -25,8 +26,15 @@ mod Factory { IPoolingManagerDispatcher, IPoolingManagerDispatcherTrait }; + // Components + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + impl InternalUpgradeableImpl = UpgradeableComponent::InternalImpl; + #[storage] struct Storage { + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, pooling_manager: ContractAddress, token_class_hash: ClassHash, token_manager_class_hash: ClassHash @@ -36,6 +44,8 @@ mod Factory { #[event] #[derive(Drop, starknet::Event)] enum Event { + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, TokenHashUpdated: TokenHashUpdated, TokenManagerHashUpdated: TokenManagerHashUpdated } @@ -76,6 +86,13 @@ mod Factory { self._set_token_manager_class_hash(token_manager_class_hash); } + /// @notice Upgrade contract + /// @param New contract class hash + #[external(v0)] + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self._assert_only_owner(); + self.upgradeable._upgrade(new_class_hash); + } #[abi(embed_v0)] impl Factory of IFactory { @@ -91,6 +108,12 @@ mod Factory { self.token_class_hash.read() } + /// @notice Reads the pooling manager contract address + /// @return The address of the pooling manager + fn pooling_manager_address(self: @ContractState) -> ContractAddress { + self.pooling_manager.read() + } + /// @notice Deploys a new strategy with specified parameters /// @dev Only callable by the owner of the contract /// @param l1_strategy The Ethereum address of the L1 strategy diff --git a/src/factory/interface.cairo b/src/factory/interface.cairo index 89e153c..cab3cb9 100644 --- a/src/factory/interface.cairo +++ b/src/factory/interface.cairo @@ -4,6 +4,7 @@ use starknet::{ContractAddress, ClassHash, eth_address::EthAddress}; trait IFactory { fn token_manager_class_hash(self: @TContractState) -> ClassHash; fn token_class_hash(self: @TContractState) -> ClassHash; + fn pooling_manager_address(self: @TContractState) -> ContractAddress; fn deploy_strategy( ref self: TContractState, diff --git a/src/lib.cairo b/src/lib.cairo index c86891b..efe0cb2 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -4,5 +4,6 @@ mod pooling_manager; mod token_manager; mod factory; mod token_bridge; +mod mocks; #[cfg(test)] mod tests; diff --git a/src/mocks.cairo b/src/mocks.cairo new file mode 100644 index 0000000..7552882 --- /dev/null +++ b/src/mocks.cairo @@ -0,0 +1,3 @@ +mod mock_pooling_manager; +mod mock_factory; +mod mock_token_manager; diff --git a/src/mocks/mock_factory.cairo b/src/mocks/mock_factory.cairo new file mode 100644 index 0000000..f1a823b --- /dev/null +++ b/src/mocks/mock_factory.cairo @@ -0,0 +1,306 @@ +/// @title Factory Module +/// @notice Responsible for deploying strategies and their associated tokens. +#[starknet::contract] +mod MockFactory { + // Core lib imports. + use core::result::ResultTrait; + use starknet::{ + get_caller_address, ContractAddress, contract_address_const, ClassHash, + eth_address::EthAddress, Zeroable + }; + use starknet::syscalls::deploy_syscall; + use core::poseidon::poseidon_hash_span; + + + // OZ imports + use openzeppelin::access::accesscontrol::interface::{ + IAccessControlDispatcher, IAccessControlDispatcherTrait + }; + use openzeppelin::token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::upgrades::UpgradeableComponent; + + + // Local imports. + use nimbora_yields::factory::interface::{IFactory}; + use nimbora_yields::pooling_manager::interface::{ + IPoolingManagerDispatcher, IPoolingManagerDispatcherTrait + }; + + // Components + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + impl InternalUpgradeableImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + pooling_manager: ContractAddress, + token_class_hash: ClassHash, + token_manager_class_hash: ClassHash + } + + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + TokenHashUpdated: TokenHashUpdated, + TokenManagerHashUpdated: TokenManagerHashUpdated + } + + #[derive(Drop, starknet::Event)] + struct TokenHashUpdated { + previous_hash: ClassHash, + new_hash: ClassHash + } + + #[derive(Drop, starknet::Event)] + struct TokenManagerHashUpdated { + previous_hash: ClassHash, + new_hash: ClassHash + } + + + mod Errors { + const ZERO_ADDRESS: felt252 = 'Address is zero'; + const ZERO_HASH: felt252 = 'Hash is zero'; + const INVALID_CALLER: felt252 = 'Invalid caller'; + } + + /// @notice Constructor for the Factory contract. + /// @param pooling_manager The address of the pooling manager. + /// @param token_class_hash The class hash of the token. + /// @param token_manager_class_hash The class hash of the token manager. + #[constructor] + fn constructor( + ref self: ContractState, + pooling_manager: ContractAddress, + token_class_hash: ClassHash, + token_manager_class_hash: ClassHash + ) { + assert(pooling_manager.is_non_zero(), Errors::ZERO_ADDRESS); + self.pooling_manager.write(pooling_manager); + self._set_token_class_hash(token_class_hash); + self._set_token_manager_class_hash(token_manager_class_hash); + } + + /// @notice Upgrade contract + /// @param New contract class hash + #[external(v0)] + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self._assert_only_owner(); + self.upgradeable._upgrade(new_class_hash); + } + + /// @notice Function to test upgradable + /// @param None + #[external(v0)] + fn get_thousand(self: @ContractState) -> felt252 { + 1000 + } + + #[abi(embed_v0)] + impl Factory of IFactory { + /// @notice Reads the class hash of the token manager + /// @return The class hash of the token manager + fn token_manager_class_hash(self: @ContractState) -> ClassHash { + self.token_manager_class_hash.read() + } + + /// @notice Reads the class hash of the token + /// @return The class hash of the token + fn token_class_hash(self: @ContractState) -> ClassHash { + self.token_class_hash.read() + } + + /// @notice Reads the pooling manager contract address + /// @return The address of the pooling manager + fn pooling_manager_address(self: @ContractState) -> ContractAddress { + self.pooling_manager.read() + } + + /// @notice Deploys a new strategy with specified parameters + /// @dev Only callable by the owner of the contract + /// @param l1_strategy The Ethereum address of the L1 strategy + /// @param underlying The contract address of the underlying asset + /// @param token_name The name for the new token + /// @param token_symbol The symbol for the new token + /// @param performance_fees The performance fees for the strategy + /// @param min_deposit The minimum deposit limit + /// @param max_deposit The maximum deposit limit + /// @param min_withdrawal The minimum withdrawal limit + /// @param max_withdrawal The maximum withdrawal limit + /// @param withdrawal_epoch_delay The delay in epochs for withdrawals + /// @param dust_limit The dust limit for the strategy + /// @return The addresses of the deployed token manager and token + fn deploy_strategy( + ref self: ContractState, + l1_strategy: EthAddress, + underlying: ContractAddress, + token_name: felt252, + token_symbol: felt252, + performance_fees: u256, + min_deposit: u256, + max_deposit: u256, + min_withdrawal: u256, + max_withdrawal: u256, + withdrawal_epoch_delay: u256, + dust_limit: u256 + ) -> (ContractAddress, ContractAddress) { + self._assert_only_owner(); + let (token_manager_salt, token_salt) = self + ._compute_salt_for_strategy(l1_strategy, underlying, token_name, token_symbol); + let pooling_manager = self.pooling_manager.read(); + let mut constructor_token_manager_calldata = array![ + pooling_manager.into(), + l1_strategy.into(), + underlying.into(), + performance_fees.low.into(), + performance_fees.high.into(), + min_deposit.low.into(), + min_deposit.high.into(), + max_deposit.low.into(), + max_deposit.high.into(), + min_withdrawal.low.into(), + min_withdrawal.high.into(), + max_withdrawal.low.into(), + max_withdrawal.high.into(), + withdrawal_epoch_delay.low.into(), + withdrawal_epoch_delay.high.into(), + dust_limit.low.into(), + dust_limit.high.into() + ]; + + let (token_manager_deployed_address, _) = deploy_syscall( + self.token_manager_class_hash.read(), + token_manager_salt, + constructor_token_manager_calldata.span(), + false + ) + .expect('failed to deploy tm'); + + let token_disp = ERC20ABIDispatcher { contract_address: underlying }; + let decimals = token_disp.decimals(); + + let mut constructor_token_calldata = array![ + token_manager_deployed_address.into(), + token_name.into(), + token_symbol.into(), + decimals.into() + ]; + + let (token_deployed_address, _) = deploy_syscall( + self.token_class_hash.read(), token_salt, constructor_token_calldata.span(), false + ) + .expect('failed to deploy t'); + + let pooling_manager = self.pooling_manager.read(); + let manager_disp = IPoolingManagerDispatcher { contract_address: pooling_manager }; + manager_disp + .register_strategy( + token_manager_deployed_address, + token_deployed_address, + l1_strategy, + underlying, + performance_fees, + min_deposit, + max_deposit, + min_withdrawal, + max_withdrawal + ); + (token_manager_deployed_address, token_deployed_address) + } + + + /// @notice Sets a new class hash for the token manager + /// @dev Only callable by the owner of the contract + /// @param new_token_manager_class_hash The new class hash to be set for the token manager + fn set_token_manager_class_hash( + ref self: ContractState, new_token_manager_class_hash: ClassHash, + ) { + self._assert_only_owner(); + self._set_token_manager_class_hash(new_token_manager_class_hash); + let pooling_manager = self.pooling_manager.read(); + let pooling_manager_disp = IPoolingManagerDispatcher { + contract_address: pooling_manager + }; + pooling_manager_disp + .emit_token_manager_class_hash_updated_event(new_token_manager_class_hash); + } + + /// @notice Sets a new class hash for the token + /// @dev Only callable by the owner of the contract + /// @param new_token_class_hash The new class hash to be set for the token + fn set_token_class_hash(ref self: ContractState, new_token_class_hash: ClassHash,) { + self._assert_only_owner(); + self._set_token_class_hash(new_token_class_hash); + let pooling_manager = self.pooling_manager.read(); + let pooling_manager_disp = IPoolingManagerDispatcher { + contract_address: pooling_manager + }; + pooling_manager_disp.emit_token_class_hash_updated_event(new_token_class_hash); + } + } + + + #[generate_trait] + impl InternalImpl of InternalTrait { + /// @notice Asserts that the caller has the owner role + /// @dev Verifies the caller's role using the Access Control Dispatcher + fn _assert_only_owner(self: @ContractState) { + let caller = get_caller_address(); + let pooling_manager = self.pooling_manager.read(); + let access_disp = IAccessControlDispatcher { contract_address: pooling_manager }; + let has_role = access_disp.has_role(0, caller); + assert(has_role, Errors::INVALID_CALLER); + } + + + /// @notice Computes the salts for token manager and token based on strategy parameters + /// @param l1_strategy The Ethereum address of the L1 strategy + /// @param underlying The contract address of the underlying asset + /// @param token_name The name of the token + /// @param token_symbol The symbol of the token + /// @return A tuple containing the salts for the token manager and token + fn _compute_salt_for_strategy( + self: @ContractState, + l1_strategy: EthAddress, + underlying: ContractAddress, + token_name: felt252, + token_symbol: felt252 + ) -> (felt252, felt252) { + let mut token_manager_data = array![]; + token_manager_data.append('TOKEN_MANAGER'); + token_manager_data.append(l1_strategy.into()); + token_manager_data.append(underlying.into()); + let token_manager_salt = poseidon_hash_span(token_manager_data.span()); + + let mut token_data = array![]; + token_data.append('TOKEN'); + token_data.append(token_name.into()); + token_data.append(token_symbol.into()); + let token_salt = poseidon_hash_span(token_data.span()); + + (token_manager_salt, token_salt) + } + + + /// @notice Sets the class hash for the token manager + /// @dev Ensures that the provided class hash is non-zero before updating + /// @param token_manager_hash The new class hash to be set for the token manager + fn _set_token_manager_class_hash(ref self: ContractState, token_manager_hash: ClassHash) { + assert(token_manager_hash.is_non_zero(), Errors::ZERO_HASH); + self.token_manager_class_hash.write(token_manager_hash); + } + + /// @notice Sets the class hash for the token + /// @dev Ensures that the provided class hash is non-zero before updating + /// @param token_hash The new class hash to be set for the token + fn _set_token_class_hash(ref self: ContractState, token_hash: ClassHash) { + assert(token_hash.is_non_zero(), Errors::ZERO_HASH); + self.token_class_hash.write(token_hash); + } + } +} diff --git a/src/mocks/mock_pooling_manager.cairo b/src/mocks/mock_pooling_manager.cairo new file mode 100644 index 0000000..ff3d917 --- /dev/null +++ b/src/mocks/mock_pooling_manager.cairo @@ -0,0 +1,1088 @@ +#[starknet::contract] +mod MockPoolingManager { + // Starknet import + use starknet::{ + ContractAddress, get_caller_address, eth_address::{EthAddress, EthAddressZeroable}, + Zeroable, ClassHash, syscalls::{send_message_to_l1_syscall} + }; + use core::nullable::{nullable_from_box, match_nullable, FromNullableResult}; + use core::integer::{u128_byte_reverse}; + + // OZ import + use openzeppelin::access::accesscontrol::{ + AccessControlComponent, interface::{IAccessControlDispatcher, IAccessControlDispatcherTrait} + }; + use openzeppelin::token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::upgrades::UpgradeableComponent; + + // Local import + use nimbora_yields::pooling_manager::interface::{ + IPoolingManager, StrategyReportL1, BridgeInteractionInfo + }; + use nimbora_yields::token_manager::interface::{ + ITokenManagerDispatcher, ITokenManagerDispatcherTrait, StrategyReportL2 + }; + use nimbora_yields::token_bridge::interface::{ + ITokenBridgeDispatcher, ITokenBridgeDispatcherTrait + }; + + // Components + component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + impl InternalUpgradeableImpl = UpgradeableComponent::InternalImpl; + + #[abi(embed_v0)] + impl AccessControlImpl = + AccessControlComponent::AccessControlImpl; + impl AccessControlInternalImpl = AccessControlComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + accesscontrol: AccessControlComponent::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + factory: ContractAddress, + fees_recipient: ContractAddress, + l1_pooling_manager: EthAddress, + underlying_to_bridge: LegacyMap, + l2_bridge_to_l1_bridge: LegacyMap, + l1_strategy_to_token_manager: LegacyMap, + l1_report_hash: LegacyMap, + pending_strategies_to_initialize: LegacyMap, + pending_strategies_to_initialize_len: u256, + general_epoch: u256 + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AccessControlEvent: AccessControlComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, + FactoryUpdated: FactoryUpdated, + FeesRecipientUpdated: FeesRecipientUpdated, + StrategyRegistered: StrategyRegistered, + UnderlyingRegistered: UnderlyingRegistered, + DepositLimitUpdated: DepositLimitUpdated, + WithdrawalLimitUpdated: WithdrawalLimitUpdated, + PerformanceFeesUpdated: PerformanceFeesUpdated, + WithdrawalEpochUpdated: WithdrawalEpochUpdated, + DustLimitUpdated: DustLimitUpdated, + Deposit: Deposit, + RequestWithdrawal: RequestWithdrawal, + ClaimWithdrawal: ClaimWithdrawal, + TokenManagerClassHashUpdated: TokenManagerClassHashUpdated, + TokenClassHashUpdated: TokenClassHashUpdated, + L1PoolingManagerUpdated: L1PoolingManagerUpdated, + NewL1ReportHash: NewL1ReportHash, + NewL2Report: NewL2Report + } + + #[derive(Drop, starknet::Event)] + struct FactoryUpdated { + new_factory: ContractAddress + } + + #[derive(Drop, starknet::Event)] + struct FeesRecipientUpdated { + new_fees_recipient: ContractAddress + } + + #[derive(Drop, starknet::Event)] + struct L1PoolingManagerUpdated { + new_l1_pooling_manager: EthAddress + } + + #[derive(Drop, starknet::Event)] + struct NewL1ReportHash { + new_l1_report_hash: u256 + } + + #[derive(Drop, starknet::Event)] + struct NewL2Report { + new_epoch: u256, + new_bridge_deposit: Array, + new_l2_report: Array, + new_bridge_withdraw: Array + } + + + #[derive(Drop, starknet::Event)] + struct StrategyRegistered { + token_manager: ContractAddress, + token: ContractAddress, + l1_strategy: EthAddress, + underlying: ContractAddress, + performance_fees: u256, + min_deposit: u256, + max_deposit: u256, + min_withdrawal: u256, + max_withdrawal: u256 + } + + + #[derive(Drop, starknet::Event)] + struct UnderlyingRegistered { + underlying: ContractAddress, + bridge: ContractAddress, + l1_bridge: felt252 + } + + + #[derive(Drop, starknet::Event)] + struct DepositLimitUpdated { + l1_strategy: EthAddress, + new_min_deposit_limit: u256, + new_max_deposit_limit: u256 + } + + + #[derive(Drop, starknet::Event)] + struct WithdrawalLimitUpdated { + l1_strategy: EthAddress, + new_min_withdrawal_limit: u256, + new_max_withdrawal_limit: u256 + } + + + #[derive(Drop, starknet::Event)] + struct PerformanceFeesUpdated { + l1_strategy: EthAddress, + new_performance_fees: u256 + } + + #[derive(Drop, starknet::Event)] + struct WithdrawalEpochUpdated { + l1_strategy: EthAddress, + new_withdrawal_epoch_delay: u256 + } + + #[derive(Drop, starknet::Event)] + struct DustLimitUpdated { + l1_strategy: EthAddress, + new_dust_limit: u256 + } + + + #[derive(Drop, starknet::Event)] + struct Deposit { + l1_strategy: EthAddress, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256, + referal: ContractAddress + } + + + #[derive(Drop, starknet::Event)] + struct RequestWithdrawal { + l1_strategy: EthAddress, + caller: ContractAddress, + assets: u256, + shares: u256, + id: u256, + epoch: u256 + } + + #[derive(Drop, starknet::Event)] + struct ClaimWithdrawal { + l1_strategy: EthAddress, + caller: ContractAddress, + id: u256, + underlying_amount: u256 + } + + #[derive(Drop, starknet::Event)] + struct TokenManagerClassHashUpdated { + new_token_manager_class_hash: ClassHash + } + + #[derive(Drop, starknet::Event)] + struct TokenClassHashUpdated { + new_token_class_hash: ClassHash + } + + + mod Errors { + const ZERO_ADDRESS: felt252 = 'Zero address'; + const INVALID_CALLER: felt252 = 'Invalid caller'; + const NOT_SUPPORTED: felt252 = 'Token not supported'; + const ALREADY_REGISTERED: felt252 = 'Strategy already registered'; + const PENDING_HASH: felt252 = 'Pending hash'; + const NO_L1_REPORT: felt252 = 'No l1 report'; + const INVALID_DATA: felt252 = 'Invalid data'; + const EMPTY_ARRAY: felt252 = 'Empty array'; + const UNKNOWN_STRATEGY: felt252 = 'Unknown strategy'; + const TOTAL_ASSETS_NUL: felt252 = 'Total assets nul'; + const NOT_INITIALISED: felt252 = 'Not initialised'; + const BUFFER_NUL: felt252 = 'Buffer is nul'; + const INVALID_EPOCH: felt252 = 'Invalid Epoch'; + } + + /// @notice Constructor for initializing the contract + /// @param owner The address of the contract owner + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress) { + self.accesscontrol.initializer(); + self.accesscontrol._grant_role(0, owner); + } + + /// @notice Upgrade contract + /// @param New contract class hash + #[external(v0)] + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.accesscontrol.assert_only_role(0); + self.upgradeable._upgrade(new_class_hash); + } + + /// @notice Function to test upgradable + /// @param None + #[external(v0)] + fn get_thousand(self: @ContractState) -> felt252 { + 1000 + } + + fn reverse_endianness(value: u256) -> u256 { + let new_low = u128_byte_reverse(value.high); + let new_high = u128_byte_reverse(value.low); + u256 { low: new_low, high: new_high } + } + + /// @notice Handler for incoming messages from L1 contract + /// @dev This function should only be called by the authorized L1 pooling manager + /// @param from_address The address of the sender from L1 + /// @param hash The hash of the report from L1 + #[l1_handler] + fn handle_response(ref self: ContractState, from_address: felt252, epoch: u256, hash: u256) { + let l1_pooling_manager = self.l1_pooling_manager.read(); + assert(l1_pooling_manager.into() == from_address, Errors::INVALID_CALLER); + let general_epoch = self.general_epoch.read(); + assert(general_epoch == epoch, Errors::INVALID_EPOCH); + let l1_report_hash = self.l1_report_hash.read(general_epoch); + assert(l1_report_hash.is_zero(), Errors::PENDING_HASH); + self.l1_report_hash.write(general_epoch, hash); + self.emit(NewL1ReportHash { new_l1_report_hash: hash }) + } + + + #[abi(embed_v0)] + impl PoolingManager of IPoolingManager { + /// @notice Returns the factory address + /// @return The address of the factory + fn factory(self: @ContractState) -> ContractAddress { + self.factory.read() + } + + /// @notice Returns the fees recipient address + /// @return The address of the fees recipient + fn fees_recipient(self: @ContractState) -> ContractAddress { + self.fees_recipient.read() + } + + /// @notice Returns the L1 pooling manager address + /// @return The address of the L1 pooling manager + fn l1_pooling_manager(self: @ContractState) -> EthAddress { + self.l1_pooling_manager.read() + } + + /// @notice Checks if the contract is initialised + /// @return True if the contract is initialised, false otherwise + fn is_initialised(self: @ContractState) -> bool { + self._is_initialised() + } + + /// @notice Maps L1 strategy to token manager + /// @param l1_strategy The L1 strategy address + /// @return The corresponding token manager address + fn l1_strategy_to_token_manager( + self: @ContractState, l1_strategy: EthAddress + ) -> ContractAddress { + self.l1_strategy_to_token_manager.read(l1_strategy) + } + + /// @notice Maps the underlying asset to its corresponding bridge + /// @param underlying The address of the underlying asset + /// @return The address of the corresponding bridge + fn underlying_to_bridge( + self: @ContractState, underlying: ContractAddress + ) -> ContractAddress { + self.underlying_to_bridge.read(underlying) + } + + /// @notice Maps the l2 bridge to the l1 corresponding bridge + /// @param bridge The address of the l2 bridge + /// @return The address of the corresponding l1 bridge + fn l2_bridge_to_l1_bridge(self: @ContractState, bridge: ContractAddress) -> felt252 { + self.l2_bridge_to_l1_bridge.read(bridge) + } + + /// @notice Reads the L1 report hash for a given epoch + /// @param general_epoch The epoch for which to retrieve the hash + /// @return The L1 report hash for the specified epoch + fn l1_report_hash(self: @ContractState, general_epoch: u256) -> u256 { + self.l1_report_hash.read(general_epoch) + } + + /// @notice Generates a hash from L1 data + /// @param calldata The data to be hashed + /// @return The hash of the provided L1 data + fn hash_l1_data(self: @ContractState, calldata: Span) -> u256 { + self._hash_l1_data(calldata) + } + + /// @notice Generates a hash from L2 data + /// @param new_epoch of pooling manager + /// @param bridge_deposit_info Span of StrategyReportL2 data + /// @param strategy_report_l2 Span of StrategyReportL2 data + /// @param bridge_withdrawal_info Span of StrategyReportL2 data + /// @return Hash of the L2 data + fn hash_l2_data( + self: @ContractState, + new_epoch: u256, + bridge_deposit_info: Span, + strategy_report_l2: Span, + bridge_withdrawal_info: Span + ) -> u256 { + self + ._hash_l2_data( + new_epoch, bridge_deposit_info, strategy_report_l2, bridge_withdrawal_info + ) + } + + /// @notice Reads the general epoch + /// @return The general epoch + fn general_epoch(self: @ContractState) -> u256 { + self.general_epoch.read() + } + + /// @notice Retrieves the list of pending strategies to be initialized + /// @return Array of Ethereum addresses representing the pending strategies + fn pending_strategies_to_initialize(self: @ContractState) -> Array { + self._pending_strategies_to_initialize() + } + + /// @notice Sets the fees recipient address + /// @dev This function can only be called by an account with the appropriate role + /// @param new_fees_recipient The new address to receive fees + fn set_fees_recipient(ref self: ContractState, new_fees_recipient: ContractAddress) { + self.accesscontrol.assert_only_role(0); + assert(new_fees_recipient.is_non_zero(), Errors::ZERO_ADDRESS); + self.fees_recipient.write(new_fees_recipient); + self.emit(FeesRecipientUpdated { new_fees_recipient: new_fees_recipient }); + } + + /// @notice Updates the L1 pooling manager address + /// @dev This function can only be called by an account with the appropriate role + /// @param new_l1_pooling_manager The new L1 pooling manager address + fn set_l1_pooling_manager(ref self: ContractState, new_l1_pooling_manager: EthAddress) { + self.accesscontrol.assert_only_role(0); + assert(new_l1_pooling_manager.is_non_zero(), Errors::ZERO_ADDRESS); + self.l1_pooling_manager.write(new_l1_pooling_manager); + self.emit(L1PoolingManagerUpdated { new_l1_pooling_manager: new_l1_pooling_manager }); + } + + /// @notice Sets a new factory address + /// @dev This function can only be called by an account with the appropriate role + /// @param new_factory The new factory address + fn set_factory(ref self: ContractState, new_factory: ContractAddress) { + self.accesscontrol.assert_only_role(0); + assert(new_factory.is_non_zero(), Errors::ZERO_ADDRESS); + self.factory.write(new_factory); + self.emit(FactoryUpdated { new_factory: new_factory }); + } + + /// @notice Registers a new strategy within the contract + /// @dev This function can only be called by the factory + /// @param token_manager_deployed_address The deployed address of the token manager associated with the strategy + /// @param token_deployed_address The deployed address of the token associated with the strategy + /// @param l1_strategy The Ethereum address of the L1 strategy + /// @param underlying The contract address of the underlying asset for the strategy + /// @param performance_fees The performance fee (as a percentage) associated with the strategy + /// @param min_deposit The minimum deposit amount allowed for the strategy + /// @param max_deposit The maximum deposit amount allowed for the strategy + /// @param min_withdrawal The minimum withdrawal amount allowed for the strategy + /// @param max_withdrawal The maximum withdrawal amount allowed for the strategy + /// @desc This function initializes a new strategy by setting up the token manager, registering the strategy with its L1 counterpart, + /// and defining the key parameters like deposit/withdrawal limits and fees. It also adds the strategy to a list of pending strategies to be initialized. + fn register_strategy( + ref self: ContractState, + token_manager_deployed_address: ContractAddress, + token_deployed_address: ContractAddress, + l1_strategy: EthAddress, + underlying: ContractAddress, + performance_fees: u256, + min_deposit: u256, + max_deposit: u256, + min_withdrawal: u256, + max_withdrawal: u256 + ) { + self._assert_caller_is_factory(); + let bridge = self.underlying_to_bridge.read(underlying); + assert(bridge.is_non_zero(), Errors::NOT_SUPPORTED); + let token_manager_disp = ITokenManagerDispatcher { + contract_address: token_manager_deployed_address + }; + token_manager_disp.initialiser(token_deployed_address); + let current_l1_strategy_to_token_manager = self + .l1_strategy_to_token_manager + .read(l1_strategy); + assert(current_l1_strategy_to_token_manager.is_zero(), Errors::ALREADY_REGISTERED); + self.l1_strategy_to_token_manager.write(l1_strategy, token_manager_deployed_address); + + let pending_strategies_to_initialize_len = self + .pending_strategies_to_initialize_len + .read(); + self + .pending_strategies_to_initialize + .write(pending_strategies_to_initialize_len, l1_strategy); + self + .pending_strategies_to_initialize_len + .write(pending_strategies_to_initialize_len + 1); + self + .emit( + StrategyRegistered { + token_manager: token_manager_deployed_address, + token: token_deployed_address, + l1_strategy: l1_strategy, + underlying: underlying, + performance_fees: performance_fees, + min_deposit: min_deposit, + max_deposit: max_deposit, + min_withdrawal: min_withdrawal, + max_withdrawal: max_withdrawal + } + ); + } + /// @notice Allowance for + /// @dev This function can only be called by an account with the appropriate role + /// @param new_l1_pooling_manager The new L1 pooling manager address + fn set_allowance( + ref self: ContractState, + spender: ContractAddress, + token_address: ContractAddress, + amount: u256 + ) { + self.accesscontrol.assert_only_role(0); + assert(spender.is_non_zero() && token_address.is_non_zero(), Errors::ZERO_ADDRESS); + let underlying_disp = ERC20ABIDispatcher { contract_address: token_address }; + underlying_disp.approve(spender, amount); + } + + /// @notice Registers an underlying asset and its corresponding bridge contract + /// @dev This function can only be called by an account with the appropriate role (typically admin) + /// @param underlying The contract address of the underlying asset to be registered + /// @param bridge The contract address of the bridge associated with the underlying asset + /// @desc This function is used to link an underlying asset with its corresponding bridge contract in the system. + /// It ensures that the underlying asset can be bridged properly and is a critical part of setting up the contract's infrastructure. + /// The function also emits an event upon successful registration. + fn register_underlying( + ref self: ContractState, + underlying: ContractAddress, + bridge: ContractAddress, + l1_bridge: felt252 + ) { + self.accesscontrol.assert_only_role(0); + assert( + underlying.is_non_zero() && bridge.is_non_zero() && l1_bridge.is_non_zero(), + Errors::ZERO_ADDRESS + ); + let bridge_disp = ITokenBridgeDispatcher { contract_address: bridge }; + self.underlying_to_bridge.write(underlying, bridge); + // let l1_bridge = bridge_disp.get_l1_bridge(); + self.l2_bridge_to_l1_bridge.write(bridge, l1_bridge); + self + .emit( + UnderlyingRegistered { + underlying: underlying, bridge: bridge, l1_bridge: l1_bridge + } + ); + } + + /// @notice Handles a mass report of L1 strategy data, processing and updating L2 state accordingly + /// @dev This function processes reports from L1, verifies data integrity, and performs necessary transfers and updates + /// @param calldata The span of StrategyReportL1 data received from L1 + /// @desc This function is crucial for synchronizing L1 strategy data with the L2 contract's state. + /// It reads the general epoch and corresponding L1 report hash, verifies the received data against the expected hash, + /// and processes each report element. For each report, it handles asset transfers, updates strategy data, + /// and accumulates bridge amounts for withdrawal initiation. + /// After processing all elements, it initiates withdrawals to L1 and emits an event with the new L2 report data. + /// The function ensures that only valid and expected data is processed and that the contract's state remains consistent with L1. + fn handle_mass_report(ref self: ContractState, calldata: Span) { + let general_epoch = self.general_epoch.read(); + let l1_report_hash = self.l1_report_hash.read(general_epoch); + + if (general_epoch.is_non_zero()) { + assert(l1_report_hash.is_non_zero(), Errors::NO_L1_REPORT); + let calldata_hash = self._hash_l1_data(calldata); + assert(calldata_hash == l1_report_hash, Errors::INVALID_DATA); + } else { + // CHECK IF EPOCH 0 -> CHECK CALLDATA IS EMPTY + let is_initialised = self._is_initialised(); + assert(is_initialised, Errors::NOT_INITIALISED); + } + + let full_strategy_report_l1 = self._add_pending_strategies_to_initialize(calldata); + + let array_len = full_strategy_report_l1.len(); + assert(array_len.is_non_zero(), Errors::EMPTY_ARRAY); + + let mut strategy_report_l2_array = ArrayTrait::new(); + + let mut dict_bridge_deposit_keys = ArrayTrait::new(); + let mut bridge_deposit_amount: Felt252Dict> = Default::default(); + + let mut dict_bridge_withdrawal_keys = ArrayTrait::new(); + let mut bridge_withdrawal_amount: Felt252Dict> = Default::default(); + + let mut i = 0; + loop { + if (i == array_len) { + break (); + } + let elem = *full_strategy_report_l1.at(i); + let strategy = self.l1_strategy_to_token_manager.read(elem.l1_strategy); + let strategy_disp = ITokenManagerDispatcher { contract_address: strategy }; + let underlying = strategy_disp.underlying(); + let underlying_disp = ERC20ABIDispatcher { contract_address: underlying }; + if (elem.underlying_bridged_amount.is_non_zero()) { + underlying_disp.transfer(strategy, elem.underlying_bridged_amount); + } + let ret = strategy_disp + .handle_report(elem.l1_net_asset_value, elem.underlying_bridged_amount); + strategy_report_l2_array.append(ret); + + if (ret.action_id == 0) { + let bridge = self.underlying_to_bridge.read(underlying); + let val = bridge_deposit_amount.get(bridge.into()); + let current_bridge_deposit_amount: u256 = match match_nullable(val) { + FromNullableResult::Null => 0, + FromNullableResult::NotNull(val) => val.unbox(), + }; + if (current_bridge_deposit_amount.is_zero()) { + let new_amount = nullable_from_box(BoxTrait::new(ret.amount)); + dict_bridge_deposit_keys.append(bridge); + bridge_deposit_amount.insert(bridge.into(), new_amount); + } else { + let new_amount = nullable_from_box( + BoxTrait::new(ret.amount + current_bridge_deposit_amount) + ); + bridge_deposit_amount.insert(bridge.into(), new_amount); + } + } + + if (ret.action_id == 2) { + let bridge = self.underlying_to_bridge.read(underlying); + let val = bridge_withdrawal_amount.get(bridge.into()); + let current_bridge_withdrawal_amount: u256 = match match_nullable(val) { + FromNullableResult::Null => 0, + FromNullableResult::NotNull(val) => val.unbox(), + }; + if (current_bridge_withdrawal_amount.is_zero()) { + let new_amount = nullable_from_box(BoxTrait::new(ret.amount)); + dict_bridge_withdrawal_keys.append(bridge); + bridge_withdrawal_amount.insert(bridge.into(), new_amount); + } else { + let new_amount = nullable_from_box( + BoxTrait::new(ret.amount + current_bridge_withdrawal_amount) + ); + bridge_withdrawal_amount.insert(bridge.into(), new_amount); + } + } + + i += 1; + }; + + let l1_pooling_manager = self.l1_pooling_manager.read(); + + let mut bridge_deposit_info = ArrayTrait::new(); + let mut j = 0; + let dict_bridge_deposit_keys_len = dict_bridge_deposit_keys.len(); + loop { + if (j == dict_bridge_deposit_keys_len) { + break (); + } + let bridge_address = *dict_bridge_deposit_keys.at(j); + let val = bridge_deposit_amount.get(bridge_address.into()); + let amount: u256 = match match_nullable(val) { + FromNullableResult::Null => 0, + FromNullableResult::NotNull(val) => val.unbox(), + }; + let bridge_disp = ITokenBridgeDispatcher { contract_address: bridge_address }; + bridge_disp.initiate_withdraw(l1_pooling_manager.into(), amount); + + let l1_bridge = self.l2_bridge_to_l1_bridge.read(bridge_address); + bridge_deposit_info + .append(BridgeInteractionInfo { l1_bridge: l1_bridge, amount: amount }); + j += 1; + }; + + let mut bridge_withdrawal_info = ArrayTrait::new(); + j = 0; + let dict_bridge_withdrawal_keys_len = dict_bridge_withdrawal_keys.len(); + loop { + if (j == dict_bridge_withdrawal_keys_len) { + break (); + } + let bridge_address = *dict_bridge_withdrawal_keys.at(j); + let val = bridge_withdrawal_amount.get(bridge_address.into()); + let amount: u256 = match match_nullable(val) { + FromNullableResult::Null => 0, + FromNullableResult::NotNull(val) => val.unbox(), + }; + + let l1_bridge = self.l2_bridge_to_l1_bridge.read(bridge_address); + bridge_withdrawal_info + .append(BridgeInteractionInfo { l1_bridge: l1_bridge, amount: amount }); + j += 1; + }; + + let new_epoch = general_epoch + 1; + self.general_epoch.write(new_epoch); + + let ret_hash = self + ._hash_l2_data( + new_epoch, + bridge_deposit_info.span(), + strategy_report_l2_array.span(), + bridge_withdrawal_info.span() + ); + let mut message_payload: Array = ArrayTrait::new(); + message_payload.append(ret_hash.low.into()); + message_payload.append(ret_hash.high.into()); + send_message_to_l1_syscall( + to_address: l1_pooling_manager.into(), payload: message_payload.span() + ); + self + .emit( + NewL2Report { + new_epoch: new_epoch, + new_bridge_deposit: bridge_deposit_info, + new_l2_report: strategy_report_l2_array, + new_bridge_withdraw: bridge_withdrawal_info + } + ); + } + + fn delete_all_pending_strategy(ref self: ContractState) { + self.accesscontrol.assert_only_role(0); + let mut i = 0; + let pending_strategies_to_initialize_len = self + .pending_strategies_to_initialize_len + .read(); + loop { + if (i == pending_strategies_to_initialize_len) { + break (); + } + self.pending_strategies_to_initialize.write(i, EthAddressZeroable::zero()); + i += 1; + }; + self.pending_strategies_to_initialize_len.write(0); + } + + /// @notice Emits an event when deposit limits are updated for a strategy + /// @dev Only callable by a registered token manager + /// @param l1_strategy The Ethereum address of the L1 strategy for which limits are updated + /// @param new_min_deposit_limit The updated minimum deposit limit + /// @param new_max_deposit_limit The updated maximum deposit limit + fn emit_deposit_limit_updated_event( + ref self: ContractState, + l1_strategy: EthAddress, + new_min_deposit_limit: u256, + new_max_deposit_limit: u256 + ) { + self._assert_caller_is_registered_token_manager(l1_strategy); + self + .emit( + DepositLimitUpdated { + l1_strategy: l1_strategy, + new_min_deposit_limit: new_min_deposit_limit, + new_max_deposit_limit: new_max_deposit_limit + } + ); + } + + /// @notice Emits an event when withdrawal limits are updated for a strategy + /// @dev Only callable by a registered token manager + /// @param l1_strategy The Ethereum address of the L1 strategy for which limits are updated + /// @param new_min_withdrawal_limit The updated minimum withdrawal limit + /// @param new_max_withdrawal_limit The updated maximum withdrawal limit + fn emit_withdrawal_limit_updated_event( + ref self: ContractState, + l1_strategy: EthAddress, + new_min_withdrawal_limit: u256, + new_max_withdrawal_limit: u256 + ) { + self._assert_caller_is_registered_token_manager(l1_strategy); + self + .emit( + WithdrawalLimitUpdated { + l1_strategy: l1_strategy, + new_min_withdrawal_limit: new_min_withdrawal_limit, + new_max_withdrawal_limit: new_max_withdrawal_limit + } + ); + } + + /// @notice Emits an event when performance fees are updated for a strategy + /// @dev Only callable by a registered token manager + /// @param l1_strategy The Ethereum address of the L1 strategy + /// @param new_performance_fees The updated performance fees + fn emit_performance_fees_updated_event( + ref self: ContractState, l1_strategy: EthAddress, new_performance_fees: u256 + ) { + self._assert_caller_is_registered_token_manager(l1_strategy); + self + .emit( + PerformanceFeesUpdated { + l1_strategy: l1_strategy, new_performance_fees: new_performance_fees + } + ); + } + + + /// @notice Emits a deposit event for a strategy + /// @dev Only callable by a registered token manager + /// @param l1_strategy The Ethereum address of the L1 strategy + /// @param caller The address of the caller + /// @param receiver The address of the receiver + /// @param assets The amount of assets deposited + /// @param shares The amount of shares received + /// @param referal The address of the referal + fn emit_deposit_event( + ref self: ContractState, + l1_strategy: EthAddress, + caller: ContractAddress, + receiver: ContractAddress, + assets: u256, + shares: u256, + referal: ContractAddress + ) { + self._assert_caller_is_registered_token_manager(l1_strategy); + self + .emit( + Deposit { + l1_strategy: l1_strategy, + caller: caller, + receiver: receiver, + assets: assets, + shares: shares, + referal: referal + } + ); + } + + /// @notice Emits an event for a withdrawal request + /// @dev Only callable by a registered token manager + /// @param l1_strategy The Ethereum address of the L1 strategy + /// @param caller The address of the caller + /// @param assets The amount of assets requested for withdrawal + /// @param shares The amount of shares to be redeemed + /// @param id The unique identifier of the withdrawal for a user + /// @param epoch The epoch during which the request was made + fn emit_request_withdrawal_event( + ref self: ContractState, + l1_strategy: EthAddress, + caller: ContractAddress, + assets: u256, + shares: u256, + id: u256, + epoch: u256 + ) { + self._assert_caller_is_registered_token_manager(l1_strategy); + self + .emit( + RequestWithdrawal { + l1_strategy: l1_strategy, + caller: caller, + assets: assets, + shares: shares, + id: id, + epoch: epoch + } + ); + } + + /// @notice Emits an event when a withdrawal is claimed + /// @dev Only callable by a registered token manager + /// @param l1_strategy The Ethereum address of the L1 strategy + /// @param caller The address of the caller + /// @param id The unique identifier of the withdrawal for a user + /// @param underlying_amount The amount of underlying asset withdrawn + fn emit_claim_withdrawal_event( + ref self: ContractState, + l1_strategy: EthAddress, + caller: ContractAddress, + id: u256, + underlying_amount: u256 + ) { + self._assert_caller_is_registered_token_manager(l1_strategy); + self + .emit( + ClaimWithdrawal { + l1_strategy: l1_strategy, + caller: caller, + id: id, + underlying_amount: underlying_amount + } + ); + } + + /// @notice Emits an event when the withdrawal epoch delay is updated for a strategy + /// @dev Only callable by a registered token manager + /// @param l1_strategy The Ethereum address of the L1 strategy + /// @param new_withdrawal_epoch_delay The updated withdrawal epoch delay + fn emit_withdrawal_epoch_delay_updated_event( + ref self: ContractState, l1_strategy: EthAddress, new_withdrawal_epoch_delay: u256 + ) { + self._assert_caller_is_registered_token_manager(l1_strategy); + self + .emit( + WithdrawalEpochUpdated { + l1_strategy: l1_strategy, + new_withdrawal_epoch_delay: new_withdrawal_epoch_delay + } + ); + } + + + /// @notice Emits an event when the dust limit is updated for a strategy + /// @dev Only callable by a registered token manager + /// @param l1_strategy The Ethereum address of the L1 strategy + /// @param new_dust_limit The updated dust limit + fn emit_dust_limit_updated_event( + ref self: ContractState, l1_strategy: EthAddress, new_dust_limit: u256 + ) { + self._assert_caller_is_registered_token_manager(l1_strategy); + self + .emit( + DustLimitUpdated { l1_strategy: l1_strategy, new_dust_limit: new_dust_limit } + ); + } + + /// @notice Emits an event when the token manager class hash is updated + /// @dev Only callable by the factory + /// @param new_token_manager_class_hash The updated class hash for the token manager + fn emit_token_manager_class_hash_updated_event( + ref self: ContractState, new_token_manager_class_hash: ClassHash + ) { + self._assert_caller_is_factory(); + self + .emit( + TokenManagerClassHashUpdated { + new_token_manager_class_hash: new_token_manager_class_hash + } + ); + } + + /// @notice Emits an event when the token class hash is updated + /// @dev Only callable by the factory + /// @param new_token_class_hash The updated class hash for the token + fn emit_token_class_hash_updated_event( + ref self: ContractState, new_token_class_hash: ClassHash + ) { + self._assert_caller_is_factory(); + self.emit(TokenClassHashUpdated { new_token_class_hash: new_token_class_hash }); + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + /// @notice Asserts that the caller is a registered token manager for the given L1 strategy + /// @dev Verifies if the caller address matches the registered token manager for the specified L1 strategy + /// @param l1_strategy The Ethereum address of the L1 strategy + fn _assert_caller_is_registered_token_manager( + self: @ContractState, l1_strategy: EthAddress + ) { + let caller = get_caller_address(); + let token_manager = self.l1_strategy_to_token_manager.read(l1_strategy); + assert(token_manager == caller, Errors::INVALID_CALLER); + } + + /// @notice Asserts that the caller is the factory + /// @dev Verifies if the caller address matches the registered factory address + fn _assert_caller_is_factory(self: @ContractState) { + let caller_address = get_caller_address(); + let factory = self.factory.read(); + assert(caller_address == factory, Errors::INVALID_CALLER); + } + + /// @notice Checks if the contract is initialized + /// @return True if the contract is initialized, otherwise false + /// @dev Initialization is determined based on whether key contract addresses are non-zero + fn _is_initialised(self: @ContractState) -> bool { + let l1_pooling_manager = self.l1_pooling_manager.read(); + let fees_recipient = self.fees_recipient.read(); + let factory = self.factory.read(); + if (l1_pooling_manager.is_zero() || fees_recipient.is_zero() || factory.is_zero()) { + false + } else { + true + } + } + + /// @notice Converts a span of L1 strategy reports to a span of u256 + /// @param calldata Span of StrategyReportL1 data + /// @return Span of u256 values representing the L1 strategy reports + fn _strategy_report_l1_to_u256_span( + self: @ContractState, calldata: Span + ) -> Span { + let mut ret_array = ArrayTrait::new(); + let array_len = calldata.len(); + let mut i = 0; + loop { + if (i == array_len) { + break (); + } + let elem = *calldata.at(i); + let l1_strategy_felt: felt252 = elem.l1_strategy.into(); + let l1_strategy_u256: u256 = l1_strategy_felt.into(); + ret_array.append(l1_strategy_u256); + ret_array.append(elem.l1_net_asset_value); + ret_array.append(elem.underlying_bridged_amount); + i += 1; + }; + ret_array.span() + } + + /// @notice Converts a span of L2 strategy reports to a span of u256 + /// @param new_epoch of pooling manager + /// @param bridge_deposit_info Span of StrategyReportL2 data + /// @param strategy_report_l2 Span of StrategyReportL2 data + /// @param bridge_withdrawal_info Span of StrategyReportL2 data + /// @return Span of u256 values representing the L2 strategy reports + fn _strategy_report_l2_to_u256_span( + self: @ContractState, + new_epoch: u256, + bridge_deposit_info: Span, + strategy_report_l2: Span, + bridge_withdrawal_info: Span + ) -> Span { + let mut ret_array = ArrayTrait::new(); + ret_array.append(new_epoch); + let array_len = bridge_deposit_info.len(); + let mut i = 0; + loop { + if (i == array_len) { + break (); + } + let bridge_deposit_info_elem = *bridge_deposit_info.at(i); + let l1_bridge_u256: u256 = bridge_deposit_info_elem.l1_bridge.into(); + ret_array.append(l1_bridge_u256); + ret_array.append(bridge_deposit_info_elem.amount); + + let strategy_report_l2_elem = *strategy_report_l2.at(i); + let l1_strategy_felt: felt252 = strategy_report_l2_elem.l1_strategy.into(); + let l1_strategy_u256: u256 = l1_strategy_felt.into(); + ret_array.append(l1_strategy_u256); + ret_array.append(strategy_report_l2_elem.action_id); + ret_array.append(strategy_report_l2_elem.amount); + + let bridge_withdrawal_info_elem = *bridge_withdrawal_info.at(i); + let l1_bridge_u256: u256 = bridge_withdrawal_info_elem.l1_bridge.into(); + ret_array.append(l1_bridge_u256); + ret_array.append(bridge_withdrawal_info_elem.amount); + i += 1; + }; + ret_array.span() + } + + /// @notice Generates a hash from L1 data + /// @param calldata Span of StrategyReportL1 data + /// @return Hash of the L1 data + fn _hash_l1_data(self: @ContractState, calldata: Span) -> u256 { + let u256_span = self._strategy_report_l1_to_u256_span(calldata); + let hash = keccak::keccak_u256s_be_inputs(u256_span); + reverse_endianness(hash) + } + + /// @notice Generates a hash from L2 data + /// @param new_epoch of pooling manager + /// @param bridge_deposit_info Span of StrategyReportL2 data + /// @param strategy_report_l2 Span of StrategyReportL2 data + /// @param bridge_withdrawal_info Span of StrategyReportL2 data + /// @return Hash of the L2 data + fn _hash_l2_data( + self: @ContractState, + new_epoch: u256, + bridge_deposit_info: Span, + strategy_report_l2: Span, + bridge_withdrawal_info: Span + ) -> u256 { + let u256_span = self + ._strategy_report_l2_to_u256_span( + new_epoch, bridge_deposit_info, strategy_report_l2, bridge_withdrawal_info + ); + let hash = keccak::keccak_u256s_be_inputs(u256_span); + reverse_endianness(hash) + } + + // @notice Retrieves pending strategies to be initialized + /// @return Array of Ethereum addresses representing the pending strategies + fn _pending_strategies_to_initialize(self: @ContractState) -> Array { + let mut i = 0; + let pending_strategies_to_initialize_len = self + .pending_strategies_to_initialize_len + .read(); + let mut ret_array = ArrayTrait::new(); + loop { + if (i == pending_strategies_to_initialize_len) { + break (); + } + let elem = self.pending_strategies_to_initialize.read(i); + ret_array.append(elem); + i += 1; + }; + ret_array + } + + /// @notice Adds pending strategies to be initialized to the span of StrategyReportL1 data + /// @param calldata Span of StrategyReportL1 data + /// @return Span of StrategyReportL1 data including pending strategies + fn _add_pending_strategies_to_initialize( + self: @ContractState, calldata: Span + ) -> Span { + let mut ret_array = ArrayTrait::new(); + let calldata_len = calldata.len(); + let pending_strategies_to_initialize = self._pending_strategies_to_initialize().span(); + let pending_strategies_to_initialize_len = pending_strategies_to_initialize.len(); + let mut i = 0; + loop { + if (i == calldata_len) { + break (); + } + ret_array.append(*calldata.at(i)); + i += 1; + }; + + i = 0; + loop { + if (i == pending_strategies_to_initialize_len) { + break (); + } + let mut l1_strategy = self.pending_strategies_to_initialize.read(i.into()); + let token_manager = self.l1_strategy_to_token_manager.read(l1_strategy); + let token_manager_disp = ITokenManagerDispatcher { + contract_address: token_manager + }; + let buffer = token_manager_disp.buffer(); + assert(buffer.is_non_zero(), Errors::BUFFER_NUL); + let new_elem = StrategyReportL1 { + l1_strategy: l1_strategy, l1_net_asset_value: 0, underlying_bridged_amount: 0 + }; + ret_array.append(new_elem); + i += 1; + }; + ret_array.span() + } + } +} diff --git a/src/mocks/mock_token_manager.cairo b/src/mocks/mock_token_manager.cairo new file mode 100644 index 0000000..19c67ce --- /dev/null +++ b/src/mocks/mock_token_manager.cairo @@ -0,0 +1,777 @@ +#[starknet::contract] +mod MockTokenManager { + use starknet::{ + ContractAddress, get_caller_address, get_contract_address, eth_address::EthAddress, + Zeroable, ClassHash + }; + + + use openzeppelin::token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + use openzeppelin::token::erc721::interface::{ERC721ABIDispatcher, ERC721ABIDispatcherTrait}; + use openzeppelin::access::accesscontrol::interface::{ + IAccessControlDispatcher, IAccessControlDispatcherTrait + }; + use openzeppelin::upgrades::UpgradeableComponent; + + + use nimbora_yields::token_manager::interface::{ITokenManager, WithdrawalInfo, StrategyReportL2}; + use nimbora_yields::token::interface::{ITokenDispatcher, ITokenDispatcherTrait}; + use nimbora_yields::pooling_manager::interface::{ + IPoolingManagerDispatcher, IPoolingManagerDispatcherTrait + }; + + use nimbora_yields::utils::{CONSTANTS, MATH}; + + // Components + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + impl InternalUpgradeableImpl = UpgradeableComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, + pooling_manager: ContractAddress, + l1_strategy: EthAddress, + underlying: ContractAddress, + performance_fees: u256, + deposit_limit_low: u256, + deposit_limit_high: u256, + withdrawal_limit_low: u256, + withdrawal_limit_high: u256, + withdrawal_epoch_delay: u256, + token: ContractAddress, + epoch: u256, + l1_net_asset_value: u256, + underlying_transit: u256, + buffer: u256, + handled_epoch_withdrawal_len: u256, + withdrawal_info: LegacyMap<(ContractAddress, u256), WithdrawalInfo>, + dust_limit: u256, + withdrawal_pool: LegacyMap, + withdrawal_share: LegacyMap, + user_withdrawal_len: LegacyMap + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } + + + mod Errors { + const INVALID_CALLER: felt252 = 'Invalid caller'; + const INVALID_FEES: felt252 = 'Fee amount too high'; + const ZERO_AMOUNT: felt252 = 'Amount nul'; + const ZERO_ADDRESS: felt252 = 'Address is zero'; + const INVALID_LIMIT: felt252 = 'Invalid limit'; + const LOW_LIMIT: felt252 = 'Low limit reacher'; + const HIGH_LIMIT: felt252 = 'High limit reacher'; + const NOT_OWNER: felt252 = 'Not owner'; + const WITHDRAWAL_NOT_REDY: felt252 = 'Withdrawal not ready'; + const ALREADY_CLAIMED: felt252 = 'Already claimed'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + pooling_manager: ContractAddress, + l1_strategy: EthAddress, + underlying: ContractAddress, + performance_fees: u256, + min_deposit: u256, + max_deposit: u256, + min_withdrawal: u256, + max_withdrawal: u256, + withdrawal_epoch_delay: u256, + dust_limit: u256 + ) { + self.pooling_manager.write(pooling_manager); + assert(l1_strategy.is_non_zero(), Errors::ZERO_ADDRESS); + self.l1_strategy.write(l1_strategy); + self.underlying.write(underlying); + self._set_performance_fees(performance_fees); + self._set_deposit_limit(min_deposit, max_deposit); + self._set_withdrawal_limit(min_withdrawal, max_withdrawal); + self._set_withdrawal_epoch_delay(withdrawal_epoch_delay); + self._set_dust_limit(dust_limit); + } + + /// @notice Upgrade contract + /// @param New contract class hash + #[external(v0)] + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self._assert_only_owner(); + self.upgradeable._upgrade(new_class_hash); + } + + /// @notice Function to test upgradable + /// @param None + #[external(v0)] + fn get_thousand(self: @ContractState) -> felt252 { + 1000 + } + + #[abi(embed_v0)] + impl TokenManager of ITokenManager { + /// @notice Returns the pooling manager address + /// @return The contract address of the pooling manager + fn pooling_manager(self: @ContractState) -> ContractAddress { + self.pooling_manager.read() + } + + /// @notice Returns the L1 strategy address + /// @return The Ethereum address of the L1 strategy + fn l1_strategy(self: @ContractState) -> EthAddress { + self.l1_strategy.read() + } + + /// @notice Returns the underlying asset address + /// @return The contract address of the underlying asset + fn underlying(self: @ContractState) -> ContractAddress { + self.underlying.read() + } + + /// @notice Returns the token address + /// @return The contract address of the token + fn token(self: @ContractState) -> ContractAddress { + self.token.read() + } + + /// @notice Returns the current performance fees + /// @return The performance fees as a u256 value + fn performance_fees(self: @ContractState) -> u256 { + self.performance_fees.read() + } + + /// @notice Reads the low deposit limit + /// @return The current low limit for deposits + fn deposit_limit_low(self: @ContractState) -> u256 { + self.deposit_limit_low.read() + } + + /// @notice Reads the high deposit limit + /// @return The current high limit for deposits + fn deposit_limit_high(self: @ContractState) -> u256 { + self.deposit_limit_high.read() + } + + /// @notice Reads the low withdrawal limit + /// @return The current low limit for withdrawals + fn withdrawal_limit_low(self: @ContractState) -> u256 { + self.withdrawal_limit_low.read() + } + + /// @notice Reads the high withdrawal limit + /// @return The current high limit for withdrawals + fn withdrawal_limit_high(self: @ContractState) -> u256 { + self.withdrawal_limit_high.read() + } + + /// @notice Reads the withdrawal epoch delay + /// @return The current delay in epochs for withdrawals + fn withdrawal_epoch_delay(self: @ContractState) -> u256 { + self.withdrawal_epoch_delay.read() + } + + + /// @notice Reads the current epoch + /// @return The current epoch value + fn epoch(self: @ContractState) -> u256 { + self.epoch.read() + } + + /// @notice Reads the net asset value from L1 + /// @return The net asset value from L1 + fn l1_net_asset_value(self: @ContractState) -> u256 { + self.l1_net_asset_value.read() + } + + /// @notice Reads the underlying asset in transit + /// @return The amount of underlying asset currently in transit + fn underlying_transit(self: @ContractState) -> u256 { + self.underlying_transit.read() + } + + /// @notice Reads the buffer value + /// @return The current buffer value + fn buffer(self: @ContractState) -> u256 { + self.buffer.read() + } + + /// @notice Reads the length of handled epoch withdrawals + /// @return The length of handled epoch withdrawals + fn handled_epoch_withdrawal_len(self: @ContractState) -> u256 { + self.handled_epoch_withdrawal_len.read() + } + + /// @notice Reads withdrawal information for a given user and ID + /// @param user The address of the user + /// @param id The unique identifier of the withdrawal + /// @return Withdrawal information corresponding to the user and ID + fn withdrawal_info( + self: @ContractState, user: ContractAddress, id: u256 + ) -> WithdrawalInfo { + self.withdrawal_info.read((user, id)) + } + + /// @notice Reads the length of withdrawals for a user + /// @param user The address of the user + /// @return The number of withdrawals associated with the user + fn user_withdrawal_len(self: @ContractState, user: ContractAddress) -> u256 { + self.user_withdrawal_len.read(user) + } + + /// @notice Reads the dust limit + /// @return The current dust limit + fn dust_limit(self: @ContractState) -> u256 { + self.dust_limit.read() + } + + /// @notice Calculates the total assets + /// @return The total assets calculated + fn total_assets(self: @ContractState) -> u256 { + self._total_assets() + } + + /// @notice Calculates the total underlying due + /// @return The total underlying due calculated + fn total_underlying_due(self: @ContractState) -> u256 { + let handled_epoch_withdrawal_len = self.handled_epoch_withdrawal_len.read(); + let epoch = self.epoch.read(); + self._total_underlying_due(handled_epoch_withdrawal_len, epoch) + } + + /// @notice Calculates the withdrawal exchange rate for a given epoch + /// @param epoch The epoch for which to calculate the exchange rate + /// @return The withdrawal exchange rate for the specified epoch + fn withdrawal_exchange_rate(self: @ContractState, epoch: u256) -> u256 { + self._withdrawal_exchange_rate(epoch) + } + + /// @notice Reads the withdrawal pool for a given epoch + /// @param epoch The epoch for which to read the withdrawal pool + /// @return The withdrawal pool for the specified epoch + fn withdrawal_pool(self: @ContractState, epoch: u256) -> u256 { + self.withdrawal_pool.read(epoch) + } + + /// @notice Reads the withdrawal share for a given epoch + /// @param epoch The epoch for which to read the withdrawal share + /// @return The withdrawal share for the specified epoch + fn withdrawal_share(self: @ContractState, epoch: u256) -> u256 { + self.withdrawal_share.read(epoch) + } + + + /// @notice Sets the token for this contract + /// @dev Only callable by the pooling manager + /// @param token The contract address of the token + fn initialiser(ref self: ContractState, token: ContractAddress) { + self._assert_only_pooling_manager(); + self.token.write(token); + } + + /// @notice Sets new performance fees + /// @dev Only callable by the owner of the contract + /// @param new_performance_fees The new performance fees value to be set + fn set_performance_fees(ref self: ContractState, new_performance_fees: u256) { + self._assert_only_owner(); + self._set_performance_fees(new_performance_fees); + let l1_strategy = self.l1_strategy.read(); + let pooling_manager = self.pooling_manager.read(); + let pooling_manager_disp = IPoolingManagerDispatcher { + contract_address: pooling_manager + }; + pooling_manager_disp + .emit_performance_fees_updated_event(l1_strategy, new_performance_fees); + } + + /// @notice Sets new deposit limits + /// @dev Only callable by the owner of the contract + /// @param new_deposit_limit_low The new lower limit for deposits + /// @param new_deposit_limit_high The new upper limit for deposits + fn set_deposit_limit( + ref self: ContractState, new_deposit_limit_low: u256, new_deposit_limit_high: u256 + ) { + self._assert_only_owner(); + self._set_deposit_limit(new_deposit_limit_low, new_deposit_limit_high); + let l1_strategy = self.l1_strategy.read(); + let pooling_manager = self.pooling_manager.read(); + let pooling_manager_disp = IPoolingManagerDispatcher { + contract_address: pooling_manager + }; + pooling_manager_disp + .emit_deposit_limit_updated_event( + l1_strategy, new_deposit_limit_low, new_deposit_limit_high + ); + } + + /// @notice Sets new withdrawal limits + /// @dev Only callable by the owner of the contract + /// @param new_withdrawal_limit_low The new lower limit for withdrawals + /// @param new_withdrawal_limit_high The new upper limit for withdrawals + fn set_withdrawal_limit( + ref self: ContractState, new_withdrawal_limit_low: u256, new_withdrawal_limit_high: u256 + ) { + self._assert_only_owner(); + self._set_withdrawal_limit(new_withdrawal_limit_low, new_withdrawal_limit_high); + let l1_strategy = self.l1_strategy.read(); + let pooling_manager = self.pooling_manager.read(); + let pooling_manager_disp = IPoolingManagerDispatcher { + contract_address: pooling_manager + }; + pooling_manager_disp + .emit_withdrawal_limit_updated_event( + l1_strategy, new_withdrawal_limit_low, new_withdrawal_limit_high + ); + } + + /// @notice Sets a new withdrawal epoch delay + /// @dev Only callable by the owner of the contract + /// @param new_withdrawal_epoch_delay The new withdrawal epoch delay to be set + fn set_withdrawal_epoch_delay(ref self: ContractState, new_withdrawal_epoch_delay: u256) { + self._assert_only_owner(); + self._set_withdrawal_epoch_delay(new_withdrawal_epoch_delay); + let l1_strategy = self.l1_strategy.read(); + let pooling_manager = self.pooling_manager.read(); + let pooling_manager_disp = IPoolingManagerDispatcher { + contract_address: pooling_manager + }; + pooling_manager_disp + .emit_withdrawal_epoch_delay_updated_event(l1_strategy, new_withdrawal_epoch_delay); + } + + /// @notice Sets a new dust limit + /// @dev Only callable by the owner of the contract + /// @param new_dust_limit The new dust limit to be set + fn set_dust_limit(ref self: ContractState, new_dust_limit: u256) { + self._assert_only_owner(); + self._set_dust_limit(new_dust_limit); + let l1_strategy = self.l1_strategy.read(); + let pooling_manager = self.pooling_manager.read(); + let pooling_manager_disp = IPoolingManagerDispatcher { + contract_address: pooling_manager + }; + pooling_manager_disp.emit_dust_limit_updated_event(l1_strategy, new_dust_limit); + } + + + /// @notice Allows a user to deposit assets into the contract + /// @dev Checks if the deposit amount is within the set limits before accepting the deposit + /// @param assets The amount of assets to deposit + /// @param receiver The address to receive the minted shares + /// @param referal The referral address for the deposit + fn deposit( + ref self: ContractState, + assets: u256, + receiver: ContractAddress, + referal: ContractAddress + ) { + let deposit_limit_low = self.deposit_limit_low.read(); + let deposit_limit_high = self.deposit_limit_high.read(); + + assert(assets >= deposit_limit_low, Errors::LOW_LIMIT); + assert(assets <= deposit_limit_high, Errors::HIGH_LIMIT); + + let underlying = self.underlying.read(); + let erc20_disp = ERC20ABIDispatcher { contract_address: underlying }; + let caller = get_caller_address(); + let this = get_contract_address(); + erc20_disp.transferFrom(caller, this, assets); + let buffer = self.buffer.read(); + let new_buffer = buffer + assets; + self.buffer.write(new_buffer); + + let shares = self._convert_to_shares(assets); + let token = self.token.read(); + let token_disp = ITokenDispatcher { contract_address: token }; + token_disp.mint(receiver, shares); + + let l1_strategy = self.l1_strategy.read(); + let pooling_manager = self.pooling_manager.read(); + let pooling_manager_disp = IPoolingManagerDispatcher { + contract_address: pooling_manager + }; + pooling_manager_disp + .emit_deposit_event(l1_strategy, caller, receiver, assets, shares, referal); + } + + /// @notice Allows a user to request a withdrawal from the contract + /// @dev Checks if the withdrawal amount is within the set limits before processing the withdrawal + /// @param shares The amount of shares to withdraw + fn request_withdrawal(ref self: ContractState, shares: u256) { + let withdrawal_limit_low = self.withdrawal_limit_low.read(); + let withdrawal_limit_high = self.withdrawal_limit_high.read(); + assert(shares >= withdrawal_limit_low, Errors::LOW_LIMIT); + assert(shares <= withdrawal_limit_high, Errors::HIGH_LIMIT); + + let token = self.token.read(); + let token_disp = ITokenDispatcher { contract_address: token }; + let caller = get_caller_address(); + token_disp.burn(caller, shares); + + let epoch = self.epoch.read(); + let assets = self._convert_to_assets(shares); + let withdrawal_pool_share = (assets * CONSTANTS::WAD) + / self._withdrawal_exchange_rate(epoch); + + let withdrawal_pool = self.withdrawal_pool.read(epoch); + let withdrawal_share = self.withdrawal_share.read(epoch); + self.withdrawal_pool.write(epoch, withdrawal_pool + assets); + self.withdrawal_share.write(epoch, withdrawal_share + withdrawal_pool_share); + + let user_withdrawal_len = self.user_withdrawal_len.read(caller); + self + .withdrawal_info + .write( + (caller, user_withdrawal_len), + WithdrawalInfo { shares: shares, epoch: epoch, claimed: false } + ); + + let l1_strategy = self.l1_strategy.read(); + let pooling_manager = self.pooling_manager.read(); + let pooling_manager_disp = IPoolingManagerDispatcher { + contract_address: pooling_manager + }; + pooling_manager_disp + .emit_request_withdrawal_event( + l1_strategy, caller, assets, shares, user_withdrawal_len, epoch + ); + } + + + /// @notice Allows a user to claim a withdrawal + /// @dev Validates that the withdrawal is ready to be claimed and processes it + /// @param id The unique identifier of the withdrawal request + fn claim_withdrawal(ref self: ContractState, id: u256) { + let caller = get_caller_address(); + let withdrawal_info = self.withdrawal_info.read((caller, id)); + assert(!withdrawal_info.claimed, Errors::ALREADY_CLAIMED); + let handled_epoch_withdrawal_len = self.handled_epoch_withdrawal_len.read(); + assert( + handled_epoch_withdrawal_len > withdrawal_info.epoch, Errors::WITHDRAWAL_NOT_REDY + ); + + self + .withdrawal_info + .write( + (caller, id), + WithdrawalInfo { + shares: withdrawal_info.shares, epoch: withdrawal_info.epoch, claimed: true + } + ); + + let withdrawal_exchange_rate = self._withdrawal_exchange_rate(withdrawal_info.epoch); + let assets = (withdrawal_exchange_rate * withdrawal_info.shares) / CONSTANTS::WAD; + + let withdrawal_pool = self.withdrawal_pool.read(withdrawal_info.epoch); + let withdrawal_share = self.withdrawal_share.read(withdrawal_info.epoch); + self.withdrawal_pool.write(withdrawal_info.epoch, withdrawal_pool - assets); + self + .withdrawal_share + .write(withdrawal_info.epoch, withdrawal_share - withdrawal_info.shares); + + let underlying = self.underlying.read(); + let underlying_disp = ERC20ABIDispatcher { contract_address: underlying }; + underlying_disp.transfer(caller, assets); + + let l1_strategy = self.l1_strategy.read(); + let pooling_manager = self.pooling_manager.read(); + let pooling_manager_disp = IPoolingManagerDispatcher { + contract_address: pooling_manager + }; + pooling_manager_disp.emit_claim_withdrawal_event(l1_strategy, caller, id, assets); + } + + + /// @notice Handles the report from the L1 strategy + /// @dev Only callable by the pooling manager, processes the report and updates the contract state + /// @param l1_net_asset_value The net asset value reported from L1 + /// @param underlying_bridged_amount The amount of underlying asset bridged + /// @return StrategyReportL2 object containing the strategy report data + fn handle_report( + ref self: ContractState, l1_net_asset_value: u256, underlying_bridged_amount: u256 + ) -> StrategyReportL2 { + self._assert_only_pooling_manager(); + + let epoch = self.epoch.read(); + let prev_l1_net_asset_value = self.l1_net_asset_value.read(); + let prev_underlying_transit = self.underlying_transit.read(); + + let sent_to_l1 = prev_l1_net_asset_value + prev_underlying_transit; + let received_from_l1 = l1_net_asset_value + underlying_bridged_amount; + + let handled_epoch_withdrawal_len = self.handled_epoch_withdrawal_len.read(); + let buffer_mem = self.buffer.read() + underlying_bridged_amount; + + let mut profit = 0; + + if (received_from_l1 < sent_to_l1) { + let underlying_loss = sent_to_l1 - received_from_l1; + let total_underlying = buffer_mem + l1_net_asset_value; + let total_underlying_due = self + ._total_underlying_due(handled_epoch_withdrawal_len, epoch); + let amount_to_consider = total_underlying + total_underlying_due; + let mut i = handled_epoch_withdrawal_len; + loop { + if (i > epoch) { + break (); + } + let withdrawal_pool = self.withdrawal_pool.read(i); + let withdrawal_epoch_loss_incured = (underlying_loss * withdrawal_pool) + / amount_to_consider; + self.withdrawal_pool.write(i, withdrawal_pool - withdrawal_epoch_loss_incured); + i += 1; + } + } else { + // Share price increase, performance_fee is taken + profit = received_from_l1 - sent_to_l1; + } + + let mut remaining_buffer_mem = buffer_mem; + let mut cumulatif_due_underlying = 0; + let withdrawal_epoch_delay = self.withdrawal_epoch_delay.read(); + + if (epoch >= withdrawal_epoch_delay) { + let mut new_handled_epoch_withdrawal_len = handled_epoch_withdrawal_len; + let mut j = handled_epoch_withdrawal_len; + let limit_epoch = epoch - withdrawal_epoch_delay; + loop { + if (j > limit_epoch) { + break (); + } + + let withdrawal_pool = self.withdrawal_pool.read(j); + + if (remaining_buffer_mem >= withdrawal_pool) { + remaining_buffer_mem -= withdrawal_pool; + new_handled_epoch_withdrawal_len += 1; + } else { + cumulatif_due_underlying += withdrawal_pool - remaining_buffer_mem; + } + + j += 1; + }; + if (new_handled_epoch_withdrawal_len > handled_epoch_withdrawal_len) { + self.handled_epoch_withdrawal_len.write(new_handled_epoch_withdrawal_len); + } + } + + let new_epoch = epoch + 1; + self.epoch.write(new_epoch); + self.l1_net_asset_value.write(l1_net_asset_value); + + let token = self.token.read(); + let token_disp = ERC20ABIDispatcher { contract_address: token }; + let decimals = token_disp.decimals(); + let l1_strategy = self.l1_strategy.read(); + let one_share_unit = MATH::pow(10, decimals.into()); + + if (cumulatif_due_underlying > 0) { + // We need more underlying from L1 + let underlying_request_amount = cumulatif_due_underlying - remaining_buffer_mem; + self.buffer.write(remaining_buffer_mem); + self.underlying_transit.write(0); + + self._check_profit_and_mint(profit, token); + + let new_share_price = self._convert_to_assets(one_share_unit); + + StrategyReportL2 { + l1_strategy: l1_strategy, + action_id: 2, + amount: underlying_request_amount, + new_share_price: new_share_price + } + } else { + let dust_limit_factor = self.dust_limit.read(); + let dust_limit = (l1_net_asset_value * dust_limit_factor) / CONSTANTS::WAD; + + if (dust_limit > remaining_buffer_mem) { + // We are fine + self.buffer.write(remaining_buffer_mem); + self.underlying_transit.write(0); + + self._check_profit_and_mint(profit, token); + + let new_share_price = self._convert_to_assets(one_share_unit); + + StrategyReportL2 { + l1_strategy: l1_strategy, + action_id: 1, + amount: 0, + new_share_price: new_share_price + } + } else { + // We deposit underlying to L1 + self.buffer.write(0); + self.underlying_transit.write(remaining_buffer_mem); + + self._check_profit_and_mint(profit, token); + + let new_share_price = self._convert_to_assets(one_share_unit); + + StrategyReportL2 { + l1_strategy: l1_strategy, + action_id: 0, + amount: remaining_buffer_mem, + new_share_price: new_share_price + } + } + } + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + /// @notice Asserts that the caller is the pooling manager + fn _assert_only_pooling_manager(self: @ContractState) { + let caller = get_caller_address(); + let pooling_manager = self.pooling_manager.read(); + assert(caller == pooling_manager, Errors::INVALID_CALLER); + } + + /// @notice Asserts that the caller has the owner role + fn _assert_only_owner(self: @ContractState) { + let caller = get_caller_address(); + let pooling_manager = self.pooling_manager.read(); + let access_disp = IAccessControlDispatcher { contract_address: pooling_manager }; + let has_role = access_disp.has_role(0, caller); + assert(has_role, Errors::INVALID_CALLER); + } + + /// @notice Calculates the total underlying due up to the current epoch + /// @param handled_epoch_withdrawal_len The length of the handled epoch withdrawal + /// @param current_epoch The current epoch + /// @return The total underlying due + fn _total_underlying_due( + self: @ContractState, handled_epoch_withdrawal_len: u256, current_epoch: u256 + ) -> u256 { + let mut i = handled_epoch_withdrawal_len; + let mut acc = 0; + loop { + if (i > current_epoch) { + break (); + } + let withdrawal_pool = self.withdrawal_pool.read(i); + acc += withdrawal_pool; + i += 1; + }; + acc + } + + + /// @notice Calculates the total assets of the contract + /// @return The total assets + fn _total_assets(self: @ContractState) -> u256 { + let epoch = self.epoch.read(); + let handled_epoch_withdrawal_len = self.handled_epoch_withdrawal_len.read(); + let total_underlying_due = self + ._total_underlying_due(handled_epoch_withdrawal_len, epoch); + let buffer = self.buffer.read(); + let l1_net_asset_value = self.l1_net_asset_value.read(); + let underlying_transit = self.underlying_transit.read(); + (buffer + l1_net_asset_value + underlying_transit) - total_underlying_due + } + + + /// @notice Converts a given amount of assets to shares + /// @param assets The amount of assets to convert + /// @return The equivalent amount of shares + fn _convert_to_shares(self: @ContractState, assets: u256) -> u256 { + let token = self.token.read(); + let erc20_disp = ERC20ABIDispatcher { contract_address: token }; + let total_supply = erc20_disp.total_supply(); + let total_assets = self._total_assets(); + (assets * (total_supply + 1)) / (total_assets + 1) + } + + /// @notice Converts a given amount of shares to assets + /// @param shares The amount of shares to convert + /// @return The equivalent amount of assets + fn _convert_to_assets(self: @ContractState, shares: u256) -> u256 { + let token = self.token.read(); + let erc20_disp = ERC20ABIDispatcher { contract_address: token }; + let total_supply = erc20_disp.total_supply(); + let total_assets = self._total_assets(); + (shares * (total_assets + 1)) / (total_supply + 1) + } + + /// @notice Calculates the withdrawal exchange rate for a given epoch + /// @param epoch The epoch for which to calculate the rate + /// @return The withdrawal exchange rate + fn _withdrawal_exchange_rate(self: @ContractState, epoch: u256) -> u256 { + let withdrawal_pool = self.withdrawal_pool.read(epoch); + let withdrawal_share = self.withdrawal_share.read(epoch); + if (withdrawal_pool.is_zero()) { + 0 + } else { + (withdrawal_pool * CONSTANTS::WAD) / withdrawal_share + } + } + + + /// @notice Sets the performance fees for the contract + /// @param new_performance_fees The new performance fees + fn _set_performance_fees(ref self: ContractState, new_performance_fees: u256) { + assert(new_performance_fees < CONSTANTS::WAD, Errors::INVALID_FEES); + self.performance_fees.write(new_performance_fees); + } + + /// @notice Sets the deposit limits for the contract + /// @param new_deposit_limit_low The new low limit for deposits + /// @param new_deposit_limit_high The new high limit for deposits + fn _set_deposit_limit( + ref self: ContractState, new_deposit_limit_low: u256, new_deposit_limit_high: u256 + ) { + assert(new_deposit_limit_low.is_non_zero(), Errors::ZERO_AMOUNT); + assert(new_deposit_limit_high > new_deposit_limit_low, Errors::INVALID_LIMIT); + self.deposit_limit_low.write(new_deposit_limit_low); + self.deposit_limit_high.write(new_deposit_limit_high); + } + + /// @notice Sets the withdrawal limits for the contract + /// @param new_withdrawal_limit_low The new low limit for withdrawals + /// @param new_withdrawal_limit_high The new high limit for withdrawals + fn _set_withdrawal_limit( + ref self: ContractState, new_withdrawal_limit_low: u256, new_withdrawal_limit_high: u256 + ) { + assert(new_withdrawal_limit_low > 0, Errors::ZERO_AMOUNT); + assert(new_withdrawal_limit_high > new_withdrawal_limit_low, Errors::INVALID_LIMIT); + self.withdrawal_limit_low.write(new_withdrawal_limit_low); + self.withdrawal_limit_high.write(new_withdrawal_limit_high); + } + + /// @notice Sets the withdrawal epoch delay for the contract + /// @param new_withdrawal_epoch_delay The new withdrawal epoch delay + fn _set_withdrawal_epoch_delay(ref self: ContractState, new_withdrawal_epoch_delay: u256) { + assert(new_withdrawal_epoch_delay.is_non_zero(), Errors::ZERO_AMOUNT); + self.withdrawal_epoch_delay.write(new_withdrawal_epoch_delay); + } + + /// @notice Sets the dust limit for the contract + /// @param new_dust_limit The new dust limit + fn _set_dust_limit(ref self: ContractState, new_dust_limit: u256) { + assert(new_dust_limit.is_non_zero(), Errors::ZERO_AMOUNT); + self.dust_limit.write(new_dust_limit); + } + + fn _check_profit_and_mint(ref self: ContractState, profit: u256, token: ContractAddress) { + if (profit > 0) { + let performance_fees = self.performance_fees.read(); + let performance_fees_from_profit = (profit * performance_fees) / CONSTANTS::WAD; + let shares_to_mint = self._convert_to_shares(performance_fees_from_profit); + let pooling_manager = self.pooling_manager.read(); + let pooling_manager_disp = IPoolingManagerDispatcher { + contract_address: pooling_manager + }; + let fees_recipient = pooling_manager_disp.fees_recipient(); + let token_disp = ITokenDispatcher { contract_address: token }; + token_disp.mint(fees_recipient, shares_to_mint); + } + } + } +} diff --git a/src/pooling_manager/interface.cairo b/src/pooling_manager/interface.cairo index 454be55..2214baf 100644 --- a/src/pooling_manager/interface.cairo +++ b/src/pooling_manager/interface.cairo @@ -8,6 +8,12 @@ struct StrategyReportL1 { underlying_bridged_amount: u256 } +#[derive(Copy, Drop, Serde)] +struct BridgeInteractionInfo { + l1_bridge: felt252, + amount: u256 +} + #[starknet::interface] trait IPoolingManager { fn factory(self: @TContractState) -> ContractAddress; @@ -16,15 +22,34 @@ trait IPoolingManager { self: @TContractState, l1_strategy: EthAddress ) -> ContractAddress; fn underlying_to_bridge(self: @TContractState, underlying: ContractAddress) -> ContractAddress; + fn l2_bridge_to_l1_bridge(self: @TContractState, bridge: ContractAddress) -> felt252; + + fn l1_pooling_manager(self: @TContractState) -> EthAddress; fn is_initialised(self: @TContractState) -> bool; + fn hash_l1_data(self: @TContractState, calldata: Span) -> u256; - fn hash_l2_data(self: @TContractState, calldata: Span) -> u256; + + fn hash_l2_data( + self: @TContractState, + new_epoch: u256, + bridge_deposit_info: Span, + strategy_report_l2: Span, + bridge_withdrawal_info: Span + ) -> u256; + fn l1_report_hash(self: @TContractState, general_epoch: u256) -> u256; + fn general_epoch(self: @TContractState) -> u256; fn pending_strategies_to_initialize(self: @TContractState) -> Array; fn set_fees_recipient(ref self: TContractState, new_fees_recipient: ContractAddress); fn set_l1_pooling_manager(ref self: TContractState, new_l1_pooling_manager: EthAddress); fn set_factory(ref self: TContractState, new_factory: ContractAddress); + fn set_allowance( + ref self: TContractState, + spender: ContractAddress, + token_address: ContractAddress, + amount: u256 + ); fn handle_mass_report(ref self: TContractState, calldata: Span); fn register_strategy( ref self: TContractState, @@ -38,8 +63,13 @@ trait IPoolingManager { min_withdrawal: u256, max_withdrawal: u256 ); + fn delete_all_pending_strategy(ref self: TContractState); + fn register_underlying( - ref self: TContractState, underlying: ContractAddress, bridge: ContractAddress + ref self: TContractState, + underlying: ContractAddress, + bridge: ContractAddress, + l1_bridge: felt252, ); fn emit_deposit_limit_updated_event( ref self: TContractState, diff --git a/src/pooling_manager/pooling_manager.cairo b/src/pooling_manager/pooling_manager.cairo index a68629e..91f2896 100644 --- a/src/pooling_manager/pooling_manager.cairo +++ b/src/pooling_manager/pooling_manager.cairo @@ -2,11 +2,11 @@ mod PoolingManager { // Starknet import use starknet::{ - ContractAddress, get_caller_address, eth_address::EthAddress, Zeroable, ClassHash, - syscalls::{send_message_to_l1_syscall} + ContractAddress, get_caller_address, eth_address::{EthAddress, EthAddressZeroable}, + Zeroable, ClassHash, syscalls::{send_message_to_l1_syscall} }; use core::nullable::{nullable_from_box, match_nullable, FromNullableResult}; - + use core::integer::{u128_byte_reverse}; // OZ import use openzeppelin::access::accesscontrol::{ @@ -14,10 +14,12 @@ mod PoolingManager { }; use openzeppelin::token::erc20::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; use openzeppelin::introspection::src5::SRC5Component; - //use array::{ArrayTrait, ArrayDefault}; + use openzeppelin::upgrades::UpgradeableComponent; // Local import - use nimbora_yields::pooling_manager::interface::{IPoolingManager, StrategyReportL1}; + use nimbora_yields::pooling_manager::interface::{ + IPoolingManager, StrategyReportL1, BridgeInteractionInfo + }; use nimbora_yields::token_manager::interface::{ ITokenManagerDispatcher, ITokenManagerDispatcherTrait, StrategyReportL2 }; @@ -28,6 +30,9 @@ mod PoolingManager { // Components component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + impl InternalUpgradeableImpl = UpgradeableComponent::InternalImpl; #[abi(embed_v0)] impl AccessControlImpl = @@ -40,10 +45,13 @@ mod PoolingManager { accesscontrol: AccessControlComponent::Storage, #[substorage(v0)] src5: SRC5Component::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, factory: ContractAddress, fees_recipient: ContractAddress, l1_pooling_manager: EthAddress, underlying_to_bridge: LegacyMap, + l2_bridge_to_l1_bridge: LegacyMap, l1_strategy_to_token_manager: LegacyMap, l1_report_hash: LegacyMap, pending_strategies_to_initialize: LegacyMap, @@ -58,6 +66,8 @@ mod PoolingManager { AccessControlEvent: AccessControlComponent::Event, #[flat] SRC5Event: SRC5Component::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, FactoryUpdated: FactoryUpdated, FeesRecipientUpdated: FeesRecipientUpdated, StrategyRegistered: StrategyRegistered, @@ -99,7 +109,10 @@ mod PoolingManager { #[derive(Drop, starknet::Event)] struct NewL2Report { - new_l2_report: Array + new_epoch: u256, + new_bridge_deposit: Array, + new_l2_report: Array, + new_bridge_withdraw: Array } @@ -120,7 +133,8 @@ mod PoolingManager { #[derive(Drop, starknet::Event)] struct UnderlyingRegistered { underlying: ContractAddress, - bridge: ContractAddress + bridge: ContractAddress, + l1_bridge: felt252 } @@ -211,9 +225,10 @@ mod PoolingManager { const UNKNOWN_STRATEGY: felt252 = 'Unknown strategy'; const TOTAL_ASSETS_NUL: felt252 = 'Total assets nul'; const NOT_INITIALISED: felt252 = 'Not initialised'; + const BUFFER_NUL: felt252 = 'Buffer is nul'; + const INVALID_EPOCH: felt252 = 'Invalid Epoch'; } - #[constructor] /// @notice Constructor for initializing the contract /// @param owner The address of the contract owner #[constructor] @@ -222,15 +237,43 @@ mod PoolingManager { self.accesscontrol._grant_role(0, owner); } + /// @notice Upgrade contract + /// @param New contract class hash + #[external(v0)] + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.accesscontrol.assert_only_role(0); + self.upgradeable._upgrade(new_class_hash); + } + + + #[external(v0)] + fn get_pending_strategies_len(self: @ContractState) -> u256 { + self.pending_strategies_to_initialize_len.read() + } + + #[external(v0)] + fn set_pending_strategies_len(ref self: ContractState, len: u256) { + self.accesscontrol.assert_only_role(0); + self.pending_strategies_to_initialize_len.write(len); + } + + fn reverse_endianness(value: u256) -> u256 { + let new_low = u128_byte_reverse(value.high); + let new_high = u128_byte_reverse(value.low); + u256 { low: new_low, high: new_high } + } + /// @notice Handler for incoming messages from L1 contract /// @dev This function should only be called by the authorized L1 pooling manager /// @param from_address The address of the sender from L1 + /// @param epoch The epoch of the report from L1 /// @param hash The hash of the report from L1 #[l1_handler] - fn handle_response(ref self: ContractState, from_address: felt252, hash: u256) { + fn handle_response(ref self: ContractState, from_address: felt252, epoch: u256, hash: u256) { let l1_pooling_manager = self.l1_pooling_manager.read(); assert(l1_pooling_manager.into() == from_address, Errors::INVALID_CALLER); let general_epoch = self.general_epoch.read(); + assert(general_epoch == epoch, Errors::INVALID_EPOCH); let l1_report_hash = self.l1_report_hash.read(general_epoch); assert(l1_report_hash.is_zero(), Errors::PENDING_HASH); self.l1_report_hash.write(general_epoch, hash); @@ -282,6 +325,13 @@ mod PoolingManager { self.underlying_to_bridge.read(underlying) } + /// @notice Maps the l2 bridge to the l1 corresponding bridge + /// @param bridge The address of the l2 bridge + /// @return The address of the corresponding l1 bridge + fn l2_bridge_to_l1_bridge(self: @ContractState, bridge: ContractAddress) -> felt252 { + self.l2_bridge_to_l1_bridge.read(bridge) + } + /// @notice Reads the L1 report hash for a given epoch /// @param general_epoch The epoch for which to retrieve the hash /// @return The L1 report hash for the specified epoch @@ -297,12 +347,29 @@ mod PoolingManager { } /// @notice Generates a hash from L2 data - /// @param calldata The L2 strategy report data to be hashed - /// @return The hash of the provided L2 data - fn hash_l2_data(self: @ContractState, calldata: Span) -> u256 { - self._hash_l2_data(calldata) + /// @param new_epoch of pooling manager + /// @param bridge_deposit_info Span of StrategyReportL2 data + /// @param strategy_report_l2 Span of StrategyReportL2 data + /// @param bridge_withdrawal_info Span of StrategyReportL2 data + /// @return Hash of the L2 data + fn hash_l2_data( + self: @ContractState, + new_epoch: u256, + bridge_deposit_info: Span, + strategy_report_l2: Span, + bridge_withdrawal_info: Span + ) -> u256 { + self + ._hash_l2_data( + new_epoch, bridge_deposit_info, strategy_report_l2, bridge_withdrawal_info + ) } + /// @notice Reads the general epoch + /// @return The general epoch + fn general_epoch(self: @ContractState) -> u256 { + self.general_epoch.read() + } /// @notice Retrieves the list of pending strategies to be initialized /// @return Array of Ethereum addresses representing the pending strategies @@ -340,6 +407,21 @@ mod PoolingManager { self.emit(FactoryUpdated { new_factory: new_factory }); } + /// @notice Allowance for + /// @dev This function can only be called by an account with the appropriate role + /// @param new_l1_pooling_manager The new L1 pooling manager address + fn set_allowance( + ref self: ContractState, + spender: ContractAddress, + token_address: ContractAddress, + amount: u256 + ) { + self.accesscontrol.assert_only_role(0); + assert(spender.is_non_zero() && token_address.is_non_zero(), Errors::ZERO_ADDRESS); + let underlying_disp = ERC20ABIDispatcher { contract_address: token_address }; + underlying_disp.approve(spender, amount); + } + /// @notice Registers a new strategy within the contract /// @dev This function can only be called by the factory /// @param token_manager_deployed_address The deployed address of the token manager associated with the strategy @@ -411,12 +493,26 @@ mod PoolingManager { /// It ensures that the underlying asset can be bridged properly and is a critical part of setting up the contract's infrastructure. /// The function also emits an event upon successful registration. fn register_underlying( - ref self: ContractState, underlying: ContractAddress, bridge: ContractAddress + ref self: ContractState, + underlying: ContractAddress, + bridge: ContractAddress, + l1_bridge: felt252 ) { self.accesscontrol.assert_only_role(0); - assert(underlying.is_non_zero() && bridge.is_non_zero(), Errors::ZERO_ADDRESS); + assert( + underlying.is_non_zero() && bridge.is_non_zero() && l1_bridge.is_non_zero(), + Errors::ZERO_ADDRESS + ); + let bridge_disp = ITokenBridgeDispatcher { contract_address: bridge }; self.underlying_to_bridge.write(underlying, bridge); - self.emit(UnderlyingRegistered { underlying: underlying, bridge: bridge }); + // let l1_bridge = bridge_disp.get_l1_bridge(); + self.l2_bridge_to_l1_bridge.write(bridge, l1_bridge); + self + .emit( + UnderlyingRegistered { + underlying: underlying, bridge: bridge, l1_bridge: l1_bridge + } + ); } /// @notice Handles a mass report of L1 strategy data, processing and updating L2 state accordingly @@ -434,22 +530,26 @@ mod PoolingManager { if (general_epoch.is_non_zero()) { assert(l1_report_hash.is_non_zero(), Errors::NO_L1_REPORT); + let calldata_hash = self._hash_l1_data(calldata); + assert(calldata_hash == l1_report_hash, Errors::INVALID_DATA); } else { + assert(calldata.len() == 0, Errors::INVALID_DATA); let is_initialised = self._is_initialised(); assert(is_initialised, Errors::NOT_INITIALISED); } - let calldata_hash = self._hash_l1_data(calldata); - assert(calldata_hash == l1_report_hash, Errors::INVALID_DATA); - let full_strategy_report_l1 = self._add_pending_strategies_to_initialize(calldata); let array_len = full_strategy_report_l1.len(); assert(array_len.is_non_zero(), Errors::EMPTY_ARRAY); - let mut ret_array = ArrayTrait::new(); - let mut dict_bridge_keys = ArrayTrait::new(); - let mut bridge_amount: Felt252Dict> = Default::default(); + let mut strategy_report_l2_array = ArrayTrait::new(); + + let mut dict_bridge_deposit_keys = ArrayTrait::new(); + let mut bridge_deposit_amount: Felt252Dict> = Default::default(); + + let mut dict_bridge_withdrawal_keys = ArrayTrait::new(); + let mut bridge_withdrawal_amount: Felt252Dict> = Default::default(); let mut i = 0; loop { @@ -466,54 +566,133 @@ mod PoolingManager { } let ret = strategy_disp .handle_report(elem.l1_net_asset_value, elem.underlying_bridged_amount); - ret_array.append(ret); + strategy_report_l2_array.append(ret); + if (ret.action_id == 0) { let bridge = self.underlying_to_bridge.read(underlying); - let val = bridge_amount.get(bridge.into()); - let current_bridge_amount: u256 = match match_nullable(val) { + let val = bridge_deposit_amount.get(bridge.into()); + let current_bridge_deposit_amount: u256 = match match_nullable(val) { + FromNullableResult::Null => 0, + FromNullableResult::NotNull(val) => val.unbox(), + }; + if (current_bridge_deposit_amount.is_zero()) { + let new_amount = nullable_from_box(BoxTrait::new(ret.amount)); + dict_bridge_deposit_keys.append(bridge); + bridge_deposit_amount.insert(bridge.into(), new_amount); + } else { + let new_amount = nullable_from_box( + BoxTrait::new(ret.amount + current_bridge_deposit_amount) + ); + bridge_deposit_amount.insert(bridge.into(), new_amount); + } + } + + if (ret.action_id == 2) { + let bridge = self.underlying_to_bridge.read(underlying); + let val = bridge_withdrawal_amount.get(bridge.into()); + let current_bridge_withdrawal_amount: u256 = match match_nullable(val) { FromNullableResult::Null => 0, FromNullableResult::NotNull(val) => val.unbox(), }; - if (current_bridge_amount.is_zero()) { + if (current_bridge_withdrawal_amount.is_zero()) { let new_amount = nullable_from_box(BoxTrait::new(ret.amount)); - dict_bridge_keys.append(bridge); - bridge_amount.insert(bridge.into(), new_amount); + dict_bridge_withdrawal_keys.append(bridge); + bridge_withdrawal_amount.insert(bridge.into(), new_amount); } else { let new_amount = nullable_from_box( - BoxTrait::new(ret.amount + current_bridge_amount) + BoxTrait::new(ret.amount + current_bridge_withdrawal_amount) ); - bridge_amount.insert(bridge.into(), new_amount); + bridge_withdrawal_amount.insert(bridge.into(), new_amount); } } i += 1; }; let l1_pooling_manager = self.l1_pooling_manager.read(); + + let mut bridge_deposit_info = ArrayTrait::new(); let mut j = 0; - let dict_bridge_keys_len = dict_bridge_keys.len(); + let dict_bridge_deposit_keys_len = dict_bridge_deposit_keys.len(); loop { - if (j == dict_bridge_keys_len) { + if (j == dict_bridge_deposit_keys_len) { break (); } - let bridge_address = *dict_bridge_keys.at(j); - let val = bridge_amount.get(bridge_address.into()); + let bridge_address = *dict_bridge_deposit_keys.at(j); + let val = bridge_deposit_amount.get(bridge_address.into()); let amount: u256 = match match_nullable(val) { FromNullableResult::Null => 0, FromNullableResult::NotNull(val) => val.unbox(), }; let bridge_disp = ITokenBridgeDispatcher { contract_address: bridge_address }; bridge_disp.initiate_withdraw(l1_pooling_manager.into(), amount); + + let l1_bridge = self.l2_bridge_to_l1_bridge.read(bridge_address); + bridge_deposit_info + .append(BridgeInteractionInfo { l1_bridge: l1_bridge, amount: amount }); j += 1; }; - let ret_hash = self._hash_l2_data(ret_array.span()); + let mut bridge_withdrawal_info = ArrayTrait::new(); + j = 0; + let dict_bridge_withdrawal_keys_len = dict_bridge_withdrawal_keys.len(); + loop { + if (j == dict_bridge_withdrawal_keys_len) { + break (); + } + let bridge_address = *dict_bridge_withdrawal_keys.at(j); + let val = bridge_withdrawal_amount.get(bridge_address.into()); + let amount: u256 = match match_nullable(val) { + FromNullableResult::Null => 0, + FromNullableResult::NotNull(val) => val.unbox(), + }; + + let l1_bridge = self.l2_bridge_to_l1_bridge.read(bridge_address); + bridge_withdrawal_info + .append(BridgeInteractionInfo { l1_bridge: l1_bridge, amount: amount }); + j += 1; + }; + + let new_epoch = general_epoch + 1; + self.general_epoch.write(new_epoch); + + let ret_hash = self + ._hash_l2_data( + new_epoch, + bridge_deposit_info.span(), + strategy_report_l2_array.span(), + bridge_withdrawal_info.span() + ); let mut message_payload: Array = ArrayTrait::new(); message_payload.append(ret_hash.low.into()); message_payload.append(ret_hash.high.into()); send_message_to_l1_syscall( to_address: l1_pooling_manager.into(), payload: message_payload.span() ); - self.emit(NewL2Report { new_l2_report: ret_array }); + self + .emit( + NewL2Report { + new_epoch: new_epoch, + new_bridge_deposit: bridge_deposit_info, + new_l2_report: strategy_report_l2_array, + new_bridge_withdraw: bridge_withdrawal_info + } + ); + } + + fn delete_all_pending_strategy(ref self: ContractState) { + self.accesscontrol.assert_only_role(0); + let mut i = 0; + let pending_strategies_to_initialize_len = self + .pending_strategies_to_initialize_len + .read(); + loop { + if (i == pending_strategies_to_initialize_len) { + break (); + } + self.pending_strategies_to_initialize.write(i, EthAddressZeroable::zero()); + i += 1; + }; + self.pending_strategies_to_initialize_len.write(0); } @@ -783,24 +962,58 @@ mod PoolingManager { } /// @notice Converts a span of L2 strategy reports to a span of u256 - /// @param calldata Span of StrategyReportL2 data + /// @param new_epoch of pooling manager + /// @param bridge_deposit_info Span of StrategyReportL2 data + /// @param strategy_report_l2 Span of StrategyReportL2 data + /// @param bridge_withdrawal_info Span of StrategyReportL2 data /// @return Span of u256 values representing the L2 strategy reports fn _strategy_report_l2_to_u256_span( - self: @ContractState, calldata: Span + self: @ContractState, + new_epoch: u256, + bridge_deposit_info: Span, + strategy_report_l2: Span, + bridge_withdrawal_info: Span ) -> Span { let mut ret_array = ArrayTrait::new(); - let array_len = calldata.len(); + ret_array.append(new_epoch); + let mut array_len = bridge_deposit_info.len(); let mut i = 0; loop { if (i == array_len) { break (); } - let elem = *calldata.at(i); - let l1_strategy_felt: felt252 = elem.l1_strategy.into(); + let bridge_deposit_info_elem = *bridge_deposit_info.at(i); + let l1_bridge_u256: u256 = bridge_deposit_info_elem.l1_bridge.into(); + ret_array.append(l1_bridge_u256); + ret_array.append(bridge_deposit_info_elem.amount); + i += 1; + }; + + array_len = strategy_report_l2.len(); + i = 0; + loop { + if (i == array_len) { + break (); + } + let strategy_report_l2_elem = *strategy_report_l2.at(i); + let l1_strategy_felt: felt252 = strategy_report_l2_elem.l1_strategy.into(); let l1_strategy_u256: u256 = l1_strategy_felt.into(); ret_array.append(l1_strategy_u256); - ret_array.append(elem.action_id); - ret_array.append(elem.amount); + ret_array.append(strategy_report_l2_elem.action_id); + ret_array.append(strategy_report_l2_elem.amount); + i += 1; + }; + + array_len = bridge_withdrawal_info.len(); + i = 0; + loop { + if (i == array_len) { + break (); + } + let bridge_withdrawal_info_elem = *bridge_withdrawal_info.at(i); + let l1_bridge_u256: u256 = bridge_withdrawal_info_elem.l1_bridge.into(); + ret_array.append(l1_bridge_u256); + ret_array.append(bridge_withdrawal_info_elem.amount); i += 1; }; ret_array.span() @@ -810,16 +1023,30 @@ mod PoolingManager { /// @param calldata Span of StrategyReportL1 data /// @return Hash of the L1 data fn _hash_l1_data(self: @ContractState, calldata: Span) -> u256 { - let calldata_span = self._strategy_report_l1_to_u256_span(calldata); - keccak::keccak_u256s_le_inputs(calldata_span) + let u256_span = self._strategy_report_l1_to_u256_span(calldata); + let hash = keccak::keccak_u256s_be_inputs(u256_span); + reverse_endianness(hash) } /// @notice Generates a hash from L2 data - /// @param calldata Span of StrategyReportL2 data + /// @param new_epoch of pooling manager + /// @param bridge_deposit_info Span of StrategyReportL2 data + /// @param strategy_report_l2 Span of StrategyReportL2 data + /// @param bridge_withdrawal_info Span of StrategyReportL2 data /// @return Hash of the L2 data - fn _hash_l2_data(self: @ContractState, calldata: Span) -> u256 { - let calldata_span = self._strategy_report_l2_to_u256_span(calldata); - keccak::keccak_u256s_le_inputs(calldata_span) + fn _hash_l2_data( + self: @ContractState, + new_epoch: u256, + bridge_deposit_info: Span, + strategy_report_l2: Span, + bridge_withdrawal_info: Span + ) -> u256 { + let u256_span = self + ._strategy_report_l2_to_u256_span( + new_epoch, bridge_deposit_info, strategy_report_l2, bridge_withdrawal_info + ); + let hash = keccak::keccak_u256s_be_inputs(u256_span); + reverse_endianness(hash) } // @notice Retrieves pending strategies to be initialized @@ -841,15 +1068,37 @@ mod PoolingManager { ret_array } + // @notice Retrieves pending strategies to be initialized and delete + /// @return Array of Ethereum addresses representing the pending strategies + fn _get_pending_strategies_and_del(ref self: ContractState) -> Array { + let mut i = 0; + let pending_strategies_to_initialize_len = self + .pending_strategies_to_initialize_len + .read(); + let mut ret_array = ArrayTrait::new(); + loop { + if (i == pending_strategies_to_initialize_len) { + break (); + } + let elem = self.pending_strategies_to_initialize.read(i); + ret_array.append(elem); + self.pending_strategies_to_initialize.write(i, EthAddressZeroable::zero()); + i += 1; + }; + self.pending_strategies_to_initialize_len.write(0); + + ret_array + } + /// @notice Adds pending strategies to be initialized to the span of StrategyReportL1 data /// @param calldata Span of StrategyReportL1 data /// @return Span of StrategyReportL1 data including pending strategies fn _add_pending_strategies_to_initialize( - self: @ContractState, calldata: Span + ref self: ContractState, calldata: Span ) -> Span { let mut ret_array = ArrayTrait::new(); let calldata_len = calldata.len(); - let pending_strategies_to_initialize = self._pending_strategies_to_initialize().span(); + let pending_strategies_to_initialize = self._get_pending_strategies_and_del().span(); let pending_strategies_to_initialize_len = pending_strategies_to_initialize.len(); let mut i = 0; loop { @@ -865,13 +1114,13 @@ mod PoolingManager { if (i == pending_strategies_to_initialize_len) { break (); } - let mut l1_strategy = self.pending_strategies_to_initialize.read(i.into()); + let mut l1_strategy = *pending_strategies_to_initialize.at(i.into()); let token_manager = self.l1_strategy_to_token_manager.read(l1_strategy); let token_manager_disp = ITokenManagerDispatcher { contract_address: token_manager }; - let total_assets = token_manager_disp.total_assets(); - assert(total_assets.is_non_zero(), Errors::TOTAL_ASSETS_NUL); + let buffer = token_manager_disp.buffer(); + assert(buffer.is_non_zero(), Errors::BUFFER_NUL); let new_elem = StrategyReportL1 { l1_strategy: l1_strategy, l1_net_asset_value: 0, underlying_bridged_amount: 0 }; diff --git a/src/tests.cairo b/src/tests.cairo index 5857229..02c3744 100644 --- a/src/tests.cairo +++ b/src/tests.cairo @@ -1 +1,3 @@ mod test; +mod test_token_manager; +mod test_utils; diff --git a/src/tests/test.cairo b/src/tests/test.cairo index 382d75d..2486892 100644 --- a/src/tests/test.cairo +++ b/src/tests/test.cairo @@ -1,712 +1,744 @@ -// Nimbora yields contracts -use nimbora_yields::pooling_manager::pooling_manager::{PoolingManager}; -use nimbora_yields::pooling_manager::interface::{ - IPoolingManagerDispatcher, IPoolingManagerDispatcherTrait, StrategyReportL1 -}; -use nimbora_yields::factory::factory::{Factory}; -use nimbora_yields::factory::interface::{IFactoryDispatcher, IFactoryDispatcherTrait}; -use nimbora_yields::token_manager::token_manager::{TokenManager}; -use nimbora_yields::token_manager::interface::{ - ITokenManagerDispatcher, ITokenManagerDispatcherTrait, WithdrawalInfo, StrategyReportL2 -}; - -// Utils peripheric contracts -use nimbora_yields::token_bridge::token_bridge::{TokenBridge}; -use nimbora_yields::token_bridge::token_mock::{TokenMock}; -use nimbora_yields::token_bridge::interface::{ - ITokenBridgeDispatcher, IMintableTokenDispatcher, IMintableTokenDispatcherTrait -}; - -use openzeppelin::{ - token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}, - access::accesscontrol::{ - AccessControlComponent, interface::{IAccessControlDispatcher, IAccessControlDispatcherTrait} - } -}; - -use starknet::{ - get_contract_address, deploy_syscall, ClassHash, contract_address_const, ContractAddress, - get_block_timestamp, EthAddress, Zeroable -}; -use starknet::class_hash::Felt252TryIntoClassHash; -use starknet::account::{Call}; -use snforge_std::{ - declare, ContractClassTrait, start_prank, CheatTarget, ContractClass, PrintTrait, stop_prank, - start_warp, stop_warp -}; - - -fn deploy_tokens( - initial_supply: u256, recipient: ContractAddress -) -> (ERC20ABIDispatcher, ERC20ABIDispatcher, ERC20ABIDispatcher) { - let contract = declare('TokenMock'); - - let mut constructor_args: Array = ArrayTrait::new(); - Serde::serialize(@initial_supply, ref constructor_args); - Serde::serialize(@recipient, ref constructor_args); - let contract_address_1 = contract.deploy(@constructor_args).unwrap(); - let contract_address_2 = contract.deploy(@constructor_args).unwrap(); - let contract_address_3 = contract.deploy(@constructor_args).unwrap(); - - return ( - ERC20ABIDispatcher { contract_address: contract_address_1 }, - ERC20ABIDispatcher { contract_address: contract_address_2 }, - ERC20ABIDispatcher { contract_address: contract_address_3 } - ); -} +#[cfg(test)] +mod tests { + use core::option::OptionTrait; + use core::traits::TryInto; + use core::traits::Into; + // Nimbora yields contracts + use nimbora_yields::pooling_manager::pooling_manager::{PoolingManager}; + use nimbora_yields::pooling_manager::interface::{ + IPoolingManagerDispatcher, IPoolingManagerDispatcherTrait, StrategyReportL1 + }; + use nimbora_yields::factory::factory::{Factory}; + use nimbora_yields::factory::interface::{IFactoryDispatcher, IFactoryDispatcherTrait}; + use nimbora_yields::token_manager::token_manager::{TokenManager}; + use nimbora_yields::token_manager::interface::{ + ITokenManagerDispatcher, ITokenManagerDispatcherTrait, WithdrawalInfo, StrategyReportL2 + }; + // Utils peripheric contracts + use nimbora_yields::token_bridge::token_bridge::{TokenBridge}; + use nimbora_yields::token_bridge::token_mock::{TokenMock}; + use nimbora_yields::token_bridge::interface::{ + ITokenBridgeDispatcher, IMintableTokenDispatcher, IMintableTokenDispatcherTrait + }; -fn deploy_token_bridges( - l2_address_1: ContractAddress, - l1_bridge_1: felt252, - l2_address_2: ContractAddress, - l1_bridge_2: felt252, - l2_address_3: ContractAddress, - l1_bridge_3: felt252 -) -> (ITokenBridgeDispatcher, ITokenBridgeDispatcher, ITokenBridgeDispatcher) { - let contract = declare('TokenBridge'); - - let mut constructor_args_1: Array = ArrayTrait::new(); - Serde::serialize(@l2_address_1, ref constructor_args_1); - Serde::serialize(@l1_bridge_1, ref constructor_args_1); - let contract_address_1 = contract.deploy(@constructor_args_1).unwrap(); - - let mut constructor_args_2: Array = ArrayTrait::new(); - Serde::serialize(@l2_address_2, ref constructor_args_2); - Serde::serialize(@l1_bridge_2, ref constructor_args_2); - let contract_address_2 = contract.deploy(@constructor_args_2).unwrap(); - - let mut constructor_args_3: Array = ArrayTrait::new(); - Serde::serialize(@l2_address_3, ref constructor_args_3); - Serde::serialize(@l1_bridge_3, ref constructor_args_3); - let contract_address_3 = contract.deploy(@constructor_args_3).unwrap(); - - return ( - ITokenBridgeDispatcher { contract_address: contract_address_1 }, - ITokenBridgeDispatcher { contract_address: contract_address_2 }, - ITokenBridgeDispatcher { contract_address: contract_address_3 } - ); -} + use openzeppelin::{ + token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}, + access::accesscontrol::{ + AccessControlComponent, + interface::{IAccessControlDispatcher, IAccessControlDispatcherTrait} + }, + upgrades::interface::{IUpgradeableDispatcher, IUpgradeable, IUpgradeableDispatcherTrait} + }; -fn deploy_pooling_manager(owner: ContractAddress) -> IPoolingManagerDispatcher { - let contract = declare('PoolingManager'); - let mut constructor_args: Array = ArrayTrait::new(); - Serde::serialize(@owner, ref constructor_args); - let contract_address = contract.deploy(@constructor_args).unwrap(); - return IPoolingManagerDispatcher { contract_address: contract_address }; -} + use starknet::{ + get_contract_address, deploy_syscall, ClassHash, contract_address_const, ContractAddress, + get_block_timestamp, EthAddress, Zeroable + }; + use starknet::class_hash::Felt252TryIntoClassHash; + use starknet::account::{Call}; + use snforge_std::{ + declare, ContractClassTrait, get_class_hash, start_prank, CheatTarget, ContractClass, + PrintTrait, stop_prank, start_warp, stop_warp + }; -fn deploy_factory( - pooling_manager: ContractAddress, - token_class_hash: ClassHash, - token_manager_class_hash: ClassHash -) -> IFactoryDispatcher { - let contract = declare('Factory'); - let mut constructor_args: Array = ArrayTrait::new(); - Serde::serialize(@pooling_manager, ref constructor_args); - Serde::serialize(@token_class_hash, ref constructor_args); - Serde::serialize(@token_manager_class_hash, ref constructor_args); - let contract_address = contract.deploy(@constructor_args).unwrap(); - return IFactoryDispatcher { contract_address: contract_address }; -} + use nimbora_yields::tests::test_utils::{ + deploy_tokens, deploy_factory, deploy_pooling_manager, deploy_strategy, setup_0, setup_1, + setup_2 + }; -fn setup_0() -> ( - ContractAddress, - ContractAddress, - EthAddress, - IPoolingManagerDispatcher, - IFactoryDispatcher, - ClassHash, - ClassHash -) { - let owner = contract_address_const::<2300>(); - let fees_recipient = contract_address_const::<2400>(); - let l1_pooling_manager: EthAddress = 1.try_into().unwrap(); - let pooling_manager = deploy_pooling_manager(owner); - let token_hash = declare('Token'); - let token_manager_hash = declare('TokenManager'); - let factory = deploy_factory( - pooling_manager.contract_address, token_hash.class_hash, token_manager_hash.class_hash - ); - ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash.class_hash, - token_manager_hash.class_hash - ) -} + #[test] + fn deploy() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + // Constructor state update test + let pooling_manager_access_disp = IAccessControlDispatcher { + contract_address: pooling_manager.contract_address + }; + let has_role = pooling_manager_access_disp.has_role(0, owner); + let token_hash_from_pooling_manager = factory.token_class_hash(); + let token_manager_from_pooling_manager = factory.token_manager_class_hash(); + assert(has_role == true, 'Invalid owner role'); + assert(token_hash_from_pooling_manager == token_hash, 'Invalid token hash'); + assert( + token_manager_from_pooling_manager == token_manager_hash, 'Invalid token manager hash' + ); + } -#[test] -fn deploy() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - // Constructor state update test - let pooling_manager_access_disp = IAccessControlDispatcher { - contract_address: pooling_manager.contract_address - }; - let has_role = pooling_manager_access_disp.has_role(0, owner); - let token_hash_from_pooling_manager = factory.token_class_hash(); - let token_manager_from_pooling_manager = factory.token_manager_class_hash(); - assert(has_role == true, 'Invalid owner role'); - assert(token_hash_from_pooling_manager == token_hash, 'Invalid token hash'); - assert(token_manager_from_pooling_manager == token_manager_hash, 'Invalid token manager hash'); -} + #[test] + #[should_panic(expected: ('Caller is missing role',))] + fn set_fees_recipient_wrong_caller() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + pooling_manager.set_fees_recipient(fees_recipient); + } -#[test] -#[should_panic(expected: ('Caller is missing role',))] -fn set_fees_recipient_wrong_caller() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - pooling_manager.set_fees_recipient(fees_recipient); -} + #[test] + #[should_panic(expected: ('Zero address',))] + fn set_fees_recipient_zero_address() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + pooling_manager.set_fees_recipient(Zeroable::zero()); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + } -#[test] -#[should_panic(expected: ('Zero address',))] -fn set_fees_recipient_zero_address() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - start_prank(CheatTarget::One(pooling_manager.contract_address), owner); - pooling_manager.set_fees_recipient(Zeroable::zero()); - stop_prank(CheatTarget::One(pooling_manager.contract_address)); -} + #[test] + fn set_fees_recipient() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + pooling_manager.set_fees_recipient(fees_recipient); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + + let fees_recipient_from_pooling_manager = pooling_manager.fees_recipient(); + assert(fees_recipient_from_pooling_manager == fees_recipient, 'invalid fees recipient'); + } -#[test] -fn set_fees_recipient() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - start_prank(CheatTarget::One(pooling_manager.contract_address), owner); - pooling_manager.set_fees_recipient(fees_recipient); - stop_prank(CheatTarget::One(pooling_manager.contract_address)); - - let fees_recipient_from_pooling_manager = pooling_manager.fees_recipient(); - assert(fees_recipient_from_pooling_manager == fees_recipient, 'invalid fees recipient'); -} + #[test] + #[should_panic(expected: ('Caller is missing role',))] + fn set_l1_pooling_manager_wrong_caller() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + pooling_manager.set_l1_pooling_manager(l1_pooling_manager); + } -#[test] -#[should_panic(expected: ('Caller is missing role',))] -fn set_l1_pooling_manager_wrong_caller() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - pooling_manager.set_l1_pooling_manager(l1_pooling_manager); -} + #[test] + #[should_panic(expected: ('Zero address',))] + fn set_l1_pooling_manager_zero_address() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + pooling_manager.set_l1_pooling_manager(Zeroable::zero()); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + } -#[test] -#[should_panic(expected: ('Zero address',))] -fn set_l1_pooling_manager_zero_address() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - start_prank(CheatTarget::One(pooling_manager.contract_address), owner); - pooling_manager.set_l1_pooling_manager(Zeroable::zero()); - stop_prank(CheatTarget::One(pooling_manager.contract_address)); -} + #[test] + fn set_l1_pooling_manager() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + pooling_manager.set_l1_pooling_manager(l1_pooling_manager); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + + let l1_pooling_manager_from_pooling_manager = pooling_manager.l1_pooling_manager(); + assert( + l1_pooling_manager_from_pooling_manager == l1_pooling_manager, + 'invalid l1 pooling manager' + ); + } -#[test] -fn set_l1_pooling_manager() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - start_prank(CheatTarget::One(pooling_manager.contract_address), owner); - pooling_manager.set_l1_pooling_manager(l1_pooling_manager); - stop_prank(CheatTarget::One(pooling_manager.contract_address)); - - let l1_pooling_manager_from_pooling_manager = pooling_manager.l1_pooling_manager(); - assert( - l1_pooling_manager_from_pooling_manager == l1_pooling_manager, 'invalid l1 pooling manager' - ); -} + #[test] + #[should_panic(expected: ('Caller is missing role',))] + fn set_factory_wrong_caller() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + pooling_manager.set_factory(factory.contract_address); + } -#[test] -#[should_panic(expected: ('Caller is missing role',))] -fn set_factory_wrong_caller() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - pooling_manager.set_factory(factory.contract_address); -} + #[test] + #[should_panic(expected: ('Zero address',))] + fn set_factory_zero_address() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + pooling_manager.set_factory(Zeroable::zero()); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + } -#[test] -#[should_panic(expected: ('Zero address',))] -fn set_factory_zero_address() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - start_prank(CheatTarget::One(pooling_manager.contract_address), owner); - pooling_manager.set_factory(Zeroable::zero()); - stop_prank(CheatTarget::One(pooling_manager.contract_address)); -} + #[test] + fn set_factory() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + pooling_manager.set_factory(factory.contract_address); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + + let factory_from_pooling_manager = pooling_manager.factory(); + assert(factory_from_pooling_manager == factory.contract_address, 'invalid factory'); + } -#[test] -fn set_factory() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - start_prank(CheatTarget::One(pooling_manager.contract_address), owner); - pooling_manager.set_factory(factory.contract_address); - stop_prank(CheatTarget::One(pooling_manager.contract_address)); - - let factory_from_pooling_manager = pooling_manager.factory(); - assert(factory_from_pooling_manager == factory.contract_address, 'invalid factory'); -} + #[test] + #[should_panic(expected: ('Not initialised',))] + fn handle_mass_report_not_initialised() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let empty_array: Array = ArrayTrait::new(); + pooling_manager.handle_mass_report(empty_array.span()); + } -#[test] -#[should_panic(expected: ('Not initialised',))] -fn handle_mass_report_not_initialised() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - let empty_array: Array = ArrayTrait::new(); - pooling_manager.handle_mass_report(empty_array.span()); -} + #[test] + fn is_initialised() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + pooling_manager.set_factory(factory.contract_address); + pooling_manager.set_fees_recipient(fees_recipient); + pooling_manager.set_l1_pooling_manager(l1_pooling_manager); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + + let is_initialised = pooling_manager.is_initialised(); + assert(is_initialised == true, 'initialisation failed'); + } -#[test] -fn is_initialised() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - start_prank(CheatTarget::One(pooling_manager.contract_address), owner); - pooling_manager.set_factory(factory.contract_address); - pooling_manager.set_fees_recipient(fees_recipient); - pooling_manager.set_l1_pooling_manager(l1_pooling_manager); - stop_prank(CheatTarget::One(pooling_manager.contract_address)); - - let is_initialised = pooling_manager.is_initialised(); - assert(is_initialised == true, 'initialisation failed'); -} + #[test] + #[should_panic(expected: ('Invalid caller',))] + fn set_token_class_hash_wrong_caller() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + factory.set_token_class_hash(token_hash); + } -fn setup_1( - owner: ContractAddress, - l1_pooling_manager: EthAddress, - pooling_manager: IPoolingManagerDispatcher, - fees_recipient: ContractAddress, - factory: IFactoryDispatcher -) -> ( - ERC20ABIDispatcher, - ERC20ABIDispatcher, - ERC20ABIDispatcher, - ITokenBridgeDispatcher, - ITokenBridgeDispatcher, - ITokenBridgeDispatcher -) { - // Initialise - start_prank(CheatTarget::One(pooling_manager.contract_address), owner); - pooling_manager.set_fees_recipient(fees_recipient); - pooling_manager.set_l1_pooling_manager(l1_pooling_manager); - pooling_manager.set_factory(factory.contract_address); - stop_prank(CheatTarget::One(pooling_manager.contract_address)); - - // Deploy tokens and bridges - let (l1_bridge_1, l1_bridge_2, l1_bridge_3) = ( - 111.try_into().unwrap(), 112.try_into().unwrap(), 113.try_into().unwrap() - ); - let (token_1, token_2, token_3) = deploy_tokens(1000000000000000000, owner); - let (bridge_1, bridge_2, bridge_3) = deploy_token_bridges( - token_1.contract_address, - l1_bridge_1, - token_2.contract_address, - l1_bridge_2, - token_3.contract_address, - l1_bridge_3 - ); - (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) -} + #[test] + #[should_panic(expected: ('Hash is zero',))] + fn set_token_class_hash_zero_hash() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + start_prank(CheatTarget::One(factory.contract_address), owner); + factory.set_token_class_hash(Zeroable::zero()); + stop_prank(CheatTarget::One(factory.contract_address)); + } + #[test] + fn set_token_class_hash() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( + owner, l1_pooling_manager, pooling_manager, fees_recipient, factory + ); -#[test] -#[should_panic(expected: ('Invalid caller',))] -fn set_token_class_hash_wrong_caller() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - factory.set_token_class_hash(token_hash); -} + start_prank(CheatTarget::One(factory.contract_address), owner); + factory.set_token_class_hash(token_manager_hash); + stop_prank(CheatTarget::One(factory.contract_address)); -#[test] -#[should_panic(expected: ('Hash is zero',))] -fn set_token_class_hash_zero_hash() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - start_prank(CheatTarget::One(factory.contract_address), owner); - factory.set_token_class_hash(Zeroable::zero()); - stop_prank(CheatTarget::One(factory.contract_address)); -} + let token_class_hash_from_factory = factory.token_class_hash(); + assert(token_class_hash_from_factory == token_manager_hash, 'invalid token class hash') + } -#[test] -fn set_token_class_hash() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( - owner, l1_pooling_manager, pooling_manager, fees_recipient, factory - ); - - start_prank(CheatTarget::One(factory.contract_address), owner); - factory.set_token_class_hash(token_manager_hash); - stop_prank(CheatTarget::One(factory.contract_address)); - - let token_class_hash_from_factory = factory.token_class_hash(); - assert(token_class_hash_from_factory == token_manager_hash, 'invalid token class hash') -} + #[test] + #[should_panic(expected: ('Invalid caller',))] + fn set_token_manager_class_hash_wrong_caller() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + factory.set_token_manager_class_hash(token_hash); + } -#[test] -#[should_panic(expected: ('Invalid caller',))] -fn set_token_manager_class_hash_wrong_caller() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - factory.set_token_manager_class_hash(token_hash); -} + #[test] + #[should_panic(expected: ('Hash is zero',))] + fn set_token_manager_class_hash_zero_hash() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + start_prank(CheatTarget::One(factory.contract_address), owner); + factory.set_token_manager_class_hash(Zeroable::zero()); + stop_prank(CheatTarget::One(factory.contract_address)); + } -#[test] -#[should_panic(expected: ('Hash is zero',))] -fn set_token_manager_class_hash_zero_hash() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - start_prank(CheatTarget::One(factory.contract_address), owner); - factory.set_token_manager_class_hash(Zeroable::zero()); - stop_prank(CheatTarget::One(factory.contract_address)); -} + #[test] + fn set_token_manager_class_hash() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( + owner, l1_pooling_manager, pooling_manager, fees_recipient, factory + ); -#[test] -fn set_token_manager_class_hash() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( - owner, l1_pooling_manager, pooling_manager, fees_recipient, factory - ); - - start_prank(CheatTarget::One(factory.contract_address), owner); - factory.set_token_manager_class_hash(token_hash); - stop_prank(CheatTarget::One(factory.contract_address)); - - let token_manager_class_hash_from_factory = factory.token_manager_class_hash(); - assert(token_manager_class_hash_from_factory == token_hash, 'invalid token class hash') -} + start_prank(CheatTarget::One(factory.contract_address), owner); + factory.set_token_manager_class_hash(token_hash); + stop_prank(CheatTarget::One(factory.contract_address)); + let token_manager_class_hash_from_factory = factory.token_manager_class_hash(); + assert(token_manager_class_hash_from_factory == token_hash, 'invalid token class hash') + } -#[test] -#[should_panic(expected: ('Caller is missing role',))] -fn register_underlying_wrong_caller() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( - owner, l1_pooling_manager, pooling_manager, fees_recipient, factory - ); - pooling_manager.register_underlying(token_1.contract_address, bridge_1.contract_address); -} -#[test] -#[should_panic(expected: ('Zero address',))] -fn register_underlying_zero_address_1() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( - owner, l1_pooling_manager, pooling_manager, fees_recipient, factory - ); - - start_prank(CheatTarget::One(pooling_manager.contract_address), owner); - pooling_manager.register_underlying(Zeroable::zero(), bridge_1.contract_address); - stop_prank(CheatTarget::One(pooling_manager.contract_address)); -} + #[test] + #[should_panic(expected: ('Caller is missing role',))] + fn register_underlying_wrong_caller() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( + owner, l1_pooling_manager, pooling_manager, fees_recipient, factory + ); + pooling_manager.register_underlying(token_1.contract_address, bridge_1.contract_address, 5); + } -#[test] -#[should_panic(expected: ('Zero address',))] -fn register_underlying_zero_address_2() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( - owner, l1_pooling_manager, pooling_manager, fees_recipient, factory - ); - - start_prank(CheatTarget::One(pooling_manager.contract_address), owner); - pooling_manager.register_underlying(token_1.contract_address, Zeroable::zero()); - stop_prank(CheatTarget::One(pooling_manager.contract_address)); -} + #[test] + #[should_panic(expected: ('Zero address',))] + fn register_underlying_zero_address_1() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( + owner, l1_pooling_manager, pooling_manager, fees_recipient, factory + ); -#[test] -fn register_underlying() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( - owner, l1_pooling_manager, pooling_manager, fees_recipient, factory - ); - - start_prank(CheatTarget::One(pooling_manager.contract_address), owner); - pooling_manager.register_underlying(token_1.contract_address, bridge_1.contract_address); - stop_prank(CheatTarget::One(pooling_manager.contract_address)); - - let underlying_to_bridge = pooling_manager.underlying_to_bridge(token_1.contract_address); - assert(underlying_to_bridge == bridge_1.contract_address, 'wrong bridge for underlying') -} + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + pooling_manager.register_underlying(Zeroable::zero(), bridge_1.contract_address, 5); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + } + #[test] + #[should_panic(expected: ('Zero address',))] + fn register_underlying_zero_address_2() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( + owner, l1_pooling_manager, pooling_manager, fees_recipient, factory + ); -fn setup_2( - pooling_manager: IPoolingManagerDispatcher, - owner: ContractAddress, - token_1: ContractAddress, - token_2: ContractAddress, - token_3: ContractAddress, - bridge_1: ContractAddress, - bridge_2: ContractAddress, - bridge_3: ContractAddress -) { - start_prank(CheatTarget::One(pooling_manager.contract_address), owner); - pooling_manager.register_underlying(token_1, bridge_1); - pooling_manager.register_underlying(token_2, bridge_2); - pooling_manager.register_underlying(token_3, bridge_3); - stop_prank(CheatTarget::One(pooling_manager.contract_address)); -} + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + pooling_manager.register_underlying(token_1.contract_address, Zeroable::zero(), 5); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + } + + #[test] + fn register_underlying() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( + owner, l1_pooling_manager, pooling_manager, fees_recipient, factory + ); + + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + pooling_manager.register_underlying(token_1.contract_address, bridge_1.contract_address, 5); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + let underlying_to_bridge = pooling_manager.underlying_to_bridge(token_1.contract_address); + assert(underlying_to_bridge == bridge_1.contract_address, 'wrong bridge for underlying') + } -#[test] -#[should_panic(expected: ('Invalid caller',))] -fn deploy_strategy_wrong_caller() { - let ( - owner, - fees_recipient, - l1_pooling_manager, - pooling_manager, - factory, - token_hash, - token_manager_hash - ) = - setup_0(); - let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( - owner, l1_pooling_manager, pooling_manager, fees_recipient, factory - ); - setup_2( - pooling_manager, - owner, - token_1.contract_address, - token_2.contract_address, - token_3.contract_address, - bridge_1.contract_address, - bridge_2.contract_address, - bridge_3.contract_address - ); - let l1_strategy_1: EthAddress = 2.try_into().unwrap(); - let performance_fees_strategy_1 = 200000000000000000; - let min_deposit_1 = 100000000000000000; - let max_deposit_1 = 10000000000000000000; - let min_withdraw_1 = 200000000000000000; - let max_withdraw_1 = 2000000000000000000000000; - let withdrawal_epoch_delay_1 = 2; - let dust_limit_1 = 1000000000000000000; - let name_1 = 10; - let symbol_1 = 1000; - factory - .deploy_strategy( - l1_strategy_1, + #[test] + #[should_panic(expected: ('Invalid caller',))] + fn deploy_strategy_wrong_caller() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( + owner, l1_pooling_manager, pooling_manager, fees_recipient, factory + ); + setup_2( + pooling_manager, + owner, token_1.contract_address, - name_1, - symbol_1, - performance_fees_strategy_1, - min_deposit_1, - max_deposit_1, - min_withdraw_1, - max_withdraw_1, - withdrawal_epoch_delay_1, - dust_limit_1 + token_2.contract_address, + token_3.contract_address, + bridge_1.contract_address, + bridge_2.contract_address, + bridge_3.contract_address ); -} -//fn setup_full(owner: ContractAddress, pooling_manager: IPoolingManagerDispatcher, factory: IFactoryDispatcher, token_1: ERC20ABIDispatcher, token_2: ERC20ABIDispatcher, token_3: ERC20ABIDispatcher, bridge_1: ITokenBridgeDispatcher, bridge_2: ITokenBridgeDispatcher, bridge_3: ITokenBridgeDispatcher) -> (ContractAddress, ContractAddress, ContractAddress, ContractAddress, ContractAddress, ContractAddress) { -// // Register underlyings -// start_prank(CheatTarget::One(pooling_manager.contract_address), owner); -// pooling_manager.register_underlying(token_1.contract_address, bridge_1.contract_address); -// pooling_manager.register_underlying(token_2.contract_address, bridge_2.contract_address); -// pooling_manager.register_underlying(token_3.contract_address, bridge_3.contract_address); -// stop_prank(CheatTarget::One(pooling_manager.contract_address)); -// -// // Deploy and register strategies -// let l1_strategy_1 : EthAddress = 2.try_into().unwrap(); -// let l1_strategy_2 : EthAddress = 3.try_into().unwrap(); -// let l1_strategy_3 : EthAddress= 4.try_into().unwrap(); -// let (performance_fees_strategy_1, performance_fees_strategy_2, performance_fees_strategy_3) = (200000000000000000, 400000000000000000, 100000000000000000); -// let (min_deposit_1, min_deposit_2, min_deposit_3) = (100000000000000000, 200000000000000000, 300000000000000000); -// let (max_deposit_1, max_deposit_2, max_deposit_3) = (10000000000000000000, 20000000000000000000, 30000000000000000000); -// let (min_withdraw_1, min_withdraw_2, min_withdraw_3) = (200000000000000000, 400000000000000000, 600000000000000000); -// let (max_withdraw_1, max_withdraw_2, max_withdraw_3) = (20000000000000000000, 40000000000000000000, 60000000000000000000); -// let (withdrawal_epoch_delay_1, withdrawal_epoch_delay_2, withdrawal_epoch_delay_3) = (2, 3, 4); -// let (dust_limit_1, dust_limit_2, dust_limit_3) = (1000000000000000000, 2000000000000000000, 3000000000000000000); -// let (name_1, name_2, name_3) = (10, 20, 30); -// let (symbol_1, symbol_2, symbol_3) = (1000, 2000, 3000); -// -// start_prank(CheatTarget::One(factory.contract_address), owner); -// let (nimbora_token_manager_1, nimbora_token_1) = factory.deploy_strategy(l1_strategy_1, token_1.contract_address, name_1, symbol_1, performance_fees_strategy_1, min_deposit_1, max_deposit_1, min_withdraw_1, max_withdraw_1, withdrawal_epoch_delay_1, dust_limit_1); -// let (nimbora_token_manager_2, nimbora_token_2) = factory.deploy_strategy(l1_strategy_2, token_2.contract_address, name_2, symbol_2, performance_fees_strategy_2, min_deposit_2, max_deposit_2, min_withdraw_2, max_withdraw_2, withdrawal_epoch_delay_2, dust_limit_2); -// let (nimbora_token_manager_3, nimbora_token_3) = factory.deploy_strategy(l1_strategy_3, token_3.contract_address, name_3, symbol_3, performance_fees_strategy_3, min_deposit_3, max_deposit_3, min_withdraw_3, max_withdraw_3, withdrawal_epoch_delay_3, dust_limit_3); -// stop_prank(CheatTarget::One(factory.contract_address)); -// (nimbora_token_manager_1, nimbora_token_manager_2, nimbora_token_manager_3, nimbora_token_1, nimbora_token_2, nimbora_token_3) -//} + let l1_strategy_1: EthAddress = 2.try_into().unwrap(); + let performance_fees_strategy_1 = 200000000000000000; + let min_deposit_1 = 100000000000000000; + let max_deposit_1 = 10000000000000000000; + let min_withdraw_1 = 200000000000000000; + let max_withdraw_1 = 2000000000000000000000000; + let withdrawal_epoch_delay_1 = 2; + let dust_limit_1 = 1000000000000000000; + let name_1 = 10; + let symbol_1 = 1000; + factory + .deploy_strategy( + l1_strategy_1, + token_1.contract_address, + name_1, + symbol_1, + performance_fees_strategy_1, + min_deposit_1, + max_deposit_1, + min_withdraw_1, + max_withdraw_1, + withdrawal_epoch_delay_1, + dust_limit_1 + ); + } + + #[test] + fn deploy_strategy_test() { + deploy_strategy(); + } + #[test] + #[should_panic(expected: ('Token not supported',))] + fn deploy_strategy_unregister_bridge() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( + owner, l1_pooling_manager, pooling_manager, fees_recipient, factory + ); + + let l1_strategy_1: EthAddress = 2.try_into().unwrap(); + let performance_fees_strategy_1 = 200000000000000000; + let min_deposit_1 = 100000000000000000; + let max_deposit_1 = 10000000000000000000; + let min_withdraw_1 = 200000000000000000; + let max_withdraw_1 = 2000000000000000000000000; + let withdrawal_epoch_delay_1 = 2; + let dust_limit_1 = 1000000000000000000; + let name_1 = 10; + let symbol_1 = 1000; + + start_prank(CheatTarget::One(factory.contract_address), owner); + let (token_manager_deployed_address, token_deployed_address) = factory + .deploy_strategy( + l1_strategy_1, + token_2.contract_address, + name_1, + symbol_1, + performance_fees_strategy_1, + min_deposit_1, + max_deposit_1, + min_withdraw_1, + max_withdraw_1, + withdrawal_epoch_delay_1, + dust_limit_1 + ); + + stop_prank(CheatTarget::One(factory.contract_address)); + } + //fn setup_full(owner: ContractAddress, pooling_manager: IPoolingManagerDispatcher, factory: IFactoryDispatcher, token_1: ERC20ABIDispatcher, token_2: ERC20ABIDispatcher, token_3: ERC20ABIDispatcher, bridge_1: ITokenBridgeDispatcher, bridge_2: ITokenBridgeDispatcher, bridge_3: ITokenBridgeDispatcher) -> (ContractAddress, ContractAddress, ContractAddress, ContractAddress, ContractAddress, ContractAddress) { + // // Register underlyings + // start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + // pooling_manager.register_underlying(token_1.contract_address, bridge_1.contract_address); + // pooling_manager.register_underlying(token_2.contract_address, bridge_2.contract_address); + // pooling_manager.register_underlying(token_3.contract_address, bridge_3.contract_address); + // stop_prank(CheatTarget::One(pooling_manager.contract_address)); + // + // // Deploy and register strategies + // let l1_strategy_1 : EthAddress = 2.try_into().unwrap(); + // let l1_strategy_2 : EthAddress = 3.try_into().unwrap(); + // let l1_strategy_3 : EthAddress= 4.try_into().unwrap(); + // let (performance_fees_strategy_1, performance_fees_strategy_2, performance_fees_strategy_3) = (200000000000000000, 400000000000000000, 100000000000000000); + // let (min_deposit_1, min_deposit_2, min_deposit_3) = (100000000000000000, 200000000000000000, 300000000000000000); + // let (max_deposit_1, max_deposit_2, max_deposit_3) = (10000000000000000000, 20000000000000000000, 30000000000000000000); + // let (min_withdraw_1, min_withdraw_2, min_withdraw_3) = (200000000000000000, 400000000000000000, 600000000000000000); + // let (max_withdraw_1, max_withdraw_2, max_withdraw_3) = (20000000000000000000, 40000000000000000000, 60000000000000000000); + // let (withdrawal_epoch_delay_1, withdrawal_epoch_delay_2, withdrawal_epoch_delay_3) = (2, 3, 4); + // let (dust_limit_1, dust_limit_2, dust_limit_3) = (1000000000000000000, 2000000000000000000, 3000000000000000000); + // let (name_1, name_2, name_3) = (10, 20, 30); + // let (symbol_1, symbol_2, symbol_3) = (1000, 2000, 3000); + // + // start_prank(CheatTarget::One(factory.contract_address), owner); + // let (nimbora_token_manager_1, nimbora_token_1) = factory.deploy_strategy(l1_strategy_1, token_1.contract_address, name_1, symbol_1, performance_fees_strategy_1, min_deposit_1, max_deposit_1, min_withdraw_1, max_withdraw_1, withdrawal_epoch_delay_1, dust_limit_1); + // let (nimbora_token_manager_2, nimbora_token_2) = factory.deploy_strategy(l1_strategy_2, token_2.contract_address, name_2, symbol_2, performance_fees_strategy_2, min_deposit_2, max_deposit_2, min_withdraw_2, max_withdraw_2, withdrawal_epoch_delay_2, dust_limit_2); + // let (nimbora_token_manager_3, nimbora_token_3) = factory.deploy_strategy(l1_strategy_3, token_3.contract_address, name_3, symbol_3, performance_fees_strategy_3, min_deposit_3, max_deposit_3, min_withdraw_3, max_withdraw_3, withdrawal_epoch_delay_3, dust_limit_3); + // stop_prank(CheatTarget::One(factory.contract_address)); + // (nimbora_token_manager_1, nimbora_token_manager_2, nimbora_token_manager_3, nimbora_token_1, nimbora_token_2, nimbora_token_3) + //} + + #[test] + fn upgrade_pooling_manager() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + let mock_contract = declare('MockPoolingManager'); + let old_class_hash = get_class_hash(pooling_manager.contract_address); + IUpgradeableDispatcher { contract_address: pooling_manager.contract_address } + .upgrade(mock_contract.class_hash); + assert( + get_class_hash(pooling_manager.contract_address) == mock_contract.class_hash, + 'Incorrect class hash' + ); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + } + #[test] + #[should_panic(expected: ('Caller is missing role',))] + fn upgrade_pooling_manager_wrong_caller() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let mock_contract = declare('MockPoolingManager'); + let old_class_hash = get_class_hash(pooling_manager.contract_address); + IUpgradeableDispatcher { contract_address: pooling_manager.contract_address } + .upgrade(mock_contract.class_hash); + } + + #[test] + #[should_panic(expected: ('Class hash cannot be zero',))] + fn upgrade_pooling_manager_zero_class_hash() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + let old_class_hash = get_class_hash(pooling_manager.contract_address); + IUpgradeableDispatcher { contract_address: pooling_manager.contract_address } + .upgrade(Zeroable::zero()); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + } + + + #[test] + fn upgrade_factory() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + start_prank(CheatTarget::One(factory.contract_address), owner); + let mock_contract = declare('MockFactory'); + let old_class_hash = get_class_hash(factory.contract_address); + IUpgradeableDispatcher { contract_address: factory.contract_address } + .upgrade(mock_contract.class_hash); + assert( + get_class_hash(factory.contract_address) == mock_contract.class_hash, + 'Incorrect class hash' + ); + stop_prank(CheatTarget::One(factory.contract_address)); + } + + #[test] + #[should_panic(expected: ('Invalid caller',))] + fn upgrade_factory_wrong_caller() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let mock_contract = declare('MockFactory'); + let old_class_hash = get_class_hash(factory.contract_address); + IUpgradeableDispatcher { contract_address: factory.contract_address } + .upgrade(mock_contract.class_hash); + } + + #[test] + #[should_panic(expected: ('Class hash cannot be zero',))] + fn upgrade_factory_zero_class_hash() { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + start_prank(CheatTarget::One(factory.contract_address), owner); + let old_class_hash = get_class_hash(factory.contract_address); + IUpgradeableDispatcher { contract_address: factory.contract_address } + .upgrade(Zeroable::zero()); + stop_prank(CheatTarget::One(factory.contract_address)); + } +} diff --git a/src/tests/test_pooling_manager.cairo b/src/tests/test_pooling_manager.cairo new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/tests/test_pooling_manager.cairo @@ -0,0 +1 @@ + diff --git a/src/tests/test_token_manager.cairo b/src/tests/test_token_manager.cairo new file mode 100644 index 0000000..3f94452 --- /dev/null +++ b/src/tests/test_token_manager.cairo @@ -0,0 +1,878 @@ +#[cfg(test)] +mod testTokenManager { + use core::array::ArrayTrait; + use core::debug::PrintTrait; + use core::option::OptionTrait; + use core::traits::TryInto; + use core::traits::Into; + // Nimbora yields contracts + use nimbora_yields::pooling_manager::pooling_manager::{PoolingManager}; + use nimbora_yields::pooling_manager::interface::{ + IPoolingManagerDispatcher, IPoolingManagerDispatcherTrait, StrategyReportL1 + }; + use nimbora_yields::factory::factory::{Factory}; + use nimbora_yields::factory::interface::{IFactoryDispatcher, IFactoryDispatcherTrait}; + use nimbora_yields::token_manager::token_manager::{TokenManager}; + use nimbora_yields::token_manager::interface::{ + ITokenManagerDispatcher, ITokenManagerDispatcherTrait, WithdrawalInfo, StrategyReportL2 + }; + + // Utils peripheric contracts + use nimbora_yields::token_bridge::token_bridge::{TokenBridge}; + use nimbora_yields::token_bridge::token_mock::{TokenMock}; + use nimbora_yields::token_bridge::interface::{ + ITokenBridgeDispatcher, IMintableTokenDispatcher, IMintableTokenDispatcherTrait + }; + + use openzeppelin::{ + token::erc20::interface::{IERC20, ERC20ABIDispatcher, ERC20ABIDispatcherTrait}, + access::accesscontrol::{ + AccessControlComponent, + interface::{IAccessControlDispatcher, IAccessControlDispatcherTrait} + }, + upgrades::interface::{IUpgradeableDispatcher, IUpgradeable, IUpgradeableDispatcherTrait} + }; + + use starknet::{ + get_contract_address, deploy_syscall, ClassHash, contract_address_const, ContractAddress, + get_block_timestamp, EthAddress, Zeroable + }; + use starknet::class_hash::Felt252TryIntoClassHash; + use starknet::account::{Call}; + use snforge_std::{ + declare, ContractClassTrait, start_prank, CheatTarget, ContractClass, stop_prank, + start_warp, stop_warp, L1Handler, get_class_hash, spy_events, SpyOn, EventSpy, EventFetcher, + event_name_hash, Event + }; + + use nimbora_yields::tests::test_utils::{ + deploy_tokens, deploy_token_manager, deploy_strategy, deploy_two_strategy, + deploy_three_strategy, approve_to_contract, multiple_approve_to_contract, transfer_to_users, + deposit, deposit_and_handle_mass + }; + + #[test] + #[should_panic(expected: ('Invalid caller',))] + fn upgrade_token_manager_wrong_caller() { + let token_manager = deploy_token_manager(); + + let mock_contract = declare('MockTokenManager'); + let old_class_hash = get_class_hash(token_manager.contract_address); + IUpgradeableDispatcher { contract_address: token_manager.contract_address } + .upgrade(mock_contract.class_hash); + } + + #[test] + #[should_panic(expected: ('Class hash cannot be zero',))] + fn upgrade_token_manager_zero_class_hash() { + let token_manager = deploy_token_manager(); + let owner = contract_address_const::<2300>(); + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + let old_class_hash = get_class_hash(token_manager.contract_address); + IUpgradeableDispatcher { contract_address: token_manager.contract_address } + .upgrade(Zeroable::zero()); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + #[should_panic(expected: ('Invalid caller',))] + fn token_manager_set_performance_fees_with_unregister_strategy() { + let token_manager = deploy_token_manager(); + let owner = contract_address_const::<2300>(); + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_performance_fees(1000000000000000); + assert(token_manager.performance_fees() == 1000000000000000, 'Wrong performance fees'); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + fn token_manager_set_performance() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_performance_fees(1000000000000000); + assert(token_manager.performance_fees() == 1000000000000000, 'Wrong performance fees'); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + #[should_panic(expected: ('Fee amount too high',))] + fn token_manager_set_performance_fees_too_high() { + let token_manager = deploy_token_manager(); + let owner = contract_address_const::<2300>(); + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_performance_fees(1000000000000000000); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + #[should_panic(expected: ('Invalid caller',))] + fn token_manager_set_performance_fees_wrong_caller() { + let token_manager = deploy_token_manager(); + let owner = contract_address_const::<2300>(); + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_performance_fees(10000000000000000); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + fn token_manager_set_deposit_limit() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_deposit_limit(1000000000, 2000000000); + let low_limit = token_manager.deposit_limit_low(); + let high_limit = token_manager.deposit_limit_high(); + + assert(low_limit == 1000000000, 'Wrong low deposit limit'); + assert(high_limit == 2000000000, 'Wrong high deposit limit'); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + #[should_panic(expected: ('Invalid limit',))] + fn token_manager_set_deposit_limit_low_greater_high() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_deposit_limit(1000000000000000, 12); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + #[should_panic(expected: ('Amount nul',))] + fn token_manager_set_deposit_limit_zero_low() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_deposit_limit(0, 12); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + fn token_manager_set_withdrawal_limit() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_withdrawal_limit(1000000000, 2000000000); + let low_limit = token_manager.withdrawal_limit_low(); + let high_limit = token_manager.withdrawal_limit_high(); + + assert(low_limit == 1000000000, 'Wrong low withdrawal limit'); + assert(high_limit == 2000000000, 'Wrong high withdrawal limit'); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + #[should_panic(expected: ('Invalid limit',))] + fn token_manager_set_withdrawal_limit_low_greater_high() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_withdrawal_limit(1000000000000000, 12); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + #[should_panic(expected: ('Amount nul',))] + fn token_manager_set_withdrawal_limit_zero_low() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_withdrawal_limit(0, 12); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + + #[test] + fn token_manager_set_withdrawal_epoch_delay() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_withdrawal_epoch_delay(1000000000); + let epoch_delay = token_manager.withdrawal_epoch_delay(); + + assert(epoch_delay == 1000000000, 'Wrong epoch delay'); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + #[should_panic(expected: ('Invalid caller',))] + fn token_manager_set_withdrawal_epoch_delay_wrong_caller() { + let token_manager = deploy_token_manager(); + let owner = contract_address_const::<2300>(); + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_withdrawal_epoch_delay(10000000000000); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + #[should_panic(expected: ('Amount nul',))] + fn token_manager_set_withdrawal_epoch_delay_zero_epoch() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_withdrawal_epoch_delay(0); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + + #[test] + fn token_manager_set_dust_limit() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_dust_limit(1000000000); + let dust_limit = token_manager.dust_limit(); + + assert(dust_limit == 1000000000, 'Wrong dust limit'); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + #[should_panic(expected: ('Invalid caller',))] + fn token_manager_set_dust_limit_wrong_caller() { + let token_manager = deploy_token_manager(); + let owner = contract_address_const::<2300>(); + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_dust_limit(10000000000000); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + #[should_panic(expected: ('Amount nul',))] + fn token_manager_set_dust_limit_zero() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.set_dust_limit(0); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + fn get_total_assets_should_be_zero() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + let result = token_manager.total_assets(); + assert(result == 0, 'Total asset is not 0'); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + fn get_total_underlying_dueshould_be_zero() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + let result = token_manager.total_underlying_due(); + assert(result == 0, 'Total underlying due is not 0'); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + fn test_deposit() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + let receiver = contract_address_const::<24>(); + let assets = 100000000000000002; + + let token_contract = ERC20ABIDispatcher { contract_address: token_address }; + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + let underlying_token_address = token_manager.underlying(); + let underlying_token = ERC20ABIDispatcher { contract_address: underlying_token_address }; + + start_prank(CheatTarget::One(underlying_token.contract_address), owner); + underlying_token.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(underlying_token.contract_address)); + + deposit(token_manager_address, token_address, owner, assets); + + let balance = underlying_token.balance_of(token_manager_address); + assert(balance == assets, 'Wrong underlying balance'); + + let balance = token_contract.balance_of(receiver); + assert(balance == assets, 'Wrong token balance'); + } + + #[test] + #[should_panic(expected: ('Low limit reacher',))] + fn test_deposit_low_limit_reached() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + let receiver = contract_address_const::<24>(); + let assets = 10000000000000000; + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + let result = token_manager.deposit(assets, receiver, contract_address_const::<23>()); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + #[should_panic(expected: ('High limit reacher',))] + fn test_deposit_high_limit_reached() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + let receiver = contract_address_const::<24>(); + let assets = 10000000000000000001; + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + let result = token_manager.deposit(assets, contract_address_const::<23>(), receiver); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + fn test_request_withdrawal_full_shares() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + let receiver = contract_address_const::<24>(); + let assets = 200000000000000000; + + let token_contract = ERC20ABIDispatcher { contract_address: token_address }; + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + let underlying_token_address = token_manager.underlying(); + let underlying_token = ERC20ABIDispatcher { contract_address: underlying_token_address }; + + start_prank(CheatTarget::One(underlying_token.contract_address), owner); + underlying_token.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(underlying_token.contract_address)); + + deposit(token_manager_address, token_address, owner, assets); + + let balance = underlying_token.balance_of(token_manager_address); + assert(balance == assets, 'Wrong underlying balance'); + + let balance = token_contract.balance_of(receiver); + assert(balance == assets, 'Wrong token balance'); + + start_prank(CheatTarget::One(token_contract.contract_address), receiver); + token_contract.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(token_contract.contract_address)); + + start_prank(CheatTarget::One(token_manager.contract_address), receiver); + token_manager.request_withdrawal(assets); + stop_prank(CheatTarget::One(token_manager.contract_address)); + + let balance = token_contract.balance_of(receiver); + assert(balance == 0, 'Wrong new token balance'); + } + + #[test] + #[should_panic(expected: ('Low limit reacher',))] + fn test_request_withdrawal_low_limit_reacher() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + let receiver = contract_address_const::<24>(); + let assets = 100000000000000000; + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), receiver); + token_manager.request_withdrawal(assets); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + fn test_request_withdrawal_partial_shares() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + let receiver = contract_address_const::<24>(); + let assets = 300000000000000000; + + let token_contract = ERC20ABIDispatcher { contract_address: token_address }; + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + let underlying_token_address = token_manager.underlying(); + let underlying_token = ERC20ABIDispatcher { contract_address: underlying_token_address }; + + start_prank(CheatTarget::One(underlying_token.contract_address), owner); + underlying_token.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(underlying_token.contract_address)); + + deposit(token_manager_address, token_address, owner, assets); + + let balance = underlying_token.balance_of(token_manager_address); + assert(balance == assets, 'Wrong underlying balance'); + + let balance = token_contract.balance_of(receiver); + assert(balance == assets, 'Wrong token balance'); + + start_prank(CheatTarget::One(token_contract.contract_address), receiver); + token_contract.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(token_contract.contract_address)); + + start_prank(CheatTarget::One(token_manager.contract_address), receiver); + token_manager.request_withdrawal(assets - 100000000000000000); + stop_prank(CheatTarget::One(token_manager.contract_address)); + + let balance = token_contract.balance_of(receiver); + assert(balance == 100000000000000000, 'Wrong new token balance'); + } + + #[test] + #[should_panic(expected: ('High limit reacher',))] + fn test_request_withdrawal_high_limit_reacher() { + let (token_manager_address, token_address, _) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + let receiver = contract_address_const::<24>(); + let assets = 20000000000000000000000001; + + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), receiver); + token_manager.request_withdrawal(assets); + stop_prank(CheatTarget::One(token_manager.contract_address)); + } + + #[test] + fn test_handle_report() { + let (token_manager_address, token_address, pooling_manager) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + let receiver = contract_address_const::<24>(); + let l1_pooling_manager: EthAddress = 100.try_into().unwrap(); + let assets = 200000000000000000; + + let token_contract = ERC20ABIDispatcher { contract_address: token_address }; + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + let underlying_token_address = token_manager.underlying(); + let underlying_token = ERC20ABIDispatcher { contract_address: underlying_token_address }; + + start_prank(CheatTarget::One(underlying_token.contract_address), owner); + underlying_token.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(underlying_token.contract_address)); + deposit(token_manager_address, token_address, owner, assets); + + let balance = underlying_token.balance_of(token_manager_address); + assert(balance == assets, 'Wrong underlying balance'); + + let balance = token_contract.balance_of(receiver); + assert(balance == assets, 'Wrong token balance'); + + start_prank(CheatTarget::One(token_contract.contract_address), receiver); + token_contract.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(token_contract.contract_address)); + + start_prank(CheatTarget::One(token_manager.contract_address), receiver); + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + token_manager.request_withdrawal(assets); + stop_prank(CheatTarget::One(token_manager.contract_address)); + + let balance = token_contract.balance_of(receiver); + assert(balance == 0, 'Wrong new token balance'); + + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + let calldata: Array = array![]; + + pooling_manager.handle_mass_report(calldata.span()); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + } + + #[test] + fn test_handle_report_2_deposit() { + let (token_manager_address, token_address, pooling_manager) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + let user2 = contract_address_const::<2301>(); + + let receiver = contract_address_const::<24>(); + let l1_pooling_manager: EthAddress = 100.try_into().unwrap(); + let assets = 200000000000000000; + let l1_strategy: EthAddress = 2.try_into().unwrap(); + + let token_contract = ERC20ABIDispatcher { contract_address: token_address }; + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + let underlying_token_address = token_manager.underlying(); + let underlying_token = ERC20ABIDispatcher { contract_address: underlying_token_address }; + + start_prank(CheatTarget::One(underlying_token.contract_address), owner); + underlying_token.approve(token_manager_address, 1000000000000000000002); + underlying_token.transfer(user2, 300000000000000000); + stop_prank(CheatTarget::One(underlying_token.contract_address)); + + start_prank(CheatTarget::One(underlying_token.contract_address), user2); + underlying_token.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(underlying_token.contract_address)); + + deposit(token_manager_address, token_address, owner, assets); + deposit(token_manager_address, token_address, user2, assets); + + start_prank(CheatTarget::One(token_contract.contract_address), receiver); + token_contract.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(token_contract.contract_address)); + + start_prank(CheatTarget::One(token_manager.contract_address), receiver); + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + token_manager.request_withdrawal(assets); + stop_prank(CheatTarget::One(token_manager.contract_address)); + + let balance = token_contract.balance_of(receiver); + assert(balance == 200000000000000000, 'Wrong new token balance'); + + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + let calldata: Array = array![]; + + pooling_manager.handle_mass_report(calldata.span()); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + } + + #[test] + fn test_handle_report_5_deposit() { + let (token_manager_address, token_address, pooling_manager) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + let user2 = contract_address_const::<2301>(); + let user3 = contract_address_const::<2302>(); + let user4 = contract_address_const::<2303>(); + let user5 = contract_address_const::<2304>(); + + let receiver = contract_address_const::<24>(); + let l1_pooling_manager: EthAddress = 100.try_into().unwrap(); + let assets = 200000000000000000; + let l1_strategy: EthAddress = 2.try_into().unwrap(); + + let token_contract = ERC20ABIDispatcher { contract_address: token_address }; + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + let underlying_token_address = token_manager.underlying(); + let underlying_token = ERC20ABIDispatcher { contract_address: underlying_token_address }; + + start_prank(CheatTarget::One(underlying_token.contract_address), owner); + underlying_token.approve(token_manager_address, 1000000000000000000002); + underlying_token.transfer(user2, 300000000000000000); + underlying_token.transfer(user3, 300000000000000000); + underlying_token.transfer(user4, 300000000000000000); + underlying_token.transfer(user5, 300000000000000000); + stop_prank(CheatTarget::One(underlying_token.contract_address)); + + start_prank(CheatTarget::One(underlying_token.contract_address), user2); + underlying_token.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(underlying_token.contract_address)); + + start_prank(CheatTarget::One(underlying_token.contract_address), user3); + underlying_token.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(underlying_token.contract_address)); + + start_prank(CheatTarget::One(underlying_token.contract_address), user4); + underlying_token.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(underlying_token.contract_address)); + + start_prank(CheatTarget::One(underlying_token.contract_address), user5); + underlying_token.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(underlying_token.contract_address)); + + deposit(token_manager_address, token_address, owner, assets); + deposit(token_manager_address, token_address, user2, assets); + deposit(token_manager_address, token_address, user3, assets + 10000000000000000); + deposit(token_manager_address, token_address, user4, assets + 15000000000000000); + deposit(token_manager_address, token_address, user5, assets); + + let balance = underlying_token.balance_of(token_manager_address); + assert(balance == ((assets * 5) + 25000000000000000), 'Wrong underlying balance'); + + let balance = token_contract.balance_of(receiver); + assert(balance == ((assets * 5) + 25000000000000000), 'Wrong token balance'); + + start_prank(CheatTarget::One(token_contract.contract_address), receiver); + token_contract.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(token_contract.contract_address)); + + start_prank(CheatTarget::One(token_manager.contract_address), receiver); + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + token_manager.request_withdrawal(assets * 5); + stop_prank(CheatTarget::One(token_manager.contract_address)); + + let balance = token_contract.balance_of(receiver); + assert(balance == 25000000000000000, 'Wrong new token balance'); + + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + let calldata: Array = array![]; + + pooling_manager.handle_mass_report(calldata.span()); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + } + + #[test] + fn test_handle_report_multiple_strategy() { + let ( + token_manager_address, + token_address, + pooling_manager, + token_manager_address2, + token_address2 + ) = + deploy_two_strategy(); + let owner = contract_address_const::<2300>(); + let receiver = contract_address_const::<24>(); + let l1_pooling_manager: EthAddress = 100.try_into().unwrap(); + let assets = 200000000000000000; + + let token_contract = ERC20ABIDispatcher { contract_address: token_address }; + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + let token_contract2 = ERC20ABIDispatcher { contract_address: token_address2 }; + let token_manager2 = ITokenManagerDispatcher { contract_address: token_manager_address2 }; + let underlying_token_address = token_manager.underlying(); + let underlying_token = ERC20ABIDispatcher { contract_address: underlying_token_address }; + let underlying_token_address2 = token_manager2.underlying(); + let underlying_token2 = ERC20ABIDispatcher { contract_address: underlying_token_address2 }; + + start_prank(CheatTarget::One(underlying_token.contract_address), owner); + underlying_token.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(underlying_token.contract_address)); + + start_prank(CheatTarget::One(underlying_token2.contract_address), owner); + underlying_token2.approve(token_manager_address2, 1000000000000000000002); + stop_prank(CheatTarget::One(underlying_token2.contract_address)); + + deposit(token_manager_address, token_address, owner, assets); + deposit(token_manager_address2, token_address2, owner, assets + 10000000); + + let balance = underlying_token.balance_of(token_manager_address); + assert(balance == assets, 'Wrong underlying balance'); + + let balance = token_contract.balance_of(receiver); + assert(balance == assets, 'Wrong token balance'); + + let balance = underlying_token2.balance_of(token_manager_address2); + assert(balance == (assets + 10000000), 'Wrong underlying balance'); + + let balance = token_contract2.balance_of(receiver); + assert(balance == (assets + 10000000), 'Wrong token balance'); + + start_prank(CheatTarget::One(token_contract.contract_address), receiver); + token_contract.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(token_contract.contract_address)); + + start_prank(CheatTarget::One(token_contract2.contract_address), receiver); + token_contract2.approve(token_manager_address2, 1000000000000000000002); + stop_prank(CheatTarget::One(token_contract2.contract_address)); + + start_prank(CheatTarget::One(token_manager.contract_address), receiver); + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + token_manager.request_withdrawal(assets); + stop_prank(CheatTarget::One(token_manager.contract_address)); + + start_prank(CheatTarget::One(token_manager2.contract_address), receiver); + let token_manager2 = ITokenManagerDispatcher { contract_address: token_manager_address2 }; + token_manager2.request_withdrawal(assets); + stop_prank(CheatTarget::One(token_manager2.contract_address)); + + let balance = token_contract.balance_of(receiver); + assert(balance == 0, 'Wrong new token balance'); + + let balance = token_contract2.balance_of(receiver); + assert(balance == 10000000, 'Wrong new token balance'); + + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + let calldata: Array = array![]; + + pooling_manager.handle_mass_report(calldata.span()); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + } + + #[test] + fn test_handle_report_5_deposit_3_strategy() { + let ( + token_manager_address, + token_address, + pooling_manager, + token_manager_address2, + token_address2, + token_manager_address3, + token_address3 + ) = + deploy_three_strategy(); + + let owner = contract_address_const::<2300>(); + let user2 = contract_address_const::<2301>(); + let user3 = contract_address_const::<2302>(); + let user4 = contract_address_const::<2303>(); + let user5 = contract_address_const::<2304>(); + + let receiver = contract_address_const::<24>(); + let l1_pooling_manager: EthAddress = 100.try_into().unwrap(); + let assets = 200000000000000000; + + let token_contract = ERC20ABIDispatcher { contract_address: token_address }; + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + let token_contract2 = ERC20ABIDispatcher { contract_address: token_address2 }; + let token_manager2 = ITokenManagerDispatcher { contract_address: token_manager_address2 }; + let token_contract3 = ERC20ABIDispatcher { contract_address: token_address3 }; + let token_manager3 = ITokenManagerDispatcher { contract_address: token_manager_address3 }; + let underlying_token_address = token_manager.underlying(); + let underlying_token = ERC20ABIDispatcher { contract_address: underlying_token_address }; + let underlying_token_address2 = token_manager2.underlying(); + let underlying_token2 = ERC20ABIDispatcher { contract_address: underlying_token_address2 }; + let underlying_token_address3 = token_manager3.underlying(); + let underlying_token3 = ERC20ABIDispatcher { contract_address: underlying_token_address3 }; + + let user_array = @array![owner, user2, user3, user4, user5]; + let token_manager_array = @array![token_manager, token_manager2, token_manager3]; + let token_array = @array![token_contract, token_contract2, token_contract3]; + + transfer_to_users(owner, 3000000000000000000, user_array, underlying_token); + transfer_to_users(owner, 3000000000000000000, user_array, underlying_token2); + transfer_to_users(owner, 3000000000000000000, user_array, underlying_token3); + + multiple_approve_to_contract( + 1000000000000000000002, user_array, underlying_token, token_manager_array + ); + multiple_approve_to_contract( + 1000000000000000000002, user_array, underlying_token2, token_manager_array + ); + multiple_approve_to_contract( + 1000000000000000000002, user_array, underlying_token3, token_manager_array + ); + + // Deposite to token_manager + let mut i = 0; + loop { + if (i == user_array.len()) { + break (); + } + let mut j = 0; + let user = *user_array.at(i); + loop { + if (j == token_manager_array.len()) { + break (); + } + let token_manager_contract = *token_manager_array.at(j); + start_prank(CheatTarget::One(token_manager_contract.contract_address), user); + if (user == user3) { + token_manager_contract + .deposit( + assets + 10000000000000000, receiver, contract_address_const::<23>() + ); + } else if (user == user4) { + token_manager_contract + .deposit( + assets + 15000000000000000, receiver, contract_address_const::<23>() + ); + } else { + token_manager_contract + .deposit(assets, receiver, contract_address_const::<23>()); + } + stop_prank(CheatTarget::One(token_manager_contract.contract_address)); + j += 1; + }; + i += 1; + }; + + let balance = underlying_token.balance_of(token_manager_address); + assert(balance == ((assets * 5) + 25000000000000000), 'Wrong underlying balance'); + + let balance = token_contract.balance_of(receiver); + assert(balance == ((assets * 5) + 25000000000000000), 'Wrong token balance'); + + let balance = underlying_token2.balance_of(token_manager_address2); + assert(balance == ((assets * 5) + 25000000000000000), 'Wrong underlying balance'); + + let balance = token_contract2.balance_of(receiver); + assert(balance == ((assets * 5) + 25000000000000000), 'Wrong token balance'); + + let balance = underlying_token3.balance_of(token_manager_address3); + assert(balance == ((assets * 5) + 25000000000000000), 'Wrong underlying balance'); + + let balance = token_contract3.balance_of(receiver); + assert(balance == ((assets * 5) + 25000000000000000), 'Wrong token balance'); + + // Approve receiver for all token contract + approve_to_contract(1000000000000000000002, receiver, token_contract, token_manager); + approve_to_contract(1000000000000000000002, receiver, token_contract2, token_manager2); + approve_to_contract(1000000000000000000002, receiver, token_contract3, token_manager3); + + // Request Withdrawal for token_manager + start_prank(CheatTarget::One(token_manager.contract_address), receiver); + token_manager.request_withdrawal(assets * 5); + stop_prank(CheatTarget::One(token_manager.contract_address)); + + let balance = token_contract.balance_of(receiver); + assert(balance == 25000000000000000, 'Wrong new token balance'); + + // Request Withdrawal for token_manager2 + start_prank(CheatTarget::One(token_manager2.contract_address), receiver); + token_manager2.request_withdrawal(assets * 5); + stop_prank(CheatTarget::One(token_manager2.contract_address)); + + let balance = token_contract2.balance_of(receiver); + assert(balance == 25000000000000000, 'Wrong new token balance'); + + // Request Withdrawal for token_manager3 + start_prank(CheatTarget::One(token_manager3.contract_address), receiver); + token_manager3.request_withdrawal(assets * 5); + stop_prank(CheatTarget::One(token_manager3.contract_address)); + + let balance = token_contract3.balance_of(receiver); + assert(balance == 25000000000000000, 'Wrong new token balance'); + + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + let calldata: Array = array![]; + + pooling_manager.handle_mass_report(calldata.span()); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + } + + + #[test] + fn test_l1_handler() { + let (token_manager_address, token_address, pooling_manager) = deposit_and_handle_mass(); + + let owner = contract_address_const::<2300>(); + let receiver = contract_address_const::<24>(); + let l1_pooling_manager: EthAddress = 100.try_into().unwrap(); + let assets = 200000000000000000; + + let token_contract = ERC20ABIDispatcher { contract_address: token_address }; + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + let underlying_token_address = token_manager.underlying(); + let underlying_token = ERC20ABIDispatcher { contract_address: underlying_token_address }; + + let mut spy = spy_events(SpyOn::One(pooling_manager.contract_address)); + spy.fetch_events(); + + spy.events.len().print(); + } +} diff --git a/src/tests/test_utils.cairo b/src/tests/test_utils.cairo new file mode 100644 index 0000000..74f6079 --- /dev/null +++ b/src/tests/test_utils.cairo @@ -0,0 +1,583 @@ +use core::option::OptionTrait; +use core::traits::TryInto; +use core::traits::Into; +// Nimbora yields contracts +use nimbora_yields::pooling_manager::pooling_manager::{PoolingManager}; +use nimbora_yields::pooling_manager::interface::{ + IPoolingManagerDispatcher, IPoolingManagerDispatcherTrait, StrategyReportL1 +}; +use nimbora_yields::factory::factory::{Factory}; +use nimbora_yields::factory::interface::{IFactoryDispatcher, IFactoryDispatcherTrait}; +use nimbora_yields::token_manager::token_manager::{TokenManager}; +use nimbora_yields::token_manager::interface::{ + ITokenManagerDispatcher, ITokenManagerDispatcherTrait, WithdrawalInfo, StrategyReportL2 +}; +use nimbora_yields::token_bridge::token_bridge::{TokenBridge}; +use nimbora_yields::token_bridge::token_mock::{TokenMock}; +use nimbora_yields::token_bridge::interface::{ + ITokenBridgeDispatcher, IMintableTokenDispatcher, IMintableTokenDispatcherTrait +}; + +use openzeppelin::{ + token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}, + access::accesscontrol::{ + AccessControlComponent, interface::{IAccessControlDispatcher, IAccessControlDispatcherTrait} + }, + upgrades::interface::{IUpgradeableDispatcher, IUpgradeable, IUpgradeableDispatcherTrait} +}; + +use starknet::{ + get_contract_address, deploy_syscall, ClassHash, contract_address_const, ContractAddress, + get_block_timestamp, EthAddress, Zeroable +}; +use starknet::class_hash::Felt252TryIntoClassHash; +use starknet::account::{Call}; +use snforge_std::{ + declare, ContractClassTrait, start_prank, CheatTarget, ContractClass, PrintTrait, stop_prank, + start_warp, stop_warp +}; + +fn deploy_tokens( + initial_supply: u256, recipient: ContractAddress +) -> (ERC20ABIDispatcher, ERC20ABIDispatcher, ERC20ABIDispatcher) { + let contract = declare('TokenMock'); + + let mut constructor_args: Array = ArrayTrait::new(); + Serde::serialize(@initial_supply, ref constructor_args); + Serde::serialize(@recipient, ref constructor_args); + let contract_address_1 = contract.deploy(@constructor_args).unwrap(); + let contract_address_2 = contract.deploy(@constructor_args).unwrap(); + let contract_address_3 = contract.deploy(@constructor_args).unwrap(); + + return ( + ERC20ABIDispatcher { contract_address: contract_address_1 }, + ERC20ABIDispatcher { contract_address: contract_address_2 }, + ERC20ABIDispatcher { contract_address: contract_address_3 } + ); +} + +fn deploy_token_bridges( + l2_address_1: ContractAddress, + l1_bridge_1: felt252, + l2_address_2: ContractAddress, + l1_bridge_2: felt252, + l2_address_3: ContractAddress, + l1_bridge_3: felt252 +) -> (ITokenBridgeDispatcher, ITokenBridgeDispatcher, ITokenBridgeDispatcher) { + let contract = declare('TokenBridge'); + + let mut constructor_args_1: Array = ArrayTrait::new(); + Serde::serialize(@l2_address_1, ref constructor_args_1); + Serde::serialize(@l1_bridge_1, ref constructor_args_1); + let contract_address_1 = contract.deploy(@constructor_args_1).unwrap(); + + let mut constructor_args_2: Array = ArrayTrait::new(); + Serde::serialize(@l2_address_2, ref constructor_args_2); + Serde::serialize(@l1_bridge_2, ref constructor_args_2); + let contract_address_2 = contract.deploy(@constructor_args_2).unwrap(); + + let mut constructor_args_3: Array = ArrayTrait::new(); + Serde::serialize(@l2_address_3, ref constructor_args_3); + Serde::serialize(@l1_bridge_3, ref constructor_args_3); + let contract_address_3 = contract.deploy(@constructor_args_3).unwrap(); + + return ( + ITokenBridgeDispatcher { contract_address: contract_address_1 }, + ITokenBridgeDispatcher { contract_address: contract_address_2 }, + ITokenBridgeDispatcher { contract_address: contract_address_3 } + ); +} + +fn deploy_pooling_manager(owner: ContractAddress) -> IPoolingManagerDispatcher { + let contract = declare('PoolingManager'); + let mut constructor_args: Array = ArrayTrait::new(); + Serde::serialize(@owner, ref constructor_args); + let contract_address = contract.deploy(@constructor_args).unwrap(); + return IPoolingManagerDispatcher { contract_address: contract_address }; +} + +fn deploy_factory( + pooling_manager: ContractAddress, + token_class_hash: ClassHash, + token_manager_class_hash: ClassHash +) -> IFactoryDispatcher { + let contract = declare('Factory'); + let mut constructor_args: Array = ArrayTrait::new(); + Serde::serialize(@pooling_manager, ref constructor_args); + Serde::serialize(@token_class_hash, ref constructor_args); + Serde::serialize(@token_manager_class_hash, ref constructor_args); + let contract_address = contract.deploy(@constructor_args).unwrap(); + return IFactoryDispatcher { contract_address: contract_address }; +} + +fn deploy_token_manager() -> ITokenManagerDispatcher { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( + owner, l1_pooling_manager, pooling_manager, fees_recipient, factory + ); + let mut constructor_args: Array = ArrayTrait::new(); + let l1_strategy_1: EthAddress = 2.try_into().unwrap(); + let performance_fees_strategy_1: u256 = 200000000000000000; + let min_deposit_1: u256 = 100000000000000000; + let max_deposit_1: u256 = 10000000000000000000; + let min_withdraw_1: u256 = 200000000000000000; + let max_withdraw_1: u256 = 2000000000000000000000000; + let withdrawal_epoch_delay_1: u256 = 2; + let dust_limit_1: u256 = 1000000000000000000; + + Serde::serialize(@pooling_manager.contract_address, ref constructor_args); + Serde::serialize(@l1_strategy_1, ref constructor_args); + Serde::serialize(@token_1.contract_address, ref constructor_args); + Serde::serialize(@performance_fees_strategy_1, ref constructor_args); + Serde::serialize(@min_deposit_1, ref constructor_args); + Serde::serialize(@max_deposit_1, ref constructor_args); + Serde::serialize(@min_withdraw_1, ref constructor_args); + Serde::serialize(@max_withdraw_1, ref constructor_args); + Serde::serialize(@withdrawal_epoch_delay_1, ref constructor_args); + Serde::serialize(@dust_limit_1, ref constructor_args); + + let contract = ContractClass { class_hash: token_manager_hash }; + + let contract_address = contract.deploy(@constructor_args).unwrap(); + return ITokenManagerDispatcher { contract_address: contract_address }; +} + +fn setup_0() -> ( + ContractAddress, + ContractAddress, + EthAddress, + IPoolingManagerDispatcher, + IFactoryDispatcher, + ClassHash, + ClassHash +) { + let owner = contract_address_const::<2300>(); + let fees_recipient = contract_address_const::<2400>(); + let l1_pooling_manager: EthAddress = 100.try_into().unwrap(); + let pooling_manager = deploy_pooling_manager(owner); + let token_hash = declare('Token'); + let token_manager_hash = declare('TokenManager'); + let factory = deploy_factory( + pooling_manager.contract_address, token_hash.class_hash, token_manager_hash.class_hash + ); + ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash.class_hash, + token_manager_hash.class_hash + ) +} + +fn setup_1( + owner: ContractAddress, + l1_pooling_manager: EthAddress, + pooling_manager: IPoolingManagerDispatcher, + fees_recipient: ContractAddress, + factory: IFactoryDispatcher +) -> ( + ERC20ABIDispatcher, + ERC20ABIDispatcher, + ERC20ABIDispatcher, + ITokenBridgeDispatcher, + ITokenBridgeDispatcher, + ITokenBridgeDispatcher +) { + // Initialise + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + pooling_manager.set_fees_recipient(fees_recipient); + pooling_manager.set_l1_pooling_manager(l1_pooling_manager); + pooling_manager.set_factory(factory.contract_address); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + + // Deploy tokens and bridges + let (l1_bridge_1, l1_bridge_2, l1_bridge_3) = ( + 111.try_into().unwrap(), 112.try_into().unwrap(), 113.try_into().unwrap() + ); + let (token_1, token_2, token_3) = deploy_tokens(1000000000000000000000, owner); + let (bridge_1, bridge_2, bridge_3) = deploy_token_bridges( + token_1.contract_address, + l1_bridge_1, + token_2.contract_address, + l1_bridge_2, + token_3.contract_address, + l1_bridge_3 + ); + (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) +} + +fn setup_2( + pooling_manager: IPoolingManagerDispatcher, + owner: ContractAddress, + token_1: ContractAddress, + token_2: ContractAddress, + token_3: ContractAddress, + bridge_1: ContractAddress, + bridge_2: ContractAddress, + bridge_3: ContractAddress +) { + let l1_bridge_1 = 5; + let l1_bridge_2 = 6; + let l1_bridge_3 = 7; + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + pooling_manager.register_underlying(token_1, bridge_1, l1_bridge_1); + pooling_manager.register_underlying(token_2, bridge_2, l1_bridge_2); + pooling_manager.register_underlying(token_3, bridge_3, l1_bridge_3); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); +} + +fn deploy_strategy() -> (ContractAddress, ContractAddress, IPoolingManagerDispatcher) { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( + owner, l1_pooling_manager, pooling_manager, fees_recipient, factory + ); + setup_2( + pooling_manager, + owner, + token_1.contract_address, + token_2.contract_address, + token_3.contract_address, + bridge_1.contract_address, + bridge_2.contract_address, + bridge_3.contract_address + ); + let l1_strategy_1: EthAddress = 2.try_into().unwrap(); + let performance_fees_strategy_1 = 200000000000000000; + let min_deposit_1 = 100000000000000000; + let max_deposit_1 = 10000000000000000000; + let min_withdraw_1 = 200000000000000000; + let max_withdraw_1 = 2000000000000000000000000; + let withdrawal_epoch_delay_1 = 2; + let dust_limit_1 = 1000000000000000000; + let name_1 = 10; + let symbol_1 = 1000; + + start_prank(CheatTarget::One(factory.contract_address), owner); + let (token_manager_deployed_address, token_deployed_address) = factory + .deploy_strategy( + l1_strategy_1, + token_1.contract_address, + name_1, + symbol_1, + performance_fees_strategy_1, + min_deposit_1, + max_deposit_1, + min_withdraw_1, + max_withdraw_1, + withdrawal_epoch_delay_1, + dust_limit_1 + ); + stop_prank(CheatTarget::One(factory.contract_address)); + return (token_manager_deployed_address, token_deployed_address, pooling_manager); +} + +fn deploy_two_strategy() -> ( + ContractAddress, ContractAddress, IPoolingManagerDispatcher, ContractAddress, ContractAddress +) { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( + owner, l1_pooling_manager, pooling_manager, fees_recipient, factory + ); + setup_2( + pooling_manager, + owner, + token_1.contract_address, + token_2.contract_address, + token_3.contract_address, + bridge_1.contract_address, + bridge_2.contract_address, + bridge_3.contract_address + ); + let l1_strategy_1: EthAddress = 2.try_into().unwrap(); + let l1_strategy_2: EthAddress = 3.try_into().unwrap(); + let performance_fees_strategy_1 = 200000000000000000; + let min_deposit_1 = 100000000000000000; + let max_deposit_1 = 10000000000000000000; + let min_withdraw_1 = 200000000000000000; + let max_withdraw_1 = 2000000000000000000000000; + let withdrawal_epoch_delay_1 = 2; + let dust_limit_1 = 1000000000000000000; + let name_1 = 10; + let symbol_1 = 1000; + + start_prank(CheatTarget::One(factory.contract_address), owner); + let (token_manager_deployed_address, token_deployed_address) = factory + .deploy_strategy( + l1_strategy_1, + token_1.contract_address, + name_1, + symbol_1, + performance_fees_strategy_1, + min_deposit_1, + max_deposit_1, + min_withdraw_1, + max_withdraw_1, + withdrawal_epoch_delay_1, + dust_limit_1 + ); + let (token_manager_deployed_address2, token_deployed_address2) = factory + .deploy_strategy( + l1_strategy_2, + token_2.contract_address, + name_1, + symbol_1, + performance_fees_strategy_1, + min_deposit_1, + max_deposit_1, + min_withdraw_1, + max_withdraw_1, + withdrawal_epoch_delay_1, + dust_limit_1 + ); + stop_prank(CheatTarget::One(factory.contract_address)); + return ( + token_manager_deployed_address, + token_deployed_address, + pooling_manager, + token_manager_deployed_address2, + token_deployed_address2 + ); +} + + +fn deploy_three_strategy() -> ( + ContractAddress, + ContractAddress, + IPoolingManagerDispatcher, + ContractAddress, + ContractAddress, + ContractAddress, + ContractAddress +) { + let ( + owner, + fees_recipient, + l1_pooling_manager, + pooling_manager, + factory, + token_hash, + token_manager_hash + ) = + setup_0(); + let (token_1, token_2, token_3, bridge_1, bridge_2, bridge_3) = setup_1( + owner, l1_pooling_manager, pooling_manager, fees_recipient, factory + ); + setup_2( + pooling_manager, + owner, + token_1.contract_address, + token_2.contract_address, + token_3.contract_address, + bridge_1.contract_address, + bridge_2.contract_address, + bridge_3.contract_address + ); + let l1_strategy_1: EthAddress = 2.try_into().unwrap(); + let l1_strategy_2: EthAddress = 3.try_into().unwrap(); + let l1_strategy_3: EthAddress = 4.try_into().unwrap(); + + let performance_fees_strategy_1 = 200000000000000000; + let min_deposit_1 = 100000000000000000; + let max_deposit_1 = 10000000000000000000; + let min_withdraw_1 = 200000000000000000; + let max_withdraw_1 = 2000000000000000000000000; + let withdrawal_epoch_delay_1 = 2; + let dust_limit_1 = 1000000000000000000; + let name_1 = 10; + let symbol_1 = 1000; + + start_prank(CheatTarget::One(factory.contract_address), owner); + let (token_manager_deployed_address, token_deployed_address) = factory + .deploy_strategy( + l1_strategy_1, + token_1.contract_address, + name_1, + symbol_1, + performance_fees_strategy_1, + min_deposit_1, + max_deposit_1, + min_withdraw_1, + max_withdraw_1, + withdrawal_epoch_delay_1, + dust_limit_1 + ); + let (token_manager_deployed_address2, token_deployed_address2) = factory + .deploy_strategy( + l1_strategy_2, + token_2.contract_address, + name_1, + symbol_1, + performance_fees_strategy_1, + min_deposit_1, + max_deposit_1, + min_withdraw_1, + max_withdraw_1, + withdrawal_epoch_delay_1, + dust_limit_1 + ); + + let (token_manager_deployed_address3, token_deployed_address3) = factory + .deploy_strategy( + l1_strategy_3, + token_3.contract_address, + name_1, + symbol_1, + performance_fees_strategy_1, + min_deposit_1, + max_deposit_1, + min_withdraw_1, + max_withdraw_1, + withdrawal_epoch_delay_1, + dust_limit_1 + ); + stop_prank(CheatTarget::One(factory.contract_address)); + return ( + token_manager_deployed_address, + token_deployed_address, + pooling_manager, + token_manager_deployed_address2, + token_deployed_address2, + token_manager_deployed_address3, + token_deployed_address3 + ); +} + +fn transfer_to_users( + owner: ContractAddress, amount: u256, users: @Array, token: ERC20ABIDispatcher +) { + let mut i = 0; + let user_array_len = users.len(); + loop { + if (i == user_array_len) { + break (); + } + if (*users.at(i) != owner) { + start_prank(CheatTarget::One(token.contract_address), owner); + token.transfer(*users.at(i), 3000000000000000000); + stop_prank(CheatTarget::One(token.contract_address)); + } + i += 1; + }; +} + +fn multiple_approve_to_contract( + amount: u256, + users: @Array, + token: ERC20ABIDispatcher, + token_managers: @Array +) { + let mut i = 0; + let user_array_len = users.len(); + loop { + if (i == user_array_len) { + break (); + } + let mut j = 0; + loop { + if (j == token_managers.len()) { + break (); + } + approve_to_contract(amount, *users.at(i), token, *token_managers.at(j)); + j += 1; + }; + i += 1; + }; +} + +fn approve_to_contract( + amount: u256, + user: ContractAddress, + token: ERC20ABIDispatcher, + token_manager: ITokenManagerDispatcher +) { + start_prank(CheatTarget::One(token.contract_address), user); + token.approve(token_manager.contract_address, amount); + stop_prank(CheatTarget::One(token.contract_address)); +} + +fn deposit( + token_manager_address: ContractAddress, + token_address: ContractAddress, + owner: ContractAddress, + assets: u256 +) { + let receiver = contract_address_const::<24>(); + + let token_contract = ERC20ABIDispatcher { contract_address: token_address }; + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + let underlying_token_address = token_manager.underlying(); + let underlying_token = ERC20ABIDispatcher { contract_address: underlying_token_address }; + + start_prank(CheatTarget::One(token_manager.contract_address), owner); + token_manager.deposit(assets, receiver, contract_address_const::<23>()); + let token = token_manager.token(); + assert(token == token_address, 'Wrong token address'); + stop_prank(CheatTarget::One(token_manager.contract_address)); +} + +fn deposit_and_handle_mass() -> (ContractAddress, ContractAddress, IPoolingManagerDispatcher) { + let (token_manager_address, token_address, pooling_manager) = deploy_strategy(); + let owner = contract_address_const::<2300>(); + let receiver = contract_address_const::<24>(); + let assets = 200000000000000000; + + let token_contract = ERC20ABIDispatcher { contract_address: token_address }; + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + let underlying_token_address = token_manager.underlying(); + let underlying_token = ERC20ABIDispatcher { contract_address: underlying_token_address }; + + start_prank(CheatTarget::One(underlying_token.contract_address), owner); + underlying_token.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(underlying_token.contract_address)); + + deposit(token_manager_address, token_address, owner, assets); + + start_prank(CheatTarget::One(token_contract.contract_address), receiver); + token_contract.approve(token_manager_address, 1000000000000000000002); + stop_prank(CheatTarget::One(token_contract.contract_address)); + + start_prank(CheatTarget::One(token_manager.contract_address), receiver); + let token_manager = ITokenManagerDispatcher { contract_address: token_manager_address }; + token_manager.request_withdrawal(assets); + stop_prank(CheatTarget::One(token_manager.contract_address)); + + let balance = token_contract.balance_of(receiver); + assert(balance == 0, 'Wrong new token balance'); + + start_prank(CheatTarget::One(pooling_manager.contract_address), owner); + let calldata: Array = array![]; + + pooling_manager.handle_mass_report(calldata.span()); + stop_prank(CheatTarget::One(pooling_manager.contract_address)); + + return (token_manager_address, token_address, pooling_manager); +} diff --git a/src/token/token.cairo b/src/token/token.cairo index b46617e..3aedaa0 100644 --- a/src/token/token.cairo +++ b/src/token/token.cairo @@ -1,11 +1,16 @@ #[starknet::contract] mod Token { use openzeppelin::token::erc20::{ERC20Component, interface}; - use starknet::{ContractAddress, get_caller_address}; + use openzeppelin::upgrades::UpgradeableComponent; + + use starknet::{ContractAddress, get_caller_address, ClassHash}; use nimbora_yields::token::interface::{IToken}; component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + impl InternalUpgradeableImpl = UpgradeableComponent::InternalImpl; #[abi(embed_v0)] impl ERC20Impl = ERC20Component::ERC20Impl; @@ -17,6 +22,8 @@ mod Token { struct Storage { #[substorage(v0)] erc20: ERC20Component::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, token_manager: ContractAddress, decimals: u8 } @@ -25,7 +32,9 @@ mod Token { #[derive(Drop, starknet::Event)] enum Event { #[flat] - ERC20Event: ERC20Component::Event + ERC20Event: ERC20Component::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event } mod Errors { @@ -51,6 +60,13 @@ mod Token { self.decimals.write(decimals); } + /// @notice Upgrade contract + /// @param New contract class hash + #[external(v0)] + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self._assert_only_token_manager(); + self.upgradeable._upgrade(new_class_hash); + } #[abi(embed_v0)] impl Token of IToken { diff --git a/src/token_bridge/token_bridge.cairo b/src/token_bridge/token_bridge.cairo index d6597ac..a23b247 100644 --- a/src/token_bridge/token_bridge.cairo +++ b/src/token_bridge/token_bridge.cairo @@ -5,11 +5,13 @@ mod TokenBridge { use super::{ContractAddress}; use starknet::{ get_caller_address, contract_address::{Felt252TryIntoContractAddress}, - syscalls::send_message_to_l1_syscall, Zeroable + syscalls::send_message_to_l1_syscall, Zeroable, ClassHash }; use openzeppelin::{ token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait} }; + use openzeppelin::upgrades::UpgradeableComponent; + use nimbora_yields::token_bridge::interface::{ ITokenBridge, IMintableTokenDispatcher, IMintableTokenDispatcherTrait }; @@ -23,8 +25,14 @@ mod TokenBridge { const UNINITIALIZED_TOKEN: felt252 = 'UNINITIALIZED_TOKEN'; const WITHDRAW_MESSAGE: felt252 = 0; + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + impl InternalUpgradeableImpl = UpgradeableComponent::InternalImpl; + #[storage] struct Storage { + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, _l2_address: ContractAddress, _l1_bridge: felt252, } @@ -32,6 +40,8 @@ mod TokenBridge { #[event] #[derive(Drop, starknet::Event)] enum Event { + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, WithdrawInitiated: WithdrawInitiated, DepositHandled: DepositHandled, } @@ -61,6 +71,13 @@ mod TokenBridge { self._l1_bridge.write(l1_bridge); } + /// @notice Upgrade contract + /// @param New contract class hash + #[external(v0)] + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.upgradeable._upgrade(new_class_hash); + } + #[external(v0)] impl TokenBridgeImpl of ITokenBridge { /// @notice Retrieves the address of the L2 token contract. diff --git a/src/token_bridge/token_mock.cairo b/src/token_bridge/token_mock.cairo index c25025c..5eaf6c1 100644 --- a/src/token_bridge/token_mock.cairo +++ b/src/token_bridge/token_mock.cairo @@ -1,10 +1,15 @@ #[starknet::contract] mod TokenMock { use openzeppelin::token::erc20::ERC20Component; - use starknet::ContractAddress; + use openzeppelin::upgrades::UpgradeableComponent; + use starknet::{ContractAddress, ClassHash}; use nimbora_yields::token_bridge::interface::{IMintableToken}; component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + + impl InternalUpgradeableImpl = UpgradeableComponent::InternalImpl; #[abi(embed_v0)] impl ERC20Impl = ERC20Component::ERC20Impl; @@ -17,14 +22,18 @@ mod TokenMock { #[storage] struct Storage { #[substorage(v0)] - erc20: ERC20Component::Storage + erc20: ERC20Component::Storage, + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, } #[event] #[derive(Drop, starknet::Event)] enum Event { #[flat] - ERC20Event: ERC20Component::Event + ERC20Event: ERC20Component::Event, + #[flat] + UpgradeableEvent: UpgradeableComponent::Event, } #[constructor] @@ -35,14 +44,21 @@ mod TokenMock { self.erc20._mint(recipient, initial_supply); } + /// @notice Upgrade contract + /// @param New contract class hash + #[external(v0)] + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self.upgradeable._upgrade(new_class_hash); + } + #[external(v0)] impl MintableToken of IMintableToken { fn permissionedMint(ref self: ContractState, account: ContractAddress, amount: u256) { - self.erc20._burn(account, amount); + self.erc20._mint(account, amount); } fn permissionedBurn(ref self: ContractState, account: ContractAddress, amount: u256) { - self.erc20._mint(account, amount); + self.erc20._burn(account, amount); } } } diff --git a/src/token_manager/interface.cairo b/src/token_manager/interface.cairo index c98398a..8596ef9 100644 --- a/src/token_manager/interface.cairo +++ b/src/token_manager/interface.cairo @@ -40,6 +40,8 @@ trait ITokenManager { fn total_assets(self: @TContractState) -> u256; fn total_underlying_due(self: @TContractState) -> u256; fn withdrawal_exchange_rate(self: @TContractState, epoch: u256) -> u256; + fn withdrawal_pool(self: @TContractState, epoch: u256) -> u256; + fn withdrawal_share(self: @TContractState, epoch: u256) -> u256; fn initialiser(ref self: TContractState, token: ContractAddress); diff --git a/src/token_manager/token_manager.cairo b/src/token_manager/token_manager.cairo index 1657ba3..ead5ff8 100644 --- a/src/token_manager/token_manager.cairo +++ b/src/token_manager/token_manager.cairo @@ -1,7 +1,8 @@ #[starknet::contract] mod TokenManager { use starknet::{ - ContractAddress, get_caller_address, get_contract_address, eth_address::EthAddress, Zeroable + ContractAddress, get_caller_address, get_contract_address, eth_address::EthAddress, + Zeroable, ClassHash }; @@ -10,6 +11,8 @@ mod TokenManager { use openzeppelin::access::accesscontrol::interface::{ IAccessControlDispatcher, IAccessControlDispatcherTrait }; + use openzeppelin::upgrades::UpgradeableComponent; + use nimbora_yields::token_manager::interface::{ITokenManager, WithdrawalInfo, StrategyReportL2}; use nimbora_yields::token::interface::{ITokenDispatcher, ITokenDispatcherTrait}; @@ -19,9 +22,15 @@ mod TokenManager { use nimbora_yields::utils::{CONSTANTS, MATH}; + // Components + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); + + impl InternalUpgradeableImpl = UpgradeableComponent::InternalImpl; #[storage] struct Storage { + #[substorage(v0)] + upgradeable: UpgradeableComponent::Storage, pooling_manager: ContractAddress, l1_strategy: EthAddress, underlying: ContractAddress, @@ -46,7 +55,10 @@ mod TokenManager { #[event] #[derive(Drop, starknet::Event)] - enum Event {} + enum Event { + #[flat] + UpgradeableEvent: UpgradeableComponent::Event + } mod Errors { @@ -60,6 +72,7 @@ mod TokenManager { const NOT_OWNER: felt252 = 'Not owner'; const WITHDRAWAL_NOT_REDY: felt252 = 'Withdrawal not ready'; const ALREADY_CLAIMED: felt252 = 'Already claimed'; + const ZERO_SHARES: felt252 = 'Shares is zero'; } #[constructor] @@ -87,6 +100,13 @@ mod TokenManager { self._set_dust_limit(dust_limit); } + /// @notice Upgrade contract + /// @param New contract class hash + #[external(v0)] + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + self._assert_only_owner(); + self.upgradeable._upgrade(new_class_hash); + } #[abi(embed_v0)] impl TokenManager of ITokenManager { @@ -225,6 +245,19 @@ mod TokenManager { self._withdrawal_exchange_rate(epoch) } + /// @notice Reads the withdrawal pool for a given epoch + /// @param epoch The epoch for which to read the withdrawal pool + /// @return The withdrawal pool for the specified epoch + fn withdrawal_pool(self: @ContractState, epoch: u256) -> u256 { + self.withdrawal_pool.read(epoch) + } + + /// @notice Reads the withdrawal share for a given epoch + /// @param epoch The epoch for which to read the withdrawal share + /// @return The withdrawal share for the specified epoch + fn withdrawal_share(self: @ContractState, epoch: u256) -> u256 { + self.withdrawal_share.read(epoch) + } /// @notice Sets the token for this contract /// @dev Only callable by the pooling manager @@ -341,11 +374,13 @@ mod TokenManager { let caller = get_caller_address(); let this = get_contract_address(); erc20_disp.transferFrom(caller, this, assets); + + let shares = self._convert_to_shares(assets); + let buffer = self.buffer.read(); let new_buffer = buffer + assets; self.buffer.write(new_buffer); - let shares = self._convert_to_shares(assets); let token = self.token.read(); let token_disp = ITokenDispatcher { contract_address: token }; token_disp.mint(receiver, shares); @@ -371,10 +406,10 @@ mod TokenManager { let token = self.token.read(); let token_disp = ITokenDispatcher { contract_address: token }; let caller = get_caller_address(); - token_disp.burn(caller, shares); let epoch = self.epoch.read(); let assets = self._convert_to_assets(shares); + token_disp.burn(caller, shares); let withdrawal_pool_share = (assets * CONSTANTS::WAD) / self._withdrawal_exchange_rate(epoch); @@ -409,6 +444,7 @@ mod TokenManager { fn claim_withdrawal(ref self: ContractState, id: u256) { let caller = get_caller_address(); let withdrawal_info = self.withdrawal_info.read((caller, id)); + assert(withdrawal_info.shares.is_non_zero(), Errors::ZERO_SHARES); assert(!withdrawal_info.claimed, Errors::ALREADY_CLAIMED); let handled_epoch_withdrawal_len = self.handled_epoch_withdrawal_len.read(); assert( @@ -569,9 +605,12 @@ mod TokenManager { // We deposit underlying to L1 self.buffer.write(0); self.underlying_transit.write(remaining_buffer_mem); - self._check_profit_and_mint(profit, token); - + assert(self.underlying.read().is_non_zero(), 'ZERO UND'); + let underlying_disp = ERC20ABIDispatcher { + contract_address: self.underlying.read() + }; + underlying_disp.transfer(self.pooling_manager.read(), remaining_buffer_mem); let new_share_price = self._convert_to_assets(one_share_unit); StrategyReportL2 { @@ -666,11 +705,7 @@ mod TokenManager { fn _withdrawal_exchange_rate(self: @ContractState, epoch: u256) -> u256 { let withdrawal_pool = self.withdrawal_pool.read(epoch); let withdrawal_share = self.withdrawal_share.read(epoch); - if (withdrawal_pool.is_zero()) { - 0 - } else { - (withdrawal_pool * CONSTANTS::WAD) / withdrawal_share - } + ((withdrawal_pool + 1) * CONSTANTS::WAD) / (withdrawal_share + 1) }