Core Contracts

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

Attacker can drain all RaacRewards from StabilityPool

Description

Even when a user calls withdraw(deCRVUSDAmount) with a paramater deCRVUSDAmount = 1, their entire RAAC reward is transferred to them. This is because calculateRaacRewards() calculates the rewards on the user's entire deposit.
Hence attacker can make the call multiple times and drain all the rewards, effectively stealing the reward of others.

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: }
245:
246: /**
247: * @notice Calculates the pending RAAC rewards for a user.
248: * @param user Address of the user.
249: * @return Amount of RAAC rewards.
250: */
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: }

Attack Path:

  • Alice the honest user has deposited 100 rTokens and received 100 deCRVUSDAmount

  • Bob the attacker has deposited 100 rTokens too and received 100 deCRVUSDAmount

  • The raacToken.balanceOf(address(this)) is currently 300

  • Bob calls withdraw(1) and receives 1 rToken back. However he also receives his entire rewards of (300 * 100) / 200 = 150

  • Bob again calls withdraw(1) and receives 1 rToken back. He again receives rewards of (150 * 99) / 199 = ~ 75

  • Bob repeats the process again and again until raacToken.balanceOf(address(this)) < 1e6 and calculateRaacRewards() returns zero

  • Alice has no reward left to claim now, while Bob has profited at her expense.

Impact

Entire RAAC rewards can be drained.

Mitigation

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);
+ 229: uint256 raacRewards = calculateRaacRewards(rcrvUSDAmount);
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: }
245:
246: /**
247: * @notice Calculates the pending RAAC rewards for a user.
248: * @param user Address of the user.
249: * @return Amount of RAAC rewards.
250: */
- 251: function calculateRaacRewards(address user) public view returns (uint256) {
- 252: uint256 userDeposit = userDeposits[user];
+ 251: function calculateRaacRewards(uint256 forAmount) public view returns (uint256) {
+ 252: uint256 userDeposit = forAmount;
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: }

Also, we should change this:

File: contracts/core/pools/StabilityPool/StabilityPool.sol
261: /**
262: * @notice Gets the pending RAAC rewards for a user.
263: * @param user Address of the user.
264: * @return Amount of pending RAAC rewards.
265: */
266: function getPendingRewards(address user) external view returns (uint256) {
- 267: return calculateRaacRewards(user);
+ 267: return calculateRaacRewards(userDeposits[user]);
268: }
Updates

Lead Judging Commences

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

StabilityPool::withdraw can be called with partial amounts, but it always send the full rewards

Support

FAQs

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