DeFiFoundry
50,000 USDC
View results
Submission Details
Severity: low
Invalid

Miscalculations in PerpetualVault Due to Non-Normalized Token Decimals (e.g., USDC vs. DAI)

Summary

The vault’s implementation fails to normalize token values based on their specific decimal configurations. It is hardcoded to work with an assumed fixed decimal format (likely 18 decimals), so when a token with another decimal precision (such as 6 decimals) is used, arithmetic operations (e.g., total vault value, user share calculations, and collateral balance adjustments) become inaccurate.

Vulnerability Details

Code Selection for Decimal Mismatch Simulation
To demonstrate the impact of varying token decimals (e.g., USDC with 6 decimals vs. DAI with 18 decimals), I extracted key calculations from the PerpetualVault contract. These calculations play a crucial role in deposit, withdrawal, and vault valuation logic. By recreating these specific computations in a controlled environment, I was able to observe discrepancies caused by different decimal formats.

Selected Code

  1. Subtracting Deposited Amount from Index Token Balance

IERC20(indexToken).balanceOf(address(this)) - amount;
  1. Calculating the Total Value of the Vault in Collateral Tokens

uint256 total = (IERC20(indexToken).balanceOf(address(this)) *
prices.indexTokenPrice.min) /
prices.shortTokenPrice.min +
collateralToken.balanceOf(address(this)) +
positionData.netValue / prices.shortTokenPrice.min;
  1. Getting Available Collateral Token Balance for Position Opening

amountIn = collateralToken.balanceOf(address(this));
  1. Determining Swap Amount Based on User Shares

uint256 swapAmount = (IERC20(indexToken).balanceOf(address(this)) *
shares) / totalShares;
  1. Tracking Vault Balance Before Withdrawal

uint256 balanceBeforeWithdrawal = collateralToken.balanceOf(
address(this)
) - withdrawn;

Proof of Code
How to Run - forge test --mp test/PerpetualVault.t.sol --mt test_IncorrectCalculationsWith6And18DecimalTokens --via-ir --rpc-url arbitrum -vv

