高 执行逻辑错误的参数

漏洞代码:

/**
* @notice 此函数是提款流程的结束。
* @dev 应更新所有必要的全局状态变量
*
* @param withdrawn 从头寸中提取的代币数量
* @param positionClosed 当完全通过提款关闭头寸时为真,否则为假
*/
function _handleReturn(uint256 withdrawn, bool positionClosed, bool refundFee) internal {
(uint256 depositId) = flowData;
uint256 shares = depositInfo[depositId].shares;
uint256 amount;
if (positionClosed) {
amount = collateralToken.balanceOf(address(this)) * shares / totalShares;
} else {
uint256 balanceBeforeWithdrawal = collateralToken.balanceOf(address(this)) - withdrawn;
amount = withdrawn + balanceBeforeWithdrawal * shares / totalShares;
}
if (amount > 0) {
_transferToken(depositId, amount);
}
emit Burned(depositId, depositInfo[depositId].recipient, depositInfo[depositId].shares, amount);
_burn(depositId);

if (refundFee) {
uint256 usedFee = callbackGasLimit * tx.gasprice;
if (depositInfo[depositId].executionFee > usedFee) {
try IGmxProxy(gmxProxy).refundExecutionFee(depositInfo[counter].owner, depositInfo[counter].executionFee - usedFee) {} catch {}
}
}

// 更新全局状态
delete swapProgressData;
delete flowData;
delete flow;
}

refundFee 代码块中,它检查了 depositInfo[depositId].executionFee,但在调用 refundExecutionFee 时却使用了 depositInfo[counter]counter 是一个状态变量,仅在有新存款时递增1,而 depositId 来自 flowData,它被更新为请求提款的用户预期的存款ID值

调用函数时,具体分析每一个参数的意义,如果函数上下文使用不同的参数的话那么就要注意一下会不会时参数使用错误

高 计算错误,本意是计算部分的头寸,计算成了整个的

漏洞代码

function getPnl(
bytes32 key,
MarketPrices memory prices,
uint256 sizeDeltaUsd
) external view returns (int256) {
uint256 sizeInTokens = getPositionSizeInUsd(key);
if (sizeInTokens == 0) return 0;

PositionInfo memory positionInfo = gmxReader.getPositionInfo(
address(dataStore),
referralStorage,
key,
prices,
sizeDeltaUsd,
address(0),
true
);

return positionInfo.pnlAfterPriceImpactUsd;
}

PerpetualVault::_withdraw 函数中存在一个漏洞,特别是在计算盈亏(PnL)扣除时。问题是由于使用了 gmxReader.getPositionInfo,并将参数 usePositionSizeAsSizeDeltaUsd 设置为 true,导致 PnL 是针对整个头寸计算的,而不是仅针对用户的份额。这导致从用户的提款金额中扣除过多,不公平地减少了他们的资金。

实际上就是参数设置错误了,导致使用了全部的头寸

高 过早的删除数据

漏洞代码

function _handleReturn(uint256 withdrawn, bool positionClosed, bool refundFee) internal {
(uint256 depositId) = flowData;
uint256 shares = depositInfo[depositId].shares;
uint256 amount;
if (positionClosed) {
amount = collateralToken.balanceOf(address(this)) * shares / totalShares;
} else {
uint256 balanceBeforeWithdrawal = collateralToken.balanceOf(address(this)) - withdrawn;
amount = withdrawn + balanceBeforeWithdrawal * shares / totalShares;
}
if (amount > 0) {
_transferToken(depositId, amount);
}
emit Burned(depositId, depositInfo[depositId].recipient, depositInfo[depositId].shares, amount);
_burn(depositId);

if (refundFee) {
uint256 usedFee = callbackGasLimit * tx.gasprice;
if (depositInfo[depositId].executionFee > usedFee) { // 检查是否有剩余,但最终会失败
try IGmxProxy(gmxProxy).refundExecutionFee(depositInfo[counter].owner, depositInfo[counter].executionFee - usedFee) {} catch {}
}
}

// 更新全局状态
delete swapProgressData;
delete flowData;
delete flow;
}

function _burn(uint256 depositId) internal {
EnumerableSet.remove(userDeposits[depositInfo[depositId].owner], depositId);
totalShares = totalShares - depositInfo[depositId].shares;
delete depositInfo[depositId]; // 删除 depositId 的映射
}

在执行 ``PerpetualVault::_handleReturn() 函数期间,会调用 _burn() 函数,该函数会销毁用户的份额并删除跟踪用户存款的 depositInfo 映射。在销毁份额之后,函数启动退款费用流程,检查 depositInfo[depositId].executionFee > usedFee `以确定执行费用是否大于已使用的费用。然而,即使执行费用高于已使用的费用,这个条件也永远不会被执行,因为包含用户给出的执行费用等关键数据的 depositInfo 映射已经被删除。结果,用户可能会因为未处理的退款而遭受资金损失

对于删除的数据时,联想到会不会影响到其他效果

高 在计算头寸的净值时使用了头寸费用

头寸费用是按照头寸大小的一定百分比计算的,这部分费用应该从用户的存款中扣除,但在计算总金额时被错误地忽略了。
这导致后续存款的用户获得的份额比他们应得的要多,因为总金额被高估了

一个例子:

考虑以下示例(为了示例的目的,我们可以假设没有借款/资金费用,两次增加操作之间的指数代币价格相同):
我们有一个头寸,有 $1000 的抵押品和 $3000 的头寸规模(以美元计)。假设头寸费用是 0.33%(根据头寸规模计算),我们有 1000e14 总份额(1000e6 * 1e8)
Alice 存入价值 $1000 的抵押品,并获得了 1000e6 * totalShares / 1000e6 - (positionFee) 份额,这等于 1000e6 * 1000e14 / 1000e6 - 10e6,即 99999999990000000 份额
Bob 在 Alice 之后立即存入 $1000 份额,由于指数代币价格相同且没有资金/借款费用,Bob 应该获得相同数量的份额。
计算 Bob 的份额,我们得到 1000e6 * (1000e14 + Alice's shares) / (1000e6 + Alice's increase - positionFee),这等于 1000e6 * (1000e14 + 99999999990000000) / uint(1990e6 - 20e6),即 101522842634517766
每个连续的存款人将获得越来越多的不公平份额

高 计算价格的方式有些简单

这是Gamma计算价格影响的简化版本:

priceImpactInCollateralTokens = ((sizeDeltaInUsd / prices.indexTokenPrice.min) - (curSizeInTokens - prevSizeInTokens)) * prices.indexTokenPrice.min / prices.shortTokenPrice.min

基本上,它取sizeDeltaInUsd变量(这是从GMX发送过来的,所以是实际值)并除以指数代币(例如WETH)的价格,因此结果是以WETH为单位的。
然后它取curSizeInTokens,即当前头寸大小(例如以WETH计)并减去prevSizeInTokens,即用户存款前头寸大小(以代币计)。
然后从第一个结果中减去第二个结果,这使得结果基本上等于:基于sizeDeltaInUsd的头寸变化减去以代币计的头寸变化。
最后,它对结果应用价格。
Gamma计算价格影响的方式过于简单

中 不支持多个回调

漏洞函数

modifier validCallback(bytes32 key, Order.Props memory order) {
require(
msg.sender == address(orderHandler) ||
msg.sender == address(liquidationHandler) ||
msg.sender == address(adlHandler),
"invalid caller"
);
require(order.addresses.account == address(this), "not mine");
_;
}

GmxProxy合约的validCallback修饰符不支持GMX V2中可能出现的多个OrderHandler的情况。通过使用角色验证或动态白名单机制,可以增强系统的灵活性和安全性,确保即使在系统更新或扩展时也能正常运行

如果GMX更新到V2版本,并且确实有多个OrderHandler,那么Gamma协议将无法正确处理这些回调,因为validCallback修饰符会阻止这些额外的OrderHandler调用回调函数。
这将导致Gamma协议无法处理与这些新OrderHandler相关的所有操作,可能导致协议部分或完全失效

中 变量更新不及时

一旦金库被完全清算,totalDepositAmount(总存款金额)保持不变。即使存款人(withdrawer)尝试提款,totalDepositAmount也不会因depositInfo[depositId].amount(特定存款ID的金额)而减少。

金库被清算时,并没有删除totalDepositAmount

中 全局变量改变,会影响很多

如果协议更改了 lockTime(锁定时间),它应该只适用于新的存款,并且不影响现有的存款。如果延长锁定期限,用户可能被迫比预期更长的时间锁定资金,阻止了在亏损或盈利期间计划的及时提款。相反,如果缩短锁定期限,早期从旧存款中提款可能会破坏交易策略,导致被迫清算或过早平仓

总结

对于这种流动性的合约,参数,计算,头寸计算,重点在于计算上面,这次的参数就是有很多问题