Core Contracts

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

Exchange Rate Calculation Allows Excessive RToken Withdrawals

Summary

The StabilityPool's exchange rate mechanism fails to account for the relationship between RToken and DEToken balances, leading to incorrect token distributions during withdrawals. This creates a fundamental pricing mismatch that could be exploited to extract excess value from the protocol.

function getExchangeRate() public view returns (uint256) {
// The Dynamic exchange rate calculation is commented out
// This would properly calculate rate based on actual pool reserves:
// 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;
// VULNERABILITY: Fixed 1:1 exchange rate
// This breaks price discovery mechanism
return 1e18; // <<: Hardcoded exchange rate
}
function withdraw(uint256 deCRVUSDAmount) external nonReentrant whenNotPaused validAmount(deCRVUSDAmount) {
_update();
// Checks user's DEToken balance
if (deToken.balanceOf(msg.sender) < deCRVUSDAmount) revert InsufficientBalance();
// CRITICAL: Uses vulnerable exchange rate calculation
uint256 rcrvUSDAmount = calculateRcrvUSDAmount(deCRVUSDAmount);
uint256 raacRewards = calculateRaacRewards(msg.sender);
// Checks against user's recorded deposits
// But deposit amounts were calculated using the same fixed rate
if (userDeposits[msg.sender] < rcrvUSDAmount) revert InsufficientBalance();
userDeposits[msg.sender] -= rcrvUSDAmount;
// State updates using potentially incorrect amounts
if (userDeposits[msg.sender] == 0) {
delete userDeposits[msg.sender];
}
// Executes token transfers with vulnerable exchange rate
deToken.burn(msg.sender, deCRVUSDAmount);
rToken.safeTransfer(msg.sender, rcrvUSDAmount);
if (raacRewards > 0) {
raacToken.safeTransfer(msg.sender, raacRewards);
}
emit Withdraw(msg.sender, rcrvUSDAmount, deCRVUSDAmount, raacRewards);
}

The exchange rate is hardcoded to 1e18, meaning 1 DEToken always equals 1 RToken regardless of actual pool conditions. But this ignores basic economic principles where token ratios should float based on supply and demand within the pool. The vulnerability flows through both functions, with getExchangeRate() providing an incorrect fixed rate that withdraw() then uses to calculate token amounts,

Attack Flow A user deposits DETokens into the StabilityPool. Later, when withdrawing, they receive RTokens at a 1:1 ratio even if the pool's actual RToken backing has decreased due to liquidations or other factors. This fixed rate allows users to withdraw more RTokens than they should rightfully receive based on pool conditions.

Vulnerability Details

The StabilityPool's exchange rate mechanism reveals a flaw in how RToken and DEToken conversions are handled. Imagine a bank offering to exchange dollars for euros at exactly 1:1 regardless of market conditions this is effectively what the StabilityPool does with its tokens.

Let's walk through how this plays out:

When users interact with the StabilityPool, they deposit DETokens expecting fair representation of their share in the RToken reserves. However, the getExchangeRate() function in StabilityPool.sol returns a fixed 1e18 value, treating these tokens as perpetually equivalent regardless of market dynamics or pool conditions.

The real-world impact becomes clear when we examine the withdrawal process. A user who deposited 1000 DETokens when the pool was healthy can withdraw 1000 RTokens even after significant liquidations have reduced the pool's RToken backing. This creates a 'first-out' advantage where early withdrawers receive full value while later users find themselves holding DETokens backed by depleted reserves.

Looking at the numbers:

// handles the conversion from DEToken to RToken amounts during withdrawals
function calculateRcrvUSDAmount(uint256 deCRVUSDAmount) public view returns (uint256) {
uint256 scalingFactor = 10**(18 + rTokenDecimals - deTokenDecimals);
return (deCRVUSDAmount * getExchangeRate()) / scalingFactor;
}
// because getExchangeRate() returns a fixed 1e18
function withdraw(uint256 deCRVUSDAmount) external {
// ...
uint256 rcrvUSDAmount = calculateRcrvUSDAmount(deCRVUSDAmount);
// ...
}
// The withdrawal process uses this calculation through

This fixed rate ignores the fundamental principle that token ratios should reflect the pool's actual reserves, currently sitting at rToken.balanceOf(address(this)) RTokens backing deToken.totalSupply() DETokens.

Impact

The fixed exchange rate prevents proper price discovery between RToken and DEToken, potentially allowing users to withdraw more RTokens than they should receive based on pool conditions.

Recommendations

Implement proper exchange rate calculation

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

Lead Judging Commences

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