Core Contracts

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

Unfair Reward Distribution in redeemFromMarket Function

Summary

The redeemFromMarket function unfairly distributes rewards, favoring users who redeem later. As totalDeposits decreases over time while each user’s amount remains constant, the calculateReward function grants higher rewards to users who redeem later.

Vulnerability Details

function redeemFromMarket(uint256 marketId) external nonReentrant {
Market storage market = markets[marketId];
UserPosition storage position = userPositions[marketId][msg.sender];
require(position.exists, "No position found");
require(block.timestamp >= position.lockEndTime, "Lock duration has not passed");
uint256 amount = position.amount;
uint256 reward = calculateReward(marketId, amount);
market.totalDeposits -= amount;
delete userPositions[marketId][msg.sender];
market.quoteAsset.safeTransfer(msg.sender, amount);
raacToken.safeTransfer(msg.sender, reward);
emit Redeemed(marketId, msg.sender, amount, reward);
}
  • Users deposit assets into a market and receive rewards upon redemption.

  • The function calls calculateReward(marketId, amount), which likely depends on totalDeposits.

  • Since totalDeposits decreases as users redeem their positions, the same deposit amount (position.amount) results in a higher reward for later redeemers than for earlier ones.

  • This creates an unfair incentive for users to delay redemption, leading to strategic behaviors that exploit this issue.

Test:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "contracts/core/collectors/FeeCollector.sol";
import "contracts/core/tokens/RAACToken.sol";
import "contracts/core/tokens/veRAACToken.sol";
import "forge-std/Test.sol";
import "contracts/mocks/core/oracles/TestRAACHousePriceOracle.sol";
import "contracts/mocks/core/tokens/crvUSDToken.sol";
import "contracts/mocks/core/tokens/MockUSDC.sol";
import "contracts/core/tokens/RToken.sol";
import "contracts/core/tokens/DebtToken.sol";
import "contracts/core/tokens/RAACNFT.sol";
import "contracts/core/primitives/RAACHousePrices.sol";
import "contracts/core/pools/LendingPool/LendingPool.sol";
import "forge-std/Console2.sol";
import "contracts/interfaces/core/pools/LendingPool/ILendingPool.sol";
import "contracts/core/pools/StabilityPool/StabilityPool.sol";
import "contracts/core/pools/StabilityPool/NFTLiquidator.sol";
import "contracts/core/pools/StabilityPool/MarketCreator.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
contract Pool2 is Test{
RAACToken public raccToken;
veRAACToken public veraacToken;
crvUSDToken public crv;
RToken public rToken;
DebtToken public debtToken;
RAACNFT public raccNFT;
TestRAACHousePriceOracle public oracle;
RAACHousePrices public housePrice;
MockUSDC public usdc;
LendingPool public pool;
NFTLiquidator nftLq;
StabilityPool sbPool;
TransparentUpgradeableProxy proxy;
MarketCreator public market;
uint256 NFTTokenId = 1;
address alice = address(0x1001);
address bob = address(0x1002);
address candy = address(0x1003);
function setUp() public {
crv = new crvUSDToken(address(this));
rToken = new RToken("rt","rt",address(this),address(crv));
debtToken = new DebtToken("db","db",address(this));
address router;
usdc = new MockUSDC(1_000_000e6);
housePrice = new RAACHousePrices(address(this));
oracle = new TestRAACHousePriceOracle(router,bytes32('1'),address(housePrice));
raccNFT = new RAACNFT(address(usdc),address(housePrice),address(this));
pool = new LendingPool(address(crv),address(rToken),address(debtToken),address(raccNFT),address(housePrice),1e26);
rToken.setReservePool(address(pool));
housePrice.setOracle(address(this));
debtToken.setReservePool(address(pool));
nftLq = new NFTLiquidator(address(crv),address(raccNFT),address(this),50);
sbPool = new StabilityPool(address(this));
//add proxy.
proxy = new TransparentUpgradeableProxy(address(sbPool),address(this),"");
raccToken = new RAACToken(address(this),1_000,1_000);
veraacToken = new veRAACToken(address(raccToken));
market = new MarketCreator(address(this),address(raccToken),address(crv));
}
function testMarketRedeemAssets() public {
// Setup minter
raccToken.setMinter(address(this));
raccToken.whitelistAddress(address(market));
raccToken.mint(address(market),200e18);
//owner create market.
market.createMarket(address(crv), 1 days, 100e18);
//alice deposit.
crv.mint(alice,10e18);
vm.startPrank(alice);
crv.approve(address(market), 10e18);
market.participateInMarket(1,10e18);
//bob depsosit.
crv.mint(bob,10e18);
vm.startPrank(bob);
crv.approve(address(market), 10e18);
market.participateInMarket(1,10e18);
//candy deposit.
crv.mint(candy,10e18);
vm.startPrank(candy);
crv.approve(address(market), 10e18);
market.participateInMarket(1,10e18);
vm.stopPrank();
skip(2 days);
//alice redeem
vm.prank(alice);
market.redeemFromMarket(1);
//bob redeem
vm.prank(bob);
market.redeemFromMarket(1);
//candy redeem
vm.prank(candy);
market.redeemFromMarket(1);
console2.log(raccToken.balanceOf(alice));
console2.log(raccToken.balanceOf(bob));
console2.log(raccToken.balanceOf(candy));
}

Out:

Ran 1 test for tests/Pool2.t.sol:Pool2
[PASS] testMarketRedeemAssets() (gas: 712543)
Logs:
26666666666666666666
40000000000000000000
80000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.55ms (564.75µs CPU time)
Ran 1 test suite in 229.65ms (4.55ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

  • Unfair advantage for late redeemers: Users who wait longer receive disproportionately higher rewards.

  • Potential system imbalance: If all users realize the issue, they may avoid redeeming early, causing liquidity issues.

  • Risk of market manipulation: Users with large deposits could exploit this by redeeming last to maximize rewards.

Tools Used

Foundry

Recommendations

Normalize rewards using a snapshot mechanism

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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