{"author":{"address":null,"user":"https://learnblockchain.cn/people/22539"},"content":{"body":"前面几节课呢，我们都了解过GMX是如何通过限价单，市价单进行杠杆的交易。同时也大致了解了系统是怎么处理清算的逻辑的，现在我们来看看，GMX最最核心的合约:**Vault**\r\n\r\n```js\r\nstruct Position {\r\n        // 头寸的大小，表示用户在该头寸中投入的总金额\r\n        uint256 size;\r\n        // 头寸的抵押品金额 \r\n        uint256 collateral;\r\n        // 头寸的平均价格\r\n        uint256 averagePrice;\r\n        // 进入头寸时的资金费率\r\n        uint256 entryFundingRate;\r\n        // 预留金额\r\n        uint256 reserveAmount;\r\n        // 已实现的盈亏\r\n        int256 realisedPnl;\r\n        // 最后一次增加头寸的时间\r\n        uint256 lastIncreasedTime;\r\n    }\r\n```\r\n这个是Vault最主要的结构体。包含头寸大小，抵押品金额，头寸均价，预留金额......\r\n\r\n然后，我们接下来解析，核心的几个方法\r\n**首先我们来看一下buyUSDG() **\r\n这个方法主要是用来购买稳定币\r\n\r\n```js\r\nfunction buyUSDG(address _token, address _receiver) external override nonReentrant returns (uint256) {\r\n        // 校验是否是管理员\r\n        _validateManager();\r\n        // 校验token是否处于白名单里面\r\n        _validate(whitelistedTokens[_token], 16);\r\n        // 启用交换定价机制（在通过预言机获取最小价格的时候，会用到）\r\n        useSwapPricing = true;\r\n\r\n        // 获取转移数量\r\n        uint256 tokenAmount = _transferIn(_token);\r\n        // 校验转移数量\u003e0\r\n        _validate(tokenAmount \u003e 0, 17);\r\n        // 更新累积融资利率\r\n        updateCumulativeFundingRate(_token, _token);\r\n        // 获取最小价格(通过预言机获取)\r\n        uint256 price = getMinPrice(_token);\r\n        // 计算出usdg可以购买的数量\r\n        uint256 usdgAmount = tokenAmount.mul(price).div(PRICE_PRECISION);\r\n        usdgAmount = adjustForDecimals(usdgAmount, _token, usdg);\r\n        _validate(usdgAmount \u003e 0, 18);\r\n\r\n        // 获取本次购买需要缴纳的费用\r\n        uint256 feeBasisPoints = vaultUtils.getBuyUsdgFeeBasisPoints(_token, usdgAmount);\r\n        // 得到扣除了手续费用之后的净金额\r\n        uint256 amountAfterFees = _collectSwapFees(_token, tokenAmount, feeBasisPoints);\r\n        // 使用净金额计算实际可以购买的数量\r\n        uint256 mintAmount = amountAfterFees.mul(price).div(PRICE_PRECISION);\r\n        // 调整小数位精度\r\n        mintAmount = adjustForDecimals(mintAmount, _token, usdg);\r\n\r\n        // 增加USDG的供应量\r\n        _increaseUsdgAmount(_token, mintAmount);\r\n        // 增加代币池中代币的供应量\r\n        _increasePoolAmount(_token, amountAfterFees);\r\n        // 铸造 计算净额 数量的 USDG\r\n        IUSDG(usdg).mint(_receiver, mintAmount);\r\n\r\n        emit BuyUSDG(_receiver, _token, tokenAmount, mintAmount, feeBasisPoints);\r\n        // 再关闭交换定价机制\r\n        useSwapPricing = false;\r\n        return mintAmount;\r\n    }\r\n```\r\n第一步，还是进行执行前的校验，包括白名单和管理员校验\r\n第二步，获取需要兑换的token数量，并校验这个数量必须大于0\r\n第三步，更新与该代币相关的累积融资利率，通常这是在杠杆交易中跟踪与借贷相关的费用。（这里只作更新）\r\n\r\n```js\r\nfunction updateCumulativeFundingRate(address _collateralToken, address _indexToken) public {\r\n        bool shouldUpdate = vaultUtils.updateCumulativeFundingRate(_collateralToken, _indexToken);\r\n        if (!shouldUpdate) {\r\n            return;\r\n        }\r\n        // 初始化lastFundingTimes\r\n        if (lastFundingTimes[_collateralToken] == 0) {\r\n            lastFundingTimes[_collateralToken] = block.timestamp.div(fundingInterval).mul(fundingInterval);\r\n            return;\r\n        }\r\n        // 检查是否到了下一资金间隔\r\n        if (lastFundingTimes[_collateralToken].add(fundingInterval) \u003e block.timestamp) {\r\n            return;\r\n        }\r\n        // 获取下一个资金利率fundingRate  资金利率因子×预留金额×间隔数/池子资金\r\n        uint256 fundingRate = getNextFundingRate(_collateralToken);\r\n        // 增加指定token的资金利率\r\n        cumulativeFundingRates[_collateralToken] = cumulativeFundingRates[_collateralToken].add(fundingRate);\r\n        // 重新设置lastFundingTimes\r\n        lastFundingTimes[_collateralToken] = block.timestamp.div(fundingInterval).mul(fundingInterval);\r\n\r\n        emit UpdateFundingRate(_collateralToken, cumulativeFundingRates[_collateralToken]);\r\n    }\r\n```\r\n第四步，获取最小价格(通过ChainLink预言机获取)\r\n第五步，算出可以购买到的USDG数量\r\n第六步，计算出需要缴纳的费用以及扣除了手续费用之后的净金额，然后使用这个金额得到购买数量\r\n第七步，针对当前token，增加该token已经发出的USDG数量\r\n第八步，增加代币池中代币的供应量\r\n第九步，铸造指定数量的USDG给到用户\r\n第十步，发送事件，并关闭交换定价机制  \r\n通过上述的流程整理，大致的逻辑其实还是比较简单的。\r\n\r\n**接下来我们来看sellUSDG**\r\n和buyUSDG相比，就是方向相反，用USDG购买原生代币（BTC，ETH）\r\n\r\n```js\r\nfunction sellUSDG(address _token, address _receiver) external override nonReentrant returns (uint256) {\r\n        _validateManager();\r\n        _validate(whitelistedTokens[_token], 19);\r\n        useSwapPricing = true;\r\n        // 获取转usdg的转移数量\r\n        uint256 usdgAmount = _transferIn(usdg);\r\n        _validate(usdgAmount \u003e 0, 20);\r\n        // 更新累积融资利率\r\n        updateCumulativeFundingRate(_token, _token);\r\n        // 用于根据usdgAmount 计算可赎回的token数量\r\n        uint256 redemptionAmount = getRedemptionAmount(_token, usdgAmount);\r\n        _validate(redemptionAmount \u003e 0, 21);\r\n        // 降低USDG的供应量\r\n        _decreaseUsdgAmount(_token, usdgAmount);\r\n        // 降低TOKEN的供应量\r\n        _decreasePoolAmount(_token, redemptionAmount);\r\n        // 销毁转移数量的 usdg\r\n        IUSDG(usdg).burn(address(this), usdgAmount);\r\n\r\n        // the _transferIn call increased the value of tokenBalances[usdg]\r\n        // usually decreases in token balances are synced by calling _transferOut\r\n        // however, for usdg, the tokens are burnt, so _updateTokenBalance should\r\n        // be manually called to record the decrease in tokens\r\n        _updateTokenBalance(usdg);\r\n        // 获取需要缴纳的费率\r\n        uint256 feeBasisPoints = vaultUtils.getSellUsdgFeeBasisPoints(_token, usdgAmount);\r\n        // 得到扣除了手续费用之后的净额度\r\n        uint256 amountOut = _collectSwapFees(_token, redemptionAmount, feeBasisPoints);\r\n        _validate(amountOut \u003e 0, 22);\r\n        // 给_receiver 转净额度数量的token\r\n        _transferOut(_token, amountOut, _receiver);\r\n\r\n        emit SellUSDG(_receiver, _token, usdgAmount, amountOut, feeBasisPoints);\r\n\r\n        useSwapPricing = false;\r\n        return amountOut;\r\n    }\r\n```\r\n第一步  校验是否是管理员，是否代币处于白名单上面\r\n第二步  开启交换定价机制（主要是用来预言机喂价格）\r\n第三步  获取需要进行转换的USDG数量\r\n第四步  更新累积融资利率\r\n第五步  根据目标token的价格，计算出可以赎回/购买的token数量\r\n第六步，针对当前token，减少该token已经发出的USDG数量\r\n第七步，减少代币池中代币的供应量\r\n第八步，同步销毁指定数量的USDG\r\n第九步，更新当前合约中USDG剩余数量\r\n第十步，计算需要缴纳的费率，得到扣除了手续费用后的净额\r\n第十一步，转移净额度之后数数量\r\n第十二步， 关闭交换定价机制\r\n**上述两个都是管理员才有权限操作的方法。**\r\n\r\n**接下来看swap方法**，该方法主要是用来进行现货的交易\r\n\r\n```js\r\nfunction swap(address _tokenIn, address _tokenOut, address _receiver) external override nonReentrant returns (uint256) {\r\n        _validate(isSwapEnabled, 23);\r\n        _validate(whitelistedTokens[_tokenIn], 24);\r\n        _validate(whitelistedTokens[_tokenOut], 25);\r\n        _validate(_tokenIn != _tokenOut, 26);\r\n\r\n        useSwapPricing = true;\r\n        // 更新累积融资利率\r\n        updateCumulativeFundingRate(_tokenIn, _tokenIn);\r\n        // 更新累积融资利率\r\n        updateCumulativeFundingRate(_tokenOut, _tokenOut);\r\n        // 获取tokenIn的转移数量\r\n        uint256 amountIn = _transferIn(_tokenIn);\r\n        _validate(amountIn \u003e 0, 27);\r\n        // 预测tokenIn的最小价格\r\n        uint256 priceIn = getMinPrice(_tokenIn);\r\n        // 预测tokenOut的最大价格\r\n        uint256 priceOut = getMaxPrice(_tokenOut);\r\n        // 计算可以出多少数量的tokenOut\r\n        uint256 amountOut = amountIn.mul(priceIn).div(priceOut);\r\n        amountOut = adjustForDecimals(amountOut, _tokenIn, _tokenOut);\r\n\r\n        // adjust usdgAmounts by the same usdgAmount as debt is shifted between the assets\r\n        // 计算这些的转入token需要消耗多少的usdg\r\n        uint256 usdgAmount = amountIn.mul(priceIn).div(PRICE_PRECISION);\r\n        usdgAmount = adjustForDecimals(usdgAmount, _tokenIn, usdg);\r\n        // 需要交换token，需要的费率\r\n        uint256 feeBasisPoints = vaultUtils.getSwapFeeBasisPoints(_tokenIn, _tokenOut, usdgAmount);\r\n        // 得到扣除手续费后实际的交货数量\r\n        uint256 amountOutAfterFees = _collectSwapFees(_tokenOut, amountOut, feeBasisPoints);\r\n        \r\n        // 增加转入的_token 的usdg的数量 \r\n        _increaseUsdgAmount(_tokenIn, usdgAmount);\r\n        // 减少转出的_token 的usdg的数量\r\n        _decreaseUsdgAmount(_tokenOut, usdgAmount);\r\n\r\n        // 增加池子中tokenIn 的数量\r\n        _increasePoolAmount(_tokenIn, amountIn);\r\n        // 减少池子中tokenOut 的数量\r\n        _decreasePoolAmount(_tokenOut, amountOut);\r\n        // 校验池子中的token数量，必须小于缓存的数量（为什么要这步逻辑?）\r\n        _validateBufferAmount(_tokenOut);\r\n\r\n        // 转给receiver 扣除手续费后的数量\r\n        _transferOut(_tokenOut, amountOutAfterFees, _receiver);\r\n\r\n        emit Swap(_receiver, _tokenIn, _tokenOut, amountIn, amountOut, amountOutAfterFees, feeBasisPoints);\r\n\r\n        useSwapPricing = false;\r\n        return amountOutAfterFees;\r\n    }\r\n```\r\n第一步，依然是参数的合法性交易，没有什么难度\r\n第二步，开启交换定价逻辑\r\n第三步，同时更新输入 和 输出代币的累积融资利率\r\n第四步，获取输入的最小价格，输出的最大价格\r\n第五步，计算出可以获取多少的输出token\r\n第六步，计算手续费，以及扣除手续费后，用户实际可以获取的token\r\n第七步，增加转入token的USDG数量\r\n第八步，减少转出token的USDG数量\r\n第九步，增加池子中转入token的数量\r\n第十步，减少池子中转出token的数量\r\n第十一步，校验池子中的token数量，必须小于缓存的数量\r\n这一步，核心主要是避免价格操纵或异常波动，当某种代币的池中数量非常少时，流动性不足会导致价格波动加剧，容易被市场操纵。因此，设置一个代币缓存量（Buffer Amount）可以避免这种情况，保证池子的深度和稳定性。\r\n第十二步，转给receiver 兑换后的token数量\r\n第十三步，关闭交换定价逻辑\r\n\r\n**增加头寸方法increasePosition**\r\n即执行限价单和市价单之后最终的执行方法\r\n\r\n```js\r\n  \t//加码操作\r\n    function increasePosition(address _account, address _collateralToken, address _indexToken, uint256 _sizeDelta, bool _isLong) external override nonReentrant {\r\n        // 校验是否启用了杠杆\r\n        _validate(isLeverageEnabled, 28);\r\n        // 校验当前的gas小于最大的gas费率\r\n        _validateGasPrice();\r\n        // 验证路由器地址是否开启\r\n        _validateRouter(_account);\r\n        // 验证代币是否合法\r\n        _validateTokens(_collateralToken, _indexToken, _isLong);\r\n        //验证增加头寸的条件\r\n        vaultUtils.validateIncreasePosition(_account, _collateralToken, _indexToken, _sizeDelta, _isLong);\r\n        // 更新累积融资利率\r\n        updateCumulativeFundingRate(_collateralToken, _indexToken);\r\n\r\n        bytes32 key = getPositionKey(_account, _collateralToken, _indexToken, _isLong);\r\n        Position storage position = positions[key];\r\n        // 如果是多头，价格就用最大价格，否则就用最低价格\r\n        uint256 price = _isLong ? getMaxPrice(_indexToken) : getMinPrice(_indexToken);\r\n\r\n        if (position.size == 0) {\r\n            // 初始头寸，就用获取的预测价格\r\n            position.averagePrice = price;\r\n        }\r\n\r\n        if (position.size \u003e 0 \u0026\u0026 _sizeDelta \u003e 0) {\r\n            // 多头 nextSize * nextPrice / nextSize + 盈利  空头 nextSize * nextPrice / nextSize - 盈利\r\n            position.averagePrice = getNextAveragePrice(_indexToken, position.size, position.averagePrice, _isLong, price, _sizeDelta, position.lastIncreasedTime);\r\n        }\r\n        // 计算保证金费用\r\n        uint256 fee = _collectMarginFees(_account, _collateralToken, _indexToken, _isLong, _sizeDelta, position.size, position.entryFundingRate);\r\n        // 获取抵押token的移出数量\r\n        uint256 collateralDelta = _transferIn(_collateralToken);\r\n        // 将抵押token转换成USD等值\r\n        uint256 collateralDeltaUsd = tokenToUsdMin(_collateralToken, collateralDelta);\r\n        // 增加抵押\r\n        position.collateral = position.collateral.add(collateralDeltaUsd);\r\n        _validate(position.collateral \u003e= fee, 29);\r\n        // 抵押中扣除保证金\r\n        position.collateral = position.collateral.sub(fee);\r\n        // 从cumulativeFundingRates mapping中取数\r\n        position.entryFundingRate = getEntryFundingRate(_collateralToken, _indexToken, _isLong);\r\n        // 头寸增加\r\n        position.size = position.size.add(_sizeDelta);\r\n        // 记录最后一次增加时间\r\n        position.lastIncreasedTime = block.timestamp;\r\n\r\n\r\n        _validate(position.size \u003e 0, 30);\r\n        // 验证头寸和抵押品的数量\r\n        _validatePosition(position.size, position.collateral);\r\n        // 校验流动性 TODO\r\n        validateLiquidation(_account, _collateralToken, _indexToken, _isLong, true);\r\n\r\n        // reserve tokens to pay profits on the position\r\n        // 根据抵押token 和 头寸,获取最大数量的USDG\r\n        uint256 reserveDelta = usdToTokenMax(_collateralToken, _sizeDelta);\r\n        // position中增加储备金\r\n        position.reserveAmount = position.reserveAmount.add(reserveDelta);\r\n        // 池子中增加储备金\r\n        _increaseReservedAmount(_collateralToken, reserveDelta);\r\n\r\n        if (_isLong) {\r\n            // guaranteedUsd stores the sum of (position.size - position.collateral) for all positions\r\n            // if a fee is charged on the collateral then guaranteedUsd should be increased by that fee amount\r\n            // since (position.size - position.collateral) would have increased by `fee`\r\n            _increaseGuaranteedUsd(_collateralToken, _sizeDelta.add(fee));\r\n            _decreaseGuaranteedUsd(_collateralToken, collateralDeltaUsd);\r\n            // treat the deposited collateral as part of the pool\r\n            // 往抵押token池子中增加 移出数量 _transferIn\r\n            _increasePoolAmount(_collateralToken, collateralDelta);\r\n            // fees need to be deducted from the pool since fees are deducted from position.collateral\r\n            // and collateral is treated as part of the pool\r\n            // 往抵押token池子中减少 根据fee计算出最小的数量\r\n            _decreasePoolAmount(_collateralToken, usdToTokenMin(_collateralToken, fee));\r\n        } else {\r\n            // 更新空头全局平均价格\r\n            if (globalShortSizes[_indexToken] == 0) {\r\n                globalShortAveragePrices[_indexToken] = price;\r\n            } else {\r\n                globalShortAveragePrices[_indexToken] = getNextGlobalShortAveragePrice(_indexToken, price, _sizeDelta);\r\n            }\r\n            // 增加全局空头头寸\r\n            _increaseGlobalShortSize(_indexToken, _sizeDelta);\r\n        }\r\n\r\n        emit IncreasePosition(key, _account, _collateralToken, _indexToken, collateralDeltaUsd, _sizeDelta, _isLong, price, fee);\r\n        emit UpdatePosition(key, position.size, position.collateral, position.averagePrice, position.entryFundingRate, position.reserveAmount, position.realisedPnl, price);\r\n    }\r\n```\r\n第一步 先进行参数合法性校验及规则校验（是否启用杠杆，gas费率，路由器地址，代币合法性等）\r\n第二步 更新累积的融资利率\r\n第三步 根据参数，得到仓位信息\r\n第四步 得到目标token的价格（需要判断多头/空头）\r\n第五步 设置平均价格\r\n第六步 计算保证金费用\r\n第七步  获取抵押token的移出数量\r\n第八步 将抵押token转换成USD等值\r\n第九步 仓位信息中增加抵押，并扣除保证金\r\n第十步 更改仓位信息（最后一次时间，头寸）\r\n第十一步  验证头寸和流动性\r\n第十二步  根据抵押token 和 头寸,获取最大数量的USDG\r\n第十三步  在仓位中和池子中增加储备金\r\n第十四步  根据多头和空头分别执行不同的逻辑\r\n多头主要是更新保证金和抵押池\r\n空头主要是更新全局空头平均价格和全局空头头寸\r\n第十五步  发送增加仓位，更新仓位的两个事件\r\nreservedAmounts  储备金\r\nguaranteedUsd  保证金\r\n这两个资金估计大家会产生困扰，下面给大家来解释一下\r\nreservedAmounts 代表为每个抵押代币（collateralToken）保留的资金，用于开设或维持多头和空头仓位。这部分资金不能被随意使用，因为它已经与用户的仓位绑定，主要用于保障头寸的潜在亏损。\r\nguaranteedUsd 代表整个系统中所有仓位的 实际保证金总额，即仓位中的资金实际用于支撑用户头寸的部分。它通常是仓位大小减去抵押品的差额，或者是系统需要保证的资金。\r\n\r\n**最后了解一下清算的最终方法**\r\n\r\n```js\r\nfunction liquidatePosition(address _account, address _collateralToken, address _indexToken, bool _isLong, address _feeReceiver) external override nonReentrant {\r\n        if (inPrivateLiquidationMode) {\r\n            _validate(isLiquidator[msg.sender], 34);\r\n        }\r\n\r\n        // set includeAmmPrice to false to prevent manipulated liquidations\r\n        includeAmmPrice = false;\r\n        //更新累积融资利率\r\n        updateCumulativeFundingRate(_collateralToken, _indexToken);\r\n\r\n        bytes32 key = getPositionKey(_account, _collateralToken, _indexToken, _isLong);\r\n        Position memory position = positions[key];\r\n        _validate(position.size \u003e 0, 35);\r\n\r\n        (uint256 liquidationState, uint256 marginFees) = validateLiquidation(_account, _collateralToken, _indexToken, _isLong, false);\r\n        // 验证清算状态\r\n        _validate(liquidationState != 0, 36);\r\n        if (liquidationState == 2) {\r\n            // max leverage exceeded but there is collateral remaining after deducting losses so decreasePosition instead\r\n            _decreasePosition(_account, _collateralToken, _indexToken, 0, position.size, _isLong, _account);\r\n            includeAmmPrice = true;\r\n            return;\r\n        }\r\n        // 收取保证金费用，将其添加到费用储备中\r\n        uint256 feeTokens = usdToTokenMin(_collateralToken, marginFees);\r\n        feeReserves[_collateralToken] = feeReserves[_collateralToken].add(feeTokens);\r\n        emit CollectMarginFees(_collateralToken, marginFees, feeTokens);\r\n\r\n        // 减少保留金额和担保的USD\r\n        _decreaseReservedAmount(_collateralToken, position.reserveAmount);\r\n        if (_isLong) {\r\n            _decreaseGuaranteedUsd(_collateralToken, position.size.sub(position.collateral));\r\n            _decreasePoolAmount(_collateralToken, usdToTokenMin(_collateralToken, marginFees));\r\n        }\r\n\r\n        // 记录清算事件，包括仓位信息和当前价格。\r\n        uint256 markPrice = _isLong ? getMinPrice(_indexToken) : getMaxPrice(_indexToken);\r\n        emit LiquidatePosition(key, _account, _collateralToken, _indexToken, _isLong, position.size, position.collateral, position.reserveAmount, position.realisedPnl, markPrice);\r\n\r\n        //如果是空头,同时持仓费用 \u003c 抵押费用 , 仓位抵押减少持仓费用, 同时池子中 增加剩余的数量\r\n        if (!_isLong \u0026\u0026 marginFees \u003c position.collateral) {\r\n            uint256 remainingCollateral = position.collateral.sub(marginFees);\r\n            _increasePoolAmount(_collateralToken, usdToTokenMin(_collateralToken, remainingCollateral));\r\n        }\r\n\r\n        if (!_isLong) {\r\n            // 降低全局空头头寸\r\n            _decreaseGlobalShortSize(_indexToken, position.size);\r\n        }\r\n        // 删除仓位\r\n        delete positions[key];\r\n\r\n        // pay the fee receiver using the pool, we assume that in general the liquidated amount should be sufficient to cover\r\n        // the liquidation fees\r\n        // 支付清算费用给接收者：\r\n        _decreasePoolAmount(_collateralToken, usdToTokenMin(_collateralToken, liquidationFeeUsd));\r\n        _transferOut(_collateralToken, usdToTokenMin(_collateralToken, liquidationFeeUsd), _feeReceiver);\r\n\r\n        includeAmmPrice = true;\r\n    }\r\n```\r\n第一步  验证清算者的合法性\r\n第二步  关闭是否包含AMM价格\r\n第三步  更新累积融资利率\r\n第四步  根据参数获取指定的仓位信息\r\n第五步  判断仓位是否满足清算条件，根据liquidationState进行后续处理\r\n第六步  如果保证金接近爆仓边缘，可能会通过降低仓位使得安全\r\n第七步  收取保证金费用，将其添加到费用储备中\r\n第八步  减少保留金额和担保的USD\r\n第九步  记录清算事件，包括仓位信息和当前价格\r\n第十步  如果是空头，仓位抵押中减少持仓费用，增加到池子里面，同时降低全局空头头寸\r\n第十一步  删除仓位\r\n第十二步  降低池子中的金额即清算费用，将清算费用转给清算机器人\r\n\r\n清算这边的整个逻辑还是蛮复杂的，最好结合着测试用例一起看一下","title":"GMX 源码解析四，Vault的核心源码解读"},"history":null,"timestamp":1725529446,"version":1}