Summary
The prices
variable in the PerpetualVault::deposit()
function is declared as MarketPrices memory prices
in the code but is not initialised with any values. This "uninitialised" variable is then passed into the PerpetualVault::_mint()
function.
Vulnerability Details
Extract of the PerpetualVault::deposit()
function:
function deposit(uint256 amount) external payable nonReentrant {
_noneFlow();
if (depositPaused == true) {
revert Error.Paused();
}
if (amount < minDepositAmount) {
revert Error.InsufficientAmount();
}
if (totalDepositAmount + amount > maxDepositAmount) {
revert Error.ExceedMaxDepositCap();
}
flow = FLOW.DEPOSIT;
collateralToken.safeTransferFrom(msg.sender, address(this), amount);
counter++;
depositInfo[counter] = DepositInfo(amount, 0, msg.sender, 0, block.timestamp, address(0));
totalDepositAmount += amount;
EnumerableSet.add(userDeposits[msg.sender], counter);
if (positionIsClosed) {
@> MarketPrices memory prices;
@> _mint(counter, amount, false, prices);
_finalize(hex"");
} else {
_payExecutionFee(counter, true);
nextAction.selector = NextActionSelector.INCREASE_ACTION;
nextAction.data = abi.encode(beenLong);
}
}
Prices will most likely contain 0 as a value, as it is represented by uint256 as per the declarations below extracted from Libraries::StructData.sol
.
struct PriceProps {
uint256 min;
uint256 max;
}
struct MarketPrices {
PriceProps indexTokenPrice;
PriceProps longTokenPrice;
PriceProps shortTokenPrice;
}
The PerpetualVault::_mint()
function then uses prices
to calculate the totalAmountBefore
.
function _mint(uint256 depositId, uint256 amount, bool refundFee, MarketPrices memory prices) internal {
uint256 _shares;
if (totalShares == 0) {
_shares = depositInfo[depositId].amount * 1e8;
} else {
uint256 totalAmountBefore;
if (positionIsClosed == false && _isLongOneLeverage(beenLong)) {
totalAmountBefore = IERC20(indexToken).balanceOf(address(this)) - amount;
} else {
@> totalAmountBefore = _totalAmount(prices) - amount;
}
if (totalAmountBefore == 0) totalAmountBefore = 1;
_shares = amount * totalShares / totalAmountBefore;
}
depositInfo[depositId].shares = _shares;
totalShares = totalShares + _shares;
Impact
Not using actual price data can result in incorrect share calculations for the user and the user getting less funds than they should when they withdraw their funds from the protocol.
The deposit, mint and withdraw functions are critical functions to the proper functioning of the protocol and are used by each user as they either start using the protocol or want to get their funds back.
Tools Used
Manual review
Recommendations
Ensure that the prices variable is initialised with the most up to date, accurate and appropriate market data. This can be done either by fetching the market prices through the GmxProxy::getMarketPrices()
function or by directly populating the prices struc with real-time market data before passing it to mint. The preferred approach would be to use GmxProxy::getMarketPrices()
as per below:
Extract from PerpetualVault::deposit()
if (positionIsClosed) {
- MarketPrices memory prices;
+ MarketPrices memory marketPrices = gmxProxy.getMarketPrices(market);
_mint(counter, amount, false, prices);
_finalize(hex"");
There is another instance of this occurring in the PerpetualVault::withdraw()
function as per the code extract below:
function withdraw(address recipient, uint256 depositId) public payable nonReentrant {
_noneFlow();
flow = FLOW.WITHDRAW;
flowData = depositId;
if (recipient == address(0)) {
revert Error.ZeroValue();
}
if (depositInfo[depositId].timestamp + lockTime >= block.timestamp) {
revert Error.Locked();
}
if (EnumerableSet.contains(userDeposits[msg.sender], depositId) == false) {
revert Error.InvalidUser();
}
if (depositInfo[depositId].shares == 0) {
revert Error.ZeroValue();
}
depositInfo[depositId].recipient = recipient;
_payExecutionFee(depositId, false);
if (curPositionKey != bytes32(0)) {
nextAction.selector = NextActionSelector.WITHDRAW_ACTION;
_settle();
} else {
@> MarketPrices memory prices;
_withdraw(depositId, hex"", prices);
}
}
The same mitigation would apply for this instance.