Core Contracts

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

balanceOf(address(this)) in StabilityPool causes reward distribution to be higher than it should be

Description

In StabilityPool::calculateRaacRewards the function uses raacToken.balanceOf(address(this)) to calculate the rewards for a user during withdrawal, this leads to an unwanted behavior, considering the contract allows depositing RAACToken via StabilityPool::depositRAACFromPool.

Vulnerable Code

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;
}

StabilityPool::depositRAACFromPool:

function depositRAACFromPool(uint256 amount) external onlyLiquidityPool validAmount(amount) {
uint256 preBalance = raacToken.balanceOf(address(this));
raacToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 postBalance = raacToken.balanceOf(address(this));
if (postBalance != preBalance + amount) revert InvalidTransfer();
// TODO: Logic for distributing to managers based on allocation
emit RAACDepositedFromPool(msg.sender, amount);
}

As you can see, StabilityPool expects deposits from the LiquidityPool (implementation pending) for allocation towards the managers of the markets. The issue arising is that those deposits will be wrongfully distributed as rewards to the users, leaving the original intend behind.

PoC

Since the PoC is a foundry test I have added a Makefile at the end of this report to simplify installation for your convenience. Otherwise if console commands would be prefered:

First run: npm install --save-dev @nomicfoundation/hardhat-foundry

Second add: require("@nomicfoundation/hardhat-foundry"); on top of the Hardhat.Config file in the projects root directory.

Third run: npx hardhat init-foundry

And lastly, you will encounter one of the mock contracts throwing an error during compilation, this error can be circumvented by commenting out the code in entirety (ReserveLibraryMocks.sol).

And the test should be good to go:

After following above steps copy & paste the following code into ./test/invariant/PoC.t.sol and run forge test --mt test_pocDepositsToStabilityPoolAreDistributedWrogfullyAsRewards -vv

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test, console} from "forge-std/Test.sol";
import {StabilityPool} from "../../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {LendingPool} from "../../contracts/core/pools/LendingPool/LendingPool.sol";
import {CrvUSDToken} from "../../contracts/mocks/core/tokens/crvUSDToken.sol";
import {RAACHousePrices} from "../../contracts/core/oracles/RAACHousePriceOracle.sol";
import {RAACNFT} from "../../contracts/core/tokens/RAACNFT.sol";
import {RToken} from "../../contracts/core/tokens/RToken.sol";
import {DebtToken} from "../../contracts/core/tokens/DebtToken.sol";
import {DEToken} from "../../contracts/core/tokens/DEToken.sol";
import {RAACToken} from "../../contracts/core/tokens/RAACToken.sol";
import {RAACMinter} from "../../contracts/core/minters/RAACMinter/RAACMinter.sol";
contract PoC is Test {
StabilityPool public stabilityPool;
LendingPool public lendingPool;
CrvUSDToken public crvusd;
RAACHousePrices public raacHousePrices;
RAACNFT public raacNFT;
RToken public rToken;
DebtToken public debtToken;
DEToken public deToken;
RAACToken public raacToken;
RAACMinter public raacMinter;
address owner;
address oracle;
address user1;
address user2;
address user3;
uint256 constant STARTING_TIME = 1641070800;
uint256 public currentBlockTimestamp;
uint256 constant WAD = 1e18;
uint256 constant RAY = 1e27;
function setUp() public {
vm.warp(STARTING_TIME);
currentBlockTimestamp = block.timestamp;
owner = address(this);
oracle = makeAddr("oracle");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
uint256 initialPrimeRate = 0.1e27;
raacHousePrices = new RAACHousePrices(owner);
vm.prank(owner);
raacHousePrices.setOracle(oracle);
crvusd = new CrvUSDToken(owner);
raacNFT = new RAACNFT(address(crvusd), address(raacHousePrices), owner);
rToken = new RToken("RToken", "RToken", owner, address(crvusd));
debtToken = new DebtToken("DebtToken", "DT", owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
vm.prank(owner);
crvusd.setMinter(owner);
vm.prank(owner);
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
initialPrimeRate
);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
stabilityPool = new StabilityPool(address(owner));
deToken.setStabilityPool(address(stabilityPool));
raacToken = new RAACToken(owner, 0, 0);
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), owner);
stabilityPool.initialize(address(rToken), address(deToken), address(raacToken), address(raacMinter), address(crvusd), address(lendingPool));
vm.prank(owner);
raacToken.setMinter(address(raacMinter));
attacker = new Attacker(address(raacNFT));
crvusd.mint(address(attacker), type(uint128).max);
crvusd.mint(user1, type(uint128).max);
crvusd.mint(user2, type(uint128).max);
crvusd.mint(user3, type(uint128).max);
}
function test_pocDepositsToStabilityPoolAreDistributedWrogfullyAsRewards() public {
// Setting up the scenario, 2 users deposit rToken into StabilityPool
vm.startPrank(user1);
crvusd.approve(address(lendingPool), type(uint256).max);
rToken.approve(address(stabilityPool), type(uint256).max);
lendingPool.deposit(10e18);
stabilityPool.deposit(10e18);
vm.stopPrank();
vm.startPrank(user2);
crvusd.approve(address(lendingPool), type(uint256).max);
rToken.approve(address(stabilityPool), type(uint256).max);
lendingPool.deposit(10e18);
stabilityPool.deposit(10e18);
vm.stopPrank();
// Substituting liquidityPool with owner, since there is no liquidity pool yet
vm.prank(address(raacMinter));
raacToken.mint(owner, 10e18);
vm.startPrank(owner);
raacToken.manageWhitelist(address(stabilityPool), true);
raacToken.manageWhitelist(address(raacMinter), true);
raacToken.manageWhitelist(owner, true);
stabilityPool.setLiquidityPool(owner);
raacToken.approve(address(stabilityPool), 10e18);
// Depositing the RAAC tokens into the StabilityPool
stabilityPool.depositRAACFromPool(10e18);
vm.stopPrank();
assertEq(raacToken.balanceOf(address(stabilityPool)), 10e18);
// And here we withdraw, with the result that users emptied the stability pool
vm.prank(user1);
stabilityPool.withdraw(10e18);
vm.prank(user2);
stabilityPool.withdraw(10e18);
assertEq(raacToken.balanceOf(address(stabilityPool)), 0);
}
}

