diff --git a/Scarb.lock b/Scarb.lock index ac53f31..f408a7d 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -1,6 +1,11 @@ # Code generated by scarb DO NOT EDIT. version = 1 +[[package]] +name = "alexandria_storage" +version = "0.2.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?tag=cairo-v2.3.0-rc0#ae1d5149ff601a7ac5b39edc867d33ebd83d7f4f" + [[package]] name = "openzeppelin" version = "0.8.0" @@ -15,6 +20,7 @@ source = "git+https://github.com/astraly-labs/pragma-lib#24bb4da111ae7eb00e7cf40 name = "shisui" version = "0.1.0" dependencies = [ + "alexandria_storage", "openzeppelin", "pragma_lib", "snforge_std", diff --git a/Scarb.toml b/Scarb.toml index 61dd76a..6fa174d 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -12,6 +12,7 @@ starknet = "2.4.0" snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.13.0" } openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.8.0" } pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib"} +alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git", tag="cairo-v2.3.0-rc0"} [[target.starknet-contract]] sierra = true diff --git a/src/components/shisui_base.cairo b/src/components/shisui_base.cairo deleted file mode 100644 index 782d2f0..0000000 --- a/src/components/shisui_base.cairo +++ /dev/null @@ -1,54 +0,0 @@ -use starknet::ContractAddress; -use shisui::utils::array::{StoreContractAddressArray, StoreU256Array}; - -#[derive(Drop, Clone, starknet::Store, Serde)] -struct Colls { - tokens: Array, - amounts: Array, -} - -#[starknet::component] -mod ShisuiBaseComponent { - use starknet::ContractAddress; - - #[storage] - struct Storage { - address_provider: ContractAddress, - } - - #[generate_trait] - impl InternalImpl< - TContractState, +HasComponent - > of InternalTrait { - fn get_composite_debt(_asset: ContractAddress, _debt: u256) -> u256 { - return 0; - } - - fn get_net_debt(_asset: ContractAddress, _debt: u256) -> u256 { - return 0; - } - - // Return the amount of ETH to be drawn from a vessel's collateral and sent as gas compensation. - fn get_coll_gas_compensation(_asset: ContractAddress, _entire_coll: u256,) -> u256 { - return 0; - } - - fn get_entire_system_coll(_asset: ContractAddress) -> u256 { - return 0; - } - - fn get_entire_system_debt(_asset: ContractAddress) -> u256 { - return 0; - } - - fn get_TCR(_asset: ContractAddress, _price: u256) -> u256 { - return 0; - } - - fn check_recovery_mode(_asset: ContractAddress, _price: u256) -> bool { - return true; - } - - fn require_user_accepts_fee(_fee: u256, _amount: u256, _max_fee_percentage: u256) {} - } -} 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/core/vessel_manager.cairo b/src/core/vessel_manager.cairo new file mode 100644 index 0000000..608f769 --- /dev/null +++ b/src/core/vessel_manager.cairo @@ -0,0 +1,1220 @@ +use starknet::ContractAddress; + + +// ************************************************************************* +// ENUM +// ************************************************************************* +#[derive(Copy, Drop, Serde, PartialEq, starknet::Store)] +enum Status { + NonExistent, + Active, + ClosedByOwner, + ClosedByLiquidation, + ClosedByRedemption +} + +#[derive(Copy, Drop, Serde, PartialEq, starknet::Store)] +enum VesselManagerOperation { + ApplyPendingRewards, + LiquidateInNormalMode, + LiquidateInRecoveryMode, + RedeemCollateral +} + +// ************************************************************************* +// STRUCT +// ************************************************************************* +#[derive(Copy, Drop, Serde, starknet::Store)] +struct Vessel { + debt: u256, + coll: u256, + stake: u256, + status: Status, + array_index: u32 +} + +// Object containing the asset and debt token snapshots for a given active vessel +#[derive(Copy, Drop, Serde, Default, starknet::Store)] +struct RewardSnapshot { + asset: u256, + debt: u256 +} + + +#[starknet::interface] +trait IVesselManager { + fn get_address_provider(self: @TContractState) -> ContractAddress; + // Return the nominal collateral ratio (ICR) of a given Vessel, without the price. Takes a vessel's pending coll and debt rewards from redistributions into account. + fn get_nominal_icr( + self: @TContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256; + // Return the current collateral ratio (ICR) of a given Vessel. Takes a vessel's pending coll and debt rewards from redistributions into account. + fn get_current_icr( + self: @TContractState, asset: ContractAddress, borrower: ContractAddress, price: u256 + ) -> u256; + // Get the borrower's pending accumulated asset reward, earned by their stake + fn get_pending_asset_reward( + self: @TContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256; + // Get the borrower's pending accumulated debt token reward, earned by their stake + fn get_pending_debt_token_reward( + self: @TContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256; + fn has_pending_rewards( + self: @TContractState, asset: ContractAddress, borrower: ContractAddress + ) -> bool; + //return debt,coll,pending_debt_reward, pending_coll_reward + fn get_entire_debt_and_coll( + self: @TContractState, asset: ContractAddress, borrower: ContractAddress + ) -> (u256, u256, u256, u256); + + fn is_vessel_active( + self: @TContractState, asset: ContractAddress, borrower: ContractAddress + ) -> bool; + + fn get_tcr(self: @TContractState, asset: ContractAddress, price: u256) -> u256; + fn check_recovery_mode(self: @TContractState, asset: ContractAddress, price: u256) -> bool; + fn get_borrowing_rate(self: @TContractState, asset: ContractAddress) -> u256; + fn get_borrowing_fee(self: @TContractState, asset: ContractAddress, debt: u256) -> u256; + fn get_redemption_fee(self: @TContractState, asset: ContractAddress, asset_draw: u256) -> u256; + fn get_redemption_fee_with_decay( + self: @TContractState, asset: ContractAddress, asset_draw: u256 + ) -> u256; + fn get_redemption_rate(self: @TContractState, asset: ContractAddress) -> u256; + fn get_redemption_rate_with_decay(self: @TContractState, asset: ContractAddress) -> u256; + fn add_vessel_owner_to_array( + ref self: TContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256; + fn execute_full_redemption( + ref self: TContractState, asset: ContractAddress, borrower: ContractAddress, new_coll: u256 + ); + fn execute_partial_redemption( + ref self: TContractState, + asset: ContractAddress, + borrower: ContractAddress, + new_debt: u256, + new_coll: u256, + new_nicr: u256, + upper_partial_redemption_hint: ContractAddress, + low_partal_redemption_hint: ContractAddress + ); + fn finalize_redemption( + ref self: TContractState, + asset: ContractAddress, + receiver: ContractAddress, + debt_token_to_redeem: u256, + asset_fee_amount: u256, + asset_redeemed_amount: u256 + ); + fn update_base_rate_from_redemption( + ref self: TContractState, + asset: ContractAddress, + asset_drawn: u256, + price: u256, + total_debt_token_supply: u256 + ) -> u256; + fn apply_pending_rewards( + ref self: TContractState, asset: ContractAddress, borrower: ContractAddress + ); + // Move a Vessel's pending debt and collateral rewards from distributions, from the Default Pool to the Active Pool + fn move_pending_vessel_rewards_to_active_pool( + ref self: TContractState, asset: ContractAddress, debt: u256, asset_amount: u256 + ); + // Update borrower's snapshots of L_Colls and L_Debts to reflect the current values + fn update_vessel_reward_snapshots( + ref self: TContractState, asset: ContractAddress, borrower: ContractAddress + ); + fn update_stake_and_total_stakes( + ref self: TContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256; + + fn remove_stake(ref self: TContractState, asset: ContractAddress, borrower: ContractAddress); + fn redistribute_debt_and_coll( + ref self: TContractState, + asset: ContractAddress, + debt: u256, + coll: u256, + debt_to_offset: u256, + coll_to_sent_to_stability_pool: u256 + ); + fn update_system_snapshots_exclude_coll_remainder( + ref self: TContractState, asset: ContractAddress, coll_remainder: u256 + ); + fn close_vessel( + ref self: TContractState, + asset: ContractAddress, + borrower: ContractAddress, + closed_status: Status + ); + fn close_vessel_liquidation( + ref self: TContractState, asset: ContractAddress, borrower: ContractAddress + ); + fn send_gas_compensation( + ref self: TContractState, + asset: ContractAddress, + liquidator: ContractAddress, + debt_token_amount: u256, + asset_amount: u256 + ); + fn get_vessel_status( + self: @TContractState, asset: ContractAddress, borrower: ContractAddress + ) -> Status; + fn get_vessel_stake( + self: @TContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256; + fn get_vessel_debt( + self: @TContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256; + fn get_vessel_coll( + self: @TContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256; + fn get_vessel_owners_count(self: @TContractState, asset: ContractAddress) -> u32; + fn get_vessel_from_vessel_owners_array( + self: @TContractState, asset: ContractAddress, index: u32 + ) -> Option; + fn set_vessel_status( + ref self: TContractState, asset: ContractAddress, borrower: ContractAddress, status: Status + ); + fn increase_vessel_coll( + ref self: TContractState, + asset: ContractAddress, + borrower: ContractAddress, + coll_increase: u256 + ) -> u256; + fn decrease_vessel_coll( + ref self: TContractState, + asset: ContractAddress, + borrower: ContractAddress, + coll_decrease: u256 + ) -> u256; + fn increase_vessel_debt( + ref self: TContractState, + asset: ContractAddress, + borrower: ContractAddress, + debt_increase: u256 + ) -> u256; + fn decrease_vessel_debt( + ref self: TContractState, + asset: ContractAddress, + borrower: ContractAddress, + debt_decrease: u256 + ) -> u256; +} + +#[starknet::contract] +mod VesselManager { + use shisui::core::vessel_manager::IVesselManager; + use core::traits::Into; + use openzeppelin::security::reentrancyguard::ReentrancyGuardComponent::InternalTrait; + use starknet::{ + ContractAddress, contract_address_const, get_caller_address, get_contract_address, + get_block_timestamp, call_contract_syscall + }; + use shisui::core::address_provider::{ + IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey + }; + use shisui::core::admin_contract::{IAdminContractDispatcher, IAdminContractDispatcherTrait,}; + use shisui::core::debt_token::{IDebtTokenDispatcher, IDebtTokenDispatcherTrait,}; + use shisui::core::fee_collector::{IFeeCollectorDispatcher, IFeeCollectorDispatcherTrait,}; + use shisui::utils::shisui_math; + use shisui::components::shisui_base::ShisuiBaseComponent; + use shisui::utils::constants::DECIMAL_PRECISION; + use super::{RewardSnapshot, Vessel, Status, VesselManagerOperation}; + use openzeppelin::security::reentrancyguard::ReentrancyGuardComponent; + use alexandria_storage::list::{List, ListTrait}; + use core::cmp::{min, max}; + + + // ************************************************************************* + // COMPONENTS + // ************************************************************************* + component!( + path: ReentrancyGuardComponent, storage: reentrancy_guard, event: ReentrancyGuardEvent + ); + impl ReentrancyGuardInternalImpl = ReentrancyGuardComponent::InternalImpl; + + component!(path: ShisuiBaseComponent, storage: shisui_base, event: ShisuiBaseEvent); + impl ShisuiBaseInternalImpl = ShisuiBaseComponent::InternalImpl; + + // ************************************************************************* + // CONSTANTS + // ************************************************************************* + const SECONDS_IN_ONE_MINUTE: u8 = 60; + + // Half-life of 12h. 12h = 720 min + // (1/2) = d^720 => d = (1/2)^(1/720) + const MINUTE_DECAY_FACTOR: u256 = 999037758833783000; + + // BETA: 18 digit decimal. Parameter by which to divide the redeemed fraction, in order to calc the new base rate from a redemption. + // Corresponds to (1 / ALPHA) in the white paper. + const BETA: u8 = 2; + + // ************************************************************************* + // ERRORS + // ************************************************************************* + mod Errors { + const VesselManager__FeeBiggerThanAssetDraw: felt252 = 'Fee bigger than assert draw'; + const VesselManager__OnlyOneVessel: felt252 = 'Only one vessel'; + const VesselManager__OnlyVesselManagerOperations: felt252 = + 'Only vessel manager operations'; + const VesselManager__OnlyBorrowerOperations: felt252 = 'Only borrower operations'; + const VesselManager__OnlyVesselManagerOperationsOrBorrowerOperations: felt252 = + 'Only vessel mngr op or borrower'; + const VesselManager__WrongVesselStatusWhenClosing: felt252 = + 'Wrong vessel status.Cant close'; + const VesselManager__WrongVesselStatusWhenRemoving: felt252 = 'Wrong vessel status.Cant rm'; + const VesselManager__VesselNotFound: felt252 = 'Vessel not found'; + const VesselManager__BaseRateBelowZero: felt252 = 'Base rate below zero'; + const VesselManager__AssetStakesIsZero: felt252 = 'Asset stake is zero'; + } + + // ************************************************************************* + // STORAGE + // ************************************************************************* + + #[storage] + struct Storage { + #[substorage(v0)] + reentrancy_guard: ReentrancyGuardComponent::Storage, + #[substorage(v0)] + shisui_base: ShisuiBaseComponent::Storage, + base_rate: LegacyMap::, + // The timestamp of the latest fee operation (redemption or new debt token issuance) + last_fee_operation_time: LegacyMap::, + // Vessels[borrower address][Collateral address] + vessels: LegacyMap::<(ContractAddress, ContractAddress), super::Vessel>, + total_stakes: LegacyMap::, + // Snapshot of the value of totalStakes, taken immediately after the latest liquidation + total_stakes_snapshot: LegacyMap::, + // Snapshot of the total collateral across the ActivePool and DefaultPool, immediately after the latest liquidation. + total_collateral_snapshot: LegacyMap::, + // L_Colls and L_Debts track the sums of accumulated liquidation rewards per unit staked. During its lifetime, each stake earns: + // + // An asset gain of ( stake * [L_Colls - L_Colls(0)] ) + // A debt increase of ( stake * [L_Debts - L_Debts(0)] ) + // + // Where L_Colls(0) and L_Debts(0) are snapshots of L_Colls and L_Debts for the active Vessel taken at the instant the stake was made + l_colls: LegacyMap::, + l_debts: LegacyMap::, + // Map addresses with active vessels to their RewardSnapshot + reward_snapshots: LegacyMap::<(ContractAddress, ContractAddress), RewardSnapshot>, + // Array of all active vessel addresses - used to to compute an approximate hint off-chain, for the sorted list insertion + vessel_owners: LegacyMap::>, + // Error trackers for the vessel redistribution calculation + last_coll_error_redistribution: LegacyMap::, + last_debt_error_redistribution: LegacyMap::, + is_setup_initialized: bool, + address_provider: IAddressProviderDispatcher, + admin_contract: IAdminContractDispatcher, + debt_token: IDebtTokenDispatcher, + fee_collector: IFeeCollectorDispatcher + } + + // ************************************************************************* + // EVENT + // ************************************************************************* + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ReentrancyGuardEvent: ReentrancyGuardComponent::Event, + #[flat] + ShisuiBaseEvent: ShisuiBaseComponent::Event, + VesselIndexUpdated: VesselIndexUpdated, + VesselUpdated: VesselUpdated, + BaseRateUpdated: BaseRateUpdated, + LastFeeOpTimeUpdated: LastFeeOpTimeUpdated, + VesselSnapshotsUpdated: VesselSnapshotsUpdated, + TotalStakesUpdated: TotalStakesUpdated, + SystemSnapshotsUpdated: SystemSnapshotsUpdated, + LTermsUpdated: LTermsUpdated + } + + #[derive(Drop, starknet::Event)] + struct VesselIndexUpdated { + #[key] + asset: ContractAddress, + borrower: ContractAddress, + new_index: u32 + } + + #[derive(Drop, starknet::Event)] + struct VesselUpdated { + #[key] + asset: ContractAddress, + #[key] + borrower: ContractAddress, + debt: u256, + coll: u256, + stake: u256, + operation: VesselManagerOperation, + } + + #[derive(Drop, starknet::Event)] + struct BaseRateUpdated { + #[key] + asset: ContractAddress, + base_rate: u256 + } + + #[derive(Drop, starknet::Event)] + struct LastFeeOpTimeUpdated { + #[key] + asset: ContractAddress, + last_fee_op_time: u64 + } + + #[derive(Drop, starknet::Event)] + struct VesselSnapshotsUpdated { + #[key] + asset: ContractAddress, + borrower: ContractAddress, + l_coll: u256, + l_debt: u256 + } + + #[derive(Drop, starknet::Event)] + struct TotalStakesUpdated { + #[key] + asset: ContractAddress, + new_total_stakes: u256 + } + + #[derive(Drop, starknet::Event)] + struct SystemSnapshotsUpdated { + #[key] + asset: ContractAddress, + total_stakes_snapshot: u256, + total_collateral_snapshot: u256 + } + + #[derive(Drop, starknet::Event)] + struct LTermsUpdated { + #[key] + asset: ContractAddress, + l_coll: u256, + l_debt: u256 + } + + + // ************************************************************************* + // CONSTRUCTOR + // ************************************************************************* + #[constructor] + fn constructor( + ref self: ContractState, + address_provider: ContractAddress, + admin_contract: ContractAddress, + debt_token: ContractAddress, + fee_collector: ContractAddress + ) { + self + .address_provider + .write(IAddressProviderDispatcher { contract_address: address_provider }); + + self.admin_contract.write(IAdminContractDispatcher { contract_address: admin_contract }); + self.debt_token.write(IDebtTokenDispatcher { contract_address: debt_token }); + self.fee_collector.write(IFeeCollectorDispatcher { contract_address: fee_collector }); + } + + // ************************************************************************* + // EXTERNAL FUNCTIONS + // ************************************************************************* + #[external(v0)] + impl VesselManagerImpl of super::IVesselManager { + fn get_address_provider(self: @ContractState) -> ContractAddress { + return self.address_provider.read().contract_address; + } + + // Return the nominal collateral ratio (ICR) of a given Vessel, without the price. Takes a vessel's pending coll and debt rewards from redistributions into account. + fn get_nominal_icr( + self: @ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256 { + let (current_coll, current_debt) = self.get_current_vessel_amounts(asset, borrower); + shisui_math::compute_nominal_cr(current_coll, current_debt) + } + + // Return the current collateral ratio (ICR) of a given Vessel. Takes a vessel's pending coll and debt rewards from redistributions into account. + fn get_current_icr( + self: @ContractState, asset: ContractAddress, borrower: ContractAddress, price: u256 + ) -> u256 { + let (current_coll, current_debt) = self.get_current_vessel_amounts(asset, borrower); + let icr: u256 = shisui_math::compute_cr(current_coll, current_debt, price); + icr + } + + // Get the borrower's pending accumulated asset reward, earned by their stake + fn get_pending_asset_reward( + self: @ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256 { + let snapshot_asset = self.reward_snapshots.read((borrower, asset)).asset; + let reward_per_unit_staked = self.l_colls.read(asset) - snapshot_asset; + if reward_per_unit_staked == 0 || !self.is_vessel_active(asset, borrower) { + return 0; + } + let stake = self.vessels.read((borrower, asset)).stake; + (stake * reward_per_unit_staked) / DECIMAL_PRECISION + } + + // Get the borrower's pending accumulated debt token reward, earned by their stake + fn get_pending_debt_token_reward( + self: @ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256 { + let snapshot_debt = self.reward_snapshots.read((borrower, asset)).debt; + let reward_per_unit_staked = self.l_debts.read(asset) - snapshot_debt; + if reward_per_unit_staked == 0 || !self.is_vessel_active(asset, borrower) { + return 0; + } + let stake = self.vessels.read((borrower, asset)).stake; + (stake * reward_per_unit_staked) / DECIMAL_PRECISION + } + + fn has_pending_rewards( + self: @ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> bool { + if self.is_vessel_active(asset, borrower) { + return false; + } + + self.reward_snapshots.read((borrower, asset)).asset < self.l_colls.read(asset) + } + //return debt,coll,pending_debt_reward, pending_coll_reward + fn get_entire_debt_and_coll( + self: @ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> (u256, u256, u256, u256) { + let pending_debt_reward = self.get_pending_debt_token_reward(asset, borrower); + let pending_coll_reward = self.get_pending_asset_reward(asset, borrower); + let vessel = self.vessels.read((borrower, asset)); + let debt = vessel.debt + pending_debt_reward; + let coll = vessel.coll + pending_coll_reward; + + (debt, coll, pending_debt_reward, pending_coll_reward) + } + + fn is_vessel_active( + self: @ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> bool { + let status = self.get_vessel_status(asset, borrower); + status == Status::Active + } + + fn get_tcr(self: @ContractState, asset: ContractAddress, price: u256) -> u256 { + self.shisui_base.get_TCR(asset, price) + } + + fn check_recovery_mode(self: @ContractState, asset: ContractAddress, price: u256) -> bool { + self.shisui_base.check_recovery_mode(asset, price) + } + + fn get_borrowing_rate(self: @ContractState, asset: ContractAddress) -> u256 { + self.admin_contract.read().get_borrowing_fee(asset) + } + + fn get_borrowing_fee(self: @ContractState, asset: ContractAddress, debt: u256) -> u256 { + (self.admin_contract.read().get_borrowing_fee(asset) * debt) / DECIMAL_PRECISION + } + + fn get_redemption_fee( + self: @ContractState, asset: ContractAddress, asset_draw: u256 + ) -> u256 { + self.calc_redemption_fee(self.get_redemption_rate(asset), asset_draw) + } + + fn get_redemption_fee_with_decay( + self: @ContractState, asset: ContractAddress, asset_draw: u256 + ) -> u256 { + self.calc_redemption_fee(self.get_redemption_rate_with_decay(asset), asset_draw) + } + + fn get_redemption_rate(self: @ContractState, asset: ContractAddress) -> u256 { + self.calc_redemption_rate(asset, self.base_rate.read(asset)) + } + + fn get_redemption_rate_with_decay(self: @ContractState, asset: ContractAddress) -> u256 { + self.calc_redemption_rate(asset, self.calc_decayed_base_rate(asset)) + } + + fn add_vessel_owner_to_array( + ref self: ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256 { + self.only_vessel_manager_operations(); + let mut asset_owners = self.vessel_owners.read(asset); + asset_owners.append(borrower); + let index = asset_owners.len() - 1; + let mut vessel = self.vessels.read((borrower, asset)); + vessel.array_index = index.into(); + self.vessels.write((borrower, asset), vessel); + index.into() + } + + fn execute_full_redemption( + ref self: ContractState, + asset: ContractAddress, + borrower: ContractAddress, + new_coll: u256 + ) { + self.remove_stake_internal(asset, borrower); + self.close_vessel_internal(asset, borrower, Status::ClosedByRedemption); + self + .redeem_close_vessel( + asset, + borrower, + self.admin_contract.read().get_debt_token_gas_compensation(asset), + new_coll + ); + self.fee_collector.read().close_debt(borrower, asset); + self + .emit( + VesselUpdated { + asset, + borrower, + debt: 0, + coll: 0, + stake: 0, + operation: VesselManagerOperation::RedeemCollateral + } + ); + } + + //TODO call ISortedVessels(sortedVessels).reInsert() + fn execute_partial_redemption( + ref self: ContractState, + asset: ContractAddress, + borrower: ContractAddress, + new_debt: u256, + new_coll: u256, + new_nicr: u256, + upper_partial_redemption_hint: ContractAddress, + low_partal_redemption_hint: ContractAddress + ) { + //call ISortedVessels(sortedVessels).reInsert() + + let mut vessel = self.vessels.read((borrower, asset)); + let payback_fraction = ((vessel.debt - new_debt) * shisui_math::dec_pow(10, 18)) + / vessel.debt; + if payback_fraction != 0 { + self.fee_collector.read().decrease_debt(borrower, asset, payback_fraction); + } + // update vessel + vessel.debt = new_debt; + vessel.coll = new_coll; + self.vessels.write((borrower, asset), vessel); + + self.update_stake_and_total_stakes_internal(asset, borrower); + self + .emit( + VesselUpdated { + asset, + borrower, + debt: new_debt, + coll: new_coll, + stake: vessel.stake, + operation: VesselManagerOperation::RedeemCollateral + } + ); + } + + fn finalize_redemption( + ref self: ContractState, + asset: ContractAddress, + receiver: ContractAddress, + debt_token_to_redeem: u256, + asset_fee_amount: u256, + asset_redeemed_amount: u256 + ) { + self.only_vessel_manager_operations(); + // Send the asset fee + if asset_fee_amount != 0 { + let destination = self.fee_collector.read().get_protocol_revenue_destination(); + // TODO IActivePool(activePool).sendAsset(_asset, destination, _assetFeeAmount); + self.fee_collector.read().handle_redemption_fee(asset, asset_fee_amount); + } + // Burn the total debt tokens that is cancelled with debt, and send the redeemed asset to msg.sender + self.debt_token.read().burn(receiver, debt_token_to_redeem); + // Update Active Pool, and send asset to account + let coll_to_send_to_redeemer = asset_redeemed_amount - asset_fee_amount; + // TODO IActivePool(activePool).decreaseDebt(_asset, _debtToRedeem); + // TODO IActivePool(activePool).sendAsset(_asset, _receiver, collToSendToRedeemer); + } + + fn update_base_rate_from_redemption( + ref self: ContractState, + asset: ContractAddress, + asset_drawn: u256, + price: u256, + total_debt_token_supply: u256 + ) -> u256 { + self.only_vessel_manager_operations(); + let decay_base_rate = self.calc_decayed_base_rate(asset); + let redeem_debt_fraction = (asset_drawn * price) / total_debt_token_supply; + let mut new_base_rate = decay_base_rate + (redeem_debt_fraction / BETA.into()); + new_base_rate = min(new_base_rate, DECIMAL_PRECISION); + assert(new_base_rate != 0, Errors::VesselManager__BaseRateBelowZero); + self.base_rate.write(asset, new_base_rate); + self.emit(BaseRateUpdated { asset, base_rate: new_base_rate }); + self.update_last_fee_op_time(asset); + new_base_rate + } + + fn apply_pending_rewards( + ref self: ContractState, asset: ContractAddress, borrower: ContractAddress + ) { + self.only_vessel_manager_operations_or_borrower_operations(); + self.reentrancy_guard.start(); + self.apply_pending_rewards_internal(asset, borrower); + self.reentrancy_guard.end(); + } + + // Move a Vessel's pending debt and collateral rewards from distributions, from the Default Pool to the Active Pool + fn move_pending_vessel_rewards_to_active_pool( + ref self: ContractState, asset: ContractAddress, debt: u256, asset_amount: u256 + ) { + self.only_vessel_manager_operations(); + self.move_pending_vessel_rewards_to_active_pool_internal(asset, debt, asset_amount); + } + + // Update borrower's snapshots of L_Colls and L_Debts to reflect the current values + fn update_vessel_reward_snapshots( + ref self: ContractState, asset: ContractAddress, borrower: ContractAddress + ) { + self.only_borrower_operations(); + self.update_vessel_reward_snapshots_internal(asset, borrower); + } + + fn update_stake_and_total_stakes( + ref self: ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256 { + self.only_borrower_operations(); + self.update_stake_and_total_stakes_internal(asset, borrower) + } + + fn remove_stake( + ref self: ContractState, asset: ContractAddress, borrower: ContractAddress + ) { + self.only_vessel_manager_operations_or_borrower_operations(); + self.remove_stake_internal(asset, borrower); + } + + // TODO implement IStabilityPool.offset + // TODO implement IActivePool + // TODO implement IDefaultPool + fn redistribute_debt_and_coll( + ref self: ContractState, + asset: ContractAddress, + debt: u256, + coll: u256, + debt_to_offset: u256, + coll_to_sent_to_stability_pool: u256 + ) { + self.only_vessel_manager_operations(); + self.reentrancy_guard.start(); + // IStabilityPool(stabilityPool).offset(_debtToOffset, _asset, _collToSendToStabilityPool); + + if debt == 0 { + return; + } + + // Add distributed coll and debt rewards-per-unit-staked to the running totals. Division uses a "feedback" + // error correction, to keep the cumulative error low in the running totals L_Colls and L_Debts: + // + // 1) Form numerators which compensate for the floor division errors that occurred the last time this + // function was called. + // 2) Calculate "per-unit-staked" ratios. + // 3) Multiply each ratio back by its denominator, to reveal the current floor division error. + // 4) Store these errors for use in the next correction when this function is called. + // 5) Note: static analysis tools complain about this "division before multiplication", however, it is intended. + let coll_numerator = (coll * DECIMAL_PRECISION) + + self.last_coll_error_redistribution.read(asset); + let debt_numerator = (debt * DECIMAL_PRECISION) + + self.last_debt_error_redistribution.read(asset); + + // Get the per-unit-staked terms + let asset_stakes = self.total_stakes.read(asset); + let coll_reward_per_unit_staked = coll_numerator / asset_stakes; + let debt_reward_per_unit_staked = debt_numerator / asset_stakes; + + self + .last_coll_error_redistribution + .write(asset, coll_numerator - (coll_reward_per_unit_staked * asset_stakes)); + self + .last_debt_error_redistribution + .write(asset, debt_numerator - (debt_reward_per_unit_staked * asset_stakes)); + + // Add per-unit-staked terms to the running totals + let liquidated_coll = self.l_colls.read(asset) + coll_reward_per_unit_staked; + let liquidated_debt = self.l_debts.read(asset) + debt_reward_per_unit_staked; + + self.l_colls.write(asset, liquidated_coll); + self.l_debts.write(asset, liquidated_debt); + + self.emit(LTermsUpdated { asset, l_coll: liquidated_coll, l_debt: liquidated_debt }); + // IActivePool(activePool).decreaseDebt(_asset, _debt); + // IDefaultPool(defaultPool).increaseDebt(_asset, _debt); + // IActivePool(activePool).sendAsset(_asset, defaultPool, _coll); + + self.reentrancy_guard.end(); + } + + //TODO implement IActivePool,IDefaultPool + fn update_system_snapshots_exclude_coll_remainder( + ref self: ContractState, asset: ContractAddress, coll_remainder: u256 + ) { + self.only_vessel_manager_operations(); + let total_stakes_cached = self.total_stakes.read(asset); + self.total_stakes_snapshot.write(asset, total_stakes_cached); + // uint256 activeColl = IActivePool(activePool).getAssetBalance(_asset); + // uint256 liquidatedColl = IDefaultPool(defaultPool).getAssetBalance(_asset); + // uint256 _totalCollateralSnapshot = activeColl - _collRemainder + liquidatedColl; + // totalCollateralSnapshot[_asset] = _totalCollateralSnapshot; + // emit SystemSnapshotsUpdated(_asset, totalStakesCached, _totalCollateralSnapshot); + } + + fn close_vessel( + ref self: ContractState, + asset: ContractAddress, + borrower: ContractAddress, + closed_status: Status + ) { + self.only_vessel_manager_operations_or_borrower_operations(); + self.close_vessel_internal(asset, borrower, closed_status); + } + + fn close_vessel_liquidation( + ref self: ContractState, asset: ContractAddress, borrower: ContractAddress + ) { + self.only_vessel_manager_operations(); + self.close_vessel_internal(asset, borrower, Status::ClosedByLiquidation); + self.fee_collector.read().liquidate_debt(borrower, asset); + self + .emit( + VesselUpdated { + asset, + borrower, + debt: 0, + coll: 0, + stake: 0, + operation: VesselManagerOperation::LiquidateInNormalMode + } + ); + } + + + // TODO implement IActivePool + fn send_gas_compensation( + ref self: ContractState, + asset: ContractAddress, + liquidator: ContractAddress, + debt_token_amount: u256, + asset_amount: u256 + ) { + self.only_vessel_manager_operations(); + self.reentrancy_guard.start(); + if debt_token_amount != 0 { + let gas_pool_address = self + .address_provider + .read() + .get_address(AddressesKey::gas_pool); + self + .debt_token + .read() + .return_from_pool(gas_pool_address, liquidator, debt_token_amount); + } + + if asset_amount != 0 { //IActivePool(activePool).sendAsset(_asset, _liquidator, _assetAmount); + } + self.reentrancy_guard.end(); + } + + fn get_vessel_status( + self: @ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> Status { + self.vessels.read((borrower, asset)).status + } + + fn get_vessel_stake( + self: @ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256 { + self.vessels.read((borrower, asset)).stake + } + + fn get_vessel_debt( + self: @ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256 { + self.vessels.read((borrower, asset)).debt + } + + fn get_vessel_coll( + self: @ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256 { + self.vessels.read((borrower, asset)).coll + } + + fn get_vessel_owners_count(self: @ContractState, asset: ContractAddress) -> u32 { + self.vessel_owners.read(asset).len() + } + + fn get_vessel_from_vessel_owners_array( + self: @ContractState, asset: ContractAddress, index: u32 + ) -> Option { + self.vessel_owners.read(asset).get(index) + } + + fn set_vessel_status( + ref self: ContractState, + asset: ContractAddress, + borrower: ContractAddress, + status: Status + ) { + self.only_borrower_operations(); + let mut vessel = self.vessels.read((borrower, asset)); + vessel.status = status; + self.vessels.write((borrower, asset), vessel); + } + + fn increase_vessel_coll( + ref self: ContractState, + asset: ContractAddress, + borrower: ContractAddress, + coll_increase: u256 + ) -> u256 { + self.only_borrower_operations(); + let mut vessel = self.vessels.read((borrower, asset)); + let new_coll = vessel.coll + coll_increase; + vessel.coll = new_coll; + self.vessels.write((borrower, asset), vessel); + new_coll + } + + fn decrease_vessel_coll( + ref self: ContractState, + asset: ContractAddress, + borrower: ContractAddress, + coll_decrease: u256 + ) -> u256 { + self.only_borrower_operations(); + let mut vessel = self.vessels.read((borrower, asset)); + let new_coll = vessel.coll - coll_decrease; + vessel.coll = new_coll; + self.vessels.write((borrower, asset), vessel); + new_coll + } + + fn increase_vessel_debt( + ref self: ContractState, + asset: ContractAddress, + borrower: ContractAddress, + debt_increase: u256 + ) -> u256 { + self.only_borrower_operations(); + let mut vessel = self.vessels.read((borrower, asset)); + let new_debt = vessel.debt + debt_increase; + vessel.debt = new_debt; + self.vessels.write((borrower, asset), vessel); + new_debt + } + + fn decrease_vessel_debt( + ref self: ContractState, + asset: ContractAddress, + borrower: ContractAddress, + debt_decrease: u256 + ) -> u256 { + self.only_borrower_operations(); + let mut vessel = self.vessels.read((borrower, asset)); + let old_debt = vessel.debt; + if debt_decrease == 0 { + return old_debt; // no changes + } + let payback_fraction = (debt_decrease * shisui_math::dec_pow(10, 18)) / old_debt; + let new_debt = old_debt - debt_decrease; + vessel.debt = new_debt; + self.vessels.write((borrower, asset), vessel); + if payback_fraction != 0 { + self.fee_collector.read().decrease_debt(borrower, asset, payback_fraction); + } + new_debt + } + } + + // ************************************************************************* + // INTERNAL FUNCTIONS + // ************************************************************************* + #[generate_trait] + impl InternalFunctions of InternalFunctionsTrait { + fn only_vessel_manager_operations(self: @ContractState) { + assert( + get_caller_address() == self + .address_provider + .read() + .get_address(AddressesKey::vessel_manager_operations), + Errors::VesselManager__OnlyVesselManagerOperations + ) + } + + fn only_borrower_operations(self: @ContractState) { + assert( + get_caller_address() == self + .address_provider + .read() + .get_address(AddressesKey::borrower_operations), + Errors::VesselManager__OnlyBorrowerOperations + ) + } + + fn only_vessel_manager_operations_or_borrower_operations(self: @ContractState) { + let caller = get_caller_address(); + let vessel_manager_operations = self + .address_provider + .read() + .get_address(AddressesKey::vessel_manager_operations); + let borrower_operations = self + .address_provider + .read() + .get_address(AddressesKey::borrower_operations); + + assert( + caller != vessel_manager_operations && caller != borrower_operations, + Errors::VesselManager__OnlyVesselManagerOperationsOrBorrowerOperations + ) + } + + fn get_current_vessel_amounts( + self: @ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> (u256, u256) { + let pending_coll_reward = self.get_pending_asset_reward(asset, borrower); + let pending_debt_reward = self.get_pending_debt_token_reward(asset, borrower); + let vessel: super::Vessel = self.vessels.read((borrower, asset)); + let coll = vessel.coll + pending_coll_reward; + let debt = vessel.debt + pending_debt_reward; + (coll, debt) + } + + fn remove_stake_internal( + ref self: ContractState, asset: ContractAddress, borrower: ContractAddress + ) { + let mut vessel = self.vessels.read((borrower, asset)); + let new_total_stake = self.total_stakes.read(asset) - vessel.stake; + self.total_stakes.write(asset, new_total_stake); + vessel.stake = 0; + } + + // Update borrower's stake based on their latest collateral value + fn update_stake_and_total_stakes_internal( + ref self: ContractState, asset: ContractAddress, borrower: ContractAddress + ) -> u256 { + let mut vessel = self.vessels.read((borrower, asset)); + let new_stake = self.compute_new_stake(asset, vessel.coll); + let old_stake = vessel.stake; + vessel.stake = new_stake; + let new_total = self.total_stakes.read(asset) - old_stake + new_stake; + self.total_stakes.write(asset, new_total); + self.emit(TotalStakesUpdated { asset, new_total_stakes: new_total }); + new_stake + } + + // Calculate a new stake based on the snapshots of the totalStakes and totalCollateral taken at the last liquidation + fn compute_new_stake(self: @ContractState, asset: ContractAddress, coll: u256) -> u256 { + let asset_coll = self.total_collateral_snapshot.read(asset); + let mut stake: u256 = 0; + if asset_coll == 0 { + return coll; + } else { + let asset_stakes = self.total_stakes_snapshot.read(asset); + // The following assert() holds true because: + // - The system always contains >= 1 vessel + // - When we close or liquidate a vessel, we redistribute the pending rewards, so if all vessels were closed/liquidated, + // rewards would’ve been emptied and totalCollateralSnapshot would be zero too. + assert(asset_stakes != 0, Errors::VesselManager__AssetStakesIsZero); + return (coll * asset_stakes) / asset_coll; + } + } + + // TODO add check on sortedVessels when contract exist + fn close_vessel_internal( + ref self: ContractState, + asset: ContractAddress, + borrower: ContractAddress, + closed_status: Status + ) { + assert( + closed_status != Status::NonExistent && closed_status != Status::Active, + Errors::VesselManager__WrongVesselStatusWhenClosing + ); + let vessel_owners_array_length = self.vessel_owners.read(asset).len(); + assert(vessel_owners_array_length > 1, Errors::VesselManager__OnlyOneVessel); + + // update vessel + let mut vessel = self.vessels.read((borrower, asset)); + vessel.status = closed_status; + vessel.coll = 0; + vessel.debt = 0; + self.vessels.write((borrower, asset), vessel); + // update reward_snapshot + let mut snapshot_asset = self.reward_snapshots.read((borrower, asset)); + snapshot_asset.asset = 0; + snapshot_asset.debt = 0; + self.reward_snapshots.write((borrower, asset), snapshot_asset); + + self.remove_vessel_owner(asset, borrower, vessel_owners_array_length); + // TODO remove sorted vessel + } + + fn remove_vessel_owner( + ref self: ContractState, + asset: ContractAddress, + borrower: ContractAddress, + vessel_owners_array_length: u32 + ) { + let mut vessel = self.vessels.read((borrower, asset)); + assert( + vessel.status != Status::NonExistent && vessel.status != Status::Active, + Errors::VesselManager__WrongVesselStatusWhenRemoving + ); + + let index = vessel.array_index; + let last_index = vessel_owners_array_length - 1; + assert(index != 0 && index <= last_index.into(), Errors::VesselManager__VesselNotFound); + + let mut vessel_asset_owners = self.vessel_owners.read(asset); + // Specifically handle case where there is only one vessel_owner + if index == last_index.into() { + vessel_asset_owners.pop_front(); + vessel.array_index = 0; + self.vessels.write((borrower, asset), vessel); + return; + } + + let mut last_vessel = vessel_asset_owners.pop_front(); + match last_vessel { + Option::Some(last_address) => { + vessel_asset_owners.set(index, last_address); + vessel.array_index = 0; + self.vessels.write((borrower, asset), vessel); + }, + Option::None => { + // This case should never happen, because index is always <= length + return; + } + } + + self.emit(VesselIndexUpdated { asset, borrower, new_index: index }); + } + + + fn calc_redemption_rate( + self: @ContractState, asset: ContractAddress, base_rate: u256 + ) -> u256 { + min( + self.admin_contract.read().get_redemption_fee_floor(asset) + base_rate, + DECIMAL_PRECISION + ) + } + + fn calc_redemption_fee( + self: @ContractState, redemption_rate: u256, asset_draw: u256 + ) -> u256 { + let redemption_fee = (redemption_rate * asset_draw) / DECIMAL_PRECISION; + assert(redemption_fee < asset_draw, Errors::VesselManager__FeeBiggerThanAssetDraw); + redemption_fee + } + + fn calc_decayed_base_rate(self: @ContractState, asset: ContractAddress) -> u256 { + let minutes_passed = self.minutes_passed_since_last_fee_op(asset); + let decay_factor = shisui_math::dec_pow(MINUTE_DECAY_FACTOR, minutes_passed); + (self.base_rate.read(asset) * decay_factor) / DECIMAL_PRECISION + } + + fn minutes_passed_since_last_fee_op(self: @ContractState, asset: ContractAddress) -> u256 { + let time_stamp: u256 = get_block_timestamp().into(); + time_stamp - self.last_fee_operation_time.read(asset) / SECONDS_IN_ONE_MINUTE.into() + } + + // TODO implement function + fn redeem_close_vessel( + self: @ContractState, + asset: ContractAddress, + borrower: ContractAddress, + debt_token_amount: u256, + asset_mount: u256 + ) { //let gas_pool_address = self.address_provider.read().get_address(AddressesKey::gas_pool); + //self.debt_token.read().burn(gas_pool_address, debt_token_amount); + + } + + //TODO implement function + fn move_pending_vessel_rewards_to_active_pool_internal( + ref self: ContractState, + asset: ContractAddress, + debt_token_amount: u256, + asset_amount: u256 + ) { //IDefaultPool(defaultPool).decreaseDebt(_asset, _debtTokenAmount); + //IActivePool(activePool).increaseDebt(_asset, _debtTokenAmount); + //IDefaultPool(defaultPool).sendAssetToActivePool(_asset, _assetAmount); + } + + fn update_vessel_reward_snapshots_internal( + ref self: ContractState, asset: ContractAddress, borrower: ContractAddress + ) { + let liquidated_coll = self.l_colls.read(asset); + let liquidated_debt = self.l_debts.read(asset); + let mut snapshot = self.reward_snapshots.read((borrower, asset)); + snapshot.asset = liquidated_coll; + snapshot.debt = liquidated_debt; + self.reward_snapshots.write((borrower, asset), snapshot); + self + .emit( + VesselSnapshotsUpdated { + asset, borrower, l_coll: liquidated_coll, l_debt: liquidated_debt + } + ); + } + + // Add the borrowers's coll and debt rewards earned from redistributions, to their Vessel + fn apply_pending_rewards_internal( + ref self: ContractState, asset: ContractAddress, borrower: ContractAddress + ) { + if !self.has_pending_rewards(asset, borrower) { + return; + } + + // Compute pending rewards + let pending_coll_reward = self.get_pending_asset_reward(asset, borrower); + let pending_debt_reward = self.get_pending_debt_token_reward(asset, borrower); + + // Apply pending rewards to vessel's state + let mut vessel = self.vessels.read((borrower, asset)); + vessel.coll = vessel.coll + pending_coll_reward; + vessel.debt = vessel.debt + pending_debt_reward; + self.vessels.write((borrower, asset), vessel); + + self.update_vessel_reward_snapshots(asset, borrower); + + // Transfer from DefaultPool to ActivePool + self + .move_pending_vessel_rewards_to_active_pool( + asset, pending_debt_reward, pending_coll_reward + ); + self + .emit( + VesselUpdated { + asset, + borrower, + debt: vessel.debt, + coll: vessel.debt, + stake: vessel.stake, + operation: VesselManagerOperation::ApplyPendingRewards + } + ); + } + + fn update_last_fee_op_time(ref self: ContractState, asset: ContractAddress) { + let time_passed: u256 = get_block_timestamp().into() + - self.last_fee_operation_time.read(asset); + if time_passed >= SECONDS_IN_ONE_MINUTE.into() { + // Update the last fee operation time only if time passed >= decay interval. This prevents base rate griefing. + self.last_fee_operation_time.write(asset, get_block_timestamp().into()); + self.emit(LastFeeOpTimeUpdated { asset, last_fee_op_time: get_block_timestamp() }) + } + } + } +} diff --git a/src/lib.cairo b/src/lib.cairo index 830f8f2..f5bfe66 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -1,14 +1,13 @@ -mod components { - mod shisui_base; -} - mod core { mod address_provider; mod timelock; mod gas_pool; + mod debt_token; mod price_feed; mod fee_collector; mod admin_contract; + mod debt_token; + mod vessel_manager; } mod pools { @@ -16,6 +15,7 @@ mod pools { mod default_pool; mod collateral_surplus_pool; mod active_pool; + mod vessel_manager; } mod utils { @@ -25,7 +25,9 @@ mod utils { mod traits; mod math; mod shisui_math; + mod shisui_base; mod convert; + mod doubly_linked_list; } mod mocks { diff --git a/src/pools/default_pool.cairo b/src/pools/default_pool.cairo index 67af48f..1bd8659 100644 --- a/src/pools/default_pool.cairo +++ b/src/pools/default_pool.cairo @@ -47,6 +47,7 @@ mod DefaultPool { fn decrease_debt(ref self: ContractState, asset: ContractAddress, amount: u256) {} + // Useless just do a send_asset with proper access control fn send_asset_to_active_pool( ref self: ContractState, asset: ContractAddress, amount: u256 ) {} diff --git a/src/pools/sorted_vessels.cairo b/src/pools/sorted_vessels.cairo index 3d2d64a..f3e9a43 100644 --- a/src/pools/sorted_vessels.cairo +++ b/src/pools/sorted_vessels.cairo @@ -29,7 +29,7 @@ trait ISortedVessels { next_id: ContractAddress ) -> bool; fn is_empty(self: @TContractState, asset: ContractAddress) -> bool; - fn get_size(self: @TContractState) -> usize; + fn get_size(self: @TContractState, asset: ContractAddress) -> usize; fn get_first(self: @TContractState, asset: ContractAddress) -> ContractAddress; fn get_last(self: @TContractState, asset: ContractAddress) -> ContractAddress; fn get_next( @@ -38,14 +38,14 @@ trait ISortedVessels { fn get_prev( self: @TContractState, asset: ContractAddress, id: ContractAddress ) -> ContractAddress; - fn valid_insert_position( + fn is_valid_insert_position( self: @TContractState, asset: ContractAddress, NICR: u256, prev_id: ContractAddress, next_id: ContractAddress ) -> bool; - fn find_insert_position( + fn get_insert_position( self: @TContractState, asset: ContractAddress, NICR: u256, @@ -87,16 +87,45 @@ trait ISortedVessels { /// - Public functions with parameters have been made internal to save gas, and given an external wrapper function for external access #[starknet::contract] mod SortedVessels { - use starknet::ContractAddress; - use shisui::utils::traits::ContractAddressDefault; - use shisui::core::address_provider::{ - IAddressProviderDispatcher, IAddressProviderDispatcherTrait + // use shisui::pools::sorted_vessels::ISortedVessels; + use core::option::{OptionTrait}; + use zeroable::Zeroable; + use starknet::{ContractAddress, contract_address_const, get_caller_address}; + use shisui::{ + utils::{traits::ContractAddressDefault, errors::{CommunErrors, SortedVesselsErrors}}, + core::{ + address_provider::{ + IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey + } + }, + pools::vessel_manager::{IVesselManagerDispatcher, IVesselManagerDispatcherTrait} }; + use debug::PrintTrait; + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + NodeAdded: NodeAdded, + NodeRemoved: NodeRemoved + } + + #[derive(Drop, starknet::Event)] + struct NodeAdded { + asset: ContractAddress, + id: ContractAddress, + NICR: u256, + } + + #[derive(Drop, starknet::Event)] + struct NodeRemoved { + asset: ContractAddress, + id: ContractAddress, + } + // Information for a node in the list #[derive(Serde, Drop, Copy, starknet::Store, Default)] struct Node { - exist: bool, // Id of next node (smaller NICR) in the list next_id: ContractAddress, // Id of previous node (larger NICR) in the list @@ -120,11 +149,13 @@ mod SortedVessels { // asset => ordered list data: LegacyMap, // asset address => depositor address => node | Track the corresponding ids for each node in the list - nodes: LegacyMap<(ContractAddress, ContractAddress), Node>, + nodes: LegacyMap<(ContractAddress, ContractAddress), Option>, } #[constructor] - fn constructor(ref self: ContractState, address_provider: IAddressProviderDispatcher) {} + fn constructor(ref self: ContractState, address_provider: IAddressProviderDispatcher) { + self.address_provider.write(address_provider); + } #[external(v0)] impl SortedVesselsImpl of super::ISortedVessels { @@ -140,7 +171,10 @@ mod SortedVessels { NICR: u256, prev_id: ContractAddress, next_id: ContractAddress - ) {} + ) { + self.require_caller_is_bo_or_vm(); + self.int_insert(asset, id, NICR, prev_id, next_id); + } /// @dev Re-insert the node at a new position, based on its new NICR /// @param id Node's id @@ -154,11 +188,28 @@ mod SortedVessels { new_NICR: u256, prev_id: ContractAddress, next_id: ContractAddress - ) {} + ) { + self.require_caller_is_bo_or_vm(); + // List must contain the node + assert(self.nodes.read((asset, id)).is_some(), SortedVesselsErrors::NodeDoesntExist); + // NICR must be non-zero + assert(new_NICR.is_non_zero(), SortedVesselsErrors::NICRMustBePositive); + + self.int_remove(asset, id); + self.int_insert(asset, id, new_NICR, prev_id, next_id); + } /// @dev Remove a node from the list /// @param id Node's id - fn remove(ref self: ContractState, asset: ContractAddress, id: ContractAddress) {} + fn remove(ref self: ContractState, asset: ContractAddress, id: ContractAddress) { + let caller = get_caller_address(); + let vessel_manager = self + .address_provider + .read() + .get_address(AddressesKey::vessel_manager); + assert(caller == vessel_manager, CommunErrors::CommunErrors__CallerNotAuthorized); + self.int_remove(asset, id); + } /// @dev Checks if the list contains a node fn contains( @@ -169,27 +220,30 @@ mod SortedVessels { prev_id: ContractAddress, next_id: ContractAddress ) -> bool { - return false; + match self.nodes.read((asset, id)) { + Option::Some(node) => true, + Option::None => false + } } /// @dev Checks if the list is empty fn is_empty(self: @ContractState, asset: ContractAddress) -> bool { - return false; + return self.data.read(asset).size == 0; } /// @dev Returns the current size of the list - fn get_size(self: @ContractState) -> usize { - return 0; + fn get_size(self: @ContractState, asset: ContractAddress) -> usize { + return self.data.read(asset).size; } /// @dev Returns the first node in the list (node with the largest NICR) fn get_first(self: @ContractState, asset: ContractAddress) -> ContractAddress { - return Default::default(); + return self.data.read(asset).head; } /// @dev Returns the last node in the list (node with the smallest NICR) fn get_last(self: @ContractState, asset: ContractAddress) -> ContractAddress { - return Default::default(); + return self.data.read(asset).tail; } /// @dev Returns the next node (with a smaller NICR) in the list for a given node @@ -197,7 +251,7 @@ mod SortedVessels { fn get_next( self: @ContractState, asset: ContractAddress, id: ContractAddress ) -> ContractAddress { - return Default::default(); + return self.nodes.read((asset, id)).unwrap().next_id; } /// @dev Returns the previous node (with a larger NICR) in the list for a given node @@ -205,35 +259,305 @@ mod SortedVessels { fn get_prev( self: @ContractState, asset: ContractAddress, id: ContractAddress ) -> ContractAddress { - return Default::default(); + return self.nodes.read((asset, id)).unwrap().prev_id; } /// @dev Check if a pair of nodes is a valid insertion point for a new node with the given NICR /// @param NICR Node's NICR /// @param prev_id Id of previous node for the insert position /// @param next_id Id of next node for the insert position - fn valid_insert_position( + fn is_valid_insert_position( self: @ContractState, asset: ContractAddress, NICR: u256, prev_id: ContractAddress, next_id: ContractAddress ) -> bool { - return false; + return self.int_valid_insert_position(asset, NICR, prev_id, next_id); } /// @dev Find the insert position for a new node with the given NICR /// @param NICR Node's NICR /// @param prev_id Id of previous node for the insert position /// @param next_id Id of next node for the insert position - fn find_insert_position( + fn get_insert_position( self: @ContractState, asset: ContractAddress, NICR: u256, prev_id: ContractAddress, next_id: ContractAddress ) -> (ContractAddress, ContractAddress) { - return (Default::default(), Default::default()); + return self.int_find_insert_point(asset, NICR, prev_id, next_id); + } + } + + #[generate_trait] + impl InternalFunctions of InternalFunctionsTrait { + #[inline(always)] + fn int_insert( + ref self: ContractState, + asset: ContractAddress, + id: ContractAddress, + NICR: u256, + prev_id: ContractAddress, + next_id: ContractAddress + ) { + assert(self.nodes.read((asset, id)).is_none(), SortedVesselsErrors::NodeAlreadyExists); + assert(id.is_non_zero(), CommunErrors::CommunErrors__AddressZero); + assert(NICR.is_non_zero(), SortedVesselsErrors::NICRMustBePositive); + + let mut prev_id = prev_id; + let mut next_id = next_id; + + if (!self.int_valid_insert_position(asset, NICR, prev_id, next_id)) { + 'invalid insert position'.print(); + // Sender's hint was not a valid insert position + // Use sender's hint to find a valid insert position + let (new_prev_id, new_next_id) = self + .int_find_insert_point(asset, NICR, prev_id, next_id); + prev_id = new_prev_id; + next_id = new_next_id; + + prev_id.print(); + next_id.print(); + } + + let mut new_node = Node { prev_id: prev_id, next_id: next_id, }; + let mut new_data = self.data.read(asset); + new_data.size += 1; + + if (prev_id.is_zero() && next_id.is_zero()) { + // Insert as head and tail + new_data.head = id; + new_data.tail = id; + } else if (prev_id.is_zero()) { + // Insert before `prevId` as the head + new_node.next_id = new_data.head; + let mut head_node = self.nodes.read((asset, new_data.head)).unwrap(); + head_node.prev_id = id; + self.nodes.write((asset, new_data.head), Option::Some(head_node)); + new_data.head = id; + } else if (next_id.is_zero()) { + // Insert after `nextId` as the tail + new_node.prev_id = new_data.tail; + let mut tail_node = self.nodes.read((asset, new_data.tail)).unwrap(); + tail_node.next_id = id; + self.nodes.write((asset, new_data.tail), Option::Some(tail_node)); + new_data.tail = id; + } else { + // Insert at insert position between `prevId` and `nextId` + new_node.prev_id = prev_id; + new_node.next_id = next_id; + let mut prev_node = self.nodes.read((asset, prev_id)).unwrap(); + let mut next_node = self.nodes.read((asset, next_id)).unwrap(); + prev_node.next_id = id; + next_node.prev_id = id; + self.nodes.write((asset, prev_id), Option::Some(prev_node)); + self.nodes.write((asset, next_id), Option::Some(next_node)); + } + + self.nodes.write((asset, id), Option::Some(new_node)); + self.data.write(asset, new_data); + + self.emit(NodeAdded { asset: asset, id: id, NICR: NICR, }) + } + + #[inline(always)] + fn int_remove(ref self: ContractState, asset: ContractAddress, id: ContractAddress) { + // List must contain the node + assert(self.nodes.read((asset, id)).is_some(), SortedVesselsErrors::NodeDoesntExist); + + let mut node = self.nodes.read((asset, id)).unwrap(); + let mut data = self.data.read(asset); + data.size -= 1; + + if (data.size > 1) { + // List contains more than a single node + if (id == data.head) { + // The removed node is the head + // Set head to next node + data.head = node.next_id; + // Set prev pointer of new head to null + let mut head_node = self.nodes.read((asset, data.head)).unwrap(); + head_node.prev_id = contract_address_const::<0>(); + self.nodes.write((asset, data.head), Option::Some(head_node)); + } else if (id == data.tail) { + // The removed node is the tail + // Set tail to previous node + data.tail = node.prev_id; + // Set next pointer of new tail to null + let mut tail_node = self.nodes.read((asset, data.tail)).unwrap(); + tail_node.next_id = contract_address_const::<0>(); + self.nodes.write((asset, data.tail), Option::Some(tail_node)); + } else { + // The removed node is in the middle of the list + // Set next pointer of previous node to next node + let mut prev_node = self.nodes.read((asset, node.prev_id)).unwrap(); + prev_node.next_id = node.next_id; + self.nodes.write((asset, node.prev_id), Option::Some(prev_node)); + // Set prev pointer of next node to previous node + let mut next_node = self.nodes.read((asset, node.next_id)).unwrap(); + next_node.prev_id = node.prev_id; + self.nodes.write((asset, node.next_id), Option::Some(next_node)); + } + } else { + // List contains a single node + // Set the head and tail to null + data.head = contract_address_const::<0>(); + data.tail = contract_address_const::<0>(); + } + + self.nodes.write((asset, id), Option::None); + self.data.write(asset, data); + + self.emit(NodeRemoved { asset: asset, id: id, }) + } + + #[inline(always)] + fn int_valid_insert_position( + self: @ContractState, + asset: ContractAddress, + NICR: u256, + prev_id: ContractAddress, + next_id: ContractAddress + ) -> bool { + let vessel_manager = IVesselManagerDispatcher { + contract_address: self + .address_provider + .read() + .get_address(AddressesKey::vessel_manager) + }; + if (prev_id.is_zero() && next_id.is_zero()) { + // `(null, null)` is a valid insert position if the list is empty + return self.data.read(asset).size == 0; + } else if (prev_id.is_zero()) { + // `(null, _nextId)` is a valid insert position if `_nextId` is the head of the list + return self.data.read(asset).head == next_id + && NICR >= vessel_manager.get_nominal_icr(asset, next_id); + } else if (next_id.is_zero()) { + // `(_prevId, null)` is a valid insert position if `_prevId` is the tail of the list + return self.data.read(asset).tail == prev_id + && NICR <= vessel_manager.get_nominal_icr(asset, prev_id); + } else { + // `(_prevId, _nextId)` is a valid insert position if `_prevId` and `_nextId` are adjacent nodes in the list + return self.nodes.read((asset, prev_id)).unwrap().next_id == next_id + && vessel_manager.get_nominal_icr(asset, prev_id) >= NICR + && NICR >= vessel_manager.get_nominal_icr(asset, next_id); + } + } + + #[inline(always)] + fn int_find_insert_point( + self: @ContractState, + asset: ContractAddress, + NICR: u256, + prev_id: ContractAddress, + next_id: ContractAddress + ) -> (ContractAddress, ContractAddress) { + let mut prev_id = prev_id; + let mut next_id = next_id; + let vessel_manager = IVesselManagerDispatcher { + contract_address: self + .address_provider + .read() + .get_address(AddressesKey::vessel_manager) + }; + if (prev_id.is_non_zero() + && (self.nodes.read((asset, prev_id)).is_none()) + || NICR > vessel_manager.get_nominal_icr(asset, prev_id)) { + prev_id = contract_address_const::<0>(); + } + + if (next_id.is_zero() + && (self.nodes.read((asset, next_id)).is_none() + || NICR < vessel_manager.get_nominal_icr(asset, next_id))) { + next_id = contract_address_const::<0>(); + } + + if (prev_id.is_zero() && next_id.is_zero()) { + // No hint - descend list starting from head + return self.descend_list(asset, NICR, self.data.read(asset).head); + } else if (prev_id.is_zero()) { + // No `prevId` for hint - ascend list starting from `nextId` + return self.ascend_list(asset, NICR, next_id); + } else { + // Descend list starting from `prevId` + return self.descend_list(asset, NICR, prev_id); + } + } + + fn descend_list( + self: @ContractState, asset: ContractAddress, NICR: u256, start_id: ContractAddress + ) -> (ContractAddress, ContractAddress) { + // If `_startId` is the head, check if the insert position is before the head + if (start_id == self.data.read(asset).head) { + return (contract_address_const::<0>(), start_id); + } + + let mut prev_id = start_id; + let mut next_id = self.nodes.read((asset, prev_id)).unwrap().next_id; + + // Descend the list until we reach the end or until we find a valid insert position + loop { + if (prev_id.is_zero() + || !self.int_valid_insert_position(asset, NICR, prev_id, next_id)) { + break; + } + + prev_id = self.nodes.read((asset, prev_id)).unwrap().next_id; + next_id = self.nodes.read((asset, prev_id)).unwrap().next_id; + }; + + (prev_id, next_id) + } + + fn ascend_list( + self: @ContractState, asset: ContractAddress, NICR: u256, start_id: ContractAddress + ) -> (ContractAddress, ContractAddress) { + let vessel_manager = IVesselManagerDispatcher { + contract_address: self + .address_provider + .read() + .get_address(AddressesKey::vessel_manager) + }; + // If `_startId` is the tail, check if the insert position is after the tail + if (start_id == self.data.read(asset).tail + && NICR <= vessel_manager.get_nominal_icr(asset, start_id)) { + return (start_id, contract_address_const::<0>()); + } + + let mut prev_id = self.nodes.read((asset, start_id)).unwrap().prev_id; + let mut next_id = start_id; + + // Ascend the list until we reach the end or until we find a valid insert position + loop { + if (next_id.is_zero() + || !self.int_valid_insert_position(asset, NICR, prev_id, next_id)) { + break; + } + + next_id = self.nodes.read((asset, next_id)).unwrap().prev_id; + prev_id = self.nodes.read((asset, next_id)).unwrap().prev_id; + }; + + (prev_id, next_id) + } + + #[inline(always)] + fn require_caller_is_bo_or_vm(self: @ContractState) { + let caller = get_caller_address(); + let borrower_operations = self + .address_provider + .read() + .get_address(AddressesKey::borrower_operations); + let vessel_manager = self + .address_provider + .read() + .get_address(AddressesKey::vessel_manager); + assert( + caller == borrower_operations || caller == vessel_manager, + CommunErrors::CommunErrors__CallerNotAuthorized + ); } } } diff --git a/src/utils/doubly_linked_list.cairo b/src/utils/doubly_linked_list.cairo new file mode 100644 index 0000000..e6109fd --- /dev/null +++ b/src/utils/doubly_linked_list.cairo @@ -0,0 +1,105 @@ +use zeroable::Zeroable; + +#[derive(Drop)] +struct Felt252Node { + value: felt252, + next: felt252, + prev: felt252, +} + +struct Felt252List { + size: felt252, + head: Felt252Node, + tail: Felt252Node, +} + +trait Felt252NodeTrait { + fn new(value: felt252) -> Felt252Node; +} + +trait Felt252DoublyLinkedListTrait { + // fn new() -> List; + fn insert(ref self: Felt252List, value: felt252); +// fn remove(&mut self, value: N); +// fn search(&self, value: N) -> Option<&N>; +// fn is_empty(&self) -> bool; +// fn size(&self) -> usize; +// fn print(&self); +} + +impl Felt252NodeImplementation of Felt252NodeTrait { + fn new(value: felt252) -> Felt252Node { + Felt252Node { value: value, next: 0, prev: 0, } + } +} + +impl Felt252DoublyLinkedListImpl of Felt252DoublyLinkedListTrait { + // fn new() -> List { + // List { + // size: 0, + // head: Option::None, + // tail: Option::None, + // data: Felt252Dict::new(), + // } + // } + + fn insert(ref self: Felt252List, value: felt252) { + if (self.size.is_zero()) { + self.head = Felt252Node { value: value, next: 0, prev: 0, }; + self.tail = Felt252Node { value: value, next: 0, prev: 0, }; + } else { + let mut node = Felt252Node { value: value, next: 0, prev: 0, }; + } + } +// fn remove(&mut self, value: N) { +// let mut current = self.head.take(); +// while let Some(mut node) = current { +// current = node.next.take(); +// if node.value == value { +// self.size -= 1; +// if let Some(mut next) = node.next.take() { +// next.prev = node.prev.take(); +// current = Some(next); +// } else { +// self.tail = node.prev.take(); +// } +// if let Some(mut prev) = node.prev.take() { +// prev.next = node.next.take(); +// current = Some(prev); +// } else { +// self.head = node.next.take(); +// } +// } else { +// current = Some(node); +// } +// } +// } + +// fn search(&self, value: N) -> Option<&N> { +// let mut current = self.head.take(); +// while let Some(node) = current { +// if node.value == value { +// return Some(&node.value); +// } +// current = node.next.take(); +// } +// None +// } + +// fn is_empty(&self) -> bool { +// self.head.is_none() +// } + +// fn size(&self) -> usize { +// self.size +// } + +// fn print(&self) { +// let mut current = self.head.take(); +// while let Some(node) = current { +// print!("{} ", node.value); +// current = node.next.take(); +// } +// println!(""); +// } +} diff --git a/src/utils/errors.cairo b/src/utils/errors.cairo index 5a8fb0b..6841c33 100644 --- a/src/utils/errors.cairo +++ b/src/utils/errors.cairo @@ -5,3 +5,13 @@ mod CommunErrors { const CommunErrors__CallerNotAuthorized: felt252 = 'Caller is not authorized'; const CommunErrors__Invalid_amount: felt252 = 'Invalid_amount'; } + +mod DebtTokenErrors { + const DebtTokenErrors__BurnAmountGtBalance: felt252 = 'Burn amount gt balance'; +} + +mod SortedVesselsErrors { + const NodeAlreadyExists: felt252 = 'Node already exists'; + const NodeDoesntExist: felt252 = 'Node doesnt exist'; + const NICRMustBePositive: felt252 = 'NICR must be positive'; +} diff --git a/src/utils/shisui_base.cairo b/src/utils/shisui_base.cairo new file mode 100644 index 0000000..01d5f97 --- /dev/null +++ b/src/utils/shisui_base.cairo @@ -0,0 +1,95 @@ +use starknet::ContractAddress; +use shisui::utils::{ + array::{StoreContractAddressArray, StoreU256Array}, constants::DECIMAL_PRECISION, + shisui_math::compute_cr, +}; +use shisui::core::{ + address_provider::{IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey}, + admin_contract::{IAdminContractDispatcher, IAdminContractDispatcherTrait} +}; +use shisui::pools::{ + active_pool::{IActivePoolDispatcher, IActivePoolDispatcherTrait}, + default_pool::{IDefaultPoolDispatcher, IDefaultPoolDispatcherTrait} +}; + +#[derive(Drop, Clone, starknet::Store, Serde)] +struct Colls { + tokens: Array, + amounts: Array, +} + +mod Errors { + const ShisuiBaseErrors__ExceedFee: felt252 = 'Fee exceeded provided maximum'; +} + +// Returns the composite debt (drawn debt + gas compensation) of a vessel, for the purpose of ICR calculation +fn get_composite_debt( + admin_contract: IAdminContractDispatcher, asset: ContractAddress, debt: u256 +) -> u256 { + return debt + admin_contract.get_debt_token_gas_compensation(asset); +} + +fn get_net_debt( + admin_contract: IAdminContractDispatcher, asset: ContractAddress, debt: u256 +) -> u256 { + return debt - admin_contract.get_debt_token_gas_compensation(asset); +} + +// Return the amount of ETH to be drawn from a vessel's collateral and sent as gas compensation. +fn get_coll_gas_compensation( + admin_contract: IAdminContractDispatcher, asset: ContractAddress, entire_coll: u256 +) -> u256 { + return entire_coll / admin_contract.get_percent_divisor(asset); +} + +fn get_entire_system_coll( + addres_provider: IAddressProviderDispatcher, asset: ContractAddress +) -> u256 { + let active_coll = IActivePoolDispatcher { + contract_address: addres_provider.get_address(AddressesKey::active_pool) + } + .get_asset_balance(asset); + let liquidated_coll = IDefaultPoolDispatcher { + contract_address: addres_provider.get_address(AddressesKey::default_pool) + } + .get_asset_balance(asset); + return active_coll + liquidated_coll; +} + +fn get_entire_system_debt( + addres_provider: IAddressProviderDispatcher, asset: ContractAddress +) -> u256 { + let active_debt = IActivePoolDispatcher { + contract_address: addres_provider.get_address(AddressesKey::active_pool) + } + .get_debt_token_balance(asset); + let closed_debt = IDefaultPoolDispatcher { + contract_address: addres_provider.get_address(AddressesKey::default_pool) + } + .get_debt_token_balance(asset); + return active_debt + closed_debt; +} + +fn get_TCR( + addres_provider: IAddressProviderDispatcher, asset: ContractAddress, price: u256 +) -> u256 { + let entire_system_coll = get_entire_system_coll(addres_provider, asset); + let entire_system_debt = get_entire_system_debt(addres_provider, asset); + return compute_cr(entire_system_coll, entire_system_debt, price); +} + +fn check_recovery_mode( + addres_provider: IAddressProviderDispatcher, asset: ContractAddress, price: u256 +) -> bool { + let tcr = get_TCR(addres_provider, asset, price); + + return tcr < IAdminContractDispatcher { + contract_address: addres_provider.get_address(AddressesKey::admin_contract) + } + .get_ccr(asset); +} + +fn assert_user_accepts_fee(fee: u256, amount: u256, max_fee_percentage: u256) { + let fee_percentage = fee * DECIMAL_PRECISION / amount; + assert(fee_percentage <= max_fee_percentage, Errors::ShisuiBaseErrors__ExceedFee); +} diff --git a/tests/integration/components/shisui_base/check_recovery_mode/check_recovery_mode.tree b/tests/integration/components/shisui_base/check_recovery_mode/check_recovery_mode.tree deleted file mode 100644 index 51e3c5a..0000000 --- a/tests/integration/components/shisui_base/check_recovery_mode/check_recovery_mode.tree +++ /dev/null @@ -1,7 +0,0 @@ -test_check_recovery_mode.cairo -├── when the _asset is not whitelisted -│ └── it should return false -└── when the _asset is whitelisted - ├── when the _price is 0 - │ └── it should return true - └── it should return the correct bool diff --git a/tests/integration/components/shisui_base/check_recovery_mode/test_check_recovery_mode.cairo b/tests/integration/components/shisui_base/check_recovery_mode/test_check_recovery_mode.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/components/shisui_base/check_recovery_mode/test_check_recovery_mode.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration/components/shisui_base/get_TCR/get_TCR.tree b/tests/integration/components/shisui_base/get_TCR/get_TCR.tree deleted file mode 100644 index c390d4e..0000000 --- a/tests/integration/components/shisui_base/get_TCR/get_TCR.tree +++ /dev/null @@ -1,7 +0,0 @@ -test_get_TCR.cairo -├── when the _asset is not whitelisted -│ └── it should return max u256 -└── when the _asset is whitelisted - ├── when the _price is 0 - │ └── it should return 0 - └── it should return the correct amount diff --git a/tests/integration/components/shisui_base/get_TCR/test_get_TCR.cairo b/tests/integration/components/shisui_base/get_TCR/test_get_TCR.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/components/shisui_base/get_TCR/test_get_TCR.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration/components/shisui_base/get_coll_gas_compensation/get_coll_gas_compensation.tree b/tests/integration/components/shisui_base/get_coll_gas_compensation/get_coll_gas_compensation.tree deleted file mode 100644 index 79464f6..0000000 --- a/tests/integration/components/shisui_base/get_coll_gas_compensation/get_coll_gas_compensation.tree +++ /dev/null @@ -1,5 +0,0 @@ -test_get_coll_gas_compensation.cairo -├── when the _asset is not whitelisted -│ └── it should revert -└── when the _asset is whitelisted - └── it should return the correct gas compensation diff --git a/tests/integration/components/shisui_base/get_coll_gas_compensation/test_get_coll_gas_compensation.cairo b/tests/integration/components/shisui_base/get_coll_gas_compensation/test_get_coll_gas_compensation.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/components/shisui_base/get_coll_gas_compensation/test_get_coll_gas_compensation.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration/components/shisui_base/get_composite_debt/get_composite_debt.tree b/tests/integration/components/shisui_base/get_composite_debt/get_composite_debt.tree deleted file mode 100644 index 4b549d0..0000000 --- a/tests/integration/components/shisui_base/get_composite_debt/get_composite_debt.tree +++ /dev/null @@ -1,5 +0,0 @@ -test_get_composite_debt.cairo -├── when the _asset is not whitelisted -│ └── it should return the _debt -└── when the _asset is whitelisted - └── it should return the correct composite debt diff --git a/tests/integration/components/shisui_base/get_composite_debt/test_get_composite_debt.cairo b/tests/integration/components/shisui_base/get_composite_debt/test_get_composite_debt.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/components/shisui_base/get_composite_debt/test_get_composite_debt.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration/components/shisui_base/get_entire_system_coll/get_entire_system_coll.tree b/tests/integration/components/shisui_base/get_entire_system_coll/get_entire_system_coll.tree deleted file mode 100644 index f2390c1..0000000 --- a/tests/integration/components/shisui_base/get_entire_system_coll/get_entire_system_coll.tree +++ /dev/null @@ -1,5 +0,0 @@ -test_get_entire_system_coll.cairo -├── when the _asset is not whitelisted -│ └── it should return 0 -└── when the _asset is whitelisted - └── it should return the correct amount diff --git a/tests/integration/components/shisui_base/get_entire_system_coll/test_get_entire_system_coll.cairo b/tests/integration/components/shisui_base/get_entire_system_coll/test_get_entire_system_coll.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/components/shisui_base/get_entire_system_coll/test_get_entire_system_coll.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration/components/shisui_base/get_entire_system_debt/get_entire_system_debt.tree b/tests/integration/components/shisui_base/get_entire_system_debt/get_entire_system_debt.tree deleted file mode 100644 index 3852b9e..0000000 --- a/tests/integration/components/shisui_base/get_entire_system_debt/get_entire_system_debt.tree +++ /dev/null @@ -1,5 +0,0 @@ -test_get_entire_system_debt.cairo -├── when the _asset is not whitelisted -│ └── it should return 0 -└── when the _asset is whitelisted - └── it should return the correct amount diff --git a/tests/integration/components/shisui_base/get_entire_system_debt/test_get_entire_system_debt.cairo b/tests/integration/components/shisui_base/get_entire_system_debt/test_get_entire_system_debt.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/components/shisui_base/get_entire_system_debt/test_get_entire_system_debt.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration/components/shisui_base/get_net_debt/get_net_debt.tree b/tests/integration/components/shisui_base/get_net_debt/get_net_debt.tree deleted file mode 100644 index 26350dc..0000000 --- a/tests/integration/components/shisui_base/get_net_debt/get_net_debt.tree +++ /dev/null @@ -1,5 +0,0 @@ -test_get_net_debt.cairo -├── when the _asset is not whitelisted -│ └── it should return the _debt -└── when the _asset is whitelisted - └── it should return the correct net debt diff --git a/tests/integration/components/shisui_base/get_net_debt/test_get_net_debt.cairo b/tests/integration/components/shisui_base/get_net_debt/test_get_net_debt.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/components/shisui_base/get_net_debt/test_get_net_debt.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/integration/components/shisui_base/require_user_accepts_fee/require_user_accepts_fee.tree b/tests/integration/components/shisui_base/require_user_accepts_fee/require_user_accepts_fee.tree deleted file mode 100644 index 1f73280..0000000 --- a/tests/integration/components/shisui_base/require_user_accepts_fee/require_user_accepts_fee.tree +++ /dev/null @@ -1,7 +0,0 @@ -test_require_user_accepts_fee.cairo -├── when the _max_fee_percentage is zero -│ └── it should revert -└── when the _max_fee_percentage is not zero - ├── when the _max_fee_percentage is lower than the calculated fee - │ └── it should revert - └── it not revert diff --git a/tests/integration/components/shisui_base/require_user_accepts_fee/test_require_user_accepts_fee.cairo b/tests/integration/components/shisui_base/require_user_accepts_fee/test_require_user_accepts_fee.cairo deleted file mode 100644 index 8b13789..0000000 --- a/tests/integration/components/shisui_base/require_user_accepts_fee/test_require_user_accepts_fee.cairo +++ /dev/null @@ -1 +0,0 @@ - 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/integration/pools/sorted_vessels/insert/insert.tree b/tests/integration/pools/sorted_vessels/insert/insert.tree index 8facddf..f94e9d7 100644 --- a/tests/integration/pools/sorted_vessels/insert/insert.tree +++ b/tests/integration/pools/sorted_vessels/insert/insert.tree @@ -8,15 +8,13 @@ test_insert.cairo ├── when the NICR is zero │ └── it should revert with VesselManagerErrors__NICRIsZero └── when the insert position is valid - ├── when the list is empty - │ └── it should insert the node and set head and tail to the node id - ├── when the prev_id is address zero - │ └── it should insert the node and set head to the node id - ├── when the next_id is address zero - │ └── it should insert the node and set tail to the node id - ├── when the next_id and prev_id are not address zero - │ └── it should correclty insert the node between the two - ├── when the insert position is not valid - │ └── it should find the correct position and insert the node - ├── it should increase the length of the list by one - └── it should emit {NodeAdded} event \ No newline at end of file +│ ├── when the list is empty +│ │ └── it should insert the node and set head and tail to the node id +│ ├── when the prev_id is address zero +│ │ └── it should insert the node and set head to the node id +│ ├── when the next_id is address zero +│ │ └── it should insert the node and set tail to the node id +│ └── when the next_id and prev_id are not address zero +│ └── it should correclty insert the node between the two +└── when the insert position is not valid + └── it should find the correct position and insert the node diff --git a/tests/integration/pools/sorted_vessels/insert/test_insert.cairo b/tests/integration/pools/sorted_vessels/insert/test_insert.cairo index 8b13789..58bd425 100644 --- a/tests/integration/pools/sorted_vessels/insert/test_insert.cairo +++ b/tests/integration/pools/sorted_vessels/insert/test_insert.cairo @@ -1 +1,210 @@ +use core::{array::{ArrayTrait}, debug::PrintTrait}; +use starknet::{ContractAddress, get_caller_address, contract_address_const}; +use shisui::{ + core::address_provider::{ + IAddressProviderDispatcher, IAddressProviderDispatcherTrait, AddressesKey + }, + pools::sorted_vessels::{ + ISortedVesselsDispatcher, ISortedVesselsDispatcherTrait, + SortedVessels::{Node, Event, NodeAdded} + } +}; +use tests::tests_lib::{deploy_address_provider, deploy_sorted_vessels}; +use snforge_std::{ + start_prank, stop_prank, CheatTarget, spy_events, SpyOn, EventSpy, EventAssertions, + start_mock_call +}; +fn setup() -> ( + IAddressProviderDispatcher, + ISortedVesselsDispatcher, + ContractAddress, + ContractAddress, + ContractAddress, + ContractAddress, + ContractAddress, + ContractAddress, + ContractAddress +) { + let address_provider = IAddressProviderDispatcher { + contract_address: deploy_address_provider() + }; + let sorted_vessels = ISortedVesselsDispatcher { + contract_address: deploy_sorted_vessels(address_provider.contract_address) + }; + let borrower_operations = contract_address_const::<'borrower_operations'>(); + let vessel_manager = contract_address_const::<'vessel_manager'>(); + let asset = contract_address_const::<'asset'>(); + let user_1 = contract_address_const::<'user_1'>(); + let user_2 = contract_address_const::<'user_2'>(); + let user_3 = contract_address_const::<'user_3'>(); + let address_zero = contract_address_const::<0>(); + + address_provider.set_address(AddressesKey::borrower_operations, borrower_operations); + address_provider.set_address(AddressesKey::vessel_manager, vessel_manager); + + ( + address_provider, + sorted_vessels, + borrower_operations, + vessel_manager, + asset, + user_1, + user_2, + user_3, + address_zero + ) +} + +#[test] +#[should_panic(expected: ('Caller is not authorized',))] +fn given_caller_is_not_borrower_operations_nor_vessel_manager_it_should_revert() { + let (_, sorted_vessels, _, _, asset, user_1, _, _, address_zero) = setup(); + sorted_vessels.insert(asset, user_1, 100, address_zero, address_zero); +} + +#[test] +#[should_panic(expected: ('Node already exists',))] +fn given_node_already_exists_it_should_revert() { + let (_, sorted_vessels, borrower_operations, _, asset, user_1, _, _, address_zero) = setup(); + start_prank(CheatTarget::One(sorted_vessels.contract_address), borrower_operations); + sorted_vessels.insert(asset, user_1, 100, address_zero, address_zero); + sorted_vessels.insert(asset, user_1, 100, address_zero, address_zero); + stop_prank(CheatTarget::One(sorted_vessels.contract_address)); +} + +#[test] +#[should_panic(expected: ('Address is zero',))] +fn given_id_is_address_zero_it_should_revert() { + let (_, sorted_vessels, borrower_operations, _, asset, _, _, _, address_zero) = setup(); + start_prank(CheatTarget::One(sorted_vessels.contract_address), borrower_operations); + sorted_vessels.insert(asset, address_zero, 100, address_zero, address_zero); + stop_prank(CheatTarget::One(sorted_vessels.contract_address)); +} + +#[test] +#[should_panic(expected: ('NICR must be positive',))] +fn given_NICR_is_zero_it_should_revert() { + let (_, sorted_vessels, borrower_operations, _, asset, user_1, _, _, address_zero) = setup(); + start_prank(CheatTarget::One(sorted_vessels.contract_address), borrower_operations); + sorted_vessels.insert(asset, user_1, 0, address_zero, address_zero); + stop_prank(CheatTarget::One(sorted_vessels.contract_address)); +} + +#[test] +fn given_valid_insert_position_it_should_insert_with_empty_list() { + let (_, sorted_vessels, borrower_operations, _, asset, user_1, _, _, address_zero) = setup(); + start_prank(CheatTarget::One(sorted_vessels.contract_address), borrower_operations); + let mut spy = spy_events(SpyOn::One(sorted_vessels.contract_address)); + sorted_vessels.insert(asset, user_1, 100, address_zero, address_zero); + + // event check + spy + .assert_emitted( + @array![ + ( + sorted_vessels.contract_address, + Event::NodeAdded(NodeAdded { asset: asset, id: user_1, NICR: 100, }) + ) + ] + ); + + assert(spy.events.len() == 0, 'There should be no events'); + assert(sorted_vessels.is_empty(asset) == false, 'List is empty'); + assert(sorted_vessels.get_size(asset) == 1, 'Invalid list size'); + assert(sorted_vessels.get_first(asset) == user_1, 'Invalid first node'); + assert(sorted_vessels.get_last(asset) == user_1, 'Invalid last node'); + assert(sorted_vessels.get_next(asset, user_1) == address_zero, 'Invalid next node'); + assert(sorted_vessels.get_prev(asset, user_1) == address_zero, 'Invalid prev node'); + + stop_prank(CheatTarget::One(sorted_vessels.contract_address)); +} + +#[test] +fn given_valid_insert_point_it_should_insert_first() { + let (_, sorted_vessels, borrower_operations, _, asset, user_1, user_2, _, address_zero) = + setup(); + start_prank(CheatTarget::One(sorted_vessels.contract_address), borrower_operations); + sorted_vessels.insert(asset, user_1, 100, address_zero, address_zero); + sorted_vessels.insert(asset, user_2, 50, address_zero, user_1); + + assert(sorted_vessels.is_empty(asset) == false, 'List is empty'); + assert(sorted_vessels.get_size(asset) == 2, 'Invalid list size'); + assert(sorted_vessels.get_first(asset) == user_2, 'Invalid first node'); + assert(sorted_vessels.get_last(asset) == user_1, 'Invalid last node'); + assert(sorted_vessels.get_next(asset, user_2) == user_1, 'Invalid next node'); + assert(sorted_vessels.get_prev(asset, user_2) == address_zero, 'Invalid prev node'); + assert(sorted_vessels.get_next(asset, user_1) == address_zero, 'Invalid next node'); + assert(sorted_vessels.get_prev(asset, user_1) == user_2, 'Invalid prev node'); + + stop_prank(CheatTarget::One(sorted_vessels.contract_address)); +} + +#[test] +fn given_valid_insert_point_it_should_insert_last() { + let (_, sorted_vessels, borrower_operations, _, asset, user_1, user_2, _, address_zero) = + setup(); + start_prank(CheatTarget::One(sorted_vessels.contract_address), borrower_operations); + sorted_vessels.insert(asset, user_1, 100, address_zero, address_zero); + sorted_vessels.insert(asset, user_2, 150, user_1, address_zero); + + assert(sorted_vessels.is_empty(asset) == false, 'List is empty'); + assert(sorted_vessels.get_size(asset) == 2, 'Invalid list size'); + assert(sorted_vessels.get_first(asset) == user_1, 'Invalid first node'); + assert(sorted_vessels.get_last(asset) == user_2, 'Invalid last node'); + assert(sorted_vessels.get_next(asset, user_1) == user_2, 'Invalid next node'); + assert(sorted_vessels.get_prev(asset, user_1) == address_zero, 'Invalid prev node'); + assert(sorted_vessels.get_next(asset, user_2) == address_zero, 'Invalid next node'); + assert(sorted_vessels.get_prev(asset, user_2) == user_1, 'Invalid prev node'); + + stop_prank(CheatTarget::One(sorted_vessels.contract_address)); +} + +#[test] +fn given_valid_insert_point_it_should_insert_middle() { + let (_, sorted_vessels, borrower_operations, _, asset, user_1, user_2, user_3, address_zero) = + setup(); + start_prank(CheatTarget::One(sorted_vessels.contract_address), borrower_operations); + sorted_vessels.insert(asset, user_1, 100, address_zero, address_zero); + sorted_vessels.insert(asset, user_3, 150, user_1, address_zero); + let mut spy = spy_events(SpyOn::One(sorted_vessels.contract_address)); + sorted_vessels.insert(asset, user_2, 125, user_1, user_3); + + assert(sorted_vessels.is_empty(asset) == false, 'List is empty'); + assert(sorted_vessels.get_size(asset) == 3, 'Invalid list size'); + assert(sorted_vessels.get_first(asset) == user_1, 'Invalid first node'); + assert(sorted_vessels.get_last(asset) == user_3, 'Invalid last node'); + assert(sorted_vessels.get_next(asset, user_1) == user_2, 'Invalid next node'); + assert(sorted_vessels.get_prev(asset, user_1) == address_zero, 'Invalid prev node'); + assert(sorted_vessels.get_next(asset, user_2) == user_3, 'Invalid next node'); + assert(sorted_vessels.get_prev(asset, user_2) == user_1, 'Invalid prev node'); + assert(sorted_vessels.get_next(asset, user_3) == address_zero, 'Invalid next node'); + assert(sorted_vessels.get_prev(asset, user_3) == user_2, 'Invalid prev node'); + + stop_prank(CheatTarget::One(sorted_vessels.contract_address)); +} + +#[test] +fn given_invalid_insert_position_it_should_find_correct_position_and_insert() { + let (_, sorted_vessels, borrower_operations, _, asset, user_1, user_2, user_3, address_zero) = + setup(); + + start_prank(CheatTarget::One(sorted_vessels.contract_address), borrower_operations); + + sorted_vessels.insert(asset, user_1, 100, address_zero, address_zero); + sorted_vessels.insert(asset, user_3, 150, user_1, address_zero); + sorted_vessels.insert(asset, user_2, 125, user_1, address_zero); + + assert(sorted_vessels.is_empty(asset) == false, 'List is empty'); + assert(sorted_vessels.get_size(asset) == 3, 'Invalid list size'); + assert(sorted_vessels.get_first(asset) == user_1, 'Invalid first node'); + assert(sorted_vessels.get_last(asset) == user_3, 'Invalid last node'); + assert(sorted_vessels.get_next(asset, user_1) == user_2, 'Invalid next node'); + assert(sorted_vessels.get_prev(asset, user_1) == address_zero, 'Invalid prev node'); + assert(sorted_vessels.get_next(asset, user_2) == user_3, 'Invalid next node'); + assert(sorted_vessels.get_prev(asset, user_2) == user_1, 'Invalid prev node'); + assert(sorted_vessels.get_next(asset, user_3) == address_zero, 'Invalid next node'); + assert(sorted_vessels.get_prev(asset, user_3) == user_2, 'Invalid prev node'); + + stop_prank(CheatTarget::One(sorted_vessels.contract_address)); +} diff --git a/tests/lib.cairo b/tests/lib.cairo index 293331c..db7e3ff 100644 --- a/tests/lib.cairo +++ b/tests/lib.cairo @@ -34,34 +34,6 @@ mod unit { } mod integration { - mod components { - mod shisui_base { - mod check_recovery_mode { - mod test_check_recovery_mode; - } - mod get_coll_gas_compensation { - mod test_get_coll_gas_compensation; - } - mod get_composite_debt { - mod test_get_composite_debt; - } - mod get_entire_system_coll { - mod test_get_entire_system_coll; - } - mod get_entire_system_debt { - mod test_get_entire_system_debt; - } - mod get_net_debt { - mod test_get_net_debt; - } - mod get_TCR { - mod test_get_TCR; - } - mod require_user_accepts_fee { - mod test_require_user_accepts_fee; - } - } - } mod core { mod admin_contract { mod add_new_collateral { @@ -125,30 +97,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; } diff --git a/tests/tests_lib.cairo b/tests/tests_lib.cairo index 2dfeb41..e47bfb2 100644 --- a/tests/tests_lib.cairo +++ b/tests/tests_lib.cairo @@ -73,3 +73,23 @@ fn deploy_address_provider() -> ContractAddress { let contract = declare('AddressProvider'); deploy_mock_contract(contract, @array![]) } + +/// Utility function to deploy a SortedVessels contract and return its address. +/// +/// # Returns +/// +/// * `ContractAddress` - The address of the deployed data store contract. +fn deploy_sorted_vessels(address_provider: ContractAddress) -> ContractAddress { + let contract = declare('SortedVessels'); + deploy_mock_contract(contract, @array![address_provider.into()]) +} + +/// 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()]) +}