Light ModeLight
Light ModeDark

One Bug Per Day

One H/M every day from top Wardens

Checkmark

Join over 445 wardens!

Checkmark

Receive the email at any hour!

Ad

The provide() function does not reset withdrawal requests, allowing an attacker to bypass risk-free yield tactics protection

mediumCode4rena

Lines of code

https://github.com/code-423n4/2024-01-opus/blob/04583e0411dbf8027952d668a8678fda0cb5b160/src/core/absorber.cairo#L441

Vulnerability details

Impact

In the Opus protocol, the absorber is a stability pool that permits yin holders to contribute their yin and participate in liquidations (also known as absorptions) as a consolidated pool. Provided yin from users could be absorbed during an absorption. In return, users receive yang, which usually has a higher value than the absorbed yin. However, in the event of bad debt, it could be less. This situation could lead to risk-free yield frontrunning tactics, where an attacker only provides yin when the absorption brings profit then removing all yin right after that.

The Opus team is aware of this attack vector and has therefore implemented a request-withdraw procedure. Users must first request a withdrawal and wait for a certain period before executing the withdrawal.

However, the provide() method does not reset these request timestamps, which allows an attacker to bypass this safeguard.

Proof of Concept

The attacker's tactics will be:

  1. Deposit a small amount of yin into 100 of his accounts. Note that the total value of these small amounts is negligible. Also please note that the value 100 is just arbitrary value to describe the idea. It could be higher or lower depending on the REQUEST_BASE_TIMELOCK and REQUEST_VALIDITY_PERIOD.
  2. Regularly request withdrawals for these accounts to ensure that at least one account has a valid request at any given time. This can be easily achieved by requesting a withdrawal from the 1st account at timestamp X, the 2nd at X + REQUEST_VALIDITY_PERIOD, the 3rd at X + 2 * REQUEST_VALIDITY_PERIOD, and so on. The 1st account request will expire at X + REQUEST_BASE_TIMELOCK + REQUEST_VALIDITY_PERIOD, but now 2nd request account become valid because it has requested at X + REQUEST_VALIDITY_PERIOD.
  3. Since the provide() function does not reset the request, the attacker can simply select one account with a valid removing request and perform the "risk-free yield tactics". He does this by providing a large amount of yin to this account to earn yield and then immediately calling remove().

Tools Used

Manual Review

Recommended Mitigation Steps

Reset the withdrawal request in the provide() function.

Assessed type

MEV