Opus - kfx's results

A cross margin credit protocol with autonomous monetary policy and dynamic risk parameters.

General Information

Platform: Code4rena

Start Date: 09/01/2024

Pot Size: $100,000 USDC

Total HM: 13

Participants: 28

Period: 28 days

Judge: 0xsomeone

Total Solo HM: 8

Id: 319

League: ETH

Opus

Findings Distribution

Researcher Performance

Rank: 7/28

Findings: 2

Award: $4,339.57

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: kfx

Also found by: TrungOre, bin2chen, minhquanym

Labels

bug
3 (High Risk)
primary issue
satisfactory
selected for report
sufficient quality report
H-03

Awards

2751.9218 USDC - $2,751.92

External Links

Lines of code

https://github.com/code-423n4/2024-01-opus/blob/main/src/core/shrine.cairo#L1753

Vulnerability details

Impact

Let's assume two yangs in the system, yang A and yang B, and two users:

  • User U1 with trove #1 with zero A units, 1000 B units, and 500 yin debt;
  • User U2 with trove #2 10000 A unit, 1000 B units, and 500 yin debt.

If the user U1 can force redistribution of their position, then they can steal from the shrine due to a bug in the code. The function redistribute_helper loops through all yangs in order, including those not in the trove #1. Since trove_yang_amt.is_zero() returns true for yang A, the updated_trove_yang_balances array is updated early and then continue statement is executed.

However, since the new_yang_totals array is not updated in the iteration of the loop, some values of updated_trove_yang_balances end up never being used.

Let's assume 100% redistribution. After the all loop is fully executed, the two arrays contain: updated_trove_yang_balances = [(A, 0), (B, 0)]; new_yang_totals = [(B, 1000)];

The final loop of the function is executed just once. Its first and only iteration writes the new total B value. However, it does not update the amount of B in the trove #1, since (B, 0) is the second element of the first array. The final state is that trove #1 still has 1000 units of B, but no more debt. The user U1 can now withdraw all 1000 units from the trove #1.

This bug violates the shrine invariant "The total amount of a yang is equal to the sum of all troves' deposits of that yang (this includes any exceptionally redistributed yangs and their accompanying errors) and the initial amount seeded at the time of add_yang."

Proof of Concept

#[test] fn test_shrine_redistribution_bug() { let shrine: IShrineDispatcher = shrine_utils::shrine_setup_with_feed(Option::None); // Manually set up troves so that all troves uses just yang1 let yangs: Span<ContractAddress> = shrine_utils::three_yang_addrs_reversed(); let yang_addr = *yangs.at(1); // select the middle one let forge_amt: u128 = 1_000_000_000_000_000_000_000; // Set up trove1 with some yang and some debt let trove1_owner = common::trove1_owner_addr(); let redistributed_trove: u64 = common::TROVE_1; start_prank(CheatTarget::All, shrine_utils::admin()); shrine.deposit(yang_addr, redistributed_trove, shrine_utils::TROVE1_YANG1_DEPOSIT.into()); shrine.forge(trove1_owner, redistributed_trove, forge_amt.into(), 0_u128.into()); // Set up trove1 with some yang and some debt let trove2_owner = common::trove2_owner_addr(); let recipient_trove: u64 = common::TROVE_2; shrine.deposit(yang_addr, recipient_trove, shrine_utils::TROVE1_YANG1_DEPOSIT.into()); shrine.forge(trove2_owner, recipient_trove, forge_amt.into(), 0_u128.into()); println!("before:"); println!(" trove1 yang={}", shrine.get_deposit(yang_addr, redistributed_trove).val); println!(" trove2 yang={}", shrine.get_deposit(yang_addr, recipient_trove).val); println!(" total yang: {}", shrine.get_yang_total(yang_addr)); // Simulate complete redistribution of trove1 shrine.redistribute(redistributed_trove, trove1_health.debt, RAY_ONE.into()); println!("after:"); println!(" trove1 yang={}", shrine.get_deposit(yang_addr, redistributed_trove).val); println!(" trove2 yang={}", shrine.get_deposit(yang_addr, recipient_trove).val); println!(" total yang: {}", shrine.get_yang_total(yang_addr)); shrine_utils::assert_shrine_invariants(shrine, yangs, 2); }

