From 3bcfff8f53f56dff2f9bb86a78fa4e88c8a447d2 Mon Sep 17 00:00:00 2001 From: 0xKaizokuOu <154588428+0xKaizokuOu@users.noreply.github.com> Date: Fri, 22 Dec 2023 16:39:59 +0400 Subject: [PATCH 1/7] debt token --- src/core/debt_token.cairo | 224 ++++++++++++------ src/lib.cairo | 1 + src/utils/errors.cairo | 4 + .../add_whitelist/add_whitelist.tree | 2 + .../add_whitelist/test_add_whitelist.cairo | 67 ++++++ .../core/debt_token/burn/test_burn.cairo | 97 ++++++++ .../test_burn_from_whitelisted_contract.cairo | 65 +++++ .../emergency_stop_minting.tree | 6 - .../test_emergency_stop_minting.cairo | 1 - .../core/debt_token/mint/mint.tree | 5 +- .../core/debt_token/mint/test_mint.cairo | 54 +++++ .../test_mint_from_whitelisted_contract.cairo | 57 +++++ .../test_remove_whitelist.cairo | 65 +++++ .../return_from_pool/return_from_pool.tree | 7 - .../test_return_from_pool.cairo | 1 - .../debt_token/send_to_pool/send_to_pool.tree | 7 - .../send_to_pool/test_send_to_pool.cairo | 1 - .../debt_token/transfer/test_transfer.cairo | 1 - .../core/debt_token/transfer/transfer.tree | 7 - .../transfer_from/test_transfer_from.cairo | 1 - .../transfer_from/transfer_from.tree | 7 - tests/tests_lib.cairo | 10 + 22 files changed, 570 insertions(+), 120 deletions(-) delete mode 100644 tests/integration/core/debt_token/emergency_stop_minting/emergency_stop_minting.tree delete mode 100644 tests/integration/core/debt_token/emergency_stop_minting/test_emergency_stop_minting.cairo delete mode 100644 tests/integration/core/debt_token/return_from_pool/return_from_pool.tree delete mode 100644 tests/integration/core/debt_token/return_from_pool/test_return_from_pool.cairo delete mode 100644 tests/integration/core/debt_token/send_to_pool/send_to_pool.tree delete mode 100644 tests/integration/core/debt_token/send_to_pool/test_send_to_pool.cairo delete mode 100644 tests/integration/core/debt_token/transfer/test_transfer.cairo delete mode 100644 tests/integration/core/debt_token/transfer/transfer.tree delete mode 100644 tests/integration/core/debt_token/transfer_from/test_transfer_from.cairo delete mode 100644 tests/integration/core/debt_token/transfer_from/transfer_from.tree diff --git a/src/core/debt_token.cairo b/src/core/debt_token.cairo index 71f7f68..9743cbd 100644 --- a/src/core/debt_token.cairo +++ b/src/core/debt_token.cairo @@ -1,111 +1,181 @@ use starknet::ContractAddress; - #[starknet::interface] trait IDebtToken { - fn emergency_stop_minting(ref self: TContractState, _delay: u256); - - fn mint( - ref self: TContractState, _asset: ContractAddress, _account: ContractAddress, _amount: u256 - ); - - fn mint_from_whitelisted_contract(ref self: TContractState, _amount: u256); + fn mint(ref self: TContractState, account: ContractAddress, amount: u256); - fn burn_from_whitelisted_contract(ref self: TContractState, _amount: u256); + fn mint_from_whitelisted_contract(ref self: TContractState, amount: u256); - fn burn(ref self: TContractState, _account: ContractAddress, _amount: u256); + fn burn_from_whitelisted_contract(ref self: TContractState, amount: u256); - fn send_to_pool( - ref self: TContractState, - _sender: ContractAddress, - poolAddress: ContractAddress, - _amount: u256 - ); + fn burn(ref self: TContractState, account: ContractAddress, amount: u256); - fn return_from_pool( - ref self: TContractState, poolAddress: ContractAddress, user: ContractAddress, _amount: u256 - ); + fn add_whitelist(ref self: TContractState, address: ContractAddress); - fn add_whitelist(ref self: TContractState, _address: ContractAddress); + fn remove_whitelist(ref self: TContractState, address: ContractAddress); - fn remove_whitelist(ref self: TContractState, _address: ContractAddress); - - fn transfer(ref self: TContractState, _recipient: ContractAddress, _amount: u256); - - fn transfer_from( - ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256 - ); + fn is_whitelisted(self: @TContractState, address: ContractAddress) -> bool; } #[starknet::contract] mod DebtToken { - use starknet::ContractAddress; - + use core::starknet::event::EventEmitter; + use openzeppelin::access::ownable::ownable::OwnableComponent::InternalTrait as OwnableInternalTrait; + use shisui::core::debt_token::IDebtToken; + use openzeppelin::token::erc20::erc20::ERC20Component::InternalTrait as ERC20InternalTrait; + use shisui::core::address_provider::IAddressProviderDispatcherTrait; + use starknet::{ContractAddress, get_caller_address}; + use openzeppelin::{ + access::ownable::{OwnableComponent, OwnableComponent::InternalImpl}, + token::erc20::ERC20Component + }; + use shisui::{ + utils::errors::{CommunErrors, DebtTokenErrors}, + core::address_provider::{IAddressProviderDispatcher, AddressesKey} + }; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl OwnableImp = OwnableComponent::OwnableImpl; + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; #[storage] struct Storage { address_provider: ContractAddress, emergency_stop_minting_collateral: LegacyMap::, whitelisted_contracts: LegacyMap::, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + ERC20Event: ERC20Component::Event, + WhitelistChanged: WhitelistChanged, + } + + #[derive(Drop, starknet::Event)] + struct WhitelistChanged { + address: ContractAddress, + is_whitelisted: bool, } #[constructor] - fn constructor(ref self: ContractState, address_provider: ContractAddress) {} + fn constructor(ref self: ContractState, address_provider: ContractAddress) { + self.ownable.initializer(get_caller_address()); + self.address_provider.write(address_provider); + } #[external(v0)] impl DebtTokenImpl of super::IDebtToken { - fn emergency_stop_minting(ref self: ContractState, _delay: u256) {} - - fn mint( - ref self: ContractState, - _asset: ContractAddress, - _account: ContractAddress, - _amount: u256 - ) {} - - fn mint_from_whitelisted_contract(ref self: ContractState, _amount: u256) {} - - fn burn_from_whitelisted_contract(ref self: ContractState, _amount: u256) {} - - fn burn(ref self: ContractState, _account: ContractAddress, _amount: u256) {} - - fn send_to_pool( - ref self: ContractState, - _sender: ContractAddress, - poolAddress: ContractAddress, - _amount: u256 - ) {} - - fn return_from_pool( - ref self: ContractState, - poolAddress: ContractAddress, - user: ContractAddress, - _amount: u256 - ) {} - - fn add_whitelist(ref self: ContractState, _address: ContractAddress) {} - - fn remove_whitelist(ref self: ContractState, _address: ContractAddress) {} - - fn transfer(ref self: ContractState, _recipient: ContractAddress, _amount: u256) {} - - fn transfer_from( - ref self: ContractState, - sender: ContractAddress, - recipient: ContractAddress, - amount: u256 - ) {} + fn mint(ref self: ContractState, account: ContractAddress, amount: u256) { + self.require_caller_is_borrower_operations(); + self.erc20._mint(account, amount); + } + + fn mint_from_whitelisted_contract(ref self: ContractState, amount: u256) { + self.require_caller_is_whitelisted_contract(); + self.erc20._mint(get_caller_address(), amount); + } + + fn burn_from_whitelisted_contract(ref self: ContractState, amount: u256) { + self.require_caller_is_whitelisted_contract(); + self.erc20._burn(get_caller_address(), amount); + } + + fn burn(ref self: ContractState, account: ContractAddress, amount: u256) { + self.require_caller_is_bo_or_vesselm_or_sp(); + self.erc20._burn(account, amount); + } + + fn add_whitelist(ref self: ContractState, address: ContractAddress) { + self.ownable.assert_only_owner(); + assert(address.is_non_zero(), CommunErrors::CommunErrors__AddressZero); + self.whitelisted_contracts.write(address, true); + self.emit(WhitelistChanged { address: address, is_whitelisted: true }); + } + + fn remove_whitelist(ref self: ContractState, address: ContractAddress) { + self.ownable.assert_only_owner(); + self.whitelisted_contracts.write(address, false); + self.emit(WhitelistChanged { address: address, is_whitelisted: false }); + } + + fn is_whitelisted(self: @ContractState, address: ContractAddress) -> bool { + self.whitelisted_contracts.read(address) + } } #[generate_trait] impl InternalFunctions of InternalFunctionsTrait { - fn require_valid_recipient(self: @ContractState, _recipient: ContractAddress) {} - fn require_caller_is_whitelisted_contract(self: @ContractState) {} - fn require_caller_is_borrower_operations(self: @ContractState) {} - fn require_caller_is_bo_or_vesselm_or_sp(self: @ContractState) {} - fn require_caller_is_stability_pool(self: @ContractState) {} - fn require_caller_is_vesselm_or_sp(self: @ContractState) {} + #[inline(always)] + fn require_caller_is_whitelisted_contract(self: @ContractState) { + let caller = get_caller_address(); + assert( + self.is_whitelisted(caller) == true, CommunErrors::CommunErrors__CallerNotAuthorized + ); + } + #[inline(always)] + fn require_caller_is_borrower_operations(self: @ContractState) { + let caller = get_caller_address(); + let address_provider = IAddressProviderDispatcher { + contract_address: (self.address_provider.read()) + }; + let borrower_operations_manager = address_provider + .get_address(AddressesKey::borrower_operations); + assert( + caller == borrower_operations_manager, + CommunErrors::CommunErrors__CallerNotAuthorized + ); + } + #[inline(always)] + fn require_caller_is_bo_or_vesselm_or_sp(self: @ContractState) { + let caller = get_caller_address(); + let address_provider = IAddressProviderDispatcher { + contract_address: (self.address_provider.read()) + }; + let borrower_operations_manager = address_provider + .get_address(AddressesKey::borrower_operations); + let vessel_manager = address_provider.get_address(AddressesKey::vessel_manager); + let stability_pool = address_provider.get_address(AddressesKey::stability_pool); + assert( + caller == borrower_operations_manager + || caller == vessel_manager + || caller == stability_pool, + CommunErrors::CommunErrors__CallerNotAuthorized + ); + } + #[inline(always)] + fn require_caller_is_stability_pool(self: @ContractState) { + let caller = get_caller_address(); + let address_provider = IAddressProviderDispatcher { + contract_address: (self.address_provider.read()) + }; + let stability_pool = address_provider.get_address(AddressesKey::stability_pool); + assert(caller == stability_pool, CommunErrors::CommunErrors__CallerNotAuthorized); + } + #[inline(always)] + fn require_caller_is_vesselm_or_sp(self: @ContractState) { + let caller = get_caller_address(); + let address_provider = IAddressProviderDispatcher { + contract_address: (self.address_provider.read()) + }; + let vessel_manager = address_provider.get_address(AddressesKey::vessel_manager); + let stability_pool = address_provider.get_address(AddressesKey::stability_pool); + assert( + caller == vessel_manager || caller == stability_pool, + CommunErrors::CommunErrors__CallerNotAuthorized + ); + } } } diff --git a/src/lib.cairo b/src/lib.cairo index 830f8f2..c27a85f 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -9,6 +9,7 @@ mod core { mod price_feed; mod fee_collector; mod admin_contract; + mod debt_token; } mod pools { diff --git a/src/utils/errors.cairo b/src/utils/errors.cairo index d76e031..1d06fa2 100644 --- a/src/utils/errors.cairo +++ b/src/utils/errors.cairo @@ -5,3 +5,7 @@ mod CommunErrors { const CallerNotAuthorized: felt252 = 'Caller not authorized'; const Invalid_amount: felt252 = 'Amount Invalid'; } + +mod DebtTokenErrors { + const DebtTokenErrors__BurnAmountGtBalance: felt252 = 'Burn amount gt balance'; +} diff --git a/tests/integration/core/debt_token/add_whitelist/add_whitelist.tree b/tests/integration/core/debt_token/add_whitelist/add_whitelist.tree index 9a9f3ea..611d277 100644 --- a/tests/integration/core/debt_token/add_whitelist/add_whitelist.tree +++ b/tests/integration/core/debt_token/add_whitelist/add_whitelist.tree @@ -2,6 +2,8 @@ test_add_whitelist.cairo ├── when caller is not owner │ └── it should revert └── when caller is owner + ├── when whitelisted address is address zero + │ └── it should revert with CommunErrors__AddressZero ├── it should set whitelist to true for the given address └── it should emit {WhitelistChanged} event diff --git a/tests/integration/core/debt_token/add_whitelist/test_add_whitelist.cairo b/tests/integration/core/debt_token/add_whitelist/test_add_whitelist.cairo index 8b13789..032192c 100644 --- a/tests/integration/core/debt_token/add_whitelist/test_add_whitelist.cairo +++ b/tests/integration/core/debt_token/add_whitelist/test_add_whitelist.cairo @@ -1 +1,68 @@ +use starknet::{ContractAddress, contract_address_const}; +use shisui::core::{ + debt_token::{IDebtToken, IDebtTokenDispatcher, IDebtTokenDispatcherTrait, DebtToken}, + address_provider::{IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey}, +}; +use tests::tests_lib::{deploy_address_provider, deploy_debt_token}; +use snforge_std::{start_prank, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions}; +fn setup() -> (IAddressProviderDispatcher, IDebtTokenDispatcher) { + let address_provider_address: ContractAddress = deploy_address_provider(); + let address_provider: IAddressProviderDispatcher = IAddressProviderDispatcher { + contract_address: address_provider_address + }; + + let debt_token_address: ContractAddress = deploy_debt_token(address_provider_address); + let debt_token: IDebtTokenDispatcher = IDebtTokenDispatcher { + contract_address: debt_token_address + }; + + (address_provider, debt_token) +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn given_caller_is_not_owner_on_add_whitelist_it_should_revert() { + let (_, debt_token) = setup(); + start_prank( + CheatTarget::One(debt_token.contract_address), contract_address_const::<'not_owner'>() + ); + debt_token.add_whitelist(contract_address_const::<'fee_collector'>()); +} + +#[test] +#[should_panic(expected: ('Address is zero',))] +fn given_caller_is_owner_whitelisting_new_address_zero_it_should_revert() { + let (_, debt_token) = setup(); + debt_token.add_whitelist(contract_address_const::<0x00>()); +} + +#[test] +fn given_caller_is_owner_it_should_add_whitelist() { + let (_, debt_token) = setup(); + let mut spy = spy_events(SpyOn::One(debt_token.contract_address)); + + debt_token.add_whitelist(contract_address_const::<'fee_collector'>()); + + // event check + spy + .assert_emitted( + @array![ + ( + debt_token.contract_address, + DebtToken::Event::WhitelistChanged( + DebtToken::WhitelistChanged { + address: contract_address_const::<'fee_collector'>(), + is_whitelisted: true + } + ) + ) + ] + ); + + assert(spy.events.len() == 0, 'There should be no events'); + assert( + debt_token.is_whitelisted(contract_address_const::<'fee_collector'>()), + 'Address should be whitelisted' + ); +} diff --git a/tests/integration/core/debt_token/burn/test_burn.cairo b/tests/integration/core/debt_token/burn/test_burn.cairo index 8b13789..8b599da 100644 --- a/tests/integration/core/debt_token/burn/test_burn.cairo +++ b/tests/integration/core/debt_token/burn/test_burn.cairo @@ -1 +1,98 @@ +use starknet::{ContractAddress, contract_address_const, get_caller_address}; +use shisui::core::{ + debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait, DebtToken}, + address_provider::{IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey}, +}; +use tests::tests_lib::{deploy_address_provider, deploy_debt_token}; +use snforge_std::{ + start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions, PrintTrait +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +const MINT_AMOUNT: u256 = 1000; + +fn setup() -> (IAddressProviderDispatcher, IDebtTokenDispatcher, ContractAddress) { + let address_provider_address: ContractAddress = deploy_address_provider(); + let address_provider: IAddressProviderDispatcher = IAddressProviderDispatcher { + contract_address: address_provider_address + }; + let debt_token_address: ContractAddress = deploy_debt_token(address_provider_address); + let debt_token: IDebtTokenDispatcher = IDebtTokenDispatcher { + contract_address: debt_token_address + }; + let caller = contract_address_const::<'caller'>(); + + address_provider + .set_address( + AddressesKey::borrower_operations, contract_address_const::<'borrower_operations'>() + ); + address_provider + .set_address(AddressesKey::vessel_manager, contract_address_const::<'vessel_manager'>()); + address_provider + .set_address(AddressesKey::stability_pool, contract_address_const::<'stability_pool'>()); + + start_prank( + CheatTarget::One(debt_token.contract_address), + contract_address_const::<'borrower_operations'>() + ); + debt_token.mint(caller, MINT_AMOUNT); + stop_prank(CheatTarget::One(debt_token.contract_address)); + + (address_provider, debt_token, caller) +} + +#[test] +#[should_panic(expected: ('Caller is not authorized',))] +fn given_caller_is_not_borrower_operations_nor_vessel_manager_nor_stability_pool_it_should_revert() { + let (_, debt_token, caller) = setup(); + debt_token.burn(caller, MINT_AMOUNT); +} + +#[test] +fn given_caller_is_borrower_operations_it_should_burn() { + let (_, debt_token, caller) = setup(); + start_prank( + CheatTarget::One(debt_token.contract_address), + contract_address_const::<'borrower_operations'>() + ); + debt_token.burn(caller, MINT_AMOUNT); + stop_prank(CheatTarget::One(debt_token.contract_address)); + + let debt_token: IERC20Dispatcher = IERC20Dispatcher { + contract_address: debt_token.contract_address + }; + + assert(debt_token.balance_of(caller) == 0, 'Wrong balance'); +} + +#[test] +fn given_caller_is_vessel_manager_it_should_burn() { + let (_, debt_token, caller) = setup(); + start_prank( + CheatTarget::One(debt_token.contract_address), contract_address_const::<'vessel_manager'>() + ); + debt_token.burn(caller, MINT_AMOUNT); + stop_prank(CheatTarget::One(debt_token.contract_address)); + + let debt_token: IERC20Dispatcher = IERC20Dispatcher { + contract_address: debt_token.contract_address + }; + + assert(debt_token.balance_of(caller) == 0, 'Wrong balance'); +} + +#[test] +fn given_caller_is_stability_pool_it_should_burn() { + let (_, debt_token, caller) = setup(); + start_prank( + CheatTarget::One(debt_token.contract_address), contract_address_const::<'stability_pool'>() + ); + debt_token.burn(caller, MINT_AMOUNT); + stop_prank(CheatTarget::One(debt_token.contract_address)); + + let debt_token: IERC20Dispatcher = IERC20Dispatcher { + contract_address: debt_token.contract_address + }; + + assert(debt_token.balance_of(caller) == 0, 'Wrong balance'); +} diff --git a/tests/integration/core/debt_token/burn_from_whitelisted_contract/test_burn_from_whitelisted_contract.cairo b/tests/integration/core/debt_token/burn_from_whitelisted_contract/test_burn_from_whitelisted_contract.cairo index 8b13789..4f9af4b 100644 --- a/tests/integration/core/debt_token/burn_from_whitelisted_contract/test_burn_from_whitelisted_contract.cairo +++ b/tests/integration/core/debt_token/burn_from_whitelisted_contract/test_burn_from_whitelisted_contract.cairo @@ -1 +1,66 @@ +use starknet::{ContractAddress, contract_address_const, get_caller_address}; +use shisui::core::{ + debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait, DebtToken}, + address_provider::{IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey}, +}; +use tests::tests_lib::{deploy_address_provider, deploy_debt_token}; +use snforge_std::{ + start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions, PrintTrait +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +const MINT_AMOUNT: u256 = 1000; + +fn setup() -> (IAddressProviderDispatcher, IDebtTokenDispatcher, ContractAddress, ContractAddress) { + let address_provider_address: ContractAddress = deploy_address_provider(); + let address_provider: IAddressProviderDispatcher = IAddressProviderDispatcher { + contract_address: address_provider_address + }; + let debt_token_address: ContractAddress = deploy_debt_token(address_provider_address); + let debt_token: IDebtTokenDispatcher = IDebtTokenDispatcher { + contract_address: debt_token_address + }; + let caller = contract_address_const::<'caller'>(); + let not_caller = contract_address_const::<'not_caller'>(); + + address_provider + .set_address( + AddressesKey::borrower_operations, contract_address_const::<'borrower_operations'>() + ); + + debt_token.add_whitelist(caller); + + start_prank( + CheatTarget::One(debt_token.contract_address), + contract_address_const::<'borrower_operations'>() + ); + debt_token.mint(caller, MINT_AMOUNT); + stop_prank(CheatTarget::One(debt_token.contract_address)); + + start_prank(CheatTarget::One(debt_token.contract_address), caller); + + (address_provider, debt_token, caller, not_caller) +} + +#[test] +#[should_panic(expected: ('Caller is not authorized',))] +fn given_caller_is_not_whitelisted_it_should_revert() { + let (_, debt_token, caller, not_caller) = setup(); + + start_prank(CheatTarget::One(debt_token.contract_address), not_caller); + debt_token.burn_from_whitelisted_contract(MINT_AMOUNT); + stop_prank(CheatTarget::One(debt_token.contract_address)); +} + +#[test] +fn given_caller_is_whitelisted_should_burn() { + let (_, debt_token, caller, _) = setup(); + + debt_token.burn_from_whitelisted_contract(MINT_AMOUNT); + + let debt_token: IERC20Dispatcher = IERC20Dispatcher { + contract_address: debt_token.contract_address + }; + + assert(debt_token.balance_of(caller) == 0, 'Wrong balance'); +} diff --git a/tests/integration/core/debt_token/emergency_stop_minting/emergency_stop_minting.tree b/tests/integration/core/debt_token/emergency_stop_minting/emergency_stop_minting.tree deleted file mode 100644 index 1a2ec60..0000000 --- a/tests/integration/core/debt_token/emergency_stop_minting/emergency_stop_minting.tree +++ /dev/null @@ -1,6 +0,0 @@ -test_emergency_stop_minting.cairo -├── when caller is not owner -│ └── it should revert -└── when caller is owner - ├── it should set emergency stop to true - └── it should emit {EmergencyStopMintingCollateral} event \ No newline at end of file diff --git a/tests/integration/core/debt_token/emergency_stop_minting/test_emergency_stop_minting.cairo b/tests/integration/core/debt_token/emergency_stop_minting/test_emergency_stop_minting.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/core/debt_token/emergency_stop_minting/test_emergency_stop_minting.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration/core/debt_token/mint/mint.tree b/tests/integration/core/debt_token/mint/mint.tree index 4c41c34..60d4ccf 100644 --- a/tests/integration/core/debt_token/mint/mint.tree +++ b/tests/integration/core/debt_token/mint/mint.tree @@ -2,7 +2,4 @@ test_mint.cairo ├── when caller is not the Borrower Operations contract │ └── it should revert └── when caller is the Borrower Operations contract - ├── when _asset is under emergency stop minting collateral - │ └── it should revet - └── when _asset is not under emergency stop minting collateral - └── it should mint new tokens to the recipient \ No newline at end of file + └── it should mint new tokens to the recipient \ No newline at end of file diff --git a/tests/integration/core/debt_token/mint/test_mint.cairo b/tests/integration/core/debt_token/mint/test_mint.cairo index 8b13789..a0e17c6 100644 --- a/tests/integration/core/debt_token/mint/test_mint.cairo +++ b/tests/integration/core/debt_token/mint/test_mint.cairo @@ -1 +1,55 @@ +use starknet::{ContractAddress, contract_address_const, get_caller_address}; +use shisui::core::{ + debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait, DebtToken}, + address_provider::{IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey}, +}; +use tests::tests_lib::{deploy_address_provider, deploy_debt_token}; +use snforge_std::{ + start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions, PrintTrait +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +const MINT_AMOUNT: u256 = 1000; + +fn setup() -> (IAddressProviderDispatcher, IDebtTokenDispatcher, ContractAddress) { + let address_provider_address: ContractAddress = deploy_address_provider(); + let address_provider: IAddressProviderDispatcher = IAddressProviderDispatcher { + contract_address: address_provider_address + }; + let debt_token_address: ContractAddress = deploy_debt_token(address_provider_address); + let debt_token: IDebtTokenDispatcher = IDebtTokenDispatcher { + contract_address: debt_token_address + }; + let caller = contract_address_const::<'caller'>(); + + address_provider + .set_address( + AddressesKey::borrower_operations, contract_address_const::<'borrower_operations'>() + ); + + (address_provider, debt_token, caller) +} + +#[test] +#[should_panic(expected: ('Caller is not authorized',))] +fn given_caller_is_not_borrower_operations_it_should_revert() { + let (_, debt_token, caller) = setup(); + debt_token.burn(caller, MINT_AMOUNT); +} + +#[test] +fn given_caller_is_borrower_operations_it_should_mint() { + let (_, debt_token, caller) = setup(); + start_prank( + CheatTarget::One(debt_token.contract_address), + contract_address_const::<'borrower_operations'>() + ); + debt_token.mint(caller, MINT_AMOUNT); + stop_prank(CheatTarget::One(debt_token.contract_address)); + + let debt_token: IERC20Dispatcher = IERC20Dispatcher { + contract_address: debt_token.contract_address + }; + + assert(debt_token.balance_of(caller) == MINT_AMOUNT, 'Wrong balance'); +} diff --git a/tests/integration/core/debt_token/mint_from_whitelisted_contract/test_mint_from_whitelisted_contract.cairo b/tests/integration/core/debt_token/mint_from_whitelisted_contract/test_mint_from_whitelisted_contract.cairo index 8b13789..d13d6c3 100644 --- a/tests/integration/core/debt_token/mint_from_whitelisted_contract/test_mint_from_whitelisted_contract.cairo +++ b/tests/integration/core/debt_token/mint_from_whitelisted_contract/test_mint_from_whitelisted_contract.cairo @@ -1 +1,58 @@ +use starknet::{ContractAddress, contract_address_const, get_caller_address}; +use shisui::core::{ + debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait, DebtToken}, + address_provider::{IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey}, +}; +use tests::tests_lib::{deploy_address_provider, deploy_debt_token}; +use snforge_std::{ + start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions, PrintTrait +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +const MINT_AMOUNT: u256 = 1000; + +fn setup() -> (IAddressProviderDispatcher, IDebtTokenDispatcher, ContractAddress, ContractAddress) { + let address_provider_address: ContractAddress = deploy_address_provider(); + let address_provider: IAddressProviderDispatcher = IAddressProviderDispatcher { + contract_address: address_provider_address + }; + let debt_token_address: ContractAddress = deploy_debt_token(address_provider_address); + let debt_token: IDebtTokenDispatcher = IDebtTokenDispatcher { + contract_address: debt_token_address + }; + let caller = contract_address_const::<'caller'>(); + let not_caller = contract_address_const::<'not_caller'>(); + + address_provider + .set_address( + AddressesKey::borrower_operations, contract_address_const::<'borrower_operations'>() + ); + debt_token.add_whitelist(caller); + + start_prank(CheatTarget::One(debt_token.contract_address), caller); + + (address_provider, debt_token, caller, not_caller) +} + +#[test] +#[should_panic(expected: ('Caller is not authorized',))] +fn given_caller_is_not_whitelisted_it_should_revert() { + let (_, debt_token, caller, not_caller) = setup(); + + start_prank(CheatTarget::One(debt_token.contract_address), not_caller); + debt_token.mint_from_whitelisted_contract(MINT_AMOUNT); + stop_prank(CheatTarget::One(debt_token.contract_address)); +} + +#[test] +fn given_caller_is_whitelisted_should_mint() { + let (_, debt_token, caller, _) = setup(); + + debt_token.mint_from_whitelisted_contract(MINT_AMOUNT); + + let debt_token: IERC20Dispatcher = IERC20Dispatcher { + contract_address: debt_token.contract_address + }; + + assert(debt_token.balance_of(caller) == MINT_AMOUNT, 'Wrong balance'); +} diff --git a/tests/integration/core/debt_token/remove_whitelist/test_remove_whitelist.cairo b/tests/integration/core/debt_token/remove_whitelist/test_remove_whitelist.cairo index 8b13789..04bcc53 100644 --- a/tests/integration/core/debt_token/remove_whitelist/test_remove_whitelist.cairo +++ b/tests/integration/core/debt_token/remove_whitelist/test_remove_whitelist.cairo @@ -1 +1,66 @@ +use starknet::{ContractAddress, contract_address_const, get_caller_address}; +use shisui::core::{ + debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait, DebtToken}, + address_provider::{IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey}, +}; +use tests::tests_lib::{deploy_address_provider, deploy_debt_token}; +use snforge_std::{ + start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions, PrintTrait +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +const MINT_AMOUNT: u256 = 1000; + +fn setup() -> (IAddressProviderDispatcher, IDebtTokenDispatcher, ContractAddress, ContractAddress) { + let address_provider_address: ContractAddress = deploy_address_provider(); + let address_provider: IAddressProviderDispatcher = IAddressProviderDispatcher { + contract_address: address_provider_address + }; + let debt_token_address: ContractAddress = deploy_debt_token(address_provider_address); + let debt_token: IDebtTokenDispatcher = IDebtTokenDispatcher { + contract_address: debt_token_address + }; + let caller = contract_address_const::<'caller'>(); + let not_caller = contract_address_const::<'not_caller'>(); + + debt_token.add_whitelist(caller); + + start_prank(CheatTarget::One(debt_token.contract_address), caller); + + (address_provider, debt_token, caller, not_caller) +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn given_caller_is_not_owner_it_should_revert() { + let (_, debt_token, caller, not_caller) = setup(); + + start_prank(CheatTarget::One(debt_token.contract_address), not_caller); + debt_token.remove_whitelist(caller); + stop_prank(CheatTarget::One(debt_token.contract_address)); +} + +#[test] +fn given_caller_is_owner_it_should_remove_whitelist() { + let (_, debt_token, caller, _) = setup(); + let mut spy = spy_events(SpyOn::One(debt_token.contract_address)); + + debt_token.remove_whitelist(caller); + + // event check + spy + .assert_emitted( + @array![ + ( + debt_token.contract_address, + DebtToken::Event::WhitelistChanged( + DebtToken::WhitelistChanged { address: caller, is_whitelisted: false } + ) + ) + ] + ); + + assert(spy.events.len() == 0, 'There should be no events'); + + assert(debt_token.is_whitelisted(caller) == false, 'Address still whitelisted'); +} diff --git a/tests/integration/core/debt_token/return_from_pool/return_from_pool.tree b/tests/integration/core/debt_token/return_from_pool/return_from_pool.tree deleted file mode 100644 index 2243e93..0000000 --- a/tests/integration/core/debt_token/return_from_pool/return_from_pool.tree +++ /dev/null @@ -1,7 +0,0 @@ -test_return_to_pool.cairo -├── when caller is neither the Vessel Manager nor the Stability Pool -│ └── it should revert -└── when caller is either the Stability Pool or the Stability Pool - └── it should transfer the _amount from the _pool_address to the _receiver - - diff --git a/tests/integration/core/debt_token/return_from_pool/test_return_from_pool.cairo b/tests/integration/core/debt_token/return_from_pool/test_return_from_pool.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/core/debt_token/return_from_pool/test_return_from_pool.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration/core/debt_token/send_to_pool/send_to_pool.tree b/tests/integration/core/debt_token/send_to_pool/send_to_pool.tree deleted file mode 100644 index 6206bcc..0000000 --- a/tests/integration/core/debt_token/send_to_pool/send_to_pool.tree +++ /dev/null @@ -1,7 +0,0 @@ -test_send_to_pool.cairo -├── when caller is not the Stability Pool -│ └── it should revert -└── when caller is the Stability Pool - └── it should transfer the amount from the sender to the pool - - diff --git a/tests/integration/core/debt_token/send_to_pool/test_send_to_pool.cairo b/tests/integration/core/debt_token/send_to_pool/test_send_to_pool.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/core/debt_token/send_to_pool/test_send_to_pool.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration/core/debt_token/transfer/test_transfer.cairo b/tests/integration/core/debt_token/transfer/test_transfer.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/core/debt_token/transfer/test_transfer.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration/core/debt_token/transfer/transfer.tree b/tests/integration/core/debt_token/transfer/transfer.tree deleted file mode 100644 index 60fa9f1..0000000 --- a/tests/integration/core/debt_token/transfer/transfer.tree +++ /dev/null @@ -1,7 +0,0 @@ -test_transfer.cairo -├── when _recipient is address zero -│ └── it should revert -├── when _recipient is the contract itself -│ └── it should revert -└── when _recipient is neither the debt token address nor address zero - └── it should transfer the debt token to the recipient \ No newline at end of file diff --git a/tests/integration/core/debt_token/transfer_from/test_transfer_from.cairo b/tests/integration/core/debt_token/transfer_from/test_transfer_from.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/core/debt_token/transfer_from/test_transfer_from.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration/core/debt_token/transfer_from/transfer_from.tree b/tests/integration/core/debt_token/transfer_from/transfer_from.tree deleted file mode 100644 index e3df2dd..0000000 --- a/tests/integration/core/debt_token/transfer_from/transfer_from.tree +++ /dev/null @@ -1,7 +0,0 @@ -test_transfer.cairo -├── when _recipient is address zero -│ └── it should revert -├── when _recipient is the contract itself -│ └── it should revert -└── when _recipient is neither the debt token address nor address zero - └── it should transfer the debt token from the sender to the recipient \ No newline at end of file diff --git a/tests/tests_lib.cairo b/tests/tests_lib.cairo index 2dfeb41..12b4a46 100644 --- a/tests/tests_lib.cairo +++ b/tests/tests_lib.cairo @@ -73,3 +73,13 @@ fn deploy_address_provider() -> ContractAddress { let contract = declare('AddressProvider'); deploy_mock_contract(contract, @array![]) } + +/// Utility function to deploy a DebtToken contract and return its address. +/// +/// # Returns +/// +/// * `ContractAddress` - The address of the deployed data store contract. +fn deploy_debt_token(address_provider: ContractAddress) -> ContractAddress { + let contract = declare('DebtToken'); + deploy_mock_contract(contract, @array![address_provider.into()]) +} From 1cda11f02ab9042e2ac8b710f26fad71390e539f Mon Sep 17 00:00:00 2001 From: 0xKaizokuOu <154588428+0xKaizokuOu@users.noreply.github.com> Date: Fri, 22 Dec 2023 16:48:05 +0400 Subject: [PATCH 2/7] update tests/lib.cairo --- tests/lib.cairo | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/lib.cairo b/tests/lib.cairo index 293331c..c1a34cf 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -125,30 +125,15 @@ mod integration { mod mint { mod test_mint; } - mod transfer { - mod test_transfer; - } - mod transfer_from { - mod test_transfer_from; - } mod add_whitelist { mod test_add_whitelist; } mod remove_whitelist { mod test_remove_whitelist; } - mod return_from_pool { - mod test_return_from_pool; - } - mod send_to_pool { - mod test_send_to_pool; - } mod mint_from_whitelisted_contract { mod test_mint_from_whitelisted_contract; } - mod emergency_stop_minting { - mod test_emergency_stop_minting; - } mod burn_from_whitelisted_contract { mod test_burn_from_whitelisted_contract; } From 387c6babe60c9dced1011c6e58bfb99d7675e94b Mon Sep 17 00:00:00 2001 From: FabienC Date: Fri, 22 Dec 2023 14:39:11 +0200 Subject: [PATCH 3/7] feat: add interfaces for deposit --- src/interfaces/deposit.cairo | 6 ++++++ src/lib.cairo | 5 +++++ 2 files changed, 11 insertions(+) create mode 100644 src/interfaces/deposit.cairo diff --git a/src/interfaces/deposit.cairo b/src/interfaces/deposit.cairo new file mode 100644 index 0000000..1245a7b --- /dev/null +++ b/src/interfaces/deposit.cairo @@ -0,0 +1,6 @@ +use starknet::ContractAddress; + +#[starknet::interface] +trait IDeposit { + fn received_erc20(ref self: TContractState, asset: ContractAddress, amount: u256); +} diff --git a/src/lib.cairo b/src/lib.cairo index c27a85f..bf04543 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -2,6 +2,10 @@ mod components { mod shisui_base; } +mod interfaces{ + mod deposit; +} + mod core { mod address_provider; mod timelock; @@ -20,6 +24,7 @@ mod pools { } mod utils { + mod asserts; mod errors; mod constants; mod array; From 7452691e8b2a8b9965cd3ef2180da1402bd0c243 Mon Sep 17 00:00:00 2001 From: FabienC Date: Fri, 22 Dec 2023 14:39:32 +0200 Subject: [PATCH 4/7] feat: add assert on address zero --- src/utils/asserts.cairo | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/utils/asserts.cairo diff --git a/src/utils/asserts.cairo b/src/utils/asserts.cairo new file mode 100644 index 0000000..a0b792c --- /dev/null +++ b/src/utils/asserts.cairo @@ -0,0 +1,6 @@ +use starknet::ContractAddress; +use shisui::utils::errors::CommunErrors; + +fn assert_address_non_zero(address: ContractAddress) { + assert(address.is_non_zero(), CommunErrors::CommunErrors__AddressZero); +} \ No newline at end of file From 5a2391caa39da45d35eb25e420040cc2e17ebd51 Mon Sep 17 00:00:00 2001 From: FabienC Date: Fri, 22 Dec 2023 20:44:10 +0200 Subject: [PATCH 5/7] feat: implement active_pool with tests --- src/lib.cairo | 3 +- src/mocks/erc20_mock.cairo | 27 ++- src/mocks/receive_erc20_mock.cairo | 29 +++ src/pools/active_pool.cairo | 165 ++++++++++++++- src/utils/asserts.cairo | 2 +- .../decrease_debt/decrease_debt.tree | 12 +- .../decrease_debt/test_decrease_debt.cairo | 107 ++++++++++ .../increase_debt/test_increase_debt.cairo | 53 +++++ .../received_erc20/test_received_erc20.cairo | 65 ++++++ .../active_pool/send_asset/send_asset.tree | 2 +- .../send_asset/test_send_asset.cairo | 191 ++++++++++++++++++ .../integration/pools/active_pool/setup.cairo | 34 ++++ tests/lib.cairo | 1 + tests/tests_lib.cairo | 17 ++ tests/utils/callers.cairo | 32 +++ 15 files changed, 715 insertions(+), 25 deletions(-) create mode 100644 src/mocks/receive_erc20_mock.cairo create mode 100644 tests/integration/pools/active_pool/setup.cairo diff --git a/src/lib.cairo b/src/lib.cairo index bf04543..269fc79 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -2,7 +2,7 @@ mod components { mod shisui_base; } -mod interfaces{ +mod interfaces { mod deposit; } @@ -37,5 +37,6 @@ mod utils { mod mocks { mod erc20_mock; mod pragma_oracle_mock; + mod receive_erc20_mock; } diff --git a/src/mocks/erc20_mock.cairo b/src/mocks/erc20_mock.cairo index 6f67664..6a36277 100644 --- a/src/mocks/erc20_mock.cairo +++ b/src/mocks/erc20_mock.cairo @@ -1,4 +1,14 @@ //! Contract to mock ERC20 with specfic decimals +use starknet::ContractAddress; +use openzeppelin::token::erc20::interface::IERC20; + +#[starknet::interface] +trait IERC20MintBurn { + fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256); + + fn burn(ref self: TContractState, recipient: ContractAddress, amount: u256); +} + #[starknet::contract] mod ERC20Mock { @@ -44,7 +54,7 @@ mod ERC20Mock { #[constructor] fn constructor(ref self: ContractState, decimals: u8) { // Call the internal function that writes decimals to storage - self._set_decimals(decimals); + self.decimals.write(decimals); // Initialize ERC20 let name = 'ERC20Mock'; let symbol = 'MOCK'; @@ -70,13 +80,14 @@ mod ERC20Mock { } } - // ************************************************************************* - // INTERNAL FUNCTIONS - // ************************************************************************* - #[generate_trait] - impl InternalImpl of InternalTrait { - fn _set_decimals(ref self: ContractState, decimals: u8) { - self.decimals.write(decimals); + #[external(v0)] + impl ERC20MockImpl of super::IERC20MintBurn { + fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { + self.erc20._mint(recipient, amount); + } + + fn burn(ref self: ContractState, recipient: ContractAddress, amount: u256) { + self.erc20._burn(recipient, amount); } } } diff --git a/src/mocks/receive_erc20_mock.cairo b/src/mocks/receive_erc20_mock.cairo new file mode 100644 index 0000000..a7169c9 --- /dev/null +++ b/src/mocks/receive_erc20_mock.cairo @@ -0,0 +1,29 @@ +#[starknet::interface] +trait IIsCalled { + fn is_called(self: @TContractState) -> bool; +} + +#[starknet::contract] +mod ReceiveERC20Mock { + use starknet::ContractAddress; + use shisui::interfaces::deposit::IDeposit; + + #[storage] + struct Storage { + is_called: bool, + } + + #[external(v0)] + impl DepositImpl of IDeposit { + fn received_erc20(ref self: ContractState, asset: ContractAddress, amount: u256) { + self.is_called.write(true); + } + } + + #[external(v0)] + impl IsCalled of super::IIsCalled { + fn is_called(self: @ContractState) -> bool { + self.is_called.read() + } + } +} diff --git a/src/pools/active_pool.cairo b/src/pools/active_pool.cairo index ef10e05..99f26e7 100644 --- a/src/pools/active_pool.cairo +++ b/src/pools/active_pool.cairo @@ -25,50 +25,193 @@ trait IActivePool { /// Stability Pool, the Default Pool, or both, depending on the liquidation conditions. #[starknet::contract] mod ActivePool { - use starknet::{ContractAddress, get_caller_address}; + use starknet::{ContractAddress, get_caller_address, get_contract_address}; + use openzeppelin::{ + token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}, + security::reentrancyguard::{ + ReentrancyGuardComponent, ReentrancyGuardComponent::InternalImpl + } + }; use shisui::core::address_provider::{ - IAddressProviderDispatcher, IAddressProviderDispatcherTrait + IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey }; + use shisui::utils::{ + errors::CommunErrors, asserts::assert_address_non_zero, convert::decimals_correction + }; + use shisui::interfaces::deposit::{IDepositDispatcher, IDepositDispatcherTrait}; + use super::IActivePool; + use snforge_std::PrintTrait; + component!(path: ReentrancyGuardComponent, storage: reentrancy, event: ReentrancyEvent); #[storage] struct Storage { address_provider: IAddressProviderDispatcher, assets_balances: LegacyMap, debt_token_balances: LegacyMap, + #[substorage(v0)] + reentrancy: ReentrancyGuardComponent::Storage, + } + + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ReentrancyEvent: ReentrancyGuardComponent::Event, + ActivePoolAssetBalanceUpdated: ActivePoolAssetBalanceUpdated, + ActivePoolDebtUpdated: ActivePoolDebtUpdated, + AssetSent: AssetSent + } + + + #[derive(Drop, starknet::Event)] + struct ActivePoolAssetBalanceUpdated { + asset: ContractAddress, + old_balance: u256, + new_balance: u256, + } + + #[derive(Drop, starknet::Event)] + struct ActivePoolDebtUpdated { + asset: ContractAddress, + old_balance: u256, + new_balance: u256, + } + + + #[derive(Drop, starknet::Event)] + struct AssetSent { + account: ContractAddress, + asset: ContractAddress, + amount: u256, } #[constructor] fn constructor(ref self: ContractState, address_provider: IAddressProviderDispatcher) { + assert_address_non_zero(address_provider.contract_address); self.address_provider.write(address_provider); } #[external(v0)] - impl ActivePoolImpl of super::IActivePool { - fn increase_debt(ref self: ContractState, asset: ContractAddress, amount: u256) {} + impl ActivePoolImpl of IActivePool { + fn increase_debt(ref self: ContractState, asset: ContractAddress, amount: u256) { + self.assert_caller_is_borrow_operations_or_vessel_manager(); + let old_balance = self.debt_token_balances.read(asset); + let new_balance = old_balance + amount; + self.debt_token_balances.write(asset, new_balance); + self.emit(ActivePoolDebtUpdated { asset, old_balance, new_balance }); + } - fn decrease_debt(ref self: ContractState, asset: ContractAddress, amount: u256) {} + fn decrease_debt(ref self: ContractState, asset: ContractAddress, amount: u256) { + self.assert_caller_is_borrow_operations_or_stability_pool_or_vessel_manager(); + let old_balance = self.debt_token_balances.read(asset); + let new_balance = old_balance - amount; + self.debt_token_balances.write(asset, new_balance); + self.emit(ActivePoolDebtUpdated { asset, old_balance, new_balance }); + } fn send_asset( ref self: ContractState, asset: ContractAddress, account: ContractAddress, amount: u256 - ) {} + ) { + self.reentrancy.start(); + self.assert_caller_is_borrow_operations_or_stability_pool_or_vessel(); + let safety_transfer_amount = decimals_correction(asset, amount); + if (safety_transfer_amount == 0) { + self.reentrancy.end(); + return; + } + + let old_balance = self.assets_balances.read(asset); + let new_balance = old_balance - amount; + self.assets_balances.write(asset, new_balance); + + IERC20Dispatcher { contract_address: asset }.transfer(account, safety_transfer_amount); + if (self.is_erc20_deposit_contract(account)) { + IDepositDispatcher { contract_address: account }.received_erc20(asset, amount); + } + self.emit(ActivePoolAssetBalanceUpdated { asset, old_balance, new_balance }); + self.emit(AssetSent { account, asset, amount: safety_transfer_amount }); + self.reentrancy.end(); + } - fn received_erc20(ref self: ContractState, asset: ContractAddress, amount: u256) {} + fn received_erc20(ref self: ContractState, asset: ContractAddress, amount: u256) { + self.assert_caller_is_borrow_operations_or_default_pool(); + let old_balance = self.assets_balances.read(asset); + let new_balance = old_balance + amount; + self.assets_balances.write(asset, new_balance); + + self.emit(ActivePoolAssetBalanceUpdated { asset, old_balance, new_balance }); + } fn get_asset_balance(self: @ContractState, asset: ContractAddress) -> u256 { - return 0; + return self.assets_balances.read(asset); } fn get_debt_token_balance(self: @ContractState, asset: ContractAddress) -> u256 { - return 0; + return self.debt_token_balances.read(asset); } } #[generate_trait] impl InternalFunctions of InternalFunctionsTrait { - fn _is_erc20_deposit_contract(self: @ContractState, account: ContractAddress) -> bool { - return false; + #[inline(always)] + fn is_erc20_deposit_contract(self: @ContractState, account: ContractAddress) -> bool { + let address_provider = self.address_provider.read(); + return account == address_provider.get_address(AddressesKey::default_pool) + || account == address_provider.get_address(AddressesKey::coll_surplus_pool) + || account == address_provider.get_address(AddressesKey::stability_pool); + } + + #[inline(always)] + fn assert_caller_is_borrow_operations_or_default_pool(self: @ContractState) { + let caller = get_caller_address(); + let address_provider = self.address_provider.read(); + assert( + caller == address_provider.get_address(AddressesKey::borrower_operations) + || caller == address_provider.get_address(AddressesKey::default_pool), + CommunErrors::CommunErrors__CallerNotAuthorized + ); + } + + #[inline(always)] + fn assert_caller_is_borrow_operations_or_vessel_manager(self: @ContractState) { + let caller = get_caller_address(); + let address_provider = self.address_provider.read(); + assert( + caller == address_provider.get_address(AddressesKey::borrower_operations) + || caller == address_provider.get_address(AddressesKey::vessel_manager), + CommunErrors::CommunErrors__CallerNotAuthorized + ); + } + + #[inline(always)] + fn assert_caller_is_borrow_operations_or_stability_pool_or_vessel_manager( + self: @ContractState + ) { + let caller = get_caller_address(); + let address_provider = self.address_provider.read(); + assert( + caller == address_provider.get_address(AddressesKey::borrower_operations) + || caller == address_provider.get_address(AddressesKey::stability_pool) + || caller == address_provider.get_address(AddressesKey::vessel_manager), + CommunErrors::CommunErrors__CallerNotAuthorized + ); + } + + #[inline(always)] + fn assert_caller_is_borrow_operations_or_stability_pool_or_vessel(self: @ContractState) { + let caller = get_caller_address(); + let address_provider = self.address_provider.read(); + assert( + caller == address_provider.get_address(AddressesKey::borrower_operations) + || caller == address_provider.get_address(AddressesKey::stability_pool) + || caller == address_provider.get_address(AddressesKey::vessel_manager) + || caller == address_provider + .get_address(AddressesKey::vessel_manager_operations), + CommunErrors::CommunErrors__CallerNotAuthorized + ); } } } diff --git a/src/utils/asserts.cairo b/src/utils/asserts.cairo index a0b792c..d4b5cee 100644 --- a/src/utils/asserts.cairo +++ b/src/utils/asserts.cairo @@ -3,4 +3,4 @@ use shisui::utils::errors::CommunErrors; fn assert_address_non_zero(address: ContractAddress) { assert(address.is_non_zero(), CommunErrors::CommunErrors__AddressZero); -} \ No newline at end of file +} diff --git a/tests/integration/pools/active_pool/decrease_debt/decrease_debt.tree b/tests/integration/pools/active_pool/decrease_debt/decrease_debt.tree index c7f77ef..2d46c89 100644 --- a/tests/integration/pools/active_pool/decrease_debt/decrease_debt.tree +++ b/tests/integration/pools/active_pool/decrease_debt/decrease_debt.tree @@ -1,6 +1,12 @@ test_decrease_debt.cairo ├── when caller is neither the Borrower Operation nor the Vessel Manager nor the Stability Pool contract │ └── it should revert with CallerNotAuthorized -└── when caller is either the Borrower Operation or the Vessel Manager or the Stability Pool contract - ├── it should correclty decrease the debt balance for the given asset with the given amount - └── it should emit {ActivePoolDebtUpdated} event \ No newline at end of file +└── when caller is valid + ├── when the amount if less than the current debt balance + │ ├── it should correclty decrease the debt balance for the given asset with the given amount + │ └── it should emit {ActivePoolDebtUpdated} event + ├── when the amount if equal to the current debt balance + │ ├── it should correclty set the debt balance for the given asset to 0 + │ └── it should emit {ActivePoolDebtUpdated} event + └── when the amount if greater to the current debt balance + └── it should revert with 'u256_sub Overflow' \ No newline at end of file diff --git a/tests/integration/pools/active_pool/decrease_debt/test_decrease_debt.cairo b/tests/integration/pools/active_pool/decrease_debt/test_decrease_debt.cairo index 8b13789..1aaf140 100644 --- a/tests/integration/pools/active_pool/decrease_debt/test_decrease_debt.cairo +++ b/tests/integration/pools/active_pool/decrease_debt/test_decrease_debt.cairo @@ -1 +1,108 @@ +use starknet::ContractAddress; +use snforge_std::{ + start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions +}; +use shisui::core::address_provider::{IAddressProviderDispatcher, IAddressProviderDispatcherTrait}; +use shisui::pools::active_pool::{IActivePoolDispatcher, IActivePoolDispatcherTrait, ActivePool}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use tests::utils::callers::{ + borrower_operations_address, stability_pool_address, vessel_manager_address +}; +use super::super::setup::setup; +const BASE_AMOUNT: u256 = 10_000000; + +fn test_setup() -> (IActivePoolDispatcher, IERC20Dispatcher) { + let (_, active_pool, asset) = setup(); + + start_prank(CheatTarget::One(active_pool.contract_address), borrower_operations_address()); + active_pool.increase_debt(asset.contract_address, BASE_AMOUNT); + stop_prank(CheatTarget::One(active_pool.contract_address)); + + (active_pool, asset) +} + +#[test] +#[should_panic(expected: ('Caller not authorized',))] +fn when_caller_is_neither_borrower_operations_nor_vessel_manager_it_should_revert() { + let (active_pool, asset) = test_setup(); + active_pool.decrease_debt(asset.contract_address, BASE_AMOUNT); +} + +#[test] +fn when_caller_is_valid_it_should_correctly_update_debt_token_balance() { + let (active_pool, asset) = test_setup(); + let decrease_amount: u256 = 2_000000; + let expected_amount: u256 = 8_000000; + + // Check borrower_operations is allowed to decrease debt + start_prank(CheatTarget::One(active_pool.contract_address), borrower_operations_address()); + let mut spy = spy_events(SpyOn::One(active_pool.contract_address)); + active_pool.decrease_debt(asset.contract_address, decrease_amount); + spy + .assert_emitted( + @array![ + ( + active_pool.contract_address, + ActivePool::Event::ActivePoolDebtUpdated( + ActivePool::ActivePoolDebtUpdated { + asset: asset.contract_address, + old_balance: BASE_AMOUNT, + new_balance: expected_amount + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no events'); + assert( + active_pool.get_debt_token_balance(asset.contract_address) == expected_amount, + 'Wrong borrower op decrease' + ); + stop_prank(CheatTarget::One(active_pool.contract_address)); + + // Check vessel_manager is allowed to decrease debt + start_prank(CheatTarget::One(active_pool.contract_address), vessel_manager_address()); + active_pool.decrease_debt(asset.contract_address, decrease_amount); + assert( + active_pool.get_debt_token_balance(asset.contract_address) == expected_amount + - decrease_amount, + 'Wrong vessel decrease' + ); +} + +#[test] +fn when_caller_is_valid_and_decreased_amount_equal_to_balance_it_should_set_debt_token_balance_at_zero() { + let (active_pool, asset) = test_setup(); + + start_prank(CheatTarget::One(active_pool.contract_address), borrower_operations_address()); + let mut spy = spy_events(SpyOn::One(active_pool.contract_address)); + active_pool.decrease_debt(asset.contract_address, BASE_AMOUNT); + spy + .assert_emitted( + @array![ + ( + active_pool.contract_address, + ActivePool::Event::ActivePoolDebtUpdated( + ActivePool::ActivePoolDebtUpdated { + asset: asset.contract_address, old_balance: BASE_AMOUNT, new_balance: 0 + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no events'); + + assert( + active_pool.get_debt_token_balance(asset.contract_address).is_zero(), + 'Debt balance should be zero' + ); +} + +#[test] +#[should_panic(expected: ('u256_sub Overflow',))] +fn when_caller_is_valid_and_decreased_amount_greater_than_balance_it_should_revert() { + let (active_pool, asset) = test_setup(); + start_prank(CheatTarget::One(active_pool.contract_address), borrower_operations_address()); + active_pool.decrease_debt(asset.contract_address, 10_000001); +} diff --git a/tests/integration/pools/active_pool/increase_debt/test_increase_debt.cairo b/tests/integration/pools/active_pool/increase_debt/test_increase_debt.cairo index 8b13789..15747e7 100644 --- a/tests/integration/pools/active_pool/increase_debt/test_increase_debt.cairo +++ b/tests/integration/pools/active_pool/increase_debt/test_increase_debt.cairo @@ -1 +1,54 @@ +use starknet::ContractAddress; +use snforge_std::{ + start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions +}; +use shisui::core::address_provider::{IAddressProviderDispatcher, IAddressProviderDispatcherTrait}; +use shisui::pools::active_pool::{IActivePoolDispatcher, IActivePoolDispatcherTrait, ActivePool}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use tests::utils::callers::{borrower_operations_address, vessel_manager_address}; +use super::super::setup::setup; +const AMOUNT: u256 = 5_000000; + +#[test] +#[should_panic(expected: ('Caller not authorized',))] +fn when_caller_is_neither_borrower_operations_nor_vessel_manager_it_should_revert() { + let (_, active_pool, asset) = setup(); + active_pool.increase_debt(asset.contract_address, AMOUNT); +} + +#[test] +fn when_caller_is_valid_it_should_update_debt_token_balance() { + let (_, active_pool, asset) = setup(); + // Check borrower_operations allowed to increase debt + start_prank(CheatTarget::One(active_pool.contract_address), borrower_operations_address()); + let mut spy = spy_events(SpyOn::One(active_pool.contract_address)); + active_pool.increase_debt(asset.contract_address, AMOUNT); + spy + .assert_emitted( + @array![ + ( + active_pool.contract_address, + ActivePool::Event::ActivePoolDebtUpdated( + ActivePool::ActivePoolDebtUpdated { + asset: asset.contract_address, old_balance: 0, new_balance: AMOUNT + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no events'); + assert( + active_pool.get_debt_token_balance(asset.contract_address) == AMOUNT, + 'Wrong borrower inscrease' + ); + stop_prank(CheatTarget::One(active_pool.contract_address)); + + // Check vessel_manager allowed to increase debt + start_prank(CheatTarget::One(active_pool.contract_address), vessel_manager_address()); + active_pool.increase_debt(asset.contract_address, AMOUNT); + assert( + active_pool.get_debt_token_balance(asset.contract_address) == AMOUNT * 2, + 'Wrong vessel manager increase' + ); +} diff --git a/tests/integration/pools/active_pool/received_erc20/test_received_erc20.cairo b/tests/integration/pools/active_pool/received_erc20/test_received_erc20.cairo index 8b13789..9e9bf07 100644 --- a/tests/integration/pools/active_pool/received_erc20/test_received_erc20.cairo +++ b/tests/integration/pools/active_pool/received_erc20/test_received_erc20.cairo @@ -1 +1,66 @@ +use starknet::ContractAddress; +use snforge_std::{start_prank, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions}; +use shisui::core::address_provider::{IAddressProviderDispatcher, IAddressProviderDispatcherTrait}; +use shisui::pools::active_pool::{IActivePoolDispatcher, IActivePoolDispatcherTrait, ActivePool}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use tests::utils::callers::{borrower_operations_address, default_pool_address}; +use super::super::setup::setup; +const AMOUNT: u256 = 5_000000; + +#[test] +#[should_panic(expected: ('Caller not authorized',))] +fn when_caller_is_neither_borrower_operations_nor_default_pool_it_should_revert() { + let (_, active_pool, asset) = setup(); + active_pool.received_erc20(asset.contract_address, AMOUNT); +} + +#[test] +fn when_caller_is_borrower_operations_it_should_update_assets_balance() { + let (_, active_pool, asset) = setup(); + + start_prank(CheatTarget::One(active_pool.contract_address), borrower_operations_address()); + let mut spy = spy_events(SpyOn::One(active_pool.contract_address)); + active_pool.received_erc20(asset.contract_address, AMOUNT); + spy + .assert_emitted( + @array![ + ( + active_pool.contract_address, + ActivePool::Event::ActivePoolAssetBalanceUpdated( + ActivePool::ActivePoolAssetBalanceUpdated { + asset: asset.contract_address, old_balance: 0, new_balance: AMOUNT + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no events'); + + assert(active_pool.get_asset_balance(asset.contract_address) == AMOUNT, 'Wrong asset balance'); +} + +#[test] +fn when_caller_is_default_pool_it_should_update_assets_balance() { + let (_, active_pool, asset) = setup(); + + start_prank(CheatTarget::One(active_pool.contract_address), default_pool_address()); + let mut spy = spy_events(SpyOn::One(active_pool.contract_address)); + active_pool.received_erc20(asset.contract_address, AMOUNT); + spy + .assert_emitted( + @array![ + ( + active_pool.contract_address, + ActivePool::Event::ActivePoolAssetBalanceUpdated( + ActivePool::ActivePoolAssetBalanceUpdated { + asset: asset.contract_address, old_balance: 0, new_balance: AMOUNT + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no events'); + + assert(active_pool.get_asset_balance(asset.contract_address) == AMOUNT, 'Wrong asset balance'); +} diff --git a/tests/integration/pools/active_pool/send_asset/send_asset.tree b/tests/integration/pools/active_pool/send_asset/send_asset.tree index 1c69571..5f1ebd3 100644 --- a/tests/integration/pools/active_pool/send_asset/send_asset.tree +++ b/tests/integration/pools/active_pool/send_asset/send_asset.tree @@ -1,7 +1,7 @@ test_send_asset.cairo ├── when caller is neither the Borrower Operation nor the Stability Pool nor the Vessel Manager nor the Vessel Manager Operation contract │ └── it should revert with CallerNotAuthorized -└── when caller is either the Borrower Operation or the Stability Pool or the Vessel Manager contract +└── when caller is either the Borrower Operation or the Stability Pool or the Vessel Manager or the Vessel Manager Operation contract ├── when amount scale to asset decimals is zero │ └── it should do nothing └── when amount scale to asset decimals is not zero diff --git a/tests/integration/pools/active_pool/send_asset/test_send_asset.cairo b/tests/integration/pools/active_pool/send_asset/test_send_asset.cairo index 8b13789..561490c 100644 --- a/tests/integration/pools/active_pool/send_asset/test_send_asset.cairo +++ b/tests/integration/pools/active_pool/send_asset/test_send_asset.cairo @@ -1 +1,192 @@ +use starknet::ContractAddress; +use snforge_std::{ + PrintTrait, start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions +}; +use shisui::pools::active_pool::{IActivePoolDispatcher, IActivePoolDispatcherTrait, ActivePool}; +use shisui::mocks::{ + receive_erc20_mock::{IIsCalledDispatcher, IIsCalledDispatcherTrait}, + erc20_mock::{IERC20MintBurnDispatcher, IERC20MintBurnDispatcherTrait, ERC20Mock}, +}; +use shisui::core::address_provider::{ + IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use tests::utils::callers::{ + borrower_operations_address, vessel_manager_address, vessel_manager_operations_address, + stability_pool_address, alice +}; +use tests::tests_lib::deploy_receive_erc20_mock; +use shisui::utils::constants::ONE; +use super::super::setup::setup; + +const INITIAL_TOKEN_AMOUNT: u256 = 5_000000; // 5e6 +const INITIAL_AMOUNT_IN_10E18: u256 = 5_000000000000000000; // 1e18 + +fn test_setup() -> (IActivePoolDispatcher, IERC20Dispatcher, IIsCalledDispatcher) { + let (address_provider, active_pool, asset) = setup(); + let receiver_mock_address: ContractAddress = deploy_receive_erc20_mock(); + let receiver_mock: IIsCalledDispatcher = IIsCalledDispatcher { + contract_address: receiver_mock_address + }; + address_provider.set_address(AddressesKey::coll_surplus_pool, receiver_mock_address); + // Mint some tokens for the active pool + IERC20MintBurnDispatcher { contract_address: asset.contract_address } + .mint(active_pool.contract_address, INITIAL_TOKEN_AMOUNT); + + // Notify the contract that it has received some tokens + start_prank(CheatTarget::One(active_pool.contract_address), borrower_operations_address()); + active_pool.received_erc20(asset.contract_address, INITIAL_AMOUNT_IN_10E18); + stop_prank(CheatTarget::One(active_pool.contract_address)); + assert( + asset.balance_of(active_pool.contract_address) == INITIAL_TOKEN_AMOUNT, + 'Wrong contract balance' + ); + + // Need to prank asset otherwise tx.origin is used + start_prank(CheatTarget::One(asset.contract_address), active_pool.contract_address); + + (active_pool, asset, receiver_mock) +} + +#[test] +#[should_panic(expected: ('Caller not authorized',))] +fn when_caller_is_neither_borrower_operations_nor_stability_pool_nor_vessel_manager_nor_vessel_manager_operations_it_should_revert() { + let (active_pool, asset, _) = test_setup(); + active_pool.send_asset(asset.contract_address, alice(), ONE); +} + +#[test] +fn when_caller_is_valid_it_should_send_asset_to_alice() { + let (active_pool, asset, _) = test_setup(); + + let token_amount_send = 1_000000; // 1e6 + + // Check borrower_operations allowed to send asset + start_prank(CheatTarget::One(active_pool.contract_address), borrower_operations_address()); + let mut expected_asset_balance = INITIAL_AMOUNT_IN_10E18 - ONE; + let mut expected_contract_balance = INITIAL_TOKEN_AMOUNT - token_amount_send; + let mut expected_alice_balance = token_amount_send; + let mut spy = spy_events(SpyOn::One(active_pool.contract_address)); + + active_pool.send_asset(asset.contract_address, alice(), ONE); + spy + .assert_emitted( + @array![ + ( + active_pool.contract_address, + ActivePool::Event::ActivePoolAssetBalanceUpdated( + ActivePool::ActivePoolAssetBalanceUpdated { + asset: asset.contract_address, + old_balance: INITIAL_AMOUNT_IN_10E18, + new_balance: expected_asset_balance + } + ) + ), + ( + active_pool.contract_address, + ActivePool::Event::AssetSent( + ActivePool::AssetSent { + account: alice(), + asset: asset.contract_address, + amount: token_amount_send + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no events'); + assert( + active_pool.get_asset_balance(asset.contract_address) == expected_asset_balance, + 'Wrong asset balance 1' + ); + assert(asset.balance_of(alice()) == expected_alice_balance, 'Wrong alice balance 1'); + assert( + asset.balance_of(active_pool.contract_address) == expected_contract_balance, + 'Wrong contract balance 1' + ); + stop_prank(CheatTarget::One(active_pool.contract_address)); + + // Check stability_pool allowed to send asset + start_prank(CheatTarget::One(active_pool.contract_address), stability_pool_address()); + expected_asset_balance = expected_asset_balance - ONE; + expected_contract_balance = expected_contract_balance - token_amount_send; + expected_alice_balance = expected_alice_balance + token_amount_send; + active_pool.send_asset(asset.contract_address, alice(), ONE); + assert( + active_pool.get_asset_balance(asset.contract_address) == expected_asset_balance, + 'Wrong asset balance 2' + ); + assert(asset.balance_of(alice()) == expected_alice_balance, 'Wrong alice balance 2'); + assert( + asset.balance_of(active_pool.contract_address) == expected_contract_balance, + 'Wrong contract balance 2' + ); + stop_prank(CheatTarget::One(active_pool.contract_address)); + + // Check vessel_manager allowed to send asset + start_prank(CheatTarget::One(active_pool.contract_address), vessel_manager_address()); + expected_asset_balance = expected_asset_balance - ONE; + expected_contract_balance = expected_contract_balance - token_amount_send; + expected_alice_balance = expected_alice_balance + token_amount_send; + active_pool.send_asset(asset.contract_address, alice(), ONE); + assert( + active_pool.get_asset_balance(asset.contract_address) == expected_asset_balance, + 'Wrong asset balance 3' + ); + assert(asset.balance_of(alice()) == expected_alice_balance, 'Wrong alice balance 3'); + assert( + asset.balance_of(active_pool.contract_address) == expected_contract_balance, + 'Wrong contract balance 3' + ); + stop_prank(CheatTarget::One(active_pool.contract_address)); + + // Check vessel_manager operations allowed to send asset + start_prank( + CheatTarget::One(active_pool.contract_address), vessel_manager_operations_address() + ); + expected_asset_balance = expected_asset_balance - ONE; + expected_contract_balance = expected_contract_balance - token_amount_send; + expected_alice_balance = expected_alice_balance + token_amount_send; + active_pool.send_asset(asset.contract_address, alice(), ONE); + assert( + active_pool.get_asset_balance(asset.contract_address) == expected_asset_balance, + 'Wrong asset balance 4' + ); + assert(asset.balance_of(alice()) == expected_alice_balance, 'Wrong alice balance 4'); + assert( + asset.balance_of(active_pool.contract_address) == expected_contract_balance, + 'Wrong contract balance 4' + ); +} + + +#[test] +fn when_amount_scale_to_zero_it_should_do_nothing() { + let (active_pool, asset, _) = test_setup(); + + start_prank(CheatTarget::One(active_pool.contract_address), borrower_operations_address()); + active_pool.send_asset(asset.contract_address, alice(), 0); + + assert( + active_pool.get_asset_balance(asset.contract_address) == INITIAL_AMOUNT_IN_10E18, + 'Wrong asset balance' + ); + assert(asset.balance_of(alice()) == 0, 'Wrong alice balance'); + assert( + asset.balance_of(active_pool.contract_address) == INITIAL_TOKEN_AMOUNT, + 'Wrong contract balance' + ); +} + +#[test] +fn when_account_is_erc20_deposit_it_should_call_it() { + let (active_pool, asset, receiver_mock) = test_setup(); + assert(!receiver_mock.is_called(), 'Must be FALSE'); + + start_prank(CheatTarget::One(active_pool.contract_address), borrower_operations_address()); + + active_pool.send_asset(asset.contract_address, receiver_mock.contract_address, ONE); + + assert(receiver_mock.is_called(), 'Must be TRUE'); +} diff --git a/tests/integration/pools/active_pool/setup.cairo b/tests/integration/pools/active_pool/setup.cairo new file mode 100644 index 0000000..67f0368 --- /dev/null +++ b/tests/integration/pools/active_pool/setup.cairo @@ -0,0 +1,34 @@ +use starknet::ContractAddress; +use shisui::core::address_provider::{ + IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey +}; +use shisui::pools::active_pool::{IActivePoolDispatcher, IActivePoolDispatcherTrait}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use tests::tests_lib::{deploy_address_provider, deploy_erc20_mock, deploy_active_pool}; +use tests::utils::callers::{ + default_pool_address, stability_pool_address, borrower_operations_address, + vessel_manager_address, vessel_manager_operations_address +}; + +fn setup() -> (IAddressProviderDispatcher, IActivePoolDispatcher, IERC20Dispatcher) { + let address_provider_address: ContractAddress = deploy_address_provider(); + let address_provider: IAddressProviderDispatcher = IAddressProviderDispatcher { + contract_address: address_provider_address + }; + + let asset_address: ContractAddress = deploy_erc20_mock(6); + let asset: IERC20Dispatcher = IERC20Dispatcher { contract_address: asset_address }; + let active_pool_address: ContractAddress = deploy_active_pool(address_provider_address); + let active_pool: IActivePoolDispatcher = IActivePoolDispatcher { + contract_address: active_pool_address + }; + + address_provider.set_address(AddressesKey::vessel_manager, vessel_manager_address()); + address_provider + .set_address(AddressesKey::vessel_manager_operations, vessel_manager_operations_address()); + address_provider.set_address(AddressesKey::borrower_operations, borrower_operations_address()); + address_provider.set_address(AddressesKey::default_pool, default_pool_address()); + address_provider.set_address(AddressesKey::stability_pool, stability_pool_address()); + + return (address_provider, active_pool, asset); +} diff --git a/tests/lib.cairo b/tests/lib.cairo index c1a34cf..59eae20 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -187,6 +187,7 @@ mod integration { } } mod active_pool { + mod setup; mod decrease_debt { mod test_decrease_debt; } diff --git a/tests/tests_lib.cairo b/tests/tests_lib.cairo index 12b4a46..675f57f 100644 --- a/tests/tests_lib.cairo +++ b/tests/tests_lib.cairo @@ -83,3 +83,20 @@ fn deploy_debt_token(address_provider: ContractAddress) -> ContractAddress { let contract = declare('DebtToken'); deploy_mock_contract(contract, @array![address_provider.into()]) } + + +/// Utility function to deploy a ActivePool contract and return its address. +/// +/// # Returns +/// +/// * `ContractAddress` - The address of the deployed data store contract. +fn deploy_active_pool(address_provider: ContractAddress) -> ContractAddress { + let contract = declare('ActivePool'); + deploy_mock_contract(contract, @array![address_provider.into()]) +} + + +fn deploy_receive_erc20_mock() -> ContractAddress { + let contract = declare('ReceiveERC20Mock'); + deploy_mock_contract(contract, @array![]) +} diff --git a/tests/utils/callers.cairo b/tests/utils/callers.cairo index b314297..b16ea80 100644 --- a/tests/utils/callers.cairo +++ b/tests/utils/callers.cairo @@ -11,3 +11,35 @@ fn owner_address() -> ContractAddress { fn not_owner_address() -> ContractAddress { return contract_address_const::<'not_owner'>(); } + +fn vessel_manager_address() -> ContractAddress { + return contract_address_const::<'vessel_manager'>(); +} + +fn vessel_manager_operations_address() -> ContractAddress { + return contract_address_const::<'vessel_manager_operations'>(); +} + +fn borrower_operations_address() -> ContractAddress { + return contract_address_const::<'borrower_operations'>(); +} + +fn active_pool_address() -> ContractAddress { + return contract_address_const::<'active_pool'>(); +} + +fn default_pool_address() -> ContractAddress { + return contract_address_const::<'default_pool'>(); +} + +fn stability_pool_address() -> ContractAddress { + return contract_address_const::<'stability_pool'>(); +} + +fn alice() -> ContractAddress { + return contract_address_const::<'alice'>(); +} + +fn bob() -> ContractAddress { + return contract_address_const::<'bob'>(); +} From a6d06d55f4b1259ec83069d6106dc87195bd2655 Mon Sep 17 00:00:00 2001 From: FabienC Date: Sun, 24 Dec 2023 15:34:23 +0100 Subject: [PATCH 6/7] fix: error name --- src/core/debt_token.cairo | 17 ++++++----------- src/pools/active_pool.cairo | 9 ++++----- src/utils/asserts.cairo | 2 +- .../core/debt_token/burn/test_burn.cairo | 2 +- .../test_burn_from_whitelisted_contract.cairo | 2 +- .../core/debt_token/mint/test_mint.cairo | 2 +- .../test_mint_from_whitelisted_contract.cairo | 2 +- 7 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/core/debt_token.cairo b/src/core/debt_token.cairo index 9743cbd..ca8441e 100644 --- a/src/core/debt_token.cairo +++ b/src/core/debt_token.cairo @@ -100,7 +100,7 @@ mod DebtToken { fn add_whitelist(ref self: ContractState, address: ContractAddress) { self.ownable.assert_only_owner(); - assert(address.is_non_zero(), CommunErrors::CommunErrors__AddressZero); + assert(address.is_non_zero(), CommunErrors::AddressZero); self.whitelisted_contracts.write(address, true); self.emit(WhitelistChanged { address: address, is_whitelisted: true }); } @@ -121,9 +121,7 @@ mod DebtToken { #[inline(always)] fn require_caller_is_whitelisted_contract(self: @ContractState) { let caller = get_caller_address(); - assert( - self.is_whitelisted(caller) == true, CommunErrors::CommunErrors__CallerNotAuthorized - ); + assert(self.is_whitelisted(caller) == true, CommunErrors::CallerNotAuthorized); } #[inline(always)] fn require_caller_is_borrower_operations(self: @ContractState) { @@ -133,10 +131,7 @@ mod DebtToken { }; let borrower_operations_manager = address_provider .get_address(AddressesKey::borrower_operations); - assert( - caller == borrower_operations_manager, - CommunErrors::CommunErrors__CallerNotAuthorized - ); + assert(caller == borrower_operations_manager, CommunErrors::CallerNotAuthorized); } #[inline(always)] fn require_caller_is_bo_or_vesselm_or_sp(self: @ContractState) { @@ -152,7 +147,7 @@ mod DebtToken { caller == borrower_operations_manager || caller == vessel_manager || caller == stability_pool, - CommunErrors::CommunErrors__CallerNotAuthorized + CommunErrors::CallerNotAuthorized ); } #[inline(always)] @@ -162,7 +157,7 @@ mod DebtToken { contract_address: (self.address_provider.read()) }; let stability_pool = address_provider.get_address(AddressesKey::stability_pool); - assert(caller == stability_pool, CommunErrors::CommunErrors__CallerNotAuthorized); + assert(caller == stability_pool, CommunErrors::CallerNotAuthorized); } #[inline(always)] fn require_caller_is_vesselm_or_sp(self: @ContractState) { @@ -174,7 +169,7 @@ mod DebtToken { let stability_pool = address_provider.get_address(AddressesKey::stability_pool); assert( caller == vessel_manager || caller == stability_pool, - CommunErrors::CommunErrors__CallerNotAuthorized + CommunErrors::CallerNotAuthorized ); } } diff --git a/src/pools/active_pool.cairo b/src/pools/active_pool.cairo index 99f26e7..280d821 100644 --- a/src/pools/active_pool.cairo +++ b/src/pools/active_pool.cairo @@ -10,7 +10,6 @@ trait IActivePool { ref self: TContractState, asset: ContractAddress, account: ContractAddress, amount: u256 ); - fn received_erc20(ref self: TContractState, asset: ContractAddress, amount: u256); fn get_asset_balance(self: @TContractState, asset: ContractAddress) -> u256; @@ -171,7 +170,7 @@ mod ActivePool { assert( caller == address_provider.get_address(AddressesKey::borrower_operations) || caller == address_provider.get_address(AddressesKey::default_pool), - CommunErrors::CommunErrors__CallerNotAuthorized + CommunErrors::CallerNotAuthorized ); } @@ -182,7 +181,7 @@ mod ActivePool { assert( caller == address_provider.get_address(AddressesKey::borrower_operations) || caller == address_provider.get_address(AddressesKey::vessel_manager), - CommunErrors::CommunErrors__CallerNotAuthorized + CommunErrors::CallerNotAuthorized ); } @@ -196,7 +195,7 @@ mod ActivePool { caller == address_provider.get_address(AddressesKey::borrower_operations) || caller == address_provider.get_address(AddressesKey::stability_pool) || caller == address_provider.get_address(AddressesKey::vessel_manager), - CommunErrors::CommunErrors__CallerNotAuthorized + CommunErrors::CallerNotAuthorized ); } @@ -210,7 +209,7 @@ mod ActivePool { || caller == address_provider.get_address(AddressesKey::vessel_manager) || caller == address_provider .get_address(AddressesKey::vessel_manager_operations), - CommunErrors::CommunErrors__CallerNotAuthorized + CommunErrors::CallerNotAuthorized ); } } diff --git a/src/utils/asserts.cairo b/src/utils/asserts.cairo index d4b5cee..20c1c39 100644 --- a/src/utils/asserts.cairo +++ b/src/utils/asserts.cairo @@ -2,5 +2,5 @@ use starknet::ContractAddress; use shisui::utils::errors::CommunErrors; fn assert_address_non_zero(address: ContractAddress) { - assert(address.is_non_zero(), CommunErrors::CommunErrors__AddressZero); + assert(address.is_non_zero(), CommunErrors::AddressZero); } diff --git a/tests/integration/core/debt_token/burn/test_burn.cairo b/tests/integration/core/debt_token/burn/test_burn.cairo index 8b599da..1d1ef30 100644 --- a/tests/integration/core/debt_token/burn/test_burn.cairo +++ b/tests/integration/core/debt_token/burn/test_burn.cairo @@ -42,7 +42,7 @@ fn setup() -> (IAddressProviderDispatcher, IDebtTokenDispatcher, ContractAddress } #[test] -#[should_panic(expected: ('Caller is not authorized',))] +#[should_panic(expected: ('Caller not authorized',))] fn given_caller_is_not_borrower_operations_nor_vessel_manager_nor_stability_pool_it_should_revert() { let (_, debt_token, caller) = setup(); debt_token.burn(caller, MINT_AMOUNT); diff --git a/tests/integration/core/debt_token/burn_from_whitelisted_contract/test_burn_from_whitelisted_contract.cairo b/tests/integration/core/debt_token/burn_from_whitelisted_contract/test_burn_from_whitelisted_contract.cairo index 4f9af4b..a8e2334 100644 --- a/tests/integration/core/debt_token/burn_from_whitelisted_contract/test_burn_from_whitelisted_contract.cairo +++ b/tests/integration/core/debt_token/burn_from_whitelisted_contract/test_burn_from_whitelisted_contract.cairo @@ -43,7 +43,7 @@ fn setup() -> (IAddressProviderDispatcher, IDebtTokenDispatcher, ContractAddress } #[test] -#[should_panic(expected: ('Caller is not authorized',))] +#[should_panic(expected: ('Caller not authorized',))] fn given_caller_is_not_whitelisted_it_should_revert() { let (_, debt_token, caller, not_caller) = setup(); diff --git a/tests/integration/core/debt_token/mint/test_mint.cairo b/tests/integration/core/debt_token/mint/test_mint.cairo index a0e17c6..aca21ed 100644 --- a/tests/integration/core/debt_token/mint/test_mint.cairo +++ b/tests/integration/core/debt_token/mint/test_mint.cairo @@ -31,7 +31,7 @@ fn setup() -> (IAddressProviderDispatcher, IDebtTokenDispatcher, ContractAddress } #[test] -#[should_panic(expected: ('Caller is not authorized',))] +#[should_panic(expected: ('Caller not authorized',))] fn given_caller_is_not_borrower_operations_it_should_revert() { let (_, debt_token, caller) = setup(); debt_token.burn(caller, MINT_AMOUNT); diff --git a/tests/integration/core/debt_token/mint_from_whitelisted_contract/test_mint_from_whitelisted_contract.cairo b/tests/integration/core/debt_token/mint_from_whitelisted_contract/test_mint_from_whitelisted_contract.cairo index d13d6c3..4f7433e 100644 --- a/tests/integration/core/debt_token/mint_from_whitelisted_contract/test_mint_from_whitelisted_contract.cairo +++ b/tests/integration/core/debt_token/mint_from_whitelisted_contract/test_mint_from_whitelisted_contract.cairo @@ -35,7 +35,7 @@ fn setup() -> (IAddressProviderDispatcher, IDebtTokenDispatcher, ContractAddress } #[test] -#[should_panic(expected: ('Caller is not authorized',))] +#[should_panic(expected: ('Caller not authorized',))] fn given_caller_is_not_whitelisted_it_should_revert() { let (_, debt_token, caller, not_caller) = setup(); From 7c47d049260caa42f7743654475180316418b0f2 Mon Sep 17 00:00:00 2001 From: FabienC Date: Wed, 3 Jan 2024 11:53:01 +0200 Subject: [PATCH 7/7] feat: implement fee_collectors with tests --- src/core/debt_token.cairo | 1 - src/core/fee_collector.cairo | 520 +++++++++++++++--- src/lib.cairo | 4 + src/shvt/shvt_staking.cairo | 55 ++ .../fee_collector/close_debt/close_debt.tree | 18 +- .../close_debt/test_close_debt.cairo | 177 ++++++ .../collect_fees/collect_fees.tree | 13 +- .../collect_fees/test_collect_fees.cairo | 253 +++++++++ .../decrease_debt/decrease_debt.tree | 22 +- .../decrease_debt/test_decrease_debt.cairo | 262 +++++++++ .../handle_redemption_fee.tree | 18 +- .../test_handle_redemption_fee.cairo | 83 +++ .../increase_debt/increase_debt.tree | 16 +- .../increase_debt/test_increase_debt.cairo | 260 +++++++++ .../liquidate_debt/liquidate_debt.tree | 2 +- .../liquidate_debt/test_liquidate_debt.cairo | 107 ++++ .../set_is_route_to_SHVT_staking.tree} | 2 +- .../test_set_is_route_to_SHVT_staking.cairo | 25 + .../test_set_route_to_SHVT_staking.cairo | 1 - .../core/fee_collector/setup.cairo | 67 +++ tests/lib.cairo | 6 +- tests/tests_lib.cairo | 10 + tests/utils/asserts.cairo | 6 + tests/utils/callers.cairo | 9 + tests/utils/constant.cairo | 6 + 25 files changed, 1833 insertions(+), 110 deletions(-) create mode 100644 src/shvt/shvt_staking.cairo rename tests/integration/core/fee_collector/{set_route_to_SHVT_staking/set_route_to_SHVT_staking.tree => set_is_route_to_SHVT_staking/set_is_route_to_SHVT_staking.tree} (84%) create mode 100644 tests/integration/core/fee_collector/set_is_route_to_SHVT_staking/test_set_is_route_to_SHVT_staking.cairo delete mode 100644 tests/integration/core/fee_collector/set_route_to_SHVT_staking/test_set_route_to_SHVT_staking.cairo create mode 100644 tests/integration/core/fee_collector/setup.cairo create mode 100644 tests/utils/asserts.cairo diff --git a/src/core/debt_token.cairo b/src/core/debt_token.cairo index ca8441e..2c44ab0 100644 --- a/src/core/debt_token.cairo +++ b/src/core/debt_token.cairo @@ -20,7 +20,6 @@ trait IDebtToken { #[starknet::contract] mod DebtToken { - use core::starknet::event::EventEmitter; use openzeppelin::access::ownable::ownable::OwnableComponent::InternalTrait as OwnableInternalTrait; use shisui::core::debt_token::IDebtToken; use openzeppelin::token::erc20::erc20::ERC20Component::InternalTrait as ERC20InternalTrait; diff --git a/src/core/fee_collector.cairo b/src/core/fee_collector.cairo index 909c23b..876604f 100644 --- a/src/core/fee_collector.cairo +++ b/src/core/fee_collector.cairo @@ -3,9 +3,9 @@ use starknet::ContractAddress; #[derive(Serde, Drop, Copy, starknet::Store, Default)] struct FeeRecord { - from: u256, // timestamp in seconds - to: u256, // timestamp in seconds amount: u256, // refundable fee amount + from: u64, // timestamp in seconds + to: u64, // timestamp in seconds } #[starknet::interface] @@ -33,7 +33,7 @@ trait IFeeCollector { borrower: ContractAddress, asset: ContractAddress, payback_fraction: u256 - ); + ) -> u256; fn collect_fees( ref self: TContractState, borrowers: Span, assets: Span @@ -41,50 +41,116 @@ trait IFeeCollector { fn handle_redemption_fee(ref self: TContractState, asset: ContractAddress, amount: u256); - fn set_route_to_SHVT_staking(ref self: TContractState, route_to_SHVT_staking: bool); + fn set_is_route_to_SHVT_staking(ref self: TContractState, is_route_to_SHVT_staking: bool); + + fn get_is_route_to_SHVT_staking(self: @TContractState) -> bool; fn get_protocol_revenue_destination(self: @TContractState) -> ContractAddress; fn get_fee_record( self: @TContractState, borrower: ContractAddress, asset: ContractAddress ) -> FeeRecord; + + fn get_min_fee_duration(self: @TContractState) -> u64; + + fn get_fee_expiration_seconds(self: @TContractState) -> u64; + + fn get_min_fee_fraction(self: @TContractState) -> u256; + + fn get_precision(self: @TContractState) -> u256; } #[starknet::contract] mod FeeCollector { - use starknet::ContractAddress; - use shisui::utils::traits::ContractAddressDefault; - - use super::FeeRecord; - - const MIN_FEE_DAYS: u256 = 7; + use core::array::SpanTrait; + use snforge_std::PrintTrait; + use starknet::{ContractAddress, get_caller_address, get_block_timestamp}; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use shisui::utils::{ + errors::CommunErrors, traits::ContractAddressDefault, asserts::assert_address_non_zero, + constants::ONE + }; + use shisui::core::{ + address_provider::{ + IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey + }, + debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait} + }; + + use shisui::shvt::shvt_staking::{ISHVTStakingDispatcher, ISHVTStakingDispatcherTrait}; + + use super::{IFeeCollector, FeeRecord}; + const MIN_FEE_DURATION: u64 = consteval_int!(7 * 24 * 60 * 60); // 7 days + const ONE_DAY_TIMESTAMP: u64 = consteval_int!(24 * 60 * 60); + const FEE_EXPIRATION_SECONDS: u64 = + consteval_int!(175 * 24 * 60 * 60); // ~ 6 months, minus one week (MIN_FEE_DURATION) const MIN_FEE_FRACTION: u256 = 38461538000000000; // (1/26)e18 fee divided by 26 weeks - const ONE_DAY_TIMESTAMP: u256 = consteval_int!(24 * 60 * 60); - const FEE_EXPIRATION_SECONDS: u256 = - consteval_int!(175 * 24 * 60 * 60); // ~ 6 months, minus one week (MIN_FEE_DAYS) - + const PRECISION: u256 = 1_000000000; // 1e9 - mod Errors { - const FeeCollector__ArrayMismatch: felt252 = 'Array Mismatch'; - const FeeCollector__InvalidSHVTStakingAddress: felt252 = 'Invalid SHVT Staking Address'; + mod FeeCollectorErrors { + const PaybackFractionExceedOne: felt252 = 'Payback fraction exceed 10e18'; + const ArrayMismatch: felt252 = 'Array Mismatch'; } #[storage] struct Storage { - address_provider: ContractAddress, + address_provider: IAddressProviderDispatcher, // borrower -> asset -> fees fee_records: LegacyMap<(ContractAddress, ContractAddress), FeeRecord>, // if true, collected fees go to stakers; if false, to the treasury - route_to_SHVT_staking: bool, + is_route_to_SHVT_staking: bool, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + FeeRecordUpdated: FeeRecordUpdated, + FeeCollected: FeeCollected, + FeeRefunded: FeeRefunded, + RedemptionFeeCollected: RedemptionFeeCollected + } + + + #[derive(Drop, starknet::Event)] + struct FeeRecordUpdated { + borrower: ContractAddress, + asset: ContractAddress, + from: u64, + to: u64, + amount: u256 + } + + #[derive(Drop, starknet::Event)] + struct FeeCollected { + borrower: ContractAddress, + asset: ContractAddress, + collector: ContractAddress, + amount: u256 + } + + #[derive(Drop, starknet::Event)] + struct FeeRefunded { + borrower: ContractAddress, + asset: ContractAddress, + amount: u256 + } + + #[derive(Drop, starknet::Event)] + struct RedemptionFeeCollected { + asset: ContractAddress, + amount: u256 } #[constructor] - fn constructor(ref self: ContractState, address_provider: ContractAddress) {} + fn constructor(ref self: ContractState, address_provider: IAddressProviderDispatcher) { + assert_address_non_zero(address_provider.contract_address); + self.address_provider.write(address_provider); + } #[external(v0)] - impl FeeCollectorImpl of super::IFeeCollector { + impl FeeCollectorImpl of IFeeCollector { /// Triggered when a vessel is created and again whenever the borrower acquires additional loans. /// Collects the minimum fee to the platform, for which there is no refund; holds on to the remaining fees until /// debt is paid, liquidated, or expired. @@ -95,7 +161,16 @@ mod FeeCollector { borrower: ContractAddress, asset: ContractAddress, fee_amount: u256 - ) {} + ) { + self.assert_caller_is_borrower_operations(); + let min_fee_amount = (MIN_FEE_FRACTION * fee_amount) / ONE; + let refundable_fee_amount = fee_amount - min_fee_amount; + let fee_to_collect = self + .create_or_update_fee_record(borrower, asset, refundable_fee_amount); + InternalFunctions::collect_fee( + ref self, borrower, asset, min_fee_amount + fee_to_collect + ); + } /// Triggered when a vessel is adjusted or closed (and the borrower has paid back/decreased his loan). fn decrease_debt( @@ -103,16 +178,30 @@ mod FeeCollector { borrower: ContractAddress, asset: ContractAddress, payback_fraction: u256 - ) {} + ) { + self.assert_caller_is_borrower_operations_or_vessel_manager(); + InternalFunctions::decrease_debt(ref self, borrower, asset, payback_fraction); + } /// Triggered when a debt is paid in full. - fn close_debt(ref self: ContractState, borrower: ContractAddress, asset: ContractAddress) {} + fn close_debt(ref self: ContractState, borrower: ContractAddress, asset: ContractAddress) { + self.assert_caller_is_borrower_operations_or_vessel_manager(); + InternalFunctions::decrease_debt(ref self, borrower, asset, ONE); + } /// Triggered when a vessel is liquidated; in that case, all remaining fees are collected by the platform, /// and no refunds are generated. fn liquidate_debt( ref self: ContractState, borrower: ContractAddress, asset: ContractAddress - ) {} + ) { + self.assert_caller_is_vessel_manager(); + let record = self.fee_records.read((borrower, asset)); + if record.amount.is_non_zero() { + InternalFunctions::close_expired_or_liquidated_fee_record( + ref self, borrower, asset, record.amount + ); + } + } /// Simulates the refund due -if- vessel would be closed at this moment (helper function used by the UI). fn simulate_refund( @@ -120,87 +209,390 @@ mod FeeCollector { borrower: ContractAddress, asset: ContractAddress, payback_fraction: u256 - ) {} + ) -> u256 { + assert(payback_fraction <= ONE, FeeCollectorErrors::PaybackFractionExceedOne); + assert(payback_fraction.is_non_zero(), CommunErrors::CantBeZero); + let mut record = self.fee_records.read((borrower, asset)); + if (record.amount.is_zero() || record.to < get_block_timestamp()) { + return 0; + } + let expired_amount = InternalFunctions::calc_expired_amount( + self, record.from, record.to, record.amount + ); + if (payback_fraction == ONE) { + // full payback + return record.amount - expired_amount; + } + // calc refund amount proportional to the payment + return ((record.amount - expired_amount) * payback_fraction) / ONE; + } /// Batch collect fees from an array of borrowers/assets. fn collect_fees( - ref self: ContractState, borrowers: Span, assets: Span - ) {} + ref self: ContractState, + mut borrowers: Span, + mut assets: Span + ) { + assert( + borrowers.len() == assets.len() && !borrowers.is_empty(), + FeeCollectorErrors::ArrayMismatch + ); + let now = get_block_timestamp(); + + loop { + match borrowers.pop_front() { + Option::Some(borrower) => { + let asset = assets.pop_front().unwrap(); + let mut record = self.fee_records.read((*borrower, *asset)); + let expired_amount = InternalFunctions::calc_expired_amount( + @self, record.from, record.to, record.amount + ); + if expired_amount.is_non_zero() { + let updated_amount = record.amount - expired_amount; + record.amount = updated_amount; + record.from = now; + InternalFunctions::collect_fee( + ref self, *borrower, *asset, expired_amount + ); + self.fee_records.write((*borrower, *asset), record); + self + .emit( + FeeRecordUpdated { + borrower: *borrower, + asset: *asset, + from: now, + to: record.to, + amount: updated_amount + } + ); + } + }, + Option::None => { break; } + } + }; + } - fn handle_redemption_fee(ref self: ContractState, asset: ContractAddress, amount: u256) {} + // Triggered by VesselManager.finalize_redemption(); assumes amount of asset has been already transferred to + // get_protocol_revenue_destination(). + fn handle_redemption_fee(ref self: ContractState, asset: ContractAddress, amount: u256) { + self.assert_caller_is_vessel_manager(); + if self.is_route_to_shvt_staking() { + ISHVTStakingDispatcher { + contract_address: self + .address_provider + .read() + .get_address(AddressesKey::shvt_staking) + } + .increase_fee_asset(asset, amount); + } + self.emit(RedemptionFeeCollected { asset, amount }); + } fn get_protocol_revenue_destination(self: @ContractState) -> ContractAddress { - return Default::default(); + let address_provider = self.address_provider.read(); + if self.is_route_to_shvt_staking() { + return address_provider.get_address(AddressesKey::shvt_staking); + } + return address_provider.get_address(AddressesKey::treasury); } fn get_fee_record( self: @ContractState, borrower: ContractAddress, asset: ContractAddress ) -> FeeRecord { - return Default::default(); + return self.fee_records.read((borrower, asset)); } - fn set_route_to_SHVT_staking(ref self: ContractState, route_to_SHVT_staking: bool) {} - } + fn set_is_route_to_SHVT_staking(ref self: ContractState, is_route_to_SHVT_staking: bool) { + self.assert_caller_is_timelock(); + self.is_route_to_SHVT_staking.write(is_route_to_SHVT_staking); + } + fn get_is_route_to_SHVT_staking(self: @ContractState) -> bool { + return self.is_route_to_SHVT_staking.read(); + } + + fn get_min_fee_duration(self: @ContractState) -> u64 { + return MIN_FEE_DURATION; + } + + fn get_fee_expiration_seconds(self: @ContractState) -> u64 { + return FEE_EXPIRATION_SECONDS; + } + + fn get_min_fee_fraction(self: @ContractState) -> u256 { + return MIN_FEE_FRACTION; + } + + fn get_precision(self: @ContractState) -> u256 { + return PRECISION; + } + } #[generate_trait] impl InternalFunctions of InternalFunctionsTrait { - fn _decrease_debt( + #[inline(always)] + fn decrease_debt( ref self: ContractState, - borrowers: ContractAddress, - assets: ContractAddress, + borrower: ContractAddress, + asset: ContractAddress, payback_fraction: u256 - ) {} + ) { + assert(payback_fraction <= ONE, FeeCollectorErrors::PaybackFractionExceedOne); + assert(payback_fraction.is_non_zero(), CommunErrors::CantBeZero); + let mut record: FeeRecord = self.fee_records.read((borrower, asset)); + if record.amount.is_zero() { + return; + } + let now = get_block_timestamp(); + if (record.to <= now) { + self.close_expired_or_liquidated_fee_record(borrower, asset, record.amount); + } else { + // collect expired refund + let expired_amount = self + .calc_expired_amount(record.from, record.to, record.amount); + + self.collect_fee(borrower, asset, expired_amount); + + if (payback_fraction == ONE) { + // on a full payback, there's no refund; refund amount is discounted from final payment + let refund_amount = record.amount - expired_amount; + IDebtTokenDispatcher { + contract_address: self + .address_provider + .read() + .get_address(AddressesKey::debt_token) + } + .burn_from_whitelisted_contract(refund_amount); + record.amount = 0; + self.emit(FeeRecordUpdated { borrower, asset, from: now, to: 0, amount: 0 }); + } else { + // refund amount proportional to the payment + let refund_amount = ((record.amount - expired_amount) * payback_fraction) / ONE; + + self.refund_fee(borrower, asset, refund_amount); + let updated_amount = record.amount - expired_amount - refund_amount; + record.amount = updated_amount; + record.from = now; + + self + .emit( + FeeRecordUpdated { + borrower, asset, from: now, to: record.to, amount: updated_amount + } + ); + } + self.fee_records.write((borrower, asset), record); + } + } - fn _create_or_update_fee_record( + #[inline(always)] + fn create_or_update_fee_record( ref self: ContractState, borrower: ContractAddress, asset: ContractAddress, amount: u256 - ) {} - fn _create_fee_record( + ) -> u256 { + let mut record = self.fee_records.read((borrower, asset)); + if (record.amount.is_zero() || record.to <= get_block_timestamp()) { + let fee_to_collect = record.amount; + self.create_fee_record(borrower, asset, amount, ref record); + return fee_to_collect; + } + return self.update_fee_record(borrower, asset, amount, ref record); + } + + #[inline(always)] + fn create_fee_record( ref self: ContractState, borrower: ContractAddress, asset: ContractAddress, fee_amount: u256, - s_record: FeeRecord - ) {} + ref record: FeeRecord + ) { + let from = get_block_timestamp() + MIN_FEE_DURATION; + let to = from + FEE_EXPIRATION_SECONDS; + record.amount = fee_amount; + record.from = from; + record.to = to; + self.fee_records.write((borrower, asset), record); + self.emit(FeeRecordUpdated { borrower, asset, from, to, amount: fee_amount }); + } - fn _update_fee_record( + #[inline(always)] + fn update_fee_record( ref self: ContractState, borrower: ContractAddress, asset: ContractAddress, added_amount: u256, - s_record: FeeRecord - ) {} - fn _close_expired_or_liquidated_fee_record( + ref record: FeeRecord + ) -> u256 { + let mut now = get_block_timestamp(); + + if (now < record.from) { + // loan is still in its first week (MIN_FEE_DAYS) + now = record.from; + } + let expired_amount = self.calc_expired_amount(record.from, record.to, record.amount); + let remaining_amount = record.amount - expired_amount; + let remaining_time = record.to - now; + let updated_amount = remaining_amount + added_amount; + let update_to = now + + self.calc_new_duration(remaining_amount, remaining_time, added_amount); + record.amount = updated_amount; + record.from = now; + record.to = update_to; + self.fee_records.write((borrower, asset), record); + self + .emit( + FeeRecordUpdated { + borrower, asset, from: now, to: update_to, amount: updated_amount + } + ); + + return expired_amount; + } + + #[inline(always)] + fn close_expired_or_liquidated_fee_record( ref self: ContractState, borrower: ContractAddress, asset: ContractAddress, amount: u256 - ) {} + ) { + self.collect_fee(borrower, asset, amount); + self.fee_records.write((borrower, asset), Default::default()); + self + .emit( + FeeRecordUpdated { + borrower, asset, from: get_block_timestamp(), to: 0, amount: 0 + } + ) + } - fn _calc_expired_amount( - self: @ContractState, from: ContractAddress, to: ContractAddress, amount: u256 - ) -> u256 { - return 0; + #[inline(always)] + fn calc_expired_amount(self: @ContractState, from: u64, to: u64, amount: u256) -> u256 { + let now = get_block_timestamp(); + if (from > now) { + return 0; + } + if (now >= to) { + return amount; + } + let decay_rate = (amount * PRECISION) / (to - from).into(); + return ((now - from).into() * decay_rate) / PRECISION; } - fn _calc_new_duration( + + #[inline(always)] + fn calc_new_duration( self: @ContractState, remaining_amount: u256, - remaining_time_to_live: u256, + remaining_time_to_live: u64, added_amount: u256 - ) -> u256 { - return 0; + ) -> u64 { + let prev_weight = remaining_amount * remaining_time_to_live.into(); + let next_weight = added_amount * FEE_EXPIRATION_SECONDS.into(); + return ((prev_weight + next_weight) / (remaining_amount + added_amount)) + .try_into() + .unwrap(); + } + + #[inline(always)] + fn collect_fee( + ref self: ContractState, + borrower: ContractAddress, + asset: ContractAddress, + fee_amount: u256 + ) { + if fee_amount.is_non_zero() { + let collector = self.get_protocol_revenue_destination_internal(); + IERC20Dispatcher { + contract_address: self + .address_provider + .read() + .get_address(AddressesKey::debt_token) + } + .transfer(collector, fee_amount); + if self.is_route_to_shvt_staking() { + ISHVTStakingDispatcher { + contract_address: self + .address_provider + .read() + .get_address(AddressesKey::shvt_staking) + } + .increase_fee_debt_token(fee_amount); + } + + self.emit(FeeCollected { borrower, asset, collector, amount: fee_amount }); + } + } + + + #[inline(always)] + fn get_protocol_revenue_destination_internal(self: @ContractState) -> ContractAddress { + let address_provider = self.address_provider.read(); + if self.is_route_to_shvt_staking() { + return address_provider.get_address(AddressesKey::shvt_staking); + } + return address_provider.get_address(AddressesKey::treasury); } - fn _collect_fee(ref self: ContractState, asset: ContractAddress, fee_amount: u256) {} - fn _refund_fee( + + #[inline(always)] + fn refund_fee( ref self: ContractState, borrower: ContractAddress, asset: ContractAddress, refund_amount: u256 - ) {} + ) { + if refund_amount.is_non_zero() { + IERC20Dispatcher { + contract_address: self + .address_provider + .read() + .get_address(AddressesKey::debt_token) + } + .transfer(borrower, refund_amount); + self.emit(FeeRefunded { borrower, asset, amount: refund_amount }); + } + } + + #[inline(always)] + fn is_route_to_shvt_staking(self: @ContractState) -> bool { + return self.is_route_to_SHVT_staking.read(); + } + + #[inline(always)] + fn assert_caller_is_timelock(self: @ContractState) { + let caller = get_caller_address(); + let address_provider = self.address_provider.read(); + assert( + caller == address_provider.get_address(AddressesKey::timelock), + CommunErrors::CallerNotAuthorized + ); + } + + #[inline(always)] + fn assert_caller_is_borrower_operations(self: @ContractState) { + let caller = get_caller_address(); + let address_provider = self.address_provider.read(); + assert( + caller == address_provider.get_address(AddressesKey::borrower_operations), + CommunErrors::CallerNotAuthorized + ); + } + + #[inline(always)] + fn assert_caller_is_vessel_manager(self: @ContractState) { + let caller = get_caller_address(); + let address_provider = self.address_provider.read(); + assert( + caller == address_provider.get_address(AddressesKey::vessel_manager), + CommunErrors::CallerNotAuthorized + ); + } - fn _route_to_grvt_staking(self: @ContractState) -> bool { - return false; + #[inline(always)] + fn assert_caller_is_borrower_operations_or_vessel_manager(self: @ContractState) { + let caller = get_caller_address(); + let address_provider = self.address_provider.read(); + assert( + caller == address_provider.get_address(AddressesKey::borrower_operations) + || caller == address_provider.get_address(AddressesKey::vessel_manager), + CommunErrors::CallerNotAuthorized + ); } - fn _only_timelock(self: @ContractState) {} - fn _only_borrower_operations(self: @ContractState) {} - fn _only_vessel_manager(self: @ContractState) {} - fn _only_borrower_operations_or_vessel_manager(self: @ContractState) {} } } diff --git a/src/lib.cairo b/src/lib.cairo index 269fc79..73abfdc 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -6,6 +6,10 @@ mod interfaces { mod deposit; } +mod shvt { + mod shvt_staking; +} + mod core { mod address_provider; mod timelock; diff --git a/src/shvt/shvt_staking.cairo b/src/shvt/shvt_staking.cairo new file mode 100644 index 0000000..afd21c2 --- /dev/null +++ b/src/shvt/shvt_staking.cairo @@ -0,0 +1,55 @@ +use starknet::ContractAddress; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + +#[starknet::interface] +trait ISHVTStaking { + fn shvt_token(self: @TContractState) -> IERC20Dispatcher; + + fn stake(ref self: TContractState, shvt_amount: u256); + + fn unstake(ref self: TContractState, shvt_amount: u256); + + fn increase_fee_asset(ref self: TContractState, asset: ContractAddress, asset_fee: u256); + fn increase_fee_debt_token(ref self: TContractState, shvt_fee: u256); + fn get_pending_asset_gain( + self: @TContractState, asset: ContractAddress, user: ContractAddress + ) -> u256; + + fn get_pending_debt_token_gain(self: @TContractState, user: ContractAddress) -> u256; +} + +#[starknet::contract] +mod SHVTStaking { + use starknet::ContractAddress; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + + + #[storage] + struct Storage { + shvt_token: IERC20Dispatcher + } + + + #[external(v0)] + impl SHVTStakingImpl of super::ISHVTStaking { + fn shvt_token(self: @ContractState) -> IERC20Dispatcher { + return self.shvt_token.read(); + } + + fn stake(ref self: ContractState, shvt_amount: u256) {} + + fn unstake(ref self: ContractState, shvt_amount: u256) {} + + fn increase_fee_asset(ref self: ContractState, asset: ContractAddress, asset_fee: u256) {} + fn increase_fee_debt_token(ref self: ContractState, shvt_fee: u256) {} + fn get_pending_asset_gain( + self: @ContractState, asset: ContractAddress, user: ContractAddress + ) -> u256 { + return 0; + } + + fn get_pending_debt_token_gain(self: @ContractState, user: ContractAddress) -> u256 { + return 0; + } + } +} diff --git a/tests/integration/core/fee_collector/close_debt/close_debt.tree b/tests/integration/core/fee_collector/close_debt/close_debt.tree index bcc94d5..50fa78c 100644 --- a/tests/integration/core/fee_collector/close_debt/close_debt.tree +++ b/tests/integration/core/fee_collector/close_debt/close_debt.tree @@ -1,9 +1,13 @@ test_close_debt.cairo -├── when caller is neither the Borrower Operator nor the Vessel Manager contract +├── when caller is neither the Borrower Operations nor the Vessel Manager contract │ └── it should revert with CallerNotAuthorized -└── when caller is either the Borrower Operator or the Vessel Manager contract - └── when current timestamp is lower than the fee_record.to - ├── it should correctly burn the right amount of debt token - ├── when the current timestamp is greater than the fee_record.to - │ └── it should correctly delete the fee_record - └── it should emit {FeeRecordUpdated} event \ No newline at end of file +└── when caller is either the Borrower Operations or the Vessel Manager contract + ├── when current timestamp is lower than 1 week after the fee_record.from + │ └── it should correctly collect the min amount of fee + ├── when current timestamp is lower than the fee_record.to + │ ├── it should correctly burn the right amount of debt token + │ └── it should emit {FeeCollected} event + ├── when the current timestamp is greater than the fee_record.from + │ ├── it should correctly delete the fee_record and send all fee to the right protocol revenue destination + │ └── it should emit {FeeCollected} event + └── it should emit {FeeRecordUpdated} event \ No newline at end of file diff --git a/tests/integration/core/fee_collector/close_debt/test_close_debt.cairo b/tests/integration/core/fee_collector/close_debt/test_close_debt.cairo index 8b13789..8414ce6 100644 --- a/tests/integration/core/fee_collector/close_debt/test_close_debt.cairo +++ b/tests/integration/core/fee_collector/close_debt/test_close_debt.cairo @@ -1 +1,178 @@ +use starknet::{ContractAddress, get_block_timestamp}; +use snforge_std::{ + start_prank, stop_prank, start_warp, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions, + PrintTrait +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use shisui::core::{ + debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait}, + fee_collector::{IFeeCollectorDispatcher, IFeeCollectorDispatcherTrait, FeeCollector} +}; +use tests::utils::callers::{ + vessel_manager_address, borrower_operations_address, borrower, treasury_address +}; +use tests::utils::asserts::assert_is_approximately_equal; + +use super::super::setup::{setup, calc_fees}; + +const DEBT_AMOUNT: u256 = 100_000000000000000000; // 10e18 +const ERROR_MARGIN: u256 = 1000000000; // 1e9 +fn test_setup() -> (IFeeCollectorDispatcher, ContractAddress, ContractAddress, u256, u256) { + let (_, fee_collector, debt_token_address, asset_address) = setup(); + let (min_fee, max_fee) = calc_fees(DEBT_AMOUNT); + // Mint some debt tokens to the fee collector + start_prank(CheatTarget::One(debt_token_address), borrower_operations_address()); + + IDebtTokenDispatcher { contract_address: debt_token_address } + .mint(fee_collector.contract_address, max_fee); + stop_prank(CheatTarget::One(debt_token_address)); + + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + fee_collector.increase_debt(borrower(), asset_address, max_fee); + stop_prank(CheatTarget::One(fee_collector.contract_address)); + + (fee_collector, debt_token_address, asset_address, min_fee, max_fee) +} + +#[test] +#[should_panic(expected: ('Caller not authorized',))] +fn when_caller_is_neither_borrower_operations_nor_vessel_manager_it_should_revert() { + let (fee_collector, _, asset_address, _, _) = test_setup(); + fee_collector.close_debt(borrower(), asset_address); +} + +#[test] +fn when_caller_is_valid_and_current_timestamp_greater_than_record_to_it_should_send_debt_token_to_right_protocol_revenue_destination() { + let (fee_collector, debt_token_address, asset_address, min_fee, max_fee) = test_setup(); + let mut spy = spy_events(SpyOn::One(fee_collector.contract_address)); + + let record = fee_collector.get_fee_record(borrower(), asset_address); + start_warp(CheatTarget::One(fee_collector.contract_address), record.to + 1); + + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + fee_collector.close_debt(borrower(), asset_address); + + spy + .assert_emitted( + @array![ + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: borrower(), + asset: asset_address, + from: record.to + 1, + to: 0, + amount: 0 + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeCollected( + FeeCollector::FeeCollected { + borrower: borrower(), + asset: asset_address, + collector: treasury_address(), + amount: max_fee - min_fee + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no events'); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(treasury_address()) == max_fee, + 'Invalid treasury balance' + ); + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address) + .is_zero(), + 'Invalid fee collector balance' + ); +} + +#[test] +fn when_caller_is_valid_and_current_timestamp_lower_than_one_week_it_should_take_the_minium_of_fee() { + let (fee_collector, debt_token_address, asset_address, min_fee, _) = test_setup(); + let mut spy = spy_events(SpyOn::One(fee_collector.contract_address)); + let now = get_block_timestamp(); + + let record = fee_collector.get_fee_record(borrower(), asset_address); + + start_prank(CheatTarget::One(fee_collector.contract_address), vessel_manager_address()); + fee_collector.close_debt(borrower(), asset_address); + + spy + .assert_emitted( + @array![ + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: borrower(), asset: asset_address, from: 0, to: 0, amount: 0 + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no events'); + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(treasury_address()) == min_fee, + 'Invalid treasury balance' + ); + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address) + .is_zero(), + 'Invalid fee collector balance' + ); +} + +#[test] +fn when_caller_is_valid_and_current_timestamp_greater_than_record_from_it_should_take_the_right_amount() { + let (fee_collector, debt_token_address, asset_address, min_fee, max_fee) = test_setup(); + let mut spy = spy_events(SpyOn::One(fee_collector.contract_address)); + let record = fee_collector.get_fee_record(borrower(), asset_address); + + // 50% of the time has passed + let now = ((record.to - get_block_timestamp()) / 2); + start_warp(CheatTarget::One(fee_collector.contract_address), now); + + start_prank(CheatTarget::One(fee_collector.contract_address), vessel_manager_address()); + + fee_collector.close_debt(borrower(), asset_address); + + spy + .assert_emitted( + @array![ + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: borrower(), asset: asset_address, from: now, to: 0, amount: 0 + } + ) + ) + ] + ); + assert(spy.events.len() == 1, 'There should be one event'); + assert_is_approximately_equal( + IERC20Dispatcher { contract_address: debt_token_address }.balance_of(treasury_address()), + max_fee / 2, + ERROR_MARGIN, + 'Invalid treasury balance' + ); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address) + .is_zero(), + 'Invalid fee collector balance' + ); +} diff --git a/tests/integration/core/fee_collector/collect_fees/collect_fees.tree b/tests/integration/core/fee_collector/collect_fees/collect_fees.tree index c5b0627..7cb202d 100644 --- a/tests/integration/core/fee_collector/collect_fees/collect_fees.tree +++ b/tests/integration/core/fee_collector/collect_fees/collect_fees.tree @@ -1,8 +1,11 @@ test_collect_fees.cairo ├── when borrowers array length is 0 -│ └── it should revert with FeeCollector__ArrayMismatch +│ └── it should revert with ArrayMismatch ├── when borrowers array length is not equal to assets array length -│ └── it should revert with FeeCollector__ArrayMismatch -└── when array lengths are equal and not 0 - ├── it should correctly collect fees and delete the fee_record - └── it should emit {FeeRecordUpdated} event \ No newline at end of file +│ └── it should revert with ArrayMismatch +└── when input valid + ├── when 2 loans partially collected and then after they expired + │ ├── it should correctly collect fees + │ └── it should emit {FeeRecordUpdated} and {FeeCollected} event + └── when input does not contain fees to collect + └── it should do nothing \ No newline at end of file diff --git a/tests/integration/core/fee_collector/collect_fees/test_collect_fees.cairo b/tests/integration/core/fee_collector/collect_fees/test_collect_fees.cairo index 8b13789..55bba1e 100644 --- a/tests/integration/core/fee_collector/collect_fees/test_collect_fees.cairo +++ b/tests/integration/core/fee_collector/collect_fees/test_collect_fees.cairo @@ -1 +1,254 @@ +use starknet::{ContractAddress, contract_address_const, get_block_timestamp}; +use snforge_std::{ + start_prank, stop_prank, start_warp, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions, + PrintTrait +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use shisui::core::{ + debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait}, + fee_collector::{IFeeCollectorDispatcher, IFeeCollectorDispatcherTrait, FeeCollector} +}; +use shisui::utils::constants::ONE; +use tests::utils::{ + callers::{alice, borrower_operations_address, borrower, treasury_address}, + constant::{MIN_FEE_DURATION, FEE_EXPIRATION_SECONDS}, asserts::assert_is_approximately_equal +}; + +use super::super::setup::{setup, calc_fees, calc_new_duration, calc_expired_amount}; + +const DEBT_AMOUNT: u256 = 100_000000000000000000; // 100e18 + + +fn test_setup() -> (IFeeCollectorDispatcher, ContractAddress, ContractAddress, u256, u256) { + let (_, fee_collector, debt_token_address, asset_address) = setup(); + let (min_fee, max_fee) = calc_fees(DEBT_AMOUNT); + // Mint some debt tokens to the fee collector + start_prank(CheatTarget::One(debt_token_address), borrower_operations_address()); + + IDebtTokenDispatcher { contract_address: debt_token_address } + .mint(fee_collector.contract_address, max_fee * 2); + stop_prank(CheatTarget::One(debt_token_address)); + + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + fee_collector.increase_debt(borrower(), asset_address, max_fee); + stop_prank(CheatTarget::One(fee_collector.contract_address)); + + (fee_collector, debt_token_address, asset_address, min_fee, max_fee) +} + + +#[test] +#[should_panic(expected: ('Array Mismatch',))] +fn when_borrower_array_is_empty_it_sould_revert() { + let (fee_collector, _, asset_address, _, _) = test_setup(); + fee_collector.collect_fees(array![].span(), array![].span()); +} + +#[test] +#[should_panic(expected: ('Array Mismatch',))] +fn when_borrower_array_length_not_equal_to_assets_array_length_it_should_revert() { + let (fee_collector, _, asset_address, _, _) = test_setup(); + fee_collector.collect_fees(array![borrower(), borrower()].span(), array![asset_address].span()); +} + +#[test] +fn when_input_valid_and_partially_collected_and_then_after_they_expired_it_should_correctly_collect_fees() { + let (fee_collector, debt_token_address, asset_address, min_fee, max_fee) = test_setup(); + let fees_left = max_fee - min_fee; + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + let borrower_record = fee_collector.get_fee_record(borrower(), asset_address); + + // 25% of the time has passed and add alice as new borrower + let mut now = ((borrower_record.to - get_block_timestamp()) / 4); + start_warp(CheatTarget::One(fee_collector.contract_address), now); + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + fee_collector.increase_debt(alice(), asset_address, max_fee); + stop_prank(CheatTarget::One(fee_collector.contract_address)); + + let alice_record = fee_collector.get_fee_record(alice(), asset_address); + + // move to 50% of the time has passed for borrower + now = ((borrower_record.to - get_block_timestamp()) / 2); + start_warp(CheatTarget::One(fee_collector.contract_address), now); + + let expected_borrower_amount = fee_collector.simulate_refund(borrower(), asset_address, ONE); + let expected_borrower_fee_collected = fees_left - expected_borrower_amount; + let expected_alice_amount = fee_collector.simulate_refund(alice(), asset_address, ONE); + let expected_alice_fee_collected = fees_left - expected_alice_amount; + let total_fee_expected = expected_borrower_fee_collected + + expected_alice_fee_collected + + min_fee * 2; + let expected_fee_collector_balance = max_fee * 2 - total_fee_expected; + + let mut spy = spy_events(SpyOn::One(fee_collector.contract_address)); + + fee_collector + .collect_fees( + array![borrower(), alice()].span(), array![asset_address, asset_address].span() + ); + + spy + .assert_emitted( + @array![ + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: borrower(), + asset: asset_address, + from: now, + to: borrower_record.to, + amount: expected_borrower_amount + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeCollected( + FeeCollector::FeeCollected { + borrower: borrower(), + asset: asset_address, + collector: treasury_address(), + amount: expected_borrower_fee_collected + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: alice(), + asset: asset_address, + from: now, + to: alice_record.to, + amount: expected_alice_amount + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeCollected( + FeeCollector::FeeCollected { + borrower: alice(), + asset: asset_address, + collector: treasury_address(), + amount: expected_alice_fee_collected + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no event'); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address) == expected_fee_collector_balance, + 'Invalid fee collector balance' + ); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(treasury_address()) == total_fee_expected, + 'Invalid treasury balance' + ); + + // move after expiration + now = alice_record.to + 1; + start_warp(CheatTarget::One(fee_collector.contract_address), now); + + fee_collector + .collect_fees( + array![borrower(), alice()].span(), array![asset_address, asset_address].span() + ); + spy + .assert_emitted( + @array![ + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: borrower(), + asset: asset_address, + from: now, + to: borrower_record.to, + amount: 0 + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeCollected( + FeeCollector::FeeCollected { + borrower: borrower(), + asset: asset_address, + collector: treasury_address(), + amount: expected_borrower_amount + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: alice(), + asset: asset_address, + from: now, + to: alice_record.to, + amount: 0 + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeCollected( + FeeCollector::FeeCollected { + borrower: alice(), + asset: asset_address, + collector: treasury_address(), + amount: expected_alice_amount + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no event'); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address) + .is_zero(), + 'Invalid fee collector balance' + ); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(treasury_address()) == max_fee + * 2, + 'Invalid treasury balance' + ); +} + +#[test] +fn when_input_valid_and_no_fees_to_collect_it_should_do_nothing() { + let (fee_collector, debt_token_address, asset_address, min_fee, max_fee) = test_setup(); + + let fee_collector_balance = IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address); + let treasury_balance = IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(treasury_address()); + + fee_collector.collect_fees(array![borrower()].span(), array![asset_address].span()); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address) == fee_collector_balance, + 'Invalid fee collector balance' + ); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(treasury_address()) == treasury_balance, + 'Invalid treasury balance' + ); +} diff --git a/tests/integration/core/fee_collector/decrease_debt/decrease_debt.tree b/tests/integration/core/fee_collector/decrease_debt/decrease_debt.tree index 0e1ff71..e634570 100644 --- a/tests/integration/core/fee_collector/decrease_debt/decrease_debt.tree +++ b/tests/integration/core/fee_collector/decrease_debt/decrease_debt.tree @@ -1,16 +1,18 @@ test_decrease_debt.cairo -├── when caller is neither the Borrower Operator nor the Vessel Manager contract +├── when caller is neither the Borrower Operations nor the Vessel Manager contract │ └── it should revert with CallerNotAuthorized -└── when caller is either the Borrower Operator or the Vessel Manager contract +└── when caller is either the Borrower Operations or the Vessel Manager contract ├── when payback_fraction is 0 │ └── it should revert with CommunErrors_CantBeZero ├── when payback_fraction is greater than 1e18 │ └── it should revert with FeeCollector__PaybackFractionTooHigh - └── when current timestamp is lower than the fee_record.to - ├── when payback_fraction is equal to 1e18 - │ └── it should correctly burn the right amount of debt token - ├── when payback_fraction is lower than 1e18 - │ └── it should correctly refund the borrower - ├── when the current timestamp is greater than the fee_record.to - │ └── it should correctly delete the fee_record - └── it should emit {FeeRecordUpdated} event \ No newline at end of file + ├── when current timestamp is lower than the fee_record.to + │ ├── when payback_fraction is equal to 1e18 + │ │ ├── it should correctly burn the amount left after fees + │ │ └── it should emit {FeeRecordUpdated} and {FeeCollected} event + │ ├── when payback_fraction is lower than 1e18 + │ │ ├── it should correctly refund the borrower + │ │ └── it should emit {FeeRecordUpdated}, {FeeCollected} and {FeeRefunded} event + └── when the current timestamp is greater than the fee_record.to + ├── it should correctly delete the fee_record and send all fees + └── it should emit {FeeRecordUpdated} and {FeeCollected} event \ No newline at end of file diff --git a/tests/integration/core/fee_collector/decrease_debt/test_decrease_debt.cairo b/tests/integration/core/fee_collector/decrease_debt/test_decrease_debt.cairo index 8b13789..15906cd 100644 --- a/tests/integration/core/fee_collector/decrease_debt/test_decrease_debt.cairo +++ b/tests/integration/core/fee_collector/decrease_debt/test_decrease_debt.cairo @@ -1 +1,263 @@ +use core::array::ArrayTrait; +use starknet::{ContractAddress, contract_address_const, get_block_timestamp}; +use snforge_std::{ + start_prank, stop_prank, start_warp, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions, + PrintTrait +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use shisui::core::{ + debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait}, + fee_collector::{IFeeCollectorDispatcher, IFeeCollectorDispatcherTrait, FeeCollector} +}; +use shisui::utils::constants::ONE; +use tests::utils::{ + callers::{vessel_manager_address, borrower_operations_address, borrower, treasury_address}, + constant::{MIN_FEE_DURATION, FEE_EXPIRATION_SECONDS}, asserts::assert_is_approximately_equal +}; + +use super::super::setup::{setup, calc_fees, calc_new_duration, calc_expired_amount}; + +const DEBT_AMOUNT: u256 = 100_000000000000000000; // 100e18 +const ERROR_MARGIN: u256 = 1_000_000_000; // 1e9 + +fn test_setup() -> (IFeeCollectorDispatcher, ContractAddress, ContractAddress, u256, u256) { + let (_, fee_collector, debt_token_address, asset_address) = setup(); + let (min_fee, max_fee) = calc_fees(DEBT_AMOUNT); + // Mint some debt tokens to the fee collector + start_prank(CheatTarget::One(debt_token_address), borrower_operations_address()); + + IDebtTokenDispatcher { contract_address: debt_token_address } + .mint(fee_collector.contract_address, max_fee); + stop_prank(CheatTarget::One(debt_token_address)); + + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + fee_collector.increase_debt(borrower(), asset_address, max_fee); + stop_prank(CheatTarget::One(fee_collector.contract_address)); + + (fee_collector, debt_token_address, asset_address, min_fee, max_fee) +} + + +#[test] +#[should_panic(expected: ('Caller not authorized',))] +fn when_caller_is_neither_borrower_operations_nor_vessel_manager_it_should_revert() { + let (fee_collector, _, asset_address, _, max_fee) = test_setup(); + fee_collector.decrease_debt(borrower(), asset_address, max_fee); +} + +#[test] +#[should_panic(expected: ('Value is zero',))] +fn when_caller_is_valid_and_payback_fraction_zero_it_should_revert() { + let (fee_collector, debt_token_address, asset_address, min_fee, max_fee) = test_setup(); + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + + fee_collector.decrease_debt(borrower(), asset_address, 0); +} + +#[test] +#[should_panic(expected: ('Payback fraction exceed 10e18',))] +fn when_caller_is_valid_and_payback_fraction_exceed_100_percent_it_should_revert() { + let (fee_collector, debt_token_address, asset_address, min_fee, max_fee) = test_setup(); + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + fee_collector.decrease_debt(borrower(), asset_address, ONE + 1); +} + + +#[test] +fn when_caller_is_valid_and_current_timestamp_lower_than_record_to_and_payback_fraction_is_10e18_it_should_correctly_burn() { + let (fee_collector, debt_token_address, asset_address, min_fee, max_fee) = test_setup(); + let mut spy = spy_events(SpyOn::One(fee_collector.contract_address)); + + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + let mut record = fee_collector.get_fee_record(borrower(), asset_address); + + // 50% of the time has passed + let now = ((record.to - get_block_timestamp()) / 2); + start_warp(CheatTarget::One(fee_collector.contract_address), now); + + let expected_refundable_amount = fee_collector.simulate_refund(borrower(), asset_address, ONE); + let expected_collected_fee = record.amount - expected_refundable_amount + min_fee; + + fee_collector.decrease_debt(borrower(), asset_address, ONE); + + spy + .assert_emitted( + @array![ + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: borrower(), asset: asset_address, from: now, to: 0, amount: 0 + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeCollected( + FeeCollector::FeeCollected { + borrower: borrower(), + asset: asset_address, + collector: treasury_address(), + amount: expected_collected_fee - min_fee + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no event'); + record = fee_collector.get_fee_record(borrower(), asset_address); + assert(record.amount.is_zero(), 'Invalid record amount'); + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address) + .is_zero(), + 'Invalid fee collector balance' + ); + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(treasury_address()) == expected_collected_fee, + 'Invalid fee collector balance' + ); +} + +#[test] +fn when_caller_is_valid_and_current_timestamp_lower_than_record_to_and_payback_fraction_is_lower_than_10e18_it_should_correctly_refund() { + let (fee_collector, debt_token_address, asset_address, min_fee, max_fee) = test_setup(); + let mut spy = spy_events(SpyOn::One(fee_collector.contract_address)); + + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + let mut record = fee_collector.get_fee_record(borrower(), asset_address); + let from1 = record.from; + let to1 = record.to; + let amount1 = record.amount; + + // 50% of the time has passed + let now = ((record.to - get_block_timestamp()) / 2); + start_warp(CheatTarget::One(fee_collector.contract_address), now); + + // 50% refund + let expected_refundable_amount = fee_collector + .simulate_refund(borrower(), asset_address, ONE / 2); + let expected_collected_fee = calc_expired_amount(now, from1, to1, amount1); + let expected_record = amount1 - expected_refundable_amount - expected_collected_fee; + fee_collector.decrease_debt(borrower(), asset_address, ONE / 2); + spy + .assert_emitted( + @array![ + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: borrower(), + asset: asset_address, + from: now, + to: to1, + amount: expected_record + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeCollected( + FeeCollector::FeeCollected { + borrower: borrower(), + asset: asset_address, + collector: treasury_address(), + amount: expected_collected_fee + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRefunded( + FeeCollector::FeeRefunded { + borrower: borrower(), + asset: asset_address, + amount: expected_refundable_amount + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no event'); + + record = fee_collector.get_fee_record(borrower(), asset_address); + + assert(record.amount == expected_record, 'Invalid record amount'); + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address) == expected_record, + 'Invalid fee collector balance' + ); + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(borrower()) == expected_refundable_amount, + 'Invalid borrowrer redund' + ); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(treasury_address()) == expected_collected_fee + + min_fee, + 'Invalid treasury balance' + ); +} + +#[test] +fn when_caller_is_valid_and_current_timestamp_higher_than_record_to_it_should_correctly_delete_record() { + let (fee_collector, debt_token_address, asset_address, min_fee, max_fee) = test_setup(); + let mut spy = spy_events(SpyOn::One(fee_collector.contract_address)); + + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + let mut record = fee_collector.get_fee_record(borrower(), asset_address); + + let now = (record.to + 1); + start_warp(CheatTarget::One(fee_collector.contract_address), now); + + // what ever the payback fraction is, it will be 100% of the record amount + fee_collector.decrease_debt(borrower(), asset_address, ONE / 2); + spy + .assert_emitted( + @array![ + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: borrower(), asset: asset_address, from: now, to: 0, amount: 0 + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeCollected( + FeeCollector::FeeCollected { + borrower: borrower(), + asset: asset_address, + collector: treasury_address(), + amount: max_fee - min_fee + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no event'); + + record = fee_collector.get_fee_record(borrower(), asset_address); + + assert(record.amount.is_zero(), 'Invalid record amount'); + assert(record.to.is_zero(), 'Invalid to value'); + assert(record.from.is_zero(), 'Invalid from value'); + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address) + .is_zero(), + 'Invalid fee collector balance' + ); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(treasury_address()) == max_fee, + 'Invalid treasury balance' + ); +} diff --git a/tests/integration/core/fee_collector/handle_redemption_fee/handle_redemption_fee.tree b/tests/integration/core/fee_collector/handle_redemption_fee/handle_redemption_fee.tree index 0a310d1..c2904a8 100644 --- a/tests/integration/core/fee_collector/handle_redemption_fee/handle_redemption_fee.tree +++ b/tests/integration/core/fee_collector/handle_redemption_fee/handle_redemption_fee.tree @@ -1,11 +1,9 @@ test_handle_redemption_fee.cairo -├── when caller is not the Vessel Manager contract -│ └── it should revert with CallerNotAuthorized -└── when caller is the Vessel Manager contract - ├── when borrowers array length is 0 - │ └── it should revert with FeeCollector__ArrayMismatch - ├── when borrowers array length is not equal to assets array length - │ └── it should revert with FeeCollector__ArrayMismatch - └── when array lengths are equal and not 0 - ├── it should correctly collect fees and delete the fee_record - └── it should emit {FeeRecordUpdated} event \ No newline at end of file +# when caller is not the Vessel Manager contract +## it should revert with CallerNotAuthorized +# when caller is the Vessel Manager contract +## when is route to shvt staking is true +### it should call the increase_fee_asset function of the shvt staking contract +### it should emit {RedemptionFeeCollected} event +## when is route to shvt staking is false +### it should emit {RedemptionFeeCollected} event diff --git a/tests/integration/core/fee_collector/handle_redemption_fee/test_handle_redemption_fee.cairo b/tests/integration/core/fee_collector/handle_redemption_fee/test_handle_redemption_fee.cairo index 8b13789..802d535 100644 --- a/tests/integration/core/fee_collector/handle_redemption_fee/test_handle_redemption_fee.cairo +++ b/tests/integration/core/fee_collector/handle_redemption_fee/test_handle_redemption_fee.cairo @@ -1 +1,84 @@ +use starknet::{ContractAddress, contract_address_const, get_block_timestamp}; +use snforge_std::{ + start_prank, stop_prank, start_warp, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions, + PrintTrait +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + +use shisui::core::{ + debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait}, + fee_collector::{IFeeCollectorDispatcher, IFeeCollectorDispatcherTrait, FeeCollector} +}; +use shisui::utils::constants::ONE; +use tests::utils::{ + callers::{vessel_manager_address, borrower, timelock_address}, + constant::{MIN_FEE_DURATION, FEE_EXPIRATION_SECONDS}, asserts::assert_is_approximately_equal +}; + +use super::super::setup::{setup, calc_fees, calc_new_duration, calc_expired_amount}; + + +fn test_setup() -> (IFeeCollectorDispatcher, ContractAddress, ContractAddress) { + let (_, fee_collector, debt_token_address, asset_address) = setup(); + + (fee_collector, debt_token_address, asset_address) +} + + +#[test] +#[should_panic(expected: ('Caller not authorized',))] +fn when_caller_is_vessel_manager_it_sould_revert() { + let (fee_collector, _, asset_address) = test_setup(); + fee_collector.handle_redemption_fee(asset_address, ONE); +} + + +#[test] +fn when_caller_valid_and_is_route_to_shvt_staking_is_false_it_should_just_emit_event() { + let (fee_collector, debt_token_address, asset_address) = test_setup(); + let mut spy = spy_events(SpyOn::One(fee_collector.contract_address)); + + start_prank(CheatTarget::One(fee_collector.contract_address), vessel_manager_address()); + fee_collector.handle_redemption_fee(asset_address, ONE); + + spy + .assert_emitted( + @array![ + ( + fee_collector.contract_address, + FeeCollector::Event::RedemptionFeeCollected( + FeeCollector::RedemptionFeeCollected { asset: asset_address, amount: ONE } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no event'); +} +// TODO: test when shvt staking contract setup +// #[test] +// fn when_caller_valid_and_is_route_to_shvt_staking_is_true_it_should_call_increase_fee_asset() { +// let (fee_collector, debt_token_address, asset_address) = test_setup(); + +// start_prank(CheatTarget::One(fee_collector.contract_address), timelock_address()); +// fee_collector.set_is_route_to_SHVT_staking(true); + +// let mut spy = spy_events(SpyOn::One(fee_collector.contract_address)); + +// start_prank(CheatTarget::One(fee_collector.contract_address), vessel_manager_address()); +// fee_collector.handle_redemption_fee(asset_address, ONE); + +// spy +// .assert_emitted( +// @array![ +// ( +// fee_collector.contract_address, +// FeeCollector::Event::RedemptionFeeCollected( +// FeeCollector::RedemptionFeeCollected { asset: asset_address, amount: ONE } +// ) +// ) +// ] +// ); +// assert(spy.events.is_empty(), 'There should be no event'); +// } + diff --git a/tests/integration/core/fee_collector/increase_debt/increase_debt.tree b/tests/integration/core/fee_collector/increase_debt/increase_debt.tree index abd406e..0f13ce3 100644 --- a/tests/integration/core/fee_collector/increase_debt/increase_debt.tree +++ b/tests/integration/core/fee_collector/increase_debt/increase_debt.tree @@ -1,13 +1,13 @@ test_increase_debt.cairo -├── when caller is not the Borrower Operator contract +├── when caller is not the Borrower Operations contract │ └── it should revert with CallerNotAuthorized -└── when caller is the Borrower Operator contract +└── when caller is the Borrower Operations contract ├── when it is the first borrower debt on the asset │ └── it should correctly create the fee record └── when it is not the first borrower debt on the asset - ├── when the current timestamp is lower than the fee_record.to - │ └── it should correctly update fee_record - ├── when the current timestamp is greater than the fee_record.to - │ └── it should create a new fee_record - ├── it should correctly collect fees - └── it should emit {FeeRecordUpdated} event \ No newline at end of file + │ ├── when the current timestamp is lower than the fee_record.to + │ │ └── it should correctly update fee_record + │ ├── when the current timestamp is greater than the fee_record.to + │ │ └── it should create a new fee_record + │ └── it should correctly collect fees + └── it should emit {FeeRecordUpdated} and {FeeCollected} event \ No newline at end of file diff --git a/tests/integration/core/fee_collector/increase_debt/test_increase_debt.cairo b/tests/integration/core/fee_collector/increase_debt/test_increase_debt.cairo index 8b13789..048b789 100644 --- a/tests/integration/core/fee_collector/increase_debt/test_increase_debt.cairo +++ b/tests/integration/core/fee_collector/increase_debt/test_increase_debt.cairo @@ -1 +1,261 @@ +use starknet::{ContractAddress, contract_address_const, get_block_timestamp}; +use snforge_std::{ + start_prank, stop_prank, start_warp, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions, + PrintTrait +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use shisui::core::{ + debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait}, + fee_collector::{IFeeCollectorDispatcher, IFeeCollectorDispatcherTrait, FeeCollector} +}; +use tests::utils::{ + callers::{vessel_manager_address, borrower_operations_address, borrower, treasury_address}, + constant::{MIN_FEE_DURATION, FEE_EXPIRATION_SECONDS}, asserts::assert_is_approximately_equal +}; + +use super::super::setup::{setup, calc_fees, calc_new_duration, calc_expired_amount}; + +const DEBT_AMOUNT: u256 = 100_000000000000000000; // 100e18 +const ERROR_MARGIN: u256 = 1_000_000_000; // 1e9 + +fn test_setup() -> (IFeeCollectorDispatcher, ContractAddress, ContractAddress, u256, u256) { + let (_, fee_collector, debt_token_address, asset_address) = setup(); + let (min_fee, max_fee) = calc_fees(DEBT_AMOUNT); + + // Mint some debt tokens to the fee collector + start_prank(CheatTarget::One(debt_token_address), borrower_operations_address()); + IDebtTokenDispatcher { contract_address: debt_token_address } + .mint(fee_collector.contract_address, max_fee); + stop_prank(CheatTarget::One(debt_token_address)); + + (fee_collector, debt_token_address, asset_address, min_fee, max_fee) +} + + +#[test] +#[should_panic(expected: ('Caller not authorized',))] +fn when_caller_is_not_borrower_operations_it_should_revert() { + let (fee_collector, _, asset_address, _, max_fee) = test_setup(); + fee_collector.increase_debt(borrower(), asset_address, max_fee); +} + +#[test] +fn when_caller_is_borrower_operations_and_first_increase_it_should_create_fee_record() { + let (fee_collector, debt_token_address, asset_address, min_fee, max_fee) = test_setup(); + let mut spy = spy_events(SpyOn::One(fee_collector.contract_address)); + let expected_amount = max_fee - min_fee; + + let now = get_block_timestamp(); + + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + fee_collector.increase_debt(borrower(), asset_address, max_fee); + + spy + .assert_emitted( + @array![ + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: borrower(), + asset: asset_address, + from: now + MIN_FEE_DURATION, + to: now + MIN_FEE_DURATION + FEE_EXPIRATION_SECONDS, + amount: expected_amount + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeCollected( + FeeCollector::FeeCollected { + borrower: borrower(), + asset: asset_address, + collector: treasury_address(), + amount: min_fee + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no events'); + + let record = IFeeCollectorDispatcher { contract_address: fee_collector.contract_address } + .get_fee_record(borrower(), asset_address); + + assert(record.amount == expected_amount, 'Wrong record amount'); + assert(record.from == now + MIN_FEE_DURATION, 'Wrong record from'); + assert(record.to == now + MIN_FEE_DURATION + FEE_EXPIRATION_SECONDS, 'Wrong record to'); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address) == expected_amount, + 'Wrong fee collector balance' + ); + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(treasury_address()) == min_fee, + 'Invalid treasury balance' + ); +} + +#[test] +fn when_caller_is_borrower_operations_and_not_the_first_increase_but_current_timestamp_greater_than_record_to_it_should_create_new_fee_record() { + let (fee_collector, debt_token_address, asset_address, min_fee, max_fee) = test_setup(); + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + + fee_collector.increase_debt(borrower(), asset_address, max_fee); + let mut record = IFeeCollectorDispatcher { contract_address: fee_collector.contract_address } + .get_fee_record(borrower(), asset_address); + + // Warp to the end of the first record + start_warp(CheatTarget::One(fee_collector.contract_address), record.to + 1); + let now = record.to + 1; + + // mint some debt tokens to the fee collector + start_prank(CheatTarget::One(debt_token_address), borrower_operations_address()); + IDebtTokenDispatcher { contract_address: debt_token_address } + .mint(fee_collector.contract_address, max_fee); + stop_prank(CheatTarget::One(debt_token_address)); + let mut spy = spy_events(SpyOn::One(fee_collector.contract_address)); + let expected_amount = max_fee - min_fee; + + fee_collector.increase_debt(borrower(), asset_address, max_fee); + + spy + .assert_emitted( + @array![ + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: borrower(), + asset: asset_address, + from: now + MIN_FEE_DURATION, + to: now + MIN_FEE_DURATION + FEE_EXPIRATION_SECONDS, + amount: expected_amount + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeCollected( + FeeCollector::FeeCollected { + borrower: borrower(), + asset: asset_address, + collector: treasury_address(), + amount: max_fee + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no events'); + record = IFeeCollectorDispatcher { contract_address: fee_collector.contract_address } + .get_fee_record(borrower(), asset_address); + + assert(record.amount == expected_amount, 'Wrong record amount'); + assert(record.from == now + MIN_FEE_DURATION, 'Wrong record from'); + assert(record.to == now + MIN_FEE_DURATION + FEE_EXPIRATION_SECONDS, 'Wrong record to'); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address) == expected_amount, + 'Wrong fee collector balance' + ); + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(treasury_address()) == min_fee + + max_fee, + 'Invalid treasury balance' + ); +} + +#[test] +fn when_caller_is_borrower_operations_and_not_the_first_increase_and_current_timestamp_lower_than_record_to_it_should_update_fee_record() { + let (fee_collector, debt_token_address, asset_address, min_fee, max_fee) = test_setup(); + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + fee_collector.increase_debt(borrower(), asset_address, max_fee); + let mut record = IFeeCollectorDispatcher { contract_address: fee_collector.contract_address } + .get_fee_record(borrower(), asset_address); + let to1 = record.to; + let from1 = record.from; + let amount1 = record.amount; + + // 50% of the time has passed + let now = ((record.to - get_block_timestamp()) / 2); + start_warp(CheatTarget::One(fee_collector.contract_address), now); + + // mint some debt tokens to the fee collector + start_prank(CheatTarget::One(debt_token_address), borrower_operations_address()); + IDebtTokenDispatcher { contract_address: debt_token_address } + .mint(fee_collector.contract_address, max_fee); + stop_prank(CheatTarget::One(debt_token_address)); + let mut spy = spy_events(SpyOn::One(fee_collector.contract_address)); + + fee_collector.increase_debt(borrower(), asset_address, max_fee); + + let expected_expired_amount = calc_expired_amount(now, from1, to1, amount1); + + let expected_remaining_amount = amount1 - expected_expired_amount; + let expected_treasury_balance = (max_fee / 2) + min_fee; + let expected_fee_collector_balance = expected_remaining_amount + (max_fee - min_fee); + + let expected_new_duration = calc_new_duration( + expected_remaining_amount, to1 - now, max_fee - min_fee + ); + + spy + .assert_emitted( + @array![ + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: borrower(), + asset: asset_address, + from: now, + to: now + expected_new_duration, + amount: expected_remaining_amount + (max_fee - min_fee) + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeCollected( + FeeCollector::FeeCollected { + borrower: borrower(), + asset: asset_address, + collector: treasury_address(), + amount: min_fee + expected_expired_amount + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no events'); + record = IFeeCollectorDispatcher { contract_address: fee_collector.contract_address } + .get_fee_record(borrower(), asset_address); + + assert_is_approximately_equal( + record.amount, expected_fee_collector_balance, ERROR_MARGIN, 'Invalid record amount' + ); + assert(record.from == now, 'Wrong record from'); + assert(record.to == now + expected_new_duration, 'Wrong record to'); + + assert_is_approximately_equal( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address), + expected_fee_collector_balance, + ERROR_MARGIN, + 'Wrong fee collector balance' + ); + + assert_is_approximately_equal( + IERC20Dispatcher { contract_address: debt_token_address }.balance_of(treasury_address()), + expected_treasury_balance, + ERROR_MARGIN, + 'Invalid treasury balance' + ); +} diff --git a/tests/integration/core/fee_collector/liquidate_debt/liquidate_debt.tree b/tests/integration/core/fee_collector/liquidate_debt/liquidate_debt.tree index c8411cc..a5b5e74 100644 --- a/tests/integration/core/fee_collector/liquidate_debt/liquidate_debt.tree +++ b/tests/integration/core/fee_collector/liquidate_debt/liquidate_debt.tree @@ -6,4 +6,4 @@ test_liquidate_debt.cairo │ └── it should do nothing └── when borrower record amount is not 0 ├── it should correctly liquidate debt and collect fees - └── it should emit {FeeRecordUpdated} event \ No newline at end of file + └── it should emit {FeeRecordUpdated} and {FeeCollector} event \ No newline at end of file diff --git a/tests/integration/core/fee_collector/liquidate_debt/test_liquidate_debt.cairo b/tests/integration/core/fee_collector/liquidate_debt/test_liquidate_debt.cairo index 8b13789..3ee1459 100644 --- a/tests/integration/core/fee_collector/liquidate_debt/test_liquidate_debt.cairo +++ b/tests/integration/core/fee_collector/liquidate_debt/test_liquidate_debt.cairo @@ -1 +1,108 @@ +use starknet::{ContractAddress, contract_address_const}; +use snforge_std::{ + start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions, PrintTrait +}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use shisui::core::{ + debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait}, + fee_collector::{IFeeCollectorDispatcher, IFeeCollectorDispatcherTrait, FeeCollector} +}; +use tests::utils::callers::{ + vessel_manager_address, borrower_operations_address, borrower, treasury_address +}; + + +use super::super::setup::{setup, calc_fees}; + +const DEBT_AMOUNT: u256 = 100_000000000000000000; // 10e18 + +fn test_setup() -> (IFeeCollectorDispatcher, ContractAddress, ContractAddress, u256, u256) { + let (_, fee_collector, debt_token_address, asset_address) = setup(); + let (min_fee, max_fee) = calc_fees(DEBT_AMOUNT); + // Mint some debt tokens to the fee collector + start_prank(CheatTarget::One(debt_token_address), borrower_operations_address()); + + IDebtTokenDispatcher { contract_address: debt_token_address } + .mint(fee_collector.contract_address, max_fee); + stop_prank(CheatTarget::One(debt_token_address)); + + start_prank(CheatTarget::One(fee_collector.contract_address), borrower_operations_address()); + fee_collector.increase_debt(borrower(), asset_address, max_fee); + stop_prank(CheatTarget::One(fee_collector.contract_address)); + + (fee_collector, debt_token_address, asset_address, min_fee, max_fee) +} + +#[test] +#[should_panic(expected: ('Caller not authorized',))] +fn when_caller_is_not_vessel_manager_it_should_revert() { + let (fee_collector, _, asset_address, _, _) = test_setup(); + fee_collector.liquidate_debt(borrower(), asset_address); +} + +#[test] +fn when_caller_is_valid_and_amount_is_zero_it_should_do_nothing() { + let (fee_collector, debt_token_address, _, _, _) = test_setup(); + let no_amount_asset = contract_address_const::<'asset_2'>(); + let fee_collector_balance = IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address); + + start_prank(CheatTarget::One(fee_collector.contract_address), vessel_manager_address()); + fee_collector.liquidate_debt(borrower(), no_amount_asset); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address) == fee_collector_balance, + 'Wrong fee collector balance' + ); +} + + +#[test] +fn when_caller_is_valid_and_amount_is_not_zero_it_should_correctly_collect_fee() { + let (fee_collector, debt_token_address, asset_address, min_fee, max_fee) = test_setup(); + let mut spy = spy_events(SpyOn::One(fee_collector.contract_address)); + + start_prank(CheatTarget::One(fee_collector.contract_address), vessel_manager_address()); + fee_collector.liquidate_debt(borrower(), asset_address); + spy + .assert_emitted( + @array![ + ( + fee_collector.contract_address, + FeeCollector::Event::FeeRecordUpdated( + FeeCollector::FeeRecordUpdated { + borrower: borrower(), asset: asset_address, from: 0, to: 0, amount: 0 + } + ) + ), + ( + fee_collector.contract_address, + FeeCollector::Event::FeeCollected( + FeeCollector::FeeCollected { + borrower: borrower(), + asset: asset_address, + collector: treasury_address(), + amount: max_fee - min_fee + } + ) + ) + ] + ); + assert(spy.events.is_empty(), 'There should be no events'); + let record = fee_collector.get_fee_record(borrower(), asset_address); + assert(record.amount.is_zero(), 'Invalid record amount'); + + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(treasury_address()) == max_fee, + 'Invalid treasury balance' + ); + assert( + IERC20Dispatcher { contract_address: debt_token_address } + .balance_of(fee_collector.contract_address) + .is_zero(), + 'Invalid fee collector balance' + ); +} diff --git a/tests/integration/core/fee_collector/set_route_to_SHVT_staking/set_route_to_SHVT_staking.tree b/tests/integration/core/fee_collector/set_is_route_to_SHVT_staking/set_is_route_to_SHVT_staking.tree similarity index 84% rename from tests/integration/core/fee_collector/set_route_to_SHVT_staking/set_route_to_SHVT_staking.tree rename to tests/integration/core/fee_collector/set_is_route_to_SHVT_staking/set_is_route_to_SHVT_staking.tree index 380fce7..7bc3080 100644 --- a/tests/integration/core/fee_collector/set_route_to_SHVT_staking/set_route_to_SHVT_staking.tree +++ b/tests/integration/core/fee_collector/set_is_route_to_SHVT_staking/set_is_route_to_SHVT_staking.tree @@ -1,4 +1,4 @@ -test_set_route_to_SHVT_staking.cairo +test_is_set_route_to_SHVT_staking.cairo ├── when caller is not the Timelock contract │ └── it should revert with CallerNotAuthorized └── when caller is the Timelock contract diff --git a/tests/integration/core/fee_collector/set_is_route_to_SHVT_staking/test_set_is_route_to_SHVT_staking.cairo b/tests/integration/core/fee_collector/set_is_route_to_SHVT_staking/test_set_is_route_to_SHVT_staking.cairo new file mode 100644 index 0000000..1ee7b4e --- /dev/null +++ b/tests/integration/core/fee_collector/set_is_route_to_SHVT_staking/test_set_is_route_to_SHVT_staking.cairo @@ -0,0 +1,25 @@ +use starknet::ContractAddress; +use snforge_std::{start_prank, CheatTarget}; + +use shisui::core::fee_collector::{ + IFeeCollectorDispatcher, IFeeCollectorDispatcherTrait, FeeCollector +}; +use tests::utils::callers::timelock_address; + + +use super::super::setup::setup; + +#[test] +#[should_panic(expected: ('Caller not authorized',))] +fn when_caller_is_not_timelock_it_should_revert() { + let (_, fee_collector, _, _) = setup(); + fee_collector.set_is_route_to_SHVT_staking(true); +} + +#[test] +fn when_caller_is_timelock_it_should_update_shvt_staking_value() { + let (_, fee_collector, _, _) = setup(); + start_prank(CheatTarget::One(fee_collector.contract_address), timelock_address()); + fee_collector.set_is_route_to_SHVT_staking(true); + assert(fee_collector.get_is_route_to_SHVT_staking(), 'is route to SHVT not updated'); +} diff --git a/tests/integration/core/fee_collector/set_route_to_SHVT_staking/test_set_route_to_SHVT_staking.cairo b/tests/integration/core/fee_collector/set_route_to_SHVT_staking/test_set_route_to_SHVT_staking.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/core/fee_collector/set_route_to_SHVT_staking/test_set_route_to_SHVT_staking.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration/core/fee_collector/setup.cairo b/tests/integration/core/fee_collector/setup.cairo new file mode 100644 index 0000000..4c98161 --- /dev/null +++ b/tests/integration/core/fee_collector/setup.cairo @@ -0,0 +1,67 @@ +use starknet::{ContractAddress, contract_address_const}; +use traits::{Into, TryInto}; +use shisui::core::{ + address_provider::{IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey}, + debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait}, + fee_collector::{IFeeCollectorDispatcher, IFeeCollectorDispatcherTrait} +}; +use shisui::utils::constants::ONE; +use tests::tests_lib::{deploy_address_provider, deploy_debt_token, deploy_fee_collector}; +use tests::utils::{ + callers::{ + vessel_manager_address, timelock_address, treasury_address, borrower_operations_address + }, + constant::{MAX_FEE_FRACTION, MIN_FEE_FRACTION, FEE_EXPIRATION_SECONDS} +}; + +fn setup() -> ( + IAddressProviderDispatcher, IFeeCollectorDispatcher, ContractAddress, ContractAddress +) { + let address_provider_address: ContractAddress = deploy_address_provider(); + let address_provider: IAddressProviderDispatcher = IAddressProviderDispatcher { + contract_address: address_provider_address + }; + + let debt_token_address: ContractAddress = deploy_debt_token(address_provider_address); + let debt_token: IDebtTokenDispatcher = IDebtTokenDispatcher { + contract_address: debt_token_address + }; + let fee_collector_address: ContractAddress = deploy_fee_collector(address_provider_address); + let fee_collector: IFeeCollectorDispatcher = IFeeCollectorDispatcher { + contract_address: fee_collector_address + }; + debt_token.add_whitelist(fee_collector_address); + + address_provider.set_address(AddressesKey::debt_token, debt_token_address); + address_provider.set_address(AddressesKey::timelock, timelock_address()); + address_provider.set_address(AddressesKey::vessel_manager, vessel_manager_address()); + address_provider.set_address(AddressesKey::borrower_operations, borrower_operations_address()); + address_provider.set_address(AddressesKey::treasury, treasury_address()); + let asset_address = contract_address_const::<'asset'>(); + return (address_provider, fee_collector, debt_token_address, asset_address); +} + +fn calc_fees(debt_amount: u256) -> (u256, u256) { + let max_fee: u256 = (MAX_FEE_FRACTION * debt_amount) / ONE; + let min_fee: u256 = (MIN_FEE_FRACTION * max_fee) / ONE; + return (min_fee, max_fee); +} + +fn calc_new_duration( + remaining_amount: u256, remaining_time_to_live: u64, added_amount: u256 +) -> u64 { + let prev_weight = remaining_amount * remaining_time_to_live.into(); + let next_weight = added_amount * FEE_EXPIRATION_SECONDS.into(); + return ((prev_weight + next_weight) / (remaining_amount + added_amount)).try_into().unwrap(); +} + +fn calc_expired_amount(now: u64, from: u64, to: u64, amount: u256) -> u256 { + if (from > now) { + return 0; + } + if (now >= to) { + return amount; + } + let decay_rate = (amount * 1_000000000) / (to - from).into(); + return ((now - from).into() * decay_rate) / 1_000000000; +} diff --git a/tests/lib.cairo b/tests/lib.cairo index 59eae20..2033b16 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -151,6 +151,7 @@ mod integration { } } mod fee_collector { + mod setup; mod increase_debt { mod test_increase_debt; } @@ -169,8 +170,8 @@ mod integration { mod handle_redemption_fee { mod test_handle_redemption_fee; } - mod set_route_to_SHVT_staking { - mod test_set_route_to_SHVT_staking; + mod set_is_route_to_SHVT_staking { + mod test_set_is_route_to_SHVT_staking; } } } @@ -242,6 +243,7 @@ mod helpers { mod tests_lib; mod utils { + mod asserts; mod constant; mod aggregator; mod callers; diff --git a/tests/tests_lib.cairo b/tests/tests_lib.cairo index 675f57f..0a6584e 100644 --- a/tests/tests_lib.cairo +++ b/tests/tests_lib.cairo @@ -95,6 +95,16 @@ fn deploy_active_pool(address_provider: ContractAddress) -> ContractAddress { deploy_mock_contract(contract, @array![address_provider.into()]) } +/// Utility function to deploy a FeeCollector contract and return its address. +/// +/// # Returns +/// +/// * `ContractAddress` - The address of the deployed data store contract. +fn deploy_fee_collector(address_provider: ContractAddress) -> ContractAddress { + let contract = declare('FeeCollector'); + deploy_mock_contract(contract, @array![address_provider.into()]) +} + fn deploy_receive_erc20_mock() -> ContractAddress { let contract = declare('ReceiveERC20Mock'); diff --git a/tests/utils/asserts.cairo b/tests/utils/asserts.cairo new file mode 100644 index 0000000..277a808 --- /dev/null +++ b/tests/utils/asserts.cairo @@ -0,0 +1,6 @@ +use shisui::utils::shisui_math::get_absolute_difference; +use snforge_std::PrintTrait; +fn assert_is_approximately_equal(a: u256, b: u256, margin_error: u256, msg: felt252) { + let abs_diff = get_absolute_difference(a, b); + assert(abs_diff <= margin_error, msg); +} diff --git a/tests/utils/callers.cairo b/tests/utils/callers.cairo index b16ea80..caeb99d 100644 --- a/tests/utils/callers.cairo +++ b/tests/utils/callers.cairo @@ -28,6 +28,10 @@ fn active_pool_address() -> ContractAddress { return contract_address_const::<'active_pool'>(); } +fn treasury_address() -> ContractAddress { + return contract_address_const::<'treasury'>(); +} + fn default_pool_address() -> ContractAddress { return contract_address_const::<'default_pool'>(); } @@ -36,10 +40,15 @@ fn stability_pool_address() -> ContractAddress { return contract_address_const::<'stability_pool'>(); } +fn borrower() -> ContractAddress { + return contract_address_const::<'borrower'>(); +} + fn alice() -> ContractAddress { return contract_address_const::<'alice'>(); } + fn bob() -> ContractAddress { return contract_address_const::<'bob'>(); } diff --git a/tests/utils/constant.cairo b/tests/utils/constant.cairo index 65923cb..c963f6e 100644 --- a/tests/utils/constant.cairo +++ b/tests/utils/constant.cairo @@ -1 +1,7 @@ const DEFAULT_TIMEOUT: u64 = consteval_int!(30 * 60); // 30 minutes +const MAX_FEE_FRACTION: u256 = 5000000000000000; // 0.5% fee +const MIN_FEE_FRACTION: u256 = 38461538000000000; // (1/26)e18 fee divided by 26 weeks +const MIN_FEE_DURATION: u64 = consteval_int!(7 * 24 * 60 * 60); // 7 days +const FEE_EXPIRATION_SECONDS: u64 = + consteval_int!(175 * 24 * 60 * 60); // ~ 6 months, minus one week (MIN_FEE_DURATION) +