diff --git a/contracts/borrow-operations-contract/Forc.toml b/contracts/borrow-operations-contract/Forc.toml index eab2707..f010e5d 100644 --- a/contracts/borrow-operations-contract/Forc.toml +++ b/contracts/borrow-operations-contract/Forc.toml @@ -7,3 +7,4 @@ name = "borrow-operations-contract" [dependencies] libraries = { path = "../../libraries" } standards = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.6.1" } +sway_libs = { git = "https://github.com/FuelLabs/sway-libs", tag = "v0.23.1" } diff --git a/contracts/borrow-operations-contract/src/events.sw b/contracts/borrow-operations-contract/src/events.sw new file mode 100644 index 0000000..04d2919 --- /dev/null +++ b/contracts/borrow-operations-contract/src/events.sw @@ -0,0 +1,26 @@ +library; + +pub struct OpenTroveEvent { + pub user: Identity, + pub asset_id: AssetId, + pub collateral: u64, + pub debt: u64, +} + +pub struct AdjustTroveEvent { + pub user: Identity, + pub asset_id: AssetId, + pub collateral_change: u64, + pub debt_change: u64, + pub is_collateral_increase: bool, + pub is_debt_increase: bool, + pub total_collateral: u64, + pub total_debt: u64, +} + +pub struct CloseTroveEvent { + pub user: Identity, + pub asset_id: AssetId, + pub collateral: u64, + pub debt: u64, +} diff --git a/contracts/borrow-operations-contract/src/main.sw b/contracts/borrow-operations-contract/src/main.sw index 843d1ae..36d46a1 100644 --- a/contracts/borrow-operations-contract/src/main.sw +++ b/contracts/borrow-operations-contract/src/main.sw @@ -11,9 +11,11 @@ contract; // - Enforcing system parameters and stability conditions mod data_structures; +mod events; use standards::{src3::SRC3,}; use ::data_structures::{AssetContracts, LocalVariablesAdjustTrove, LocalVariablesOpenTrove}; +use ::events::{AdjustTroveEvent, CloseTroveEvent, OpenTroveEvent}; use libraries::trove_manager_interface::data_structures::Status; use libraries::active_pool_interface::ActivePool; use libraries::token_interface::Token; @@ -24,6 +26,7 @@ use libraries::coll_surplus_pool_interface::CollSurplusPool; use libraries::oracle_interface::Oracle; use libraries::borrow_operations_interface::BorrowOperations; use libraries::fluid_math::*; +use sway_libs::ownership::*; use std::{ asset::transfer, auth::msg_sender, @@ -34,6 +37,7 @@ use std::{ msg_amount, }, hash::*, + logging::log, }; configurable { @@ -90,9 +94,28 @@ impl BorrowOperations for Contract { .usdf_asset_id .write(AssetId::new(usdf_contract, SubId::zero())); storage.pauser.write(msg_sender().unwrap()); + initialize_ownership(msg_sender().unwrap()); storage.is_initialized.write(true); } + #[storage(read, write)] + fn set_pauser(pauser: Identity) { + only_owner(); + storage.pauser.write(pauser); + } + + #[storage(read, write)] + fn transfer_owner(new_owner: Identity) { + only_owner(); + transfer_ownership(new_owner); + } + + #[storage(read, write)] + fn renounce_owner() { + only_owner(); + renounce_ownership(); + } + // --- Borrower Trove Operations --- // Open a new trove by borrowing USDF // Differences from Liquity:0% frontend fees, no recovery mode, no gas compensation @@ -138,6 +161,12 @@ impl BorrowOperations for Contract { usdf_contract, asset_contract, ); + log(OpenTroveEvent { + user: sender, + asset_id: asset_contract, + collateral: msg_amount(), + debt: vars.net_debt, + }); } // Add collateral to an existing trove #[storage(read, write), payable] @@ -265,6 +294,12 @@ impl BorrowOperations for Contract { transfer(borrower, usdf_asset_id, excess_usdf_returned); } + log(CloseTroveEvent { + user: borrower, + asset_id: asset_contract, + collateral: coll, + debt: debt, + }); storage.lock_close_trove.write(false); } // Claim collateral from liquidations @@ -435,6 +470,16 @@ fn internal_adjust_trove( usdf_contract_cache, ); + log(AdjustTroveEvent { + user: borrower, + asset_id: asset, + collateral_change: vars.coll_change, + debt_change: vars.net_debt_change, + is_collateral_increase: vars.is_coll_increase, + is_debt_increase: is_debt_increase, + total_collateral: new_position_res.0, + total_debt: new_position_res.1, + }); storage.lock_internal_adjust_trove.write(false); } diff --git a/contracts/borrow-operations-contract/tests/events.rs b/contracts/borrow-operations-contract/tests/events.rs new file mode 100644 index 0000000..6200105 --- /dev/null +++ b/contracts/borrow-operations-contract/tests/events.rs @@ -0,0 +1,231 @@ +use fuels::{prelude::*, types::Identity}; + +use test_utils::{ + data_structures::PRECISION, + interfaces::{ + borrow_operations::{borrow_operations_abi, BorrowOperations}, + oracle::oracle_abi, + pyth_oracle::{pyth_oracle_abi, pyth_price_feed, PYTH_TIMESTAMP}, + token::token_abi, + }, + setup::common::setup_protocol, + utils::with_min_borrow_fee, +}; + +#[tokio::test] +async fn test_trove_events() { + let (contracts, admin, wallets) = setup_protocol(4, false, false).await; + + // Setup initial conditions + token_abi::mint_to_id( + &contracts.asset_contracts[0].asset, + 5000 * PRECISION, + Identity::Address(admin.address().into()), + ) + .await; + + let deposit_amount = 1200 * PRECISION; + let borrow_amount = 600 * PRECISION; + let additional_collateral = 300 * PRECISION; + + // Set oracle price + oracle_abi::set_debug_timestamp(&contracts.asset_contracts[0].oracle, PYTH_TIMESTAMP).await; + pyth_oracle_abi::update_price_feeds( + &contracts.asset_contracts[0].mock_pyth_oracle, + pyth_price_feed(1), + ) + .await; + + // Test OpenTroveEvent + let response = borrow_operations_abi::open_trove( + &contracts.borrow_operations, + &contracts.asset_contracts[0].oracle, + &contracts.asset_contracts[0].mock_pyth_oracle, + &contracts.asset_contracts[0].mock_redstone_oracle, + &contracts.asset_contracts[0].asset, + &contracts.usdf, + &contracts.fpt_staking, + &contracts.sorted_troves, + &contracts.asset_contracts[0].trove_manager, + &contracts.active_pool, + deposit_amount, + borrow_amount, + Identity::Address(Address::zeroed()), + Identity::Address(Address::zeroed()), + ) + .await + .unwrap(); + + let logs = response.decode_logs(); + let open_trove_event = logs + .results + .iter() + .find(|log| log.as_ref().unwrap().contains("OpenTroveEvent")) + .expect("OpenTroveEvent not found") + .as_ref() + .unwrap(); + + assert!( + open_trove_event.contains(&admin.address().hash().to_string()), + "OpenTroveEvent should contain user address" + ); + assert!( + open_trove_event.contains(&deposit_amount.to_string()), + "OpenTroveEvent should contain collateral amount" + ); + assert!( + open_trove_event.contains(&with_min_borrow_fee(borrow_amount).to_string()), + "OpenTroveEvent should contain debt amount" + ); + assert!( + open_trove_event.contains(&contracts.asset_contracts[0].asset_id.to_string()), + "OpenTroveEvent should contain asset id" + ); + + // Test AdjustTroveEvent + let response = borrow_operations_abi::add_coll( + &contracts.borrow_operations, + &contracts.asset_contracts[0].oracle, + &contracts.asset_contracts[0].mock_pyth_oracle, + &contracts.asset_contracts[0].mock_redstone_oracle, + &contracts.asset_contracts[0].asset, + &contracts.usdf, + &contracts.sorted_troves, + &contracts.asset_contracts[0].trove_manager, + &contracts.active_pool, + additional_collateral, + Identity::Address(Address::zeroed()), + Identity::Address(Address::zeroed()), + ) + .await + .unwrap(); + + let logs = response.decode_logs(); + let adjust_event = logs + .results + .iter() + .find(|log| log.as_ref().unwrap().contains("AdjustTroveEvent")) + .expect("AdjustTroveEvent not found") + .as_ref() + .unwrap(); + println!("adjust_event: {:?}", adjust_event); + assert!( + adjust_event.contains(&admin.address().hash().to_string()), + "AdjustTroveEvent should contain user address" + ); + assert!( + adjust_event.contains(&additional_collateral.to_string()), + "AdjustTroveEvent should contain collateral change amount" + ); + assert!( + adjust_event.contains("is_collateral_increase: true"), + "AdjustTroveEvent should indicate collateral increase" + ); + assert!( + adjust_event.contains("is_debt_increase: false"), + "AdjustTroveEvent should indicate debt is not increased" + ); + // assetid + assert!( + adjust_event.contains(&contracts.asset_contracts[0].asset_id.to_string()), + "AdjustTroveEvent should contain asset id" + ); + // total debt + assert!( + adjust_event.contains(&with_min_borrow_fee(borrow_amount).to_string()), + "AdjustTroveEvent should contain total debt" + ); + // total coll + assert!( + adjust_event.contains(&(deposit_amount + additional_collateral).to_string()), + "AdjustTroveEvent should contain total collateral" + ); + + // create one more trove to allow for closing + + let second_wallet = wallets[1].clone(); + + token_abi::mint_to_id( + &contracts.asset_contracts[0].asset, + 5000 * PRECISION, + Identity::Address(second_wallet.address().into()), + ) + .await; + + let borrow_operations_second_wallet = BorrowOperations::new( + contracts.borrow_operations.contract_id().clone(), + second_wallet.clone(), + ); + + borrow_operations_abi::open_trove( + &borrow_operations_second_wallet, + &contracts.asset_contracts[0].oracle, + &contracts.asset_contracts[0].mock_pyth_oracle, + &contracts.asset_contracts[0].mock_redstone_oracle, + &contracts.asset_contracts[0].asset, + &contracts.usdf, + &contracts.fpt_staking, + &contracts.sorted_troves, + &contracts.asset_contracts[0].trove_manager, + &contracts.active_pool, + deposit_amount, + borrow_amount, + Identity::Address(Address::zeroed()), + Identity::Address(Address::zeroed()), + ) + .await + .unwrap(); + + second_wallet + .transfer( + &admin.address(), + borrow_amount, + contracts.usdf_asset_id, + TxPolicies::default(), + ) + .await + .unwrap(); + + // Test CloseTroveEvent + let response = borrow_operations_abi::close_trove( + &contracts.borrow_operations, + &contracts.asset_contracts[0].oracle, + &contracts.asset_contracts[0].mock_pyth_oracle, + &contracts.asset_contracts[0].mock_redstone_oracle, + &contracts.asset_contracts[0].asset, + &contracts.usdf, + &contracts.fpt_staking, + &contracts.sorted_troves, + &contracts.asset_contracts[0].trove_manager, + &contracts.active_pool, + with_min_borrow_fee(borrow_amount), + ) + .await + .unwrap(); + + let logs = response.decode_logs(); + let close_event = logs + .results + .iter() + .find(|log| log.as_ref().unwrap().contains("CloseTroveEvent")) + .expect("CloseTroveEvent not found") + .as_ref() + .unwrap(); + + assert!( + close_event.contains(&admin.address().hash().to_string()), + "CloseTroveEvent should contain user address" + ); + assert!( + close_event.contains(&contracts.asset_contracts[0].asset_id.to_string()), + "CloseTroveEvent should contain asset id" + ); + assert!( + close_event.contains(&(deposit_amount + additional_collateral).to_string()), + "CloseTroveEvent should contain collateral amount" + ); + assert!( + close_event.contains(&with_min_borrow_fee(borrow_amount).to_string()), + "CloseTroveEvent should contain debt amount" + ); +} diff --git a/contracts/borrow-operations-contract/tests/pausing.rs b/contracts/borrow-operations-contract/tests/pausing.rs index 7ab4ec8..581305f 100644 --- a/contracts/borrow-operations-contract/tests/pausing.rs +++ b/contracts/borrow-operations-contract/tests/pausing.rs @@ -13,18 +13,90 @@ use test_utils::{ #[tokio::test] async fn test_permissions() { - let (contracts, admin, mut wallets) = setup_protocol(2, false, false).await; + let (contracts, admin, mut wallets) = setup_protocol(5, false, false).await; + + // Test set_pauser + let new_pauser = wallets.pop().unwrap(); + let result = borrow_operations_abi::set_pauser( + &contracts.borrow_operations, + Identity::Address(new_pauser.address().into()), + ) + .await; + let borrow_operations_new_pauser = BorrowOperations::new( + contracts.borrow_operations.contract_id().clone(), + new_pauser.clone(), + ); + assert!(result.is_ok(), "Admin should be able to set a new pauser"); + + // Verify unauthorized set_pauser + let unauthorized_wallet = wallets.pop().unwrap(); + let unauthorized_borrow_operations = BorrowOperations::new( + contracts.borrow_operations.contract_id().clone(), + unauthorized_wallet.clone(), + ); + let result = borrow_operations_abi::set_pauser( + &unauthorized_borrow_operations, + Identity::Address(unauthorized_wallet.address().into()), + ) + .await; + assert!( + result.is_err(), + "Unauthorized wallet should not be able to set pauser" + ); + + // Test transfer_owner + let new_owner = wallets.pop().unwrap(); + let result = borrow_operations_abi::transfer_owner( + &contracts.borrow_operations, + Identity::Address(new_owner.address().into()), + ) + .await; + assert!(result.is_ok(), "Admin should be able to transfer ownership"); + + // Verify old owner can't perform admin actions + let result = borrow_operations_abi::set_pauser( + &contracts.borrow_operations, + Identity::Address(admin.address().into()), + ) + .await; + assert!( + result.is_err(), + "Old owner should not be able to set pauser after transfer" + ); + + // Test renounce_owner + let new_borrow_operations = BorrowOperations::new( + contracts.borrow_operations.contract_id().clone(), + new_owner.clone(), + ); + let result = borrow_operations_abi::renounce_owner(&new_borrow_operations).await; + assert!( + result.is_ok(), + "New owner should be able to renounce ownership" + ); + + // Verify no owner can perform admin actions + let result = borrow_operations_abi::set_pauser( + &new_borrow_operations, + Identity::Address(new_owner.address().into()), + ) + .await; + assert!( + result.is_err(), + "No owner should be able to set pauser after renouncement" + ); let pauser = borrow_operations_abi::get_pauser(&contracts.borrow_operations) .await .unwrap(); assert_eq!( pauser.value, - Identity::Address(admin.address().into()), - "Pauser should be the admin" + Identity::Address(new_pauser.address().into()), + "Pauser should be the new pauser" ); + // Test setting pause status to true - let _ = borrow_operations_abi::set_pause_status(&contracts.borrow_operations, true) + let _ = borrow_operations_abi::set_pause_status(&borrow_operations_new_pauser, true) .await .unwrap(); @@ -34,7 +106,7 @@ async fn test_permissions() { assert!(status.value, "Failed to set pause status to true"); // Test setting pause status to false - let _ = borrow_operations_abi::set_pause_status(&contracts.borrow_operations, false) + let _ = borrow_operations_abi::set_pause_status(&borrow_operations_new_pauser, false) .await .unwrap(); let status = borrow_operations_abi::get_is_paused(&contracts.borrow_operations) diff --git a/contracts/fpt-staking-contract/src/events.sw b/contracts/fpt-staking-contract/src/events.sw new file mode 100644 index 0000000..911df22 --- /dev/null +++ b/contracts/fpt-staking-contract/src/events.sw @@ -0,0 +1,11 @@ +library; + +pub struct StakeEvent { + pub user: Identity, + pub amount: u64, +} + +pub struct UnstakeEvent { + pub user: Identity, + pub amount: u64, +} diff --git a/contracts/fpt-staking-contract/src/main.sw b/contracts/fpt-staking-contract/src/main.sw index 75a0637..0406cca 100644 --- a/contracts/fpt-staking-contract/src/main.sw +++ b/contracts/fpt-staking-contract/src/main.sw @@ -1,5 +1,8 @@ contract; +mod events; + +use ::events::{StakeEvent, UnstakeEvent}; use libraries::fluid_math::{ DECIMAL_PRECISION, fm_min, @@ -20,6 +23,7 @@ use std::{ hash::Hash, storage::storage_vec::*, }; + configurable { /// Initializer identity INITIALIZER: Identity = Identity::Address(Address::zero()), @@ -117,6 +121,10 @@ impl FPTStaking for Contract { .total_fpt_staked .write(storage.total_fpt_staked.read() + amount); + log(StakeEvent { + user: id, + amount: amount, + }); storage.lock_stake.write(false); } @@ -163,6 +171,11 @@ impl FPTStaking for Contract { amount_to_withdraw, ); } + + log(UnstakeEvent { + user: id, + amount: amount_to_withdraw, + }); } storage.lock_unstake.write(false); diff --git a/contracts/fpt-staking-contract/tests/events.rs b/contracts/fpt-staking-contract/tests/events.rs new file mode 100644 index 0000000..a741552 --- /dev/null +++ b/contracts/fpt-staking-contract/tests/events.rs @@ -0,0 +1,85 @@ +use fuels::{prelude::*, types::Identity}; + +use test_utils::{ + data_structures::PRECISION, + interfaces::{ + fpt_staking::fpt_staking_abi, + token::{token_abi, Token}, + }, + setup::common::setup_protocol, +}; + +#[tokio::test] +async fn test_staking_events() { + let (contracts, admin, mut wallets) = setup_protocol(4, false, true).await; + + // Setup initial conditions + let mock_token = Token::new( + contracts.fpt_token.contract_id().clone(), + wallets.pop().unwrap().clone(), + ); + + let stake_amount = 5 * PRECISION; + token_abi::mint_to_id( + &mock_token, + stake_amount, + Identity::Address(admin.address().into()), + ) + .await; + + let mock_token_asset_id = mock_token.contract_id().asset_id(&AssetId::zeroed().into()); + + // Test StakeEvent + let response = + fpt_staking_abi::stake(&contracts.fpt_staking, mock_token_asset_id, stake_amount) + .await + .unwrap(); + + let logs = response.decode_logs(); + let stake_event = logs + .results + .iter() + .find(|log| log.as_ref().unwrap().contains("StakeEvent")) + .expect("StakeEvent not found") + .as_ref() + .unwrap(); + + assert!( + stake_event.contains(&admin.address().hash().to_string()), + "StakeEvent should contain user address" + ); + assert!( + stake_event.contains(&stake_amount.to_string()), + "StakeEvent should contain stake amount" + ); + + // Test UnstakeEvent + let unstake_amount = 2 * PRECISION; + let response = fpt_staking_abi::unstake( + &contracts.fpt_staking, + &contracts.usdf, + &mock_token, + &mock_token, + unstake_amount, + ) + .await + .unwrap(); + + let logs = response.decode_logs(); + let unstake_event = logs + .results + .iter() + .find(|log| log.as_ref().unwrap().contains("UnstakeEvent")) + .expect("UnstakeEvent not found") + .as_ref() + .unwrap(); + + assert!( + unstake_event.contains(&admin.address().hash().to_string()), + "UnstakeEvent should contain user address" + ); + assert!( + unstake_event.contains(&unstake_amount.to_string()), + "UnstakeEvent should contain unstake amount" + ); +} diff --git a/contracts/protocol-manager-contract/src/main.sw b/contracts/protocol-manager-contract/src/main.sw index 46929ad..9ca4e28 100644 --- a/contracts/protocol-manager-contract/src/main.sw +++ b/contracts/protocol-manager-contract/src/main.sw @@ -269,6 +269,11 @@ impl ProtocolManager for Contract { storage.lock_redeem_collateral.write(false); } + #[storage(read, write)] + fn transfer_owner(new_owner: Identity) { + only_owner(); + transfer_ownership(new_owner); + } } impl SRC5 for Contract { diff --git a/contracts/protocol-manager-contract/tests/authorization.rs b/contracts/protocol-manager-contract/tests/authorization.rs index ccb0c78..1424a13 100644 --- a/contracts/protocol-manager-contract/tests/authorization.rs +++ b/contracts/protocol-manager-contract/tests/authorization.rs @@ -80,8 +80,57 @@ async fn test_authorizations() { assert!(result.is_err(), "Duplicate asset registration should fail"); + // Test unauthorized transfer + let result = protocol_manager_abi::transfer_owner( + &protocol_manager_attacker, + fuels::types::Identity::Address(attacker.address().into()), + ) + .await; + assert!( + result.is_err(), + "Unauthorized user should not be able to transfer ownership" + ); + if let Err(error) = result { + assert!( + error.to_string().contains("NotOwner"), + "Unexpected error message: {}", + error + ); + } + + // Test authorized transfer + let new_owner = wallets.pop().unwrap(); + let result = protocol_manager_abi::transfer_owner( + &protocol_manager_owner_contract, + fuels::types::Identity::Address(new_owner.address().into()), + ) + .await; + assert!( + result.is_ok(), + "Authorized user should be able to transfer ownership" + ); + + // Verify old owner can't perform admin actions + let asset_contracts = deploy_asset_contracts(&protocol_manager_owner, &None).await; + let result = initialize_asset(&contracts, &asset_contracts).await; + assert!( + result.is_err(), + "Old owner should not be able to initialize an asset after transfer" + ); + if let Err(error) = result { + assert!( + error.to_string().contains("NotOwner"), + "Unexpected error message: {}", + error + ); + } + + let new_protocol_manager_owner = ProtocolManager::new( + contracts.protocol_manager.contract_id().clone(), + new_owner.clone(), + ); // Test 5: Authorized renounce_admin - let result = protocol_manager_abi::renounce_admin(&protocol_manager_owner_contract).await; + let result = protocol_manager_abi::renounce_admin(&new_protocol_manager_owner).await; assert!( result.is_ok(), diff --git a/contracts/protocol-manager-contract/tests/success_redemptions.rs b/contracts/protocol-manager-contract/tests/success_redemptions.rs index 5d3f24b..e9eeea2 100644 --- a/contracts/protocol-manager-contract/tests/success_redemptions.rs +++ b/contracts/protocol-manager-contract/tests/success_redemptions.rs @@ -165,6 +165,24 @@ async fn proper_redemption_from_partially_closed() { &contracts.asset_contracts, ) .await; + + let logs = res.decode_logs(); + let redemption_event = logs + .results + .iter() + .find(|log| log.as_ref().unwrap().contains("RedemptionEvent")) + .expect("RedemptionEvent not found") + .as_ref() + .unwrap(); + + assert!( + redemption_event.contains(&healthy_wallet3.address().hash().to_string()), + "RedemptionEvent should contain user address" + ); + assert!( + redemption_event.contains(&redemption_amount.to_string()), + "RedemptionEvent should contain redemption amount" + ); print_response(&res); let active_pool_asset = active_pool_abi::get_asset( diff --git a/contracts/stability-pool-contract/src/events.sw b/contracts/stability-pool-contract/src/events.sw new file mode 100644 index 0000000..0c29638 --- /dev/null +++ b/contracts/stability-pool-contract/src/events.sw @@ -0,0 +1,21 @@ +library; + +pub struct ProvideToStabilityPoolEvent { + pub user: Identity, + pub amount_to_deposit: u64, + pub initial_amount: u64, + pub compounded_amount: u64, +} + +pub struct WithdrawFromStabilityPoolEvent { + pub user: Identity, + pub amount_to_withdraw: u64, + pub initial_amount: u64, + pub compounded_amount: u64, +} + +pub struct StabilityPoolLiquidationEvent { + pub asset_id: AssetId, + pub debt_to_offset: u64, + pub collateral_to_offset: u64, +} diff --git a/contracts/stability-pool-contract/src/main.sw b/contracts/stability-pool-contract/src/main.sw index 400d134..c62ecee 100644 --- a/contracts/stability-pool-contract/src/main.sw +++ b/contracts/stability-pool-contract/src/main.sw @@ -15,7 +15,14 @@ contract; // Solidity reference: https://github.com/liquity/dev/blob/main/packages/contracts/contracts/StabilityPool.sol mod data_structures; +mod events; use ::data_structures::{AssetContracts, Snapshots}; +use ::events::{ + ProvideToStabilityPoolEvent, + StabilityPoolLiquidationEvent, + WithdrawFromStabilityPoolEvent, +}; + use standards::src3::SRC3; use libraries::trove_manager_interface::data_structures::Status; use libraries::stability_pool_interface::StabilityPool; @@ -183,6 +190,12 @@ impl StabilityPool for Contract { storage .total_usdf_deposits .write(storage.total_usdf_deposits.read() + msg_amount()); + log(ProvideToStabilityPoolEvent { + user: msg_sender().unwrap(), + amount_to_deposit: msg_amount(), + initial_amount: initial_deposit, + compounded_amount: compounded_usdf_deposit, + }); storage.lock_provide_to_stability_pool.write(false); } /* @@ -212,6 +225,12 @@ impl StabilityPool for Contract { internal_pay_out_fpt_gains(msg_sender().unwrap()); // pay out FPT internal_update_deposits_and_snapshots(msg_sender().unwrap(), new_position); send_usdf_to_depositor(msg_sender().unwrap(), usdf_to_withdraw); + log(WithdrawFromStabilityPoolEvent { + user: msg_sender().unwrap(), + amount_to_withdraw: usdf_to_withdraw, + initial_amount: initial_deposit, + compounded_amount: compounded_usdf_deposit, + }); storage.lock_withdraw_from_stability_pool.write(false); } /* @@ -246,6 +265,11 @@ impl StabilityPool for Contract { asset_contract, ); internal_move_offset_coll_and_debt(coll_to_offset, debt_to_offset, asset_contract); + log(StabilityPoolLiquidationEvent { + asset_id: asset_contract, + debt_to_offset: debt_to_offset, + collateral_to_offset: coll_to_offset, + }); storage.lock_offset.write(false); } #[storage(read)] diff --git a/contracts/stability-pool-contract/tests/functions/success.rs b/contracts/stability-pool-contract/tests/functions/success.rs index 62a3cf3..7ff6238 100644 --- a/contracts/stability-pool-contract/tests/functions/success.rs +++ b/contracts/stability-pool-contract/tests/functions/success.rs @@ -67,17 +67,39 @@ async fn proper_stability_deposit() { ) .await .unwrap(); - - let _res = stability_pool_abi::provide_to_stability_pool( + let deposit_amount = 600 * PRECISION; + let res = stability_pool_abi::provide_to_stability_pool( &contracts.stability_pool, &contracts.community_issuance, &contracts.usdf, &contracts.asset_contracts[0].asset, - 600 * PRECISION, + deposit_amount, ) .await .unwrap(); + let logs = res.decode_logs(); + let provide_event = logs + .results + .iter() + .find(|log| { + log.as_ref() + .unwrap() + .contains("ProvideToStabilityPoolEvent") + }) + .expect("ProvideToStabilityPoolEvent not found") + .as_ref() + .unwrap(); + + assert!( + provide_event.contains(&admin.address().hash().to_string()), + "ProvideToStabilityPoolEvent should contain user address" + ); + assert!( + provide_event.contains(&deposit_amount.to_string()), + "ProvideToStabilityPoolEvent should contain deposit amount" + ); + // print_response(&res); stability_pool_utils::assert_pool_asset( @@ -152,7 +174,7 @@ async fn proper_stability_widthdrawl() { ) .await .unwrap(); - + let withdraw_amount = 300 * PRECISION; let res = stability_pool_abi::withdraw_from_stability_pool( &contracts.stability_pool, &contracts.community_issuance, @@ -163,11 +185,32 @@ async fn proper_stability_widthdrawl() { &contracts.asset_contracts[0].mock_pyth_oracle, &contracts.asset_contracts[0].mock_redstone_oracle, &contracts.asset_contracts[0].trove_manager, - 300 * PRECISION, + withdraw_amount, ) .await .unwrap(); - print_response(&res); + + let logs = res.decode_logs(); + let withdraw_event = logs + .results + .iter() + .find(|log| { + log.as_ref() + .unwrap() + .contains("WithdrawFromStabilityPoolEvent") + }) + .expect("WithdrawFromStabilityPoolEvent not found") + .as_ref() + .unwrap(); + + assert!( + withdraw_event.contains(&admin.address().hash().to_string()), + "WithdrawFromStabilityPoolEvent should contain user address" + ); + assert!( + withdraw_event.contains(&withdraw_amount.to_string()), + "WithdrawFromStabilityPoolEvent should contain withdraw amount" + ); stability_pool_utils::assert_pool_asset( &contracts.stability_pool, @@ -176,13 +219,13 @@ async fn proper_stability_widthdrawl() { ) .await; - stability_pool_utils::assert_total_usdf_deposits(&contracts.stability_pool, 300 * PRECISION) + stability_pool_utils::assert_total_usdf_deposits(&contracts.stability_pool, withdraw_amount) .await; stability_pool_utils::assert_compounded_usdf_deposit( &contracts.stability_pool, Identity::Address(admin.address().into()), - 300 * PRECISION, + withdraw_amount, ) .await; @@ -866,7 +909,7 @@ async fn proper_one_sp_depositor_position_multiple_assets() { ) .await; - trove_manager_abi::liquidate( + let res = trove_manager_abi::liquidate( &contracts.asset_contracts[0].trove_manager, &contracts.community_issuance, &contracts.stability_pool, @@ -885,6 +928,25 @@ async fn proper_one_sp_depositor_position_multiple_assets() { .await .unwrap(); + let logs = res.decode_logs(); + let liquidation_event = logs + .results + .iter() + .find(|log| { + log.as_ref() + .unwrap() + .contains("StabilityPoolLiquidationEvent") + }) + .expect("StabilityPoolLiquidationEvent not found") + .as_ref() + .unwrap(); + + assert!( + liquidation_event.contains(&contracts.asset_contracts[0].asset_id.to_string()), + "StabilityPoolLiquidationEvent should contain asset_id" + ); + println!("liquidation_event: {}", liquidation_event); + trove_manager_abi::liquidate( &contracts.asset_contracts[1].trove_manager, &contracts.community_issuance, diff --git a/contracts/trove-manager-contract/src/events.sw b/contracts/trove-manager-contract/src/events.sw new file mode 100644 index 0000000..d112491 --- /dev/null +++ b/contracts/trove-manager-contract/src/events.sw @@ -0,0 +1,20 @@ +library; + +pub struct TroveFullLiquidationEvent { + pub borrower: Identity, + pub debt: u64, + pub collateral: u64, +} + +pub struct TrovePartialLiquidationEvent { + pub borrower: Identity, + pub remaining_debt: u64, + pub remaining_collateral: u64, +} + +pub struct RedemptionEvent { + pub borrower: Identity, + pub usdf_amount: u64, + pub collateral_amount: u64, + pub collateral_price: u64, +} diff --git a/contracts/trove-manager-contract/src/main.sw b/contracts/trove-manager-contract/src/main.sw index 3012e68..7e02245 100644 --- a/contracts/trove-manager-contract/src/main.sw +++ b/contracts/trove-manager-contract/src/main.sw @@ -4,7 +4,7 @@ contract; // It also interfaces with other core contracts like StabilityPool, ActivePool, and DefaultPool. mod data_structures; mod utils; - +mod events; use ::utils::{add_liquidation_vals_to_totals, get_offset_and_redistribution_vals}; use ::data_structures::{ EntireTroveDebtAndColl, @@ -15,6 +15,7 @@ use ::data_structures::{ RedemptionTotals, Trove, }; +use ::events::{RedemptionEvent, TroveFullLiquidationEvent, TrovePartialLiquidationEvent,}; use standards::src3::SRC3; use libraries::trove_manager_interface::TroveManager; use libraries::usdf_token_interface::USDFToken; @@ -43,6 +44,7 @@ use std::{ msg_amount, }, hash::Hash, + logging::log, storage::storage_vec::*, u128::U128, }; @@ -645,6 +647,13 @@ fn internal_apply_liquidation( lower_partial_hint, asset_contract_cache, ); + + // Add TroveUpdatedEvent for partial liquidation + log(TrovePartialLiquidationEvent { + borrower: borrower, + remaining_debt: liquidation_values.remaining_trove_debt, + remaining_collateral: liquidation_values.remaining_trove_coll, + }); } else { // liquidation of entire trove, sends the surplus to the coll surplus pool let coll_surplus_contract = abi(CollSurplusPool, storage.coll_surplus_pool_contract.read().into()); @@ -656,6 +665,13 @@ fn internal_apply_liquidation( .coll_surplus, asset_contract_cache, ); + + // Add TroveLiquidatedEvent for full liquidation + log(TroveFullLiquidationEvent { + borrower: borrower, + debt: liquidation_values.entire_trove_debt, + collateral: liquidation_values.entire_trove_coll, + }); } } #[storage(read, write)] @@ -820,6 +836,13 @@ fn internal_redeem_collateral_from_trove( // Update the stake and total stakes internal_update_stake_and_total_stakes(borrower); } + // Add RedemptionEvent before returning + log(RedemptionEvent { + borrower: borrower, + usdf_amount: single_redemption_values.usdf_lot, + collateral_amount: single_redemption_values.asset_lot, + collateral_price: price, + }); storage .lock_internal_redeem_collateral_from_trove .write(false); @@ -932,16 +955,15 @@ fn internal_compute_new_stake(coll: u64) -> u64 { } return stake; } -/* -* Updates snapshots of system total stakes and total collateral, excluding a given collateral remainder from the calculation. -* Used in a liquidation sequence. -* -* The calculation excludes a portion of collateral that is in the ActivePool: -* -* the total collateral gas compensation from the liquidation sequence -* -* The collateral as compensation must be excluded as it is always sent out at the very end of the liquidation sequence. -*/ +// +// Updates snapshots of system total stakes and total collateral, excluding a given collateral remainder from the calculation. +// Used in a liquidation sequence. +// +// The calculation excludes a portion of collateral that is in the ActivePool: +// the total collateral gas compensation from the liquidation sequence +// +// The collateral as compensation must be excluded as it is always sent out at the very end of the liquidation sequence. +// #[storage(read, write)] fn internal_update_system_snapshots_exclude_coll_remainder(coll_remainder: u64) { storage diff --git a/contracts/trove-manager-contract/tests/success_full_liquidations.rs b/contracts/trove-manager-contract/tests/success_full_liquidations.rs index 265d4b1..650040c 100644 --- a/contracts/trove-manager-contract/tests/success_full_liquidations.rs +++ b/contracts/trove-manager-contract/tests/success_full_liquidations.rs @@ -127,7 +127,7 @@ async fn proper_full_liquidation_enough_usdf_in_sp() { // Wallet 1 has collateral ratio of 110% and wallet 2 has 200% so we can liquidate it - let _res = trove_manager_abi::liquidate( + let res = trove_manager_abi::liquidate( &contracts.asset_contracts[0].trove_manager, &contracts.community_issuance, &contracts.stability_pool, @@ -146,6 +146,21 @@ async fn proper_full_liquidation_enough_usdf_in_sp() { .await .unwrap(); + let logs = res.decode_logs(); + let liquidation_event = logs + .results + .iter() + .find(|log| log.as_ref().unwrap().contains("TroveFullLiquidationEvent")) + .expect("TroveFullLiquidationEvent not found") + .as_ref() + .unwrap(); + + assert!( + liquidation_event.contains(&liquidated_wallet.address().hash().to_string()), + "TroveFullLiquidationEvent should contain user address" + ); + println!("liquidated trove"); + // print_response(&res); let status = trove_manager_abi::get_trove_status( @@ -930,7 +945,7 @@ async fn test_trove_sorting_after_liquidation_and_rewards() { ) .await; - trove_manager_abi::liquidate( + let _res = trove_manager_abi::liquidate( &contracts.asset_contracts[0].trove_manager, &contracts.community_issuance, &contracts.stability_pool, @@ -948,7 +963,6 @@ async fn test_trove_sorting_after_liquidation_and_rewards() { ) .await .unwrap(); - println!("liquidated trove"); multi_trove_getter_utils::assert_sorted_troves_by_cr( &multi_trove_getter, diff --git a/contracts/trove-manager-contract/tests/success_partial_liquidations.rs b/contracts/trove-manager-contract/tests/success_partial_liquidations.rs index d6fbbb9..6a62735 100644 --- a/contracts/trove-manager-contract/tests/success_partial_liquidations.rs +++ b/contracts/trove-manager-contract/tests/success_partial_liquidations.rs @@ -121,7 +121,7 @@ async fn proper_partial_liquidation_enough_usdf_in_sp() { .await; // Wallet 1 has collateral ratio of 110% and wallet 2 has 200% so we can liquidate it - trove_manager_abi::liquidate( + let res = trove_manager_abi::liquidate( &contracts.asset_contracts[0].trove_manager, &contracts.community_issuance, &contracts.stability_pool, @@ -140,6 +140,24 @@ async fn proper_partial_liquidation_enough_usdf_in_sp() { .await .unwrap(); + let logs = res.decode_logs(); + let liquidation_event = logs + .results + .iter() + .find(|log| { + log.as_ref() + .unwrap() + .contains("TrovePartialLiquidationEvent") + }) + .expect("TrovePartialLiquidationEvent not found") + .as_ref() + .unwrap(); + + assert!( + liquidation_event.contains(&wallet1.address().hash().to_string()), + "TrovePartialLiquidationEvent should contain user address" + ); + let status = trove_manager_abi::get_trove_status( &contracts.asset_contracts[0].trove_manager, Identity::Address(wallet1.address().into()), diff --git a/libraries/src/borrow_operations_interface.sw b/libraries/src/borrow_operations_interface.sw index fd6bc3c..e998e77 100644 --- a/libraries/src/borrow_operations_interface.sw +++ b/libraries/src/borrow_operations_interface.sw @@ -18,6 +18,15 @@ abi BorrowOperations { oracle_contract: ContractId, ); + #[storage(read, write)] + fn set_pauser(pauser: Identity); + + #[storage(read, write)] + fn transfer_owner(new_owner: Identity); + + #[storage(read, write)] + fn renounce_owner(); + #[storage(read), payable] fn open_trove(usdf_amount: u64, upper_hint: Identity, lower_hint: Identity); diff --git a/libraries/src/protocol_manager_interface.sw b/libraries/src/protocol_manager_interface.sw index 9ac5b0c..d05f2a8 100644 --- a/libraries/src/protocol_manager_interface.sw +++ b/libraries/src/protocol_manager_interface.sw @@ -29,4 +29,6 @@ abi ProtocolManager { upper_partial_hint: Identity, lower_partial_hint: Identity, ); + #[storage(read, write)] + fn transfer_owner(new_owner: Identity); } diff --git a/test-utils/src/interfaces/borrow_operations.rs b/test-utils/src/interfaces/borrow_operations.rs index 8becdf4..0cb2e16 100644 --- a/test-utils/src/interfaces/borrow_operations.rs +++ b/test-utils/src/interfaces/borrow_operations.rs @@ -299,7 +299,7 @@ pub mod borrow_operations_abi { trove_manager: &TroveManagerContract, active_pool: &ActivePool, amount: u64, - ) -> CallResponse<()> { + ) -> Result, Error> { let tx_params = TxPolicies::default() .with_tip(1) .with_script_gas_limit(2000000); @@ -339,7 +339,6 @@ pub mod borrow_operations_abi { .unwrap() .call() .await - .unwrap() } pub async fn add_asset( @@ -421,6 +420,54 @@ pub mod borrow_operations_abi { .await .unwrap() } + + // Add these new functions to the module + pub async fn set_pauser( + borrow_operations: &BorrowOperations, + pauser: Identity, + ) -> Result, Error> { + let tx_params = TxPolicies::default() + .with_tip(1) + .with_script_gas_limit(2000000); + + borrow_operations + .methods() + .set_pauser(pauser) + .with_tx_policies(tx_params) + .call() + .await + } + + pub async fn transfer_owner( + borrow_operations: &BorrowOperations, + new_owner: Identity, + ) -> Result, Error> { + let tx_params = TxPolicies::default() + .with_tip(1) + .with_script_gas_limit(2000000); + + borrow_operations + .methods() + .transfer_owner(new_owner) + .with_tx_policies(tx_params) + .call() + .await + } + + pub async fn renounce_owner( + borrow_operations: &BorrowOperations, + ) -> Result, Error> { + let tx_params = TxPolicies::default() + .with_tip(1) + .with_script_gas_limit(2000000); + + borrow_operations + .methods() + .renounce_owner() + .with_tx_policies(tx_params) + .call() + .await + } } pub mod borrow_operations_utils { diff --git a/test-utils/src/interfaces/fpt_staking.rs b/test-utils/src/interfaces/fpt_staking.rs index b4af6a9..13550ab 100644 --- a/test-utils/src/interfaces/fpt_staking.rs +++ b/test-utils/src/interfaces/fpt_staking.rs @@ -56,7 +56,7 @@ pub mod fpt_staking_abi { fpt_staking: &FPTStaking, fpt_asset_id: AssetId, fpt_deposit_amount: u64, - ) -> CallResponse<()> { + ) -> Result, Error> { let tx_params = TxPolicies::default() .with_tip(1) .with_script_gas_limit(2000000); @@ -73,7 +73,6 @@ pub mod fpt_staking_abi { .unwrap() .call() .await - .unwrap() } pub async fn unstake( diff --git a/test-utils/src/interfaces/protocol_manager.rs b/test-utils/src/interfaces/protocol_manager.rs index 8345852..1654bd4 100644 --- a/test-utils/src/interfaces/protocol_manager.rs +++ b/test-utils/src/interfaces/protocol_manager.rs @@ -188,4 +188,18 @@ pub mod protocol_manager_abi { .call() .await } + + pub async fn transfer_owner( + protocol_manager: &ProtocolManager, + new_owner: Identity, + ) -> Result, Error> { + let tx_params = TxPolicies::default().with_tip(1); + + protocol_manager + .methods() + .transfer_owner(new_owner) + .with_tx_policies(tx_params) + .call() + .await + } }