Core Contracts

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

RToken holders lose yield

Summary

Users can deposit RToken into StabilityPool and receive DEToken by which users are eligible to claim rewards. However, with the current implementation, users are unable to withdraw RToken's yield from the pool, leaving yield stay in the pool

Vulnerability Details

The function StabilityPool::deposit() allows RToken holders to deposit RToken into the pool and receive DEToken with exchange rate 1:1. The user's deposit is increased such that userDeposits[msg.sender] += amount, with amount is the transferred amount of RToken.

The function StabilityPool::withdraw() allows users burn DEToken and withdraw the deposited RToken with exchange rate 1:1 and also benefit rewards.

The problem arises when users are only able to withdraw the deposited amount recorded by state userDeposits[msg.sender] when RToken is yield-bearing token that increases balance over time. This causes users unable to withdraw accrued yield that should belong to them.

For example, an user deposits 100 RToken to StabilityPool, receives 100 DEToken. After some time, that user can only burn 100 DEToken to withdraw 100 RToken and the deposited RToken's accrued yield stays in the pool. In short, it is expected that the user can withdraw more (100 + yield) RToken but he can't with current implementation

function deposit(uint256 amount) external nonReentrant whenNotPaused validAmount(amount) {
_update();
@> rToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 deCRVUSDAmount = calculateDeCRVUSDAmount(amount);
@> deToken.mint(msg.sender, deCRVUSDAmount);
@> userDeposits[msg.sender] += amount;
_mintRAACRewards();
emit Deposit(msg.sender, amount, deCRVUSDAmount);
}
function withdraw(uint256 deCRVUSDAmount) external nonReentrant whenNotPaused validAmount(deCRVUSDAmount) {
_update();
if (deToken.balanceOf(msg.sender) < deCRVUSDAmount) revert InsufficientBalance();
@> uint256 rcrvUSDAmount = calculateRcrvUSDAmount(deCRVUSDAmount);
uint256 raacRewards = calculateRaacRewards(msg.sender);
@> if (userDeposits[msg.sender] < rcrvUSDAmount) revert InsufficientBalance();
@> userDeposits[msg.sender] -= rcrvUSDAmount;
if (userDeposits[msg.sender] == 0) {
delete userDeposits[msg.sender];
}
@> deToken.burn(msg.sender, deCRVUSDAmount);
@> rToken.safeTransfer(msg.sender, rcrvUSDAmount);
if (raacRewards > 0) {
raacToken.safeTransfer(msg.sender, raacRewards);
}
emit Withdraw(msg.sender, rcrvUSDAmount, deCRVUSDAmount, raacRewards);
}

PoC

Add the test below to test/unit/core/pools/StabilityPool/StabilityPool.test.js

describe("Deposits", function () {
// ...
it.only("RToken holders lose yield", async function(){
// @audit PoC Can not withdraw yield from StabilityPool
const depositAmount = ethers.parseEther("100");
// borrow from LendingPool so that RToken accrues yield
const tokenId = 1;
const price = ethers.parseEther("100");
await raacHousePrices.setOracle(owner.address);
await raacHousePrices.setHousePrice(tokenId, price);
await crvusd.mint(user1.address, price)
await crvusd.connect(user1).approve(raacNFT.target, price)
await raacNFT.connect(user1).mint(tokenId, price);
await raacNFT.connect(user1).approve(lendingPool.target, tokenId);
await lendingPool.connect(user1).depositNFT(tokenId);
const borrowAmount = ethers.parseEther("50");
await lendingPool.connect(user1).borrow(borrowAmount);
// initial RToken balance of Stability Pool
let poolInitialRTokenBalance = await rToken.balanceOf(stabilityPool.target);
// deposit into StabilityPool
await stabilityPool.connect(user2).deposit(depositAmount);
// time passes, yield accrued
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine");
await lendingPool.connect(user1).updateState();
// user withdraws
await stabilityPool.connect(user2).withdraw(depositAmount)
console.log(`initial balance\t ${poolInitialRTokenBalance}`)
console.log(`yield stays in the pool\t ${(await rToken.balanceOf(stabilityPool.target)) - poolInitialRTokenBalance}`)
})

Run the test and console shows:

StabilityPool
Core Functionality
Deposits
initial balance 0
yield stays in the pool 234309158145249
✔ RToken holders lose yield (47ms)
1 passing (2s)

It means that the yield which should belong to the user, stays in the pool

Impact

  • Loss of yield

Tools Used

Manual

Recommendations

  • Remove the usage of the state variable userDeposits

  • Account for the yield of RToken in withdraw flow

Updates

Lead Judging Commences

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

StabilityPool's userDeposits mapping doesn't update with DEToken transfers or interest accrual, and this combined with RToken transfers causes fund loss and permanent lockup

Support

FAQs

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