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) {
userDeposits[msg.sender] += amount;
}
function withdraw(uint256 deCRVUSDAmount) external nonReentrant whenNotPaused validAmount(deCRVUSDAmount) {
uint256 raacRewards = calculateRaacRewards(msg.sender);
if (raacRewards > 0) {
raacToken.safeTransfer(msg.sender, raacRewards);
}
}
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;
}
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
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 {
implementation = new StabilityPool(address(this));
bytes memory initData = abi.encodeWithSelector(
StabilityPool.initialize.selector,
rToken,
deToken,
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 {
stabilityPool.setRAACMinter(address(0));
vm.prank(alice);
stabilityPool.deposit(5 * 1e6);
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);
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;
}