Core Contracts

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

Incorrect rewards calculation in StabilityPool's calculateRaacRewards

Summary

The calculateRaacRewards function in StabilityPool contract performs calculations using incompatible token units therefore causing incorrect rewards to be distributed when users withdraw their RTokens from the pool.

Vulnerability Details

From the StabilityPool contract, let's analyze some snippets of code in the functions below;

function deposit(uint256 amount) external nonReentrant whenNotPaused validAmount(amount) {
// ...
// @audit amount deposited is RToken (rToken units)
userDeposits[msg.sender] += amount;
}
function withdraw(uint256 deCRVUSDAmount) external nonReentrant whenNotPaused validAmount(deCRVUSDAmount) {
// ...
// @audit when a user withdraws we also calculate the rewards accumulated
uint256 raacRewards = calculateRaacRewards(msg.sender);
// @audit these rewards are sent to user when > 0
if (raacRewards > 0) {
// @audit rewards are sent in raacToken units
raacToken.safeTransfer(msg.sender, raacRewards);
}
}
function calculateRaacRewards(address user) public view returns (uint256) {
uint256 userDeposit = userDeposits[user]; // @audit rToken units
uint256 totalDeposits = deToken.totalSupply(); // @audit deToken units
uint256 totalRewards = raacToken.balanceOf(address(this)); // @audit raacToken units
if (totalDeposits < 1e6) return 0;
// @audit this is wrong since the units are incompatible
return (totalRewards * userDeposit) / totalDeposits;
}

From the code snippets above, when a user deposits the userDeposits mapping is incremented with rTokens which are obviously in RToken units, after some time rewards accumulate due to activity in the protocol, when the user withdraws by calling withdraw function, the rewards are calculated using the calculateRaacRewards function, but it is problematic. I will explain mathematically below;

Since rewards are provided in raacToken units, then the output of calculateRaacRewards should be in raacToken units. but in contrast what happens is;

return (totalRewards * userDeposit) / totalDeposits;. Here totalRewards are in raacToken units, userDeposit is in rToken units and totalDeposits is in deToken units.
so we get;

units

instead of simply just units.

So the amount of rewards calculated is always wrong.

PoC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {StabilityPool} from "contracts/core/pools/StabilityPool/StabilityPool.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockERC20 is ERC20 {
uint8 private _decimals;
constructor(string memory name, uint8 decs) ERC20(name, name) {
_decimals = decs;
}
function mint(address to, uint amt) external {
_mint(to, amt);
}
function decimals() public view override returns (uint8) {
return _decimals;
}
}
contract StabilityPoolTestPoC is Test {
address private alice = makeAddr("alice");
StabilityPool private stabilityPool;
StabilityPool private implementation;
ERC1967Proxy private proxy;
address private rToken = address(new MockERC20("RToken", 6));
address private deToken = address(new MockERC20("DeToken", 18));
address private crvUSDToken = address(new MockERC20("crvToken", 18));
address private raacToken = address(new MockERC20("RAACToken", 18));
address private mockRaacMinter = makeAddr("mockRaacMinter");
address private mockLendingPool = makeAddr("lending_pool");
function setUp() public {
// Deploy StabilityPool implementation and proxy
implementation = new StabilityPool(address(this));
// I have just used mocks for this test since the specific token implementations is not relevant in this test
// except for the decimals part (of course)
bytes memory initData = abi.encodeWithSelector(
StabilityPool.initialize.selector,
rToken, // 6 decimals
deToken, // 18 decimals
raacToken,
mockRaacMinter,
crvUSDToken,
mockLendingPool
);
proxy = new ERC1967Proxy(address(implementation), initData);
stabilityPool = StabilityPool(address(proxy));
deal(rToken, alice, 5 * 1e6, true);
vm.prank(alice);
IERC20(rToken).approve(address(stabilityPool), 5 * 1e6);
}
function testDepositAndWithdrawRewards() public {
// for simplicity, I will manually put the raacTokens in to the StabilityPool therefore am setting raacMinter to zero
stabilityPool.setRAACMinter(address(0));
vm.prank(alice);
// alice deposits 5 rTokens
// only alice will be minted deTokens
stabilityPool.deposit(5 * 1e6);
// the stability pool gets some rewards of 2 raac Tokens
// assume that the raacMinter is the one putting the tokens
uint rewardsToDistribute = 2 * 1e18;
deal(raacToken, address(stabilityPool), rewardsToDistribute, true);
console.log("rewards To distribute: ", rewardsToDistribute);
uint rewardsObtained = stabilityPool.calculateRaacRewards(alice);
console.log("rewards obtained: ", rewardsObtained);
// since only alice has the deToken and only she has her rToken deposited in the stabilityPool,
// you would expect that alice raacRewards would be equal to all the rewards, but no! see the logs
assert(rewardsObtained < rewardsToDistribute);
}
}

Impact

Users receive incorrect reward amounts (in case when either RToken, deToken or raacToken has decimals other than 18)

Recommendations

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;
+ uint256 userDepositInDeTokenUnits = calculateDeCRVUSDAmount(userDeposit);
- return (totalRewards * userDeposit) / totalDeposits;
+ return (totalRewards * userDepositInDeTokenUnits) / totalDeposits;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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