As you can easily see running the test above, all RAAC Tokens sent to the Stability Pool are now withdrawn by the 2 users.

Impact

Distributing accidentally those funds, meant for Manager/Market allocations, leaves the functionality of any following logic useless by overpaying the users. The likelihood is High since the only precondition was has to be met is, that RAAC needs to be deposited.
Also, the misdistribution of funds towards the users in this case (or a malicious actor who frontruns the reward distribution as mentioned in an earlier report), causes a High impact to the system since the funds for implemented logic are simply not there and furthermore not fairly distributed.

Therefore it is a total severity of High.

Tools Used

Manual Review

Recommended Mitigation

Disconnect from balanceOf() accounting during reward distribution by tracking uint256 totalRewards with received minted RAAC tokens - Distributed RAAC Tokens or maybe better, track deposits from Liquidity Pool seperatly and deducting them before distribution:

+ uint256 raacDepositedFromPool;
function depositRAACFromPool(uint256 amount) external onlyLiquidityPool validAmount(amount) {
uint256 preBalance = raacToken.balanceOf(address(this));
raacToken.safeTransferFrom(msg.sender, address(this), amount);
uint256 postBalance = raacToken.balanceOf(address(this));
if (postBalance != preBalance + amount) revert InvalidTransfer();
+ raacDepositedFromPool += amount;
// TODO: Logic for distributing to managers based on allocation
emit RAACDepositedFromPool(msg.sender, amount);
}
function calculateRaacRewards(address user) public view returns (uint256) {
uint256 userDeposit = userDeposits[user];
uint256 totalDeposits = deToken.totalSupply();
- uint256 totalRewards = raacToken.balanceOf(address(this));
+ uint256 totalRewards = raacToken.balanceOf(address(this)) - raacDepositedFromPool;
if (totalDeposits < 1e6) return 0;
return (totalRewards * userDeposit) / totalDeposits;
}

Appendix

Copy the following import into your Hardhat.Config file in the projects root dir:
require("@nomicfoundation/hardhat-foundry");

Paste the following into a new file "Makefile" into the projects root directory:

.PHONY: install-foundry init-foundry all
install-foundry:
npm install --save-dev @nomicfoundation/hardhat-foundry
init-foundry: install-foundry
npx hardhat init-foundry
# Default target that runs everything in sequence
all: install-foundry init-foundry

And run make all

Updates

Lead Judging Commences

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

StabilityPool::calculateRaacRewards uses contract balance for reward calculation, incorrectly including tokens meant for manager allocation - Manager allocation not implemented

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

StabilityPool::calculateRaacRewards uses contract balance for reward calculation, incorrectly including tokens meant for manager allocation - Manager allocation not implemented

Support

FAQs

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