Core Contracts

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

Incorrect Reward Tracking Logic Leads to Lost User Rewards

Summary

In FeeCollector::_calculatePendingRewards, the reward tracking logic is flawed due to incorrect state updates in claimRewards. When users claim rewards, their userRewards mapping is set to totalDistributed instead of tracking their actual claimed amount. This causes users to lose rewards in subsequent distribution periods.

Vulnerability Details

The issue occurs because:

  1. totalDistributed tracks the cumulative sum of all distributed rewards

  2. userRewards[user] is set to totalDistributed after claiming, rather than tracking actual claimed amounts

  3. This makes future reward calculations incorrect since share > userRewards[user] will evaluate to false

POC

Create a foundry setup using the commands in this document:
https://book.getfoundry.sh/config/hardhat?highlight=hardhat#adding-foundry-to-a-hardhat-project

Create a raacFoundrySetup.t.sol file under the test directory and add this code:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {LendingPool} from "contracts/core/pools/LendingPool/LendingPool.sol";
import {crvUSDToken} from "contracts/mocks/core/tokens/crvUSDToken.sol";
import {RToken} from "contracts/core/tokens/RToken.sol";
import {DebtToken} from "contracts/core/tokens/DebtToken.sol";
import {RAACNFT} from "contracts/core/tokens/RAACNFT.sol";
import {RAACHousePricesMock} from "contracts/mocks/core/primitives/RAACHousePricesMock.sol";
import {RAACHousePriceOracle} from "contracts/core/oracles/RAACHousePriceOracle.sol";
import {MockFunctionsRouter} from "contracts/mocks/core/oracles/MockFunctionsRouter.sol";
import {FeeCollector} from "contracts/core/collectors/FeeCollector.sol";
import {MockVeToken} from "contracts/mocks/core/tokens/MockVeToken.sol";
contract SetupContract is Test {
address public user1;
address public user2;
address public user3;
address public treasury;
address public repairFund;
LendingPool public lendingPool;
crvUSDToken public _crvUSDToken;
RToken public rToken;
MockVeToken public veRToken;
DebtToken public debtToken;
RAACHousePricesMock public raacHousePrices;
RAACNFT public raacNFT;
RAACHousePriceOracle public raacHousePriceOracle;
FeeCollector public feeCollector;
uint256 public constant INITIAL_PRIME_RATE = 1e26;
function setUp() external {
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
treasury = makeAddr("treasury");
repairFund = makeAddr("repairFund");
veRToken = new MockVeToken();
_crvUSDToken = new crvUSDToken(address(this));
rToken = new RToken("rtoken", "rtk", address(this), address(_crvUSDToken)); // reserve pool
debtToken = new DebtToken("debtToken", "dtk", address(this)); //reservePool
raacHousePrices = new RAACHousePricesMock();
raacNFT = new RAACNFT(address(_crvUSDToken), address(raacHousePrices), address(this));
raacHousePriceOracle = new RAACHousePriceOracle(
address(new MockFunctionsRouter()), bytes32(bytes("fun-ethereum-mainnet-1")), address(this)
);
feeCollector = new FeeCollector(address(rToken), address(veRToken), treasury, repairFund, address(this));
lendingPool = new LendingPool(
address(_crvUSDToken),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
INITIAL_PRIME_RATE
);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
_crvUSDToken.mint(user1, 10000e18);
_crvUSDToken.mint(user2, 100e18);
_crvUSDToken.mint(user3, 100e18);
}
function testFeeCollector() public {
vm.startPrank(user1);
_crvUSDToken.approve(address(lendingPool), 1000e18);
lendingPool.deposit(1000e18);
rToken.approve(address(feeCollector), 1000e18);
// sending fees to the contract;
feeCollector.collectFee(1000e18, 1);
vm.stopPrank();
// distributing the voting power;
veRToken.mint(user2, 50);
veRToken.mint(user3, 50);
feeCollector.distributeCollectedFees();
console.log("Balance before 1st claim: ", rToken.balanceOf(user2));
feeCollector.claimRewards(user2);
console.log("Balance after 1st claim: ", rToken.balanceOf(user2));
vm.startPrank(user1);
_crvUSDToken.approve(address(lendingPool), 1000e18);
lendingPool.deposit(1000e18);
rToken.approve(address(feeCollector), 1000e18);
// sending fees to the contract;
feeCollector.collectFee(1000e18, 1);
vm.stopPrank();
vm.warp(block.timestamp + 8 days);
feeCollector.distributeCollectedFees();
// this will revert will InsufficientBalance();
feeCollector.claimRewards(user2); // not getting the second reward;
console.log("Balance after 2st claim: ", rToken.balanceOf(user2));
}
}

Here’s an example of what happens (values are not real, as not all fees go to users with voting power). In the POC, only 70% of lending fees (id:1) go to veRaac holders. But for simplicity, let's assume all fees go to veRaac holders in this example.

  1. First period:

    • totalDistributed = 1000e18

    • User2 has 50% voting power

    • share = (1000e18 * 50) / 100 = 500e18

    • User claims 500e18

    • userRewards[user2] is set to totalDistributed (1000e18)

  2. Second period:

    • totalDistributed = 2000e18

    • share = (2000e18 * 50) / 100 = 1000e18

    • Since share (1000e18) > userRewards[user2] (1000e18) is false

    • User gets 0 rewards despite being entitled to 500e18 more

Recommendations

Track actual claimed amounts

function claimRewards(address user) external override nonReentrant whenNotPaused returns (uint256) {
if (user == address(0)) revert InvalidAddress();
uint256 pendingReward = _calculatePendingRewards(user);
if (pendingReward == 0) revert InsufficientBalance();
- userRewards[user] = totalDistributed;
+ userRewards[user] += pendingReward;
raacToken.safeTransfer(user, pendingReward);
emit RewardClaimed(user, pendingReward);
return pendingReward;
}
Updates

Lead Judging Commences

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

FeeCollector::claimRewards sets `userRewards[user]` to `totalDistributed` seriously grieving users from rewards

Support

FAQs

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

Give us feedback!