Closed c4-bot-7 closed 7 months ago
bytes032 marked the issue as sufficient quality report
bytes032 marked the issue as primary issue
tserg (sponsor) disputed
Tokens with callbacks are fundamentally incompatible and should not be onboarded in the first place according to the docs.
The Warden specifies how ERC-777 assets may cause misbehavior in the Opus protocol, however, as the Sponsor correctly specifies the onboarding documentation that was linked to during the contest specifies a clear list of requirements for collaterals and does not indicate support for these types of tokens.
alex-ppg marked the issue as unsatisfactory: Invalid
Lines of code
https://github.com/code-423n4/2024-01-opus/blob/04583e0411dbf8027952d668a8678fda0cb5b160/src/core/absorber.cairo#L793
Vulnerability details
Impact
The
reap_helper(...)
function in the Absorber is used to calculate and transfer the absorbed assets and rewards to the provider. It's triggered whenever a provider takes any action.However, this function doesn't follow the Check-Effects-Interaction (CEI) pattern. As the code snippet below shows, it first calculates the rewards, transfers the rewards to the provider, and then updates the provider's cumulative rewards.
If the token is ERC777 or any token with a call hook to the receiver, an attacker could call back to the Absorber and drain the reward token. This is possible because the cumulative rewards are updated after token transfers.
Proof of Concept
The Absorber has multiple epochs. A new epoch is created when the amount of yin per share drops below a threshold. However, for simplicity, we'll consider only one epoch which is the current epoch in the scenario below:
In the current epoch, assume
asset_amt_per_share = 1000
andprovider_last_reward_cumulative = 0
is the attacker's last reward cumulative. The attacker hasepoch_shares = 1000
shares in the current epoch.The attacker calls
reap()
, which in turn callsreap_helper()
. This function callsget_provider_rewards()
to calculate the provider reward using the code snippet below. The result isreward_amt = 1000 * (1000 - 0) = 1e6
.The
reap_helper()
function callstransfer_assets()
to transfer1e6
reward to the attacker. The token has a call hook to the receiver/attacker address, allowing the attacker to take control and callreap()
again.Since
provider_last_reward_cumulative
is not updated, the same calculation occurs, and the contract continues to transfer1e6
reward to the attacker. Attacker can repeat these steps until the pool is empty.Tools Used
Manual Review
Recommended Mitigation Steps
Consider adding a reentrancy guard to Absorber and follow the CEI pattern in the
reap_helper()
function.Assessed type
Reentrancy