// 3. Place Constant Values
uint256 constant orderResultDataOutputAmount = 1e6; // Assume token output from an order
uint256 constant indexTokenBalance = 2e18; // Assume 2 tokens with 18 decimals
uint256 constant shortTokenPriceMin = 500 * 1e30; // Short token price in 30 decimals
uint256 constant indexTokenPriceMin = 2500 * 1e30; // Index token price in 30 decimals
uint256 constant positionDataNetValue = 1000 * 1e30; // Position net value
uint256 constant shares = 100; // User shares
uint256 constant totalShares = 1000; // Total vault shares
uint256 constant withdrawnAmount = 500 * 1e6; // Tokens withdrawn
function test_IncorrectCalculationsWith6And18DecimalTokens() external {
// 1. Create ERC20 with Custom 6 Decimal Token (Represents USDC)
MockERC20CustomDecimals six_DecimalToken = new MockERC20CustomDecimals(
"Mock USDC",
"mUSDC",
6,
1e12 // 1e12 base units = 1e6 tokens with 6 decimals
);
// 2. Create ERC20 with Custom 18 Decimal Token (Represents DAI)
MockERC20CustomDecimals eighteen_DecimalToken = new MockERC20CustomDecimals(
"Mock USDC",
"mUSDC",
18,
1e24 // 1e24 base units = 1e6 tokens with 18 decimals
);
// 4. Six Decimal Token Calculations
/**
*Code line Simulated (PerpetutalVault.sol): IERC20(indexToken).balanceOf(address(this)) - amount;
*/
uint256 sixDecimalToken_prevCollateralBalance = six_DecimalToken
.balanceOf(address(this)) - orderResultDataOutputAmount;
/**
*Code line Simulated (PerpetutalVault.sol): uint256 total = (IERC20(indexToken).balanceOf(address(this)) *
prices.indexTokenPrice.min) /
prices.shortTokenPrice.min +
collateralToken.balanceOf(address(this)) +
positionData.netValue / prices.shortTokenPrice.min;
*/
uint256 sixDecimalToken_total = (indexTokenBalance *
indexTokenPriceMin) /
shortTokenPriceMin +
six_DecimalToken.balanceOf(address(this)) +
positionDataNetValue /
shortTokenPriceMin;
/**
*Code line Simulated (PerpetutalVault.sol): amountIn = collateralToken.balanceOf(address(this));
*/
uint256 sixDecimalToken_amountIn = six_DecimalToken.balanceOf(
address(this)
);
/**
*Code line Simulated (PerpetutalVault.sol): uint256 swapAmount = (IERC20(indexToken).balanceOf (address(this)) * shares) / totalShares;
*/
uint256 sixDecimalToken_amount = (six_DecimalToken.balanceOf(
address(this)
) * shares) / totalShares;
/**
*Code line Simulated (PerpetutalVault.sol): uint256 balanceBeforeWithdrawal = collateralToken.balanceOf(
address(this)
) - withdrawn;
*/
uint256 sixDecimalToken_balanceBeforeWithdrawal = six_DecimalToken
.balanceOf(address(this)) - withdrawnAmount;
// 5. 18 Decimal Token Calculations
/**
*Code line Simulated (PerpetutalVault.sol): IERC20(indexToken).balanceOf(address(this)) - amount;
*/
uint256 eighteenDecimalToken_prevCollateralBalance = eighteen_DecimalToken
.balanceOf(address(this)) - orderResultDataOutputAmount;
/**
*Code line Simulated (PerpetutalVault.sol): uint256 total = (IERC20(indexToken).balanceOf(address(this)) *
prices.indexTokenPrice.min) /
prices.shortTokenPrice.min +
collateralToken.balanceOf(address(this)) +
positionData.netValue / prices.shortTokenPrice.min;
*/
uint256 eighteenDecimalToken_total = (indexTokenBalance *
indexTokenPriceMin) /
shortTokenPriceMin +
eighteen_DecimalToken.balanceOf(address(this)) +
positionDataNetValue /
shortTokenPriceMin;
/**
*Code line Simulated (PerpetutalVault.sol): amountIn = collateralToken.balanceOf(address(this));
*/
uint256 eighteenDecimalToken_amountIn = eighteen_DecimalToken.balanceOf(
address(this)
);
/**
*Code line Simulated (PerpetutalVault.sol): uint256 swapAmount = (IERC20(indexToken).balanceOf (address(this)) * shares) / totalShares;
*/
uint256 eighteenDecimalToken_amount = (eighteen_DecimalToken.balanceOf(
address(this)
) * shares) / totalShares;
/**
*Code line Simulated (PerpetutalVault.sol): uint256 balanceBeforeWithdrawal = collateralToken.balanceOf(
address(this)
) - withdrawn;
*/
uint256 eighteenDecimalToken_balanceBeforeWithdrawal = eighteen_DecimalToken
.balanceOf(address(this)) - withdrawnAmount;
// 6. Assertions: Ensure Each Value Differs & Log Values for Debugging
assertFalse(
sixDecimalToken_prevCollateralBalance ==
eighteenDecimalToken_prevCollateralBalance,
"prevCollateralBalance should differ for 6 and 18 decimal tokens"
);
console.log(
"6 Decimal prevCollateralBalance:",
sixDecimalToken_prevCollateralBalance
);
console.log(
"18 Decimal prevCollateralBalance:",
eighteenDecimalToken_prevCollateralBalance
);
assertFalse(
sixDecimalToken_total == eighteenDecimalToken_total,
"Total vault value should differ"
);
console.log("6 Decimal total value:", sixDecimalToken_total);
console.log("18 Decimal total value:", eighteenDecimalToken_total);
assertFalse(
sixDecimalToken_amountIn == eighteenDecimalToken_amountIn,
"amountIn should differ"
);
console.log("6 Decimal amountIn:", sixDecimalToken_amountIn);
console.log("18 Decimal amountIn:", eighteenDecimalToken_amountIn);
assertFalse(
sixDecimalToken_amount == eighteenDecimalToken_amount,
"amount (user's share value) should differ"
);
console.log("6 Decimal user share amount:", sixDecimalToken_amount);
console.log(
"18 Decimal user share amount:",
eighteenDecimalToken_amount
);
assertFalse(
sixDecimalToken_balanceBeforeWithdrawal ==
eighteenDecimalToken_balanceBeforeWithdrawal,
"balanceBeforeWithdrawal should differ"
);
console.log(
"6 Decimal balanceBeforeWithdrawal:",
sixDecimalToken_balanceBeforeWithdrawal
);
console.log(
"18 Decimal balanceBeforeWithdrawal:",
eighteenDecimalToken_balanceBeforeWithdrawal
);
console.log(
"Test Passed: Different token decimals lead to different calculations"
);
}
Output:
Ran 1 test for test/PerpetualVault.t.sol:PerpetualVaultTest
[PASS] test_IncorrectCalculationsWith6And18DecimalTokens() (gas: 1651108)
Logs:
6 Decimal prevCollateralBalance: 999999000000
18 Decimal prevCollateralBalance: 999999999999999999000000
6 Decimal total value: 10000001000000000002
18 Decimal total value: 1000010000000000000000002
6 Decimal amountIn: 1000000000000
18 Decimal amountIn: 1000000000000000000000000
6 Decimal user share amount: 100000000000
18 Decimal user share amount: 100000000000000000000000
6 Decimal balanceBeforeWithdrawal: 999500000000
18 Decimal balanceBeforeWithdrawal: 999999999999999500000000
Test Passed: Different token decimals lead to different calculations
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 14.47ms (1.21ms CPU time)

Impact

This vulnerability introduces financial discrepancies in the vault operations. The primary risks include:

Incorrect Share Calculations – Users may receive fewer shares than expected due to improper scaling of balances.
Misrepresented Vault Value – The total vault value may be significantly skewed depending on the token's decimal format.
Incorrect Deposits & Withdrawals – Users could withdraw more or less than their fair share of funds, leading to loss of funds or unfair advantages.
Potential Exploitation – Malicious actors could strategically deposit low-decimal tokens to manipulate share distribution and extract an unfair portion of assets.

Tools Used

Manual Analysis

Recommendations

To resolve the decimal mismatch issue, the following fixes should be implemented:

Normalize Token Decimals Before Performing Arithmetic
Modify the contract to retrieve each token's decimal value using IERC20.decimals() and adjust calculations accordingly:

uint256 decimals = IERC20(collateralToken).decimals();
uint256 normalizedAmount = amount * (10 ** (18 - decimals)); // Scale to 18 decimals

This ensures that all calculations are performed in a consistent format.

Use a Standardized Unit for Vault Calculations
Instead of performing calculations in raw token units, introduce a unified precision unit (e.g., 18 decimals) to prevent inconsistencies:

uint256 normalizedBalance = IERC20(token).balanceOf(address(this)) * 1e18 / (10 ** tokenDecimals);

Audit All Deposits, Withdrawals, and Vault Value Calculations
Ensure that every function handling collateral token balances, shares, and vault value is updated to accommodate different decimal values.

Updates

Lead Judging Commences

n0kto Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Suppositions

There is no real proof, concrete root cause, specific impact, or enough details in those submissions. Examples include: "It could happen" without specifying when, "If this impossible case happens," "Unexpected behavior," etc. Make a Proof of Concept (PoC) using external functions and realistic parameters. Do not test only the internal function where you think you found something.

Support

FAQs

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

Give us feedback!