Output:

before: trove1 yang=5000000000000000000 trove2 yang=5000000000000000000 trove1 value=2895610636113415002820 ltv=345350299355935952856010534 debt=1000000000000000000000 trove2 value=2895610636113415002820 ltv=345350299355935952856010534 debt=1000000000000000000000 after: trove1 yang=5000000000000000000 trove2 yang=5000000000000000000 trove1 value=2895610636113415002820 ltv=0 debt=0 trove2 value=2895610636113415002820 ltv=690700598711871905712021068 debt=2000000000000000000000

Expected output:

after: trove1 yang=0 trove2 yang=5000000000000000000 trove1 value=0 ltv=0 debt=0 trove2 value=2895610636113415002820 ltv=690700598711871905712021068 debt=2000000000000000000000

One question is how difficult is to force the redistribution? It looks like it's a realistic option in some cases. For instance, the attacker could first drain the stability pool (absorber) by forcing absorbtions until it is empty. A liquidation can be forced by borrowing max amount and then waiting for ltv > threshold happening due to small price fluctuations, potentially even manipulating the price slightly (as only a small change is required to cross the threshold). For a collateral asset with asset's threshold > ABSORPTION_THRESHOLD it's not required that the trove's penalty == max_possible_penalty.

Tools Used

Manual review.

Do not update the array updated_trove_yang_balances before the continue statement.

Assessed type

Loop

#0 - tserg

2024-02-07T08:03:12Z

#1 - c4-pre-sort

2024-02-10T06:04:03Z

bytes032 marked the issue as sufficient quality report

#2 - c4-pre-sort

2024-02-10T06:04:17Z

bytes032 marked the issue as duplicate of #199

#3 - c4-judge

2024-02-23T20:22:28Z

alex-ppg marked the issue as selected for report

#4 - c4-judge

2024-02-26T11:25:14Z

alex-ppg marked the issue as satisfactory

#5 - alex-ppg

2024-02-26T16:48:21Z

The Warden has demonstrated how a debt redistribution will maintain incorrect entries in the updated Yang balances and total Yang balances when skipping over one, weaponizing this behavior to acquire collateral from a shrine.

I believe a high-risk evaluation is apt as collateral of other users is directly impacted.

Findings Information

🌟 Selected for report: 3docSec

Also found by: etherhood, kfx, nmirchev8

Labels

bug
3 (High Risk)
sufficient quality report
upgraded by judge
partial-75
duplicate-9

Awards

1587.6472 USDC - $1,587.65

External Links

Lines of code

https://github.com/code-423n4/2024-01-opus/blob/main/src/core/shrine.cairo#L1046

Vulnerability details

Impact

A whale can force the system to enter recovery mode simply by creating a trove with large deposits and minting a lot of yang.

In the recovery mode, the threshold for all troves is scaled down. If there are some troves in the system with already reasonably high LTV, entering the recovery mode will make these troves' LTV to cross their thresholds. The whale can now liquidate these troves and collect liquidation profits.

Proof of Concept

The PoC for this is in the existing shrine test test_recovery_mode_previously_healthy_trove_now_unhealthy. The user does not get the option to add to their position once the protocol enters the recovery mode, because both making the trove unhealthy and liquidating it can be done in a single transaction.

Tools Used

Manual review and Starknet foundry.

Allow some grace period after triggering recovery mode.

Assessed type

Other

#0 - tserg

2024-02-08T10:13:01Z

#1 - c4-pre-sort

2024-02-10T09:01:11Z

bytes032 marked the issue as sufficient quality report

#2 - c4-pre-sort

2024-02-10T09:01:33Z

bytes032 marked the issue as duplicate of #205

#3 - c4-judge

2024-02-26T16:40:55Z

alex-ppg marked the issue as partial-75

#4 - alex-ppg

2024-02-26T16:41:29Z

A penalty has been applied as the submission fails to identify the flash-loan-based exploitation path (using a whale would put assets at risk), and contains a very broad mitigation plan.

#5 - c4-judge

2024-02-26T18:19:50Z

alex-ppg changed the severity to 3 (High Risk)

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter