Steadefi

Steadefi
DeFiHardhatFoundryOracle
35,000 USDC
View results
Submission Details
Severity: high
Invalid

Unnecessary precision downscaling in the GMXOracle#`getLpTokenValue()` lead to a deflated-price of GM token

Summary

Within the GMXOracle#getLpTokenValue(), the decimals precision of each GMX MarketToken-returned via the GMXOracle#getMarketTokenInfo() would be assumed as 1e3## Summary Within the GMXOracle#getLpTokenValue(), the decimals precision of each GMX MarketToken-returned via the GMXOracle#getMarketTokenInfo()would be assumed as1e30. And therefore, it would be normalized by dividing it by 1e12. However, the actual decimals precision of each GMX MarketToken-returned via the GMXOracle#getMarketTokenInfo()would be in1e18`.

This lead to a deflated-price of GM token (LpTokenValue) due to unnecessary precision downscaling.

Vulnerability Details

When retrieving a price of a GM token (LpTokenValue), the GMXOracle#getLpTokenValue() would be called.

Within the GMXOracle#getLpTokenValue(), the GMXOracle#getMarketTokenInfo() would be called to retrieve the _marketTokenPrice. And then, it would be normalized by dividing by 1e12. Finally the result would be returned like this:
https://github.com/Cyfrin/2023-10-SteadeFi/blob/main/contracts/oracles/GMXOracle.sol#L250-L257
https://github.com/Cyfrin/2023-10-SteadeFi/blob/main/contracts/oracles/GMXOracle.sol#L263-L264

/**
* @notice Get LP (market) token reserves
* @param marketToken LP token address
* @param indexToken Index token address
* @param longToken Long token address
* @param shortToken Short token address
* @param isDeposit Boolean for deposit or withdrawal
* @param maximize Boolean for minimum or maximum price
* @return marketTokenPrice in 1e18
*/
function getLpTokenValue(
address marketToken,
address indexToken,
address longToken,
address shortToken,
bool isDeposit,
bool maximize
) public view returns (uint256) {
bytes32 _pnlFactorType;
if (isDeposit) {
_pnlFactorType = keccak256(abi.encode("MAX_PNL_FACTOR_FOR_DEPOSITS"));
} else {
_pnlFactorType = keccak256(abi.encode("MAX_PNL_FACTOR_FOR_WITHDRAWALS"));
}
(int256 _marketTokenPrice,) = getMarketTokenInfo( ///<---------------------------- @audit
marketToken,
indexToken,
longToken,
shortToken,
_pnlFactorType,
maximize
);
// If LP token value is negative, return 0
if (_marketTokenPrice < 0) {
return 0;
} else {
// Price returned in 1e30, we normalize it to 1e18 ///<---------------------------- @audit
return uint256(_marketTokenPrice) / 1e12;
}
}

Within the GMXOracle#getMarketTokenInfo(), the result of the ISyntheticReader#getMarketTokenPrice() (Reader#getMarketTokenPrice()) would be returned like this:

Note
The original contract of the ISyntheticReader would be named the Reader contract:https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/reader/Reader.sol#L187-L206
(Doc:https://gmx-docs.io/docs/api/contracts-v2#reading-values )

https://github.com/Cyfrin/2023-10-SteadeFi/blob/main/contracts/oracles/GMXOracle.sol#L178-L186

/**
* @notice Get LP (market) token info
* @param marketToken LP token address
* @param indexToken Index token address
* @param longToken Long token address
* @param shortToken Short token address
* @param pnlFactorType P&L Factory type in bytes32 hashed string
* @param maximize Min/max price boolean
* @return (marketTokenPrice, MarketPoolValueInfoProps MarketInfo)
*/
function getMarketTokenInfo(
address marketToken,
address indexToken,
address longToken,
address shortToken,
bytes32 pnlFactorType,
bool maximize
) public view returns (int256, ISyntheticReader.MarketPoolValueInfoProps memory) {
...
return syntheticReader.getMarketTokenPrice( ///<-------------------- @audit
dataStore,
_market,
_indexTokenPrice,
_longTokenPrice,
_shortTokenPrice,
pnlFactorType,
maximize
);
}

Within the Reader#getMarketTokenPrice() (ISyntheticReader#getMarketTokenPrice()), the result of the MarketUtils#getMarketTokenPrice() would be returned like this:
https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/reader/Reader.sol#L197-L205

function getMarketTokenPrice(
DataStore dataStore,
Market.Props memory market,
Price.Props memory indexTokenPrice,
Price.Props memory longTokenPrice,
Price.Props memory shortTokenPrice,
bytes32 pnlFactorType,
bool maximize
) external view returns (int256, MarketPoolValueInfo.Props memory) {
return
MarketUtils.getMarketTokenPrice( ///<------------ @audit
dataStore,
market,
indexTokenPrice,
longTokenPrice,
shortTokenPrice,
pnlFactorType,
maximize
);
}

Within the MarketUtils#getMarketTokenPrice(), the marketTokenPrice would be calculated based on the formula (Precision.mulDiv(Precision.WEI_PRECISION, poolValueInfo.poolValue, supply)). And then, the result of the calculation would be returned like this:
https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/market/MarketUtils.sol#L164-L165

// @dev get the market token's price
// @param dataStore DataStore
// @param market the market to check
// @param longTokenPrice the price of the long token
// @param shortTokenPrice the price of the short token
// @param indexTokenPrice the price of the index token
// @param maximize whether to maximize or minimize the market token price
// @return returns (the market token's price, MarketPoolValueInfo.Props)
function getMarketTokenPrice(
DataStore dataStore,
Market.Props memory market,
Price.Props memory indexTokenPrice,
Price.Props memory longTokenPrice,
Price.Props memory shortTokenPrice,
bytes32 pnlFactorType,
bool maximize
) external view returns (int256, MarketPoolValueInfo.Props memory) {
uint256 supply = getMarketTokenSupply(MarketToken(payable(market.marketToken)));
MarketPoolValueInfo.Props memory poolValueInfo = getPoolValueInfo(
dataStore,
market,
indexTokenPrice,
longTokenPrice,
shortTokenPrice,
pnlFactorType,
maximize
);
// if the supply is zero then treat the market token price as 1 USD
if (supply == 0) {
return (Precision.FLOAT_PRECISION.toInt256(), poolValueInfo);
}
if (poolValueInfo.poolValue == 0) { return (0, poolValueInfo); }
int256 marketTokenPrice = Precision.mulDiv(Precision.WEI_PRECISION, poolValueInfo.poolValue, supply); ///<------- @audit
return (marketTokenPrice, poolValueInfo); ///<------- @audit
}

Let's zooming into the calculation of the marketTokenPrice above.
Here is the calculation process of the marketTokenPrice above to observe only decimals precision transition in the calculation process of above:
https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/market/MarketUtils.sol#L164

int256 marketTokenPrice = Precision.mulDiv(Precision.WEI_PRECISION, poolValueInfo.poolValue, supply);
int256 marketTokenPrice = Precision.mulDiv(1e18, 1e18, 1e18);
int256 marketTokenPrice = 1e36 / 1e18;
int256 marketTokenPrice = 1e18;

As we can see above, the decimals precision of the marketTokenPrice would result in 1e18.

Also, according to the "Market" part in the GMX's README, the price of MarketToken, which is retrieved via the MarketUtils#getMarketTokenPrice(), would be in 1e18 decimals precision like this:
https://github.com/gmx-io/gmx-synthetics/tree/main#markets-1 \

At any point in time, the price of a MarketToken is (worth of market pool) / MarketToken.totalSupply(), the function MarketUtils.getMarketTokenPrice can be used to retrieve this value.
The worth of the market pool is the sum of

  • worth of all tokens deposited into the pool

  • total pending PnL of all open positions

  • total pending borrow fees of all open positions

In addition to that, according to the arbiscan (Etherscan for Arbitrum), the decimals precision of each GM token on Arbitrum-used in the SteadeFi protocol would be the 1e18 like this: \

  • ETH-USDC GM token:0x70d95587d40a2caf56bd97485ab3eec10bee6336
    https://arbiscan.io/token/0x70d95587d40a2caf56bd97485ab3eec10bee6336

  • WBTC-USDC GM token:0x47c031236e19d024b42f8AE6780E44A573170703
    https://arbiscan.io/token/0x47c031236e19d024b42f8AE6780E44A573170703

  • ARB-USDC GM token: 0xC25cEf6061Cf5dE5eb761b50E4743c1F5D7E5407
    https://arbiscan.io/token/0xC25cEf6061Cf5dE5eb761b50E4743c1F5D7E5407

  • LINK-USDC GM token: 0x7f1fa204bb700853D36994DA19F830b6Ad18455C
    https://arbiscan.io/token/0x7f1fa204bb700853D36994DA19F830b6Ad18455C

  • SOL-USDC GM token: 0x09400D9DB990D5ed3f35D7be61DfAEB900Af03C9
    https://arbiscan.io/token/0x09400D9DB990D5ed3f35D7be61DfAEB900Af03C9

  • UNI-USDC GM token: 0xc7Abb2C5f3BF3CEB389dF0Eecd6120D451170B50
    https://arbiscan.io/token/0xc7Abb2C5f3BF3CEB389dF0Eecd6120D451170B50

However, within the GMXOracle#getLpTokenValue(), the decimals precision of each GMX MarketToken-returned via the GMXOracle#getMarketTokenInfo() would be assumed as a 1e30.
This is problematic. Because, within the GMXOracle#getLpTokenValue(), the decimals precision of each GMX MarketToken-returned would be 1e12 times smaller than the actual decimals precision of it if the decimals precision of each GMX MarketToken-returned via the GMXOracle#getMarketTokenInfo() would be in 1e18 before normalized like this:
(NOTE:Below would track only decimals precision transition)
https://github.com/Cyfrin/2023-10-SteadeFi/blob/main/contracts/oracles/GMXOracle.sol#L263-L264

// Price returned in 1e30, we normalize it to 1e18
return uint256(_marketTokenPrice) / 1e12;
return uint256(1e18) / 1e12;
return uint256(1e6);

This lead to a deflated-price of GM token (LpTokenValue) due to unnecessary precision downscaling.

Impact

This lead to a deflated-price of GM token (LpTokenValue) due to unnecessary precision downscaling.

Tools Used

  • Foundry

Recommendations

Within the GMXOracle#getLpTokenValue(), consider removing the unnecessary division (/ 1e12) like this:

function getLpTokenValue(
address marketToken,
address indexToken,
address longToken,
address shortToken,
bool isDeposit,
bool maximize
) public view returns (uint256) {
...
(int256 _marketTokenPrice,) = getMarketTokenInfo(
marketToken,
indexToken,
longToken,
shortToken,
_pnlFactorType,
maximize
);
// If LP token value is negative, return 0
if (_marketTokenPrice < 0) {
return 0;
} else {
- // Price returned in 1e30, we normalize it to 1e18
+ return uint256(_marketTokenPrice);
- return uint256(_marketTokenPrice) / 1e12;
}
}

0. And therefore, it would be normalized by dividing it by 1e12. However, the actual decimals precision of each GMX MarketToken-returned via the GMXOracle#getMarketTokenInfo()would be in1e18`.

This lead to a deflated-price of GM token (LpTokenValue) due to unnecessary precision downscaling.

Vulnerability Details

When retrieving a price of a GM token (LpTokenValue), the GMXOracle#getLpTokenValue() would be called.

Within the GMXOracle#getLpTokenValue(), the GMXOracle#getMarketTokenInfo() would be called to retrieve the _marketTokenPrice. And then, it would be normalized by dividing by 1e12. Finally the result would be returned like this:
https://github.com/Cyfrin/2023-10-SteadeFi/blob/main/contracts/oracles/GMXOracle.sol#L250-L257
https://github.com/Cyfrin/2023-10-SteadeFi/blob/main/contracts/oracles/GMXOracle.sol#L263-L264

/**
* @notice Get LP (market) token reserves
* @param marketToken LP token address
* @param indexToken Index token address
* @param longToken Long token address
* @param shortToken Short token address
* @param isDeposit Boolean for deposit or withdrawal
* @param maximize Boolean for minimum or maximum price
* @return marketTokenPrice in 1e18
*/
function getLpTokenValue(
address marketToken,
address indexToken,
address longToken,
address shortToken,
bool isDeposit,
bool maximize
) public view returns (uint256) {
bytes32 _pnlFactorType;
if (isDeposit) {
_pnlFactorType = keccak256(abi.encode("MAX_PNL_FACTOR_FOR_DEPOSITS"));
} else {
_pnlFactorType = keccak256(abi.encode("MAX_PNL_FACTOR_FOR_WITHDRAWALS"));
}
(int256 _marketTokenPrice,) = getMarketTokenInfo( ///<---------------------------- @audit
marketToken,
indexToken,
longToken,
shortToken,
_pnlFactorType,
maximize
);
// If LP token value is negative, return 0
if (_marketTokenPrice < 0) {
return 0;
} else {
// Price returned in 1e30, we normalize it to 1e18 ///<---------------------------- @audit
return uint256(_marketTokenPrice) / 1e12;
}
}

Within the GMXOracle#getMarketTokenInfo(), the result of the ISyntheticReader#getMarketTokenPrice() (Reader#getMarketTokenPrice()) would be returned like this:

Note
The original contract of the ISyntheticReader would be named the Reader contract:https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/reader/Reader.sol#L187-L206
(Doc:https://gmx-docs.io/docs/api/contracts-v2#reading-values )
https://github.com/Cyfrin/2023-10-SteadeFi/blob/main/contracts/oracles/GMXOracle.sol#L178-L186

/**
* @notice Get LP (market) token info
* @param marketToken LP token address
* @param indexToken Index token address
* @param longToken Long token address
* @param shortToken Short token address
* @param pnlFactorType P&L Factory type in bytes32 hashed string
* @param maximize Min/max price boolean
* @return (marketTokenPrice, MarketPoolValueInfoProps MarketInfo)
*/
function getMarketTokenInfo(
address marketToken,
address indexToken,
address longToken,
address shortToken,
bytes32 pnlFactorType,
bool maximize
) public view returns (int256, ISyntheticReader.MarketPoolValueInfoProps memory) {
...
return syntheticReader.getMarketTokenPrice( ///<-------------------- @audit
dataStore,
_market,
_indexTokenPrice,
_longTokenPrice,
_shortTokenPrice,
pnlFactorType,
maximize
);
}

Within the Reader#getMarketTokenPrice() (ISyntheticReader#getMarketTokenPrice()), the result of the MarketUtils#getMarketTokenPrice() would be returned like this:
https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/reader/Reader.sol#L197-L205

function getMarketTokenPrice(
DataStore dataStore,
Market.Props memory market,
Price.Props memory indexTokenPrice,
Price.Props memory longTokenPrice,
Price.Props memory shortTokenPrice,
bytes32 pnlFactorType,
bool maximize
) external view returns (int256, MarketPoolValueInfo.Props memory) {
return
MarketUtils.getMarketTokenPrice( ///<------------ @audit
dataStore,
market,
indexTokenPrice,
longTokenPrice,
shortTokenPrice,
pnlFactorType,
maximize
);
}

Within the MarketUtils#getMarketTokenPrice(), the marketTokenPrice would be calculated based on the formula (Precision.mulDiv(Precision.WEI_PRECISION, poolValueInfo.poolValue, supply)). And then, the result of the calculation would be returned like this:
https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/market/MarketUtils.sol#L164-L165

// @dev get the market token's price
// @param dataStore DataStore
// @param market the market to check
// @param longTokenPrice the price of the long token
// @param shortTokenPrice the price of the short token
// @param indexTokenPrice the price of the index token
// @param maximize whether to maximize or minimize the market token price
// @return returns (the market token's price, MarketPoolValueInfo.Props)
function getMarketTokenPrice(
DataStore dataStore,
Market.Props memory market,
Price.Props memory indexTokenPrice,
Price.Props memory longTokenPrice,
Price.Props memory shortTokenPrice,
bytes32 pnlFactorType,
bool maximize
) external view returns (int256, MarketPoolValueInfo.Props memory) {
uint256 supply = getMarketTokenSupply(MarketToken(payable(market.marketToken)));
MarketPoolValueInfo.Props memory poolValueInfo = getPoolValueInfo(
dataStore,
market,
indexTokenPrice,
longTokenPrice,
shortTokenPrice,
pnlFactorType,
maximize
);
// if the supply is zero then treat the market token price as 1 USD
if (supply == 0) {
return (Precision.FLOAT_PRECISION.toInt256(), poolValueInfo);
}
if (poolValueInfo.poolValue == 0) { return (0, poolValueInfo); }
int256 marketTokenPrice = Precision.mulDiv(Precision.WEI_PRECISION, poolValueInfo.poolValue, supply); ///<------- @audit
return (marketTokenPrice, poolValueInfo); ///<------- @audit
}

Let's zooming into the calculation of the marketTokenPrice above.
Here is the calculation process of the marketTokenPrice above to observe only decimals precision transition in the calculation process of above:
https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/market/MarketUtils.sol#L164

int256 marketTokenPrice = Precision.mulDiv(Precision.WEI_PRECISION, poolValueInfo.poolValue, supply);
int256 marketTokenPrice = Precision.mulDiv(1e18, 1e18, 1e18);
int256 marketTokenPrice = 1e36 / 1e18;
int256 marketTokenPrice = 1e18;

As we can see above, the decimals precision of the marketTokenPrice would result in 1e18.

Also, according to the "Market" part in the GMX's README, the price of MarketToken, which is retrieved via the MarketUtils#getMarketTokenPrice(), would be in 1e18 decimals precision like this:
https://github.com/gmx-io/gmx-synthetics/tree/main#markets-1

At any point in time, the price of a MarketToken is (worth of market pool) / MarketToken.totalSupply(), the function MarketUtils.getMarketTokenPrice can be used to retrieve this value.
The worth of the market pool is the sum of

  • worth of all tokens deposited into the pool

  • total pending PnL of all open positions

  • total pending borrow fees of all open positions

In addition to that, according to the arbiscan (Etherscan for Arbitrum), the decimals precision of each GM token on Arbitrum-used in the SteadeFi protocol would be the 1e18 like this:

  • ETH-USDC GM token:0x70d95587d40a2caf56bd97485ab3eec10bee6336
    https://arbiscan.io/token/0x70d95587d40a2caf56bd97485ab3eec10bee6336

  • WBTC-USDC GM token:0x47c031236e19d024b42f8AE6780E44A573170703
    https://arbiscan.io/token/0x47c031236e19d024b42f8AE6780E44A573170703

  • ARB-USDC GM token: 0xC25cEf6061Cf5dE5eb761b50E4743c1F5D7E5407
    https://arbiscan.io/token/0xC25cEf6061Cf5dE5eb761b50E4743c1F5D7E5407

  • LINK-USDC GM token: 0x7f1fa204bb700853D36994DA19F830b6Ad18455C
    https://arbiscan.io/token/0x7f1fa204bb700853D36994DA19F830b6Ad18455C

  • SOL-USDC GM token: 0x09400D9DB990D5ed3f35D7be61DfAEB900Af03C9
    https://arbiscan.io/token/0x09400D9DB990D5ed3f35D7be61DfAEB900Af03C9

  • UNI-USDC GM token: 0xc7Abb2C5f3BF3CEB389dF0Eecd6120D451170B50
    https://arbiscan.io/token/0xc7Abb2C5f3BF3CEB389dF0Eecd6120D451170B50

However, within the GMXOracle#getLpTokenValue(), the decimals precision of each GMX MarketToken-returned via the GMXOracle#getMarketTokenInfo() would be assumed as a 1e30.
This is problematic. Because, within the GMXOracle#getLpTokenValue(), the decimals precision of each GMX MarketToken-returned would be 1e12 times smaller than the actual decimals precision of it if the decimals precision of each GMX MarketToken-returned via the GMXOracle#getMarketTokenInfo() would be in 1e18 before normalized like this:
(NOTE:Below would track only decimals precision transition)
https://github.com/Cyfrin/2023-10-SteadeFi/blob/main/contracts/oracles/GMXOracle.sol#L263-L264

// Price returned in 1e30, we normalize it to 1e18
return uint256(_marketTokenPrice) / 1e12;
return uint256(1e18) / 1e12;
return uint256(1e6);

This lead to a deflated-price of GM token (LpTokenValue) due to unnecessary precision downscaling.

Impact

This lead to a deflated-price of GM token (LpTokenValue) due to unnecessary precision downscaling.

Tools Used

  • Foundry

Recommendations

Within the GMXOracle#getLpTokenValue(), consider removing the unnecessary division (/ 1e12) like this:

function getLpTokenValue(
address marketToken,
address indexToken,
address longToken,
address shortToken,
bool isDeposit,
bool maximize
) public view returns (uint256) {
...
(int256 _marketTokenPrice,) = getMarketTokenInfo(
marketToken,
indexToken,
longToken,
shortToken,
_pnlFactorType,
maximize
);
// If LP token value is negative, return 0
if (_marketTokenPrice < 0) {
return 0;
} else {
- // Price returned in 1e30, we normalize it to 1e18
+ return uint256(_marketTokenPrice);
- return uint256(_marketTokenPrice) / 1e12;
}
}
Updates

Lead Judging Commences

hans Auditor
almost 2 years ago
hans Auditor
almost 2 years ago
hans Lead Judge almost 2 years ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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