Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

Users can flash-loan and deposit/withdraw in `StabilityPool` to get undeserved rewards tokens

Summary

StabilityPool compute users RAAC rewards based on their instantaneous balance of tokens at the time of withdrawal.
Because nothing prevents users to deposit and withdraw in a same block, it is possible to flash-loan a huge amount of tokens to get a proportional amount of rewards, without really participating in the economics of the protocol.

Vulnerability details

Let's see how rewards are computed.
First, the user must deposit rTokens into the StabilityPool:

File: contracts/core/pools/StabilityPool/StabilityPool.sol
174: function deposit(uint256 amount) external nonReentrant whenNotPaused validAmount(amount) {
175: _update();
176: rToken.safeTransferFrom(msg.sender, address(this), amount);
177: uint256 deCRVUSDAmount = calculateDeCRVUSDAmount(amount);
178: deToken.mint(msg.sender, deCRVUSDAmount);
179:
180: userDeposits[msg.sender] += amount;
181: _mintRAACRewards();
182:
183: emit Deposit(msg.sender, amount, deCRVUSDAmount);
184: }

rTokens can be obtained by depositing crvUSD into the LendingPool.
From the snippet above, we can see that depositing rTokens automatically mint deTokens and increase the userDeposit balance.
The _mintRAACRewards() mints the RAAC rewards for the entire pool, that will then be proportionally distributed to users at withdraw.

Once user has deposited rTokens, nothing prevent him to call withdraw() right away:

File: contracts/core/pools/StabilityPool/StabilityPool.sol
224: function withdraw(uint256 deCRVUSDAmount) external nonReentrant whenNotPaused validAmount(deCRVUSDAmount) {
225: _update();
226: if (deToken.balanceOf(msg.sender) < deCRVUSDAmount) revert InsufficientBalance();
227:
228: uint256 rcrvUSDAmount = calculateRcrvUSDAmount(deCRVUSDAmount);
229: uint256 raacRewards = calculateRaacRewards(msg.sender);
230: if (userDeposits[msg.sender] < rcrvUSDAmount) revert InsufficientBalance();
231: userDeposits[msg.sender] -= rcrvUSDAmount;
232:
233: if (userDeposits[msg.sender] == 0) {
234: delete userDeposits[msg.sender];
235: }
236:
237: deToken.burn(msg.sender, deCRVUSDAmount);
238: rToken.safeTransfer(msg.sender, rcrvUSDAmount);
239: if (raacRewards > 0) {
240: raacToken.safeTransfer(msg.sender, raacRewards);
241: }
242:
243: emit Withdraw(msg.sender, rcrvUSDAmount, deCRVUSDAmount, raacRewards);
244: }

In the function above, the function of interest is calculateRaacRewards() at L229 which returns raacRewards:

File: contracts/core/pools/StabilityPool/StabilityPool.sol
251: function calculateRaacRewards(address user) public view returns (uint256) {
252: uint256 userDeposit = userDeposits[user];
253: uint256 totalDeposits = deToken.totalSupply();
254:
255: uint256 totalRewards = raacToken.balanceOf(address(this));
256: if (totalDeposits < 1e6) return 0;
257:
258: return (totalRewards * userDeposit) / totalDeposits;
259: }

The function compute the user reward based on the instantaneous userDeposit and the totalDeposits and return his share of the total deposits.
Then, if we check back the withdraw() function, the user gets transferred raacRewards of RAAC tokens, even though he simply flash-loaned, deposited, and withdrawn in a same tx.

Impact

Drain of RAAC rewards out of the Stability Pool.

Recommended Mitigation Steps

Do not allow users to deposit and withdraw from the pool in a same block, this is common practice in this type of systems.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

StabilityPool::calculateRaacRewards is vulnerable to just in time deposits

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.