GMX 源码解析四,Vault的核心源码解读

  • Leo
  • 更新于 2024-09-05 17:44
  • 阅读 655

前面几节课呢,我们都了解过GMX是如何通过限价单,市价单进行杠杆的交易。同时也大致了解了系统是怎么处理清算的逻辑的,现在我们来看看,GMX最最核心的合约:VaultstructPosition{//头寸的大小,表示用户在该头寸中投入的总金额uint256

前面几节课呢,我们都了解过GMX是如何通过限价单,市价单进行杠杆的交易。同时也大致了解了系统是怎么处理清算的逻辑的,现在我们来看看,GMX最最核心的合约:Vault

struct Position {
        // 头寸的大小,表示用户在该头寸中投入的总金额
        uint256 size;
        // 头寸的抵押品金额 
        uint256 collateral;
        // 头寸的平均价格
        uint256 averagePrice;
        // 进入头寸时的资金费率
        uint256 entryFundingRate;
        // 预留金额
        uint256 reserveAmount;
        // 已实现的盈亏
        int256 realisedPnl;
        // 最后一次增加头寸的时间
        uint256 lastIncreasedTime;
    }

这个是Vault最主要的结构体。包含头寸大小,抵押品金额,头寸均价,预留金额......

然后,我们接下来解析,核心的几个方法 首先我们来看一下buyUSDG() 这个方法主要是用来购买稳定币

function buyUSDG(address _token, address _receiver) external override nonReentrant returns (uint256) {
        // 校验是否是管理员
        _validateManager();
        // 校验token是否处于白名单里面
        _validate(whitelistedTokens[_token], 16);
        // 启用交换定价机制(在通过预言机获取最小价格的时候,会用到)
        useSwapPricing = true;

        // 获取转移数量
        uint256 tokenAmount = _transferIn(_token);
        // 校验转移数量>0
        _validate(tokenAmount > 0, 17);
        // 更新累积融资利率
        updateCumulativeFundingRate(_token, _token);
        // 获取最小价格(通过预言机获取)
        uint256 price = getMinPrice(_token);
        // 计算出usdg可以购买的数量
        uint256 usdgAmount = tokenAmount.mul(price).div(PRICE_PRECISION);
        usdgAmount = adjustForDecimals(usdgAmount, _token, usdg);
        _validate(usdgAmount > 0, 18);

        // 获取本次购买需要缴纳的费用
        uint256 feeBasisPoints = vaultUtils.getBuyUsdgFeeBasisPoints(_token, usdgAmount);
        // 得到扣除了手续费用之后的净金额
        uint256 amountAfterFees = _collectSwapFees(_token, tokenAmount, feeBasisPoints);
        // 使用净金额计算实际可以购买的数量
        uint256 mintAmount = amountAfterFees.mul(price).div(PRICE_PRECISION);
        // 调整小数位精度
        mintAmount = adjustForDecimals(mintAmount, _token, usdg);

        // 增加USDG的供应量
        _increaseUsdgAmount(_token, mintAmount);
        // 增加代币池中代币的供应量
        _increasePoolAmount(_token, amountAfterFees);
        // 铸造 计算净额 数量的 USDG
        IUSDG(usdg).mint(_receiver, mintAmount);

        emit BuyUSDG(_receiver, _token, tokenAmount, mintAmount, feeBasisPoints);
        // 再关闭交换定价机制
        useSwapPricing = false;
        return mintAmount;
    }

第一步,还是进行执行前的校验,包括白名单和管理员校验 第二步,获取需要兑换的token数量,并校验这个数量必须大于0 第三步,更新与该代币相关的累积融资利率,通常这是在杠杆交易中跟踪与借贷相关的费用。(这里只作更新)

function updateCumulativeFundingRate(address _collateralToken, address _indexToken) public {
        bool shouldUpdate = vaultUtils.updateCumulativeFundingRate(_collateralToken, _indexToken);
        if (!shouldUpdate) {
            return;
        }
        // 初始化lastFundingTimes
        if (lastFundingTimes[_collateralToken] == 0) {
            lastFundingTimes[_collateralToken] = block.timestamp.div(fundingInterval).mul(fundingInterval);
            return;
        }
        // 检查是否到了下一资金间隔
        if (lastFundingTimes[_collateralToken].add(fundingInterval) > block.timestamp) {
            return;
        }
        // 获取下一个资金利率fundingRate  资金利率因子×预留金额×间隔数/池子资金
        uint256 fundingRate = getNextFundingRate(_collateralToken);
        // 增加指定token的资金利率
        cumulativeFundingRates[_collateralToken] = cumulativeFundingRates[_collateralToken].add(fundingRate);
        // 重新设置lastFundingTimes
        lastFundingTimes[_collateralToken] = block.timestamp.div(fundingInterval).mul(fundingInterval);

        emit UpdateFundingRate(_collateralToken, cumulativeFundingRates[_collateralToken]);
    }

第四步,获取最小价格(通过ChainLink预言机获取) 第五步,算出可以购买到的USDG数量 第六步,计算出需要缴纳的费用以及扣除了手续费用之后的净金额,然后使用这个金额得到购买数量 第七步,针对当前token,增加该token已经发出的USDG数量 第八步,增加代币池中代币的供应量 第九步,铸造指定数量的USDG给到用户 第十步,发送事件,并关闭交换定价机制
通过上述的流程整理,大致的逻辑其实还是比较简单的。

接下来我们来看sellUSDG 和buyUSDG相比,就是方向相反,用USDG购买原生代币(BTC,ETH)

function sellUSDG(address _token, address _receiver) external override nonReentrant returns (uint256) {
        _validateManager();
        _validate(whitelistedTokens[_token], 19);
        useSwapPricing = true;
        // 获取转usdg的转移数量
        uint256 usdgAmount = _transferIn(usdg);
        _validate(usdgAmount > 0, 20);
        // 更新累积融资利率
        updateCumulativeFundingRate(_token, _token);
        // 用于根据usdgAmount 计算可赎回的token数量
        uint256 redemptionAmount = getRedemptionAmount(_token, usdgAmount);
        _validate(redemptionAmount > 0, 21);
        // 降低USDG的供应量
        _decreaseUsdgAmount(_token, usdgAmount);
        // 降低TOKEN的供应量
        _decreasePoolAmount(_token, redemptionAmount);
        // 销毁转移数量的 usdg
        IUSDG(usdg).burn(address(this), usdgAmount);

        // the _transferIn call increased the value of tokenBalances[usdg]
        // usually decreases in token balances are synced by calling _transferOut
        // however, for usdg, the tokens are burnt, so _updateTokenBalance should
        // be manually called to record the decrease in tokens
        _updateTokenBalance(usdg);
        // 获取需要缴纳的费率
        uint256 feeBasisPoints = vaultUtils.getSellUsdgFeeBasisPoints(_token, usdgAmount);
        // 得到扣除了手续费用之后的净额度
        uint256 amountOut = _collectSwapFees(_token, redemptionAmount, feeBasisPoints);
        _validate(amountOut > 0, 22);
        // 给_receiver 转净额度数量的token
        _transferOut(_token, amountOut, _receiver);

        emit SellUSDG(_receiver, _token, usdgAmount, amountOut, feeBasisPoints);

        useSwapPricing = false;
        return amountOut;
    }

第一步 校验是否是管理员,是否代币处于白名单上面 第二步 开启交换定价机制(主要是用来预言机喂价格) 第三步 获取需要进行转换的USDG数量 第四步 更新累积融资利率 第五步 根据目标token的价格,计算出可以赎回/购买的token数量 第六步,针对当前token,减少该token已经发出的USDG数量 第七步,减少代币池中代币的供应量 第八步,同步销毁指定数量的USDG 第九步,更新当前合约中USDG剩余数量 第十步,计算需要缴纳的费率,得到扣除了手续费用后的净额 第十一步,转移净额度之后数数量 第十二步, 关闭交换定价机制 上述两个都是管理员才有权限操作的方法。

接下来看swap方法,该方法主要是用来进行现货的交易

function swap(address _tokenIn, address _tokenOut, address _receiver) external override nonReentrant returns (uint256) {
        _validate(isSwapEnabled, 23);
        _validate(whitelistedTokens[_tokenIn], 24);
        _validate(whitelistedTokens[_tokenOut], 25);
        _validate(_tokenIn != _tokenOut, 26);

        useSwapPricing = true;
        // 更新累积融资利率
        updateCumulativeFundingRate(_tokenIn, _tokenIn);
        // 更新累积融资利率
        updateCumulativeFundingRate(_tokenOut, _tokenOut);
        // 获取tokenIn的转移数量
        uint256 amountIn = _transferIn(_tokenIn);
        _validate(amountIn > 0, 27);
        // 预测tokenIn的最小价格
        uint256 priceIn = getMinPrice(_tokenIn);
        // 预测tokenOut的最大价格
        uint256 priceOut = getMaxPrice(_tokenOut);
        // 计算可以出多少数量的tokenOut
        uint256 amountOut = amountIn.mul(priceIn).div(priceOut);
        amountOut = adjustForDecimals(amountOut, _tokenIn, _tokenOut);

        // adjust usdgAmounts by the same usdgAmount as debt is shifted between the assets
        // 计算这些的转入token需要消耗多少的usdg
        uint256 usdgAmount = amountIn.mul(priceIn).div(PRICE_PRECISION);
        usdgAmount = adjustForDecimals(usdgAmount, _tokenIn, usdg);
        // 需要交换token,需要的费率
        uint256 feeBasisPoints = vaultUtils.getSwapFeeBasisPoints(_tokenIn, _tokenOut, usdgAmount);
        // 得到扣除手续费后实际的交货数量
        uint256 amountOutAfterFees = _collectSwapFees(_tokenOut, amountOut, feeBasisPoints);

        // 增加转入的_token 的usdg的数量 
        _increaseUsdgAmount(_tokenIn, usdgAmount);
        // 减少转出的_token 的usdg的数量
        _decreaseUsdgAmount(_tokenOut, usdgAmount);

        // 增加池子中tokenIn 的数量
        _increasePoolAmount(_tokenIn, amountIn);
        // 减少池子中tokenOut 的数量
        _decreasePoolAmount(_tokenOut, amountOut);
        // 校验池子中的token数量,必须小于缓存的数量(为什么要这步逻辑?)
        _validateBufferAmount(_tokenOut);

        // 转给receiver 扣除手续费后的数量
        _transferOut(_tokenOut, amountOutAfterFees, _receiver);

        emit Swap(_receiver, _tokenIn, _tokenOut, amountIn, amountOut, amountOutAfterFees, feeBasisPoints);

        useSwapPricing = false;
        return amountOutAfterFees;
    }

第一步,依然是参数的合法性交易,没有什么难度 第二步,开启交换定价逻辑 第三步,同时更新输入 和 输出代币的累积融资利率 第四步,获取输入的最小价格,输出的最大价格 第五步,计算出可以获取多少的输出token 第六步,计算手续费,以及扣除手续费后,用户实际可以获取的token 第七步,增加转入token的USDG数量 第八步,减少转出token的USDG数量 第九步,增加池子中转入token的数量 第十步,减少池子中转出token的数量 第十一步,校验池子中的token数量,必须小于缓存的数量 这一步,核心主要是避免价格操纵或异常波动,当某种代币的池中数量非常少时,流动性不足会导致价格波动加剧,容易被市场操纵。因此,设置一个代币缓存量(Buffer Amount)可以避免这种情况,保证池子的深度和稳定性。 第十二步,转给receiver 兑换后的token数量 第十三步,关闭交换定价逻辑

增加头寸方法increasePosition 即执行限价单和市价单之后最终的执行方法

    //加码操作
    function increasePosition(address _account, address _collateralToken, address _indexToken, uint256 _sizeDelta, bool _isLong) external override nonReentrant {
        // 校验是否启用了杠杆
        _validate(isLeverageEnabled, 28);
        // 校验当前的gas小于最大的gas费率
        _validateGasPrice();
        // 验证路由器地址是否开启
        _validateRouter(_account);
        // 验证代币是否合法
        _validateTokens(_collateralToken, _indexToken, _isLong);
        //验证增加头寸的条件
        vaultUtils.validateIncreasePosition(_account, _collateralToken, _indexToken, _sizeDelta, _isLong);
        // 更新累积融资利率
        updateCumulativeFundingRate(_collateralToken, _indexToken);

        bytes32 key = getPositionKey(_account, _collateralToken, _indexToken, _isLong);
        Position storage position = positions[key];
        // 如果是多头,价格就用最大价格,否则就用最低价格
        uint256 price = _isLong ? getMaxPrice(_indexToken) : getMinPrice(_indexToken);

        if (position.size == 0) {
            // 初始头寸,就用获取的预测价格
            position.averagePrice = price;
        }

        if (position.size > 0 && _sizeDelta > 0) {
            // 多头 nextSize * nextPrice / nextSize + 盈利  空头 nextSize * nextPrice / nextSize - 盈利
            position.averagePrice = getNextAveragePrice(_indexToken, position.size, position.averagePrice, _isLong, price, _sizeDelta, position.lastIncreasedTime);
        }
        // 计算保证金费用
        uint256 fee = _collectMarginFees(_account, _collateralToken, _indexToken, _isLong, _sizeDelta, position.size, position.entryFundingRate);
        // 获取抵押token的移出数量
        uint256 collateralDelta = _transferIn(_collateralToken);
        // 将抵押token转换成USD等值
        uint256 collateralDeltaUsd = tokenToUsdMin(_collateralToken, collateralDelta);
        // 增加抵押
        position.collateral = position.collateral.add(collateralDeltaUsd);
        _validate(position.collateral >= fee, 29);
        // 抵押中扣除保证金
        position.collateral = position.collateral.sub(fee);
        // 从cumulativeFundingRates mapping中取数
        position.entryFundingRate = getEntryFundingRate(_collateralToken, _indexToken, _isLong);
        // 头寸增加
        position.size = position.size.add(_sizeDelta);
        // 记录最后一次增加时间
        position.lastIncreasedTime = block.timestamp;

        _validate(position.size > 0, 30);
        // 验证头寸和抵押品的数量
        _validatePosition(position.size, position.collateral);
        // 校验流动性 TODO
        validateLiquidation(_account, _collateralToken, _indexToken, _isLong, true);

        // reserve tokens to pay profits on the position
        // 根据抵押token 和 头寸,获取最大数量的USDG
        uint256 reserveDelta = usdToTokenMax(_collateralToken, _sizeDelta);
        // position中增加储备金
        position.reserveAmount = position.reserveAmount.add(reserveDelta);
        // 池子中增加储备金
        _increaseReservedAmount(_collateralToken, reserveDelta);

        if (_isLong) {
            // guaranteedUsd stores the sum of (position.size - position.collateral) for all positions
            // if a fee is charged on the collateral then guaranteedUsd should be increased by that fee amount
            // since (position.size - position.collateral) would have increased by `fee`
            _increaseGuaranteedUsd(_collateralToken, _sizeDelta.add(fee));
            _decreaseGuaranteedUsd(_collateralToken, collateralDeltaUsd);
            // treat the deposited collateral as part of the pool
            // 往抵押token池子中增加 移出数量 _transferIn
            _increasePoolAmount(_collateralToken, collateralDelta);
            // fees need to be deducted from the pool since fees are deducted from position.collateral
            // and collateral is treated as part of the pool
            // 往抵押token池子中减少 根据fee计算出最小的数量
            _decreasePoolAmount(_collateralToken, usdToTokenMin(_collateralToken, fee));
        } else {
            // 更新空头全局平均价格
            if (globalShortSizes[_indexToken] == 0) {
                globalShortAveragePrices[_indexToken] = price;
            } else {
                globalShortAveragePrices[_indexToken] = getNextGlobalShortAveragePrice(_indexToken, price, _sizeDelta);
            }
            // 增加全局空头头寸
            _increaseGlobalShortSize(_indexToken, _sizeDelta);
        }

        emit IncreasePosition(key, _account, _collateralToken, _indexToken, collateralDeltaUsd, _sizeDelta, _isLong, price, fee);
        emit UpdatePosition(key, position.size, position.collateral, position.averagePrice, position.entryFundingRate, position.reserveAmount, position.realisedPnl, price);
    }

第一步 先进行参数合法性校验及规则校验(是否启用杠杆,gas费率,路由器地址,代币合法性等) 第二步 更新累积的融资利率 第三步 根据参数,得到仓位信息 第四步 得到目标token的价格(需要判断多头/空头) 第五步 设置平均价格 第六步 计算保证金费用 第七步 获取抵押token的移出数量 第八步 将抵押token转换成USD等值 第九步 仓位信息中增加抵押,并扣除保证金 第十步 更改仓位信息(最后一次时间,头寸) 第十一步 验证头寸和流动性 第十二步 根据抵押token 和 头寸,获取最大数量的USDG 第十三步 在仓位中和池子中增加储备金 第十四步 根据多头和空头分别执行不同的逻辑 多头主要是更新保证金和抵押池 空头主要是更新全局空头平均价格和全局空头头寸 第十五步 发送增加仓位,更新仓位的两个事件 reservedAmounts 储备金 guaranteedUsd 保证金 这两个资金估计大家会产生困扰,下面给大家来解释一下 reservedAmounts 代表为每个抵押代币(collateralToken)保留的资金,用于开设或维持多头和空头仓位。这部分资金不能被随意使用,因为它已经与用户的仓位绑定,主要用于保障头寸的潜在亏损。 guaranteedUsd 代表整个系统中所有仓位的 实际保证金总额,即仓位中的资金实际用于支撑用户头寸的部分。它通常是仓位大小减去抵押品的差额,或者是系统需要保证的资金。

最后了解一下清算的最终方法

function liquidatePosition(address _account, address _collateralToken, address _indexToken, bool _isLong, address _feeReceiver) external override nonReentrant {
        if (inPrivateLiquidationMode) {
            _validate(isLiquidator[msg.sender], 34);
        }

        // set includeAmmPrice to false to prevent manipulated liquidations
        includeAmmPrice = false;
        //更新累积融资利率
        updateCumulativeFundingRate(_collateralToken, _indexToken);

        bytes32 key = getPositionKey(_account, _collateralToken, _indexToken, _isLong);
        Position memory position = positions[key];
        _validate(position.size > 0, 35);

        (uint256 liquidationState, uint256 marginFees) = validateLiquidation(_account, _collateralToken, _indexToken, _isLong, false);
        // 验证清算状态
        _validate(liquidationState != 0, 36);
        if (liquidationState == 2) {
            // max leverage exceeded but there is collateral remaining after deducting losses so decreasePosition instead
            _decreasePosition(_account, _collateralToken, _indexToken, 0, position.size, _isLong, _account);
            includeAmmPrice = true;
            return;
        }
        // 收取保证金费用,将其添加到费用储备中
        uint256 feeTokens = usdToTokenMin(_collateralToken, marginFees);
        feeReserves[_collateralToken] = feeReserves[_collateralToken].add(feeTokens);
        emit CollectMarginFees(_collateralToken, marginFees, feeTokens);

        // 减少保留金额和担保的USD
        _decreaseReservedAmount(_collateralToken, position.reserveAmount);
        if (_isLong) {
            _decreaseGuaranteedUsd(_collateralToken, position.size.sub(position.collateral));
            _decreasePoolAmount(_collateralToken, usdToTokenMin(_collateralToken, marginFees));
        }

        // 记录清算事件,包括仓位信息和当前价格。
        uint256 markPrice = _isLong ? getMinPrice(_indexToken) : getMaxPrice(_indexToken);
        emit LiquidatePosition(key, _account, _collateralToken, _indexToken, _isLong, position.size, position.collateral, position.reserveAmount, position.realisedPnl, markPrice);

        //如果是空头,同时持仓费用 < 抵押费用 , 仓位抵押减少持仓费用, 同时池子中 增加剩余的数量
        if (!_isLong && marginFees < position.collateral) {
            uint256 remainingCollateral = position.collateral.sub(marginFees);
            _increasePoolAmount(_collateralToken, usdToTokenMin(_collateralToken, remainingCollateral));
        }

        if (!_isLong) {
            // 降低全局空头头寸
            _decreaseGlobalShortSize(_indexToken, position.size);
        }
        // 删除仓位
        delete positions[key];

        // pay the fee receiver using the pool, we assume that in general the liquidated amount should be sufficient to cover
        // the liquidation fees
        // 支付清算费用给接收者:
        _decreasePoolAmount(_collateralToken, usdToTokenMin(_collateralToken, liquidationFeeUsd));
        _transferOut(_collateralToken, usdToTokenMin(_collateralToken, liquidationFeeUsd), _feeReceiver);

        includeAmmPrice = true;
    }

第一步 验证清算者的合法性 第二步 关闭是否包含AMM价格 第三步 更新累积融资利率 第四步 根据参数获取指定的仓位信息 第五步 判断仓位是否满足清算条件,根据liquidationState进行后续处理 第六步 如果保证金接近爆仓边缘,可能会通过降低仓位使得安全 第七步 收取保证金费用,将其添加到费用储备中 第八步 减少保留金额和担保的USD 第九步 记录清算事件,包括仓位信息和当前价格 第十步 如果是空头,仓位抵押中减少持仓费用,增加到池子里面,同时降低全局空头头寸 第十一步 删除仓位 第十二步 降低池子中的金额即清算费用,将清算费用转给清算机器人

清算这边的整个逻辑还是蛮复杂的,最好结合着测试用例一起看一下

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Leo
Leo
江湖只有他的大名,没有他的介绍。