{"author":{"address":null,"user":"https://learnblockchain.cn/people/22539"},"content":{"body":"首先我们需要理清几个代币\r\nGMX \r\nGMX 是 GMX 平台的 治理和实用代币，可以参与治理，质押\r\n它可以通过直接购买，或者通过一段时间后esGMX解锁\r\n\r\n\r\nGLP\r\nGMX 平台的 流动性提供者代币，是为平台上的交易者提供流动性的工具，通过增加流动性来获得GLP奖励\r\n\r\n\r\nesGMX\r\nesGMX 是一种托管代币奖励，质押 GMX 或 GLP 可获得。它可以继续质押获得收益，或在一段时间后解锁为实际 GMX\r\n\r\n\r\nbnGMX\r\nbnGMX 是额外的奖励代币，用于提升用户的质押奖励收益，但不能交易或解锁为 GMX\r\n\r\n有了这几个基础概念之后，我们接下来结合着源码来进行解析\r\n\r\n首先我们来看质押的逻辑\r\n**质押流程**\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/09/QAIFtDez66dc15f371ab5.png)\r\n跟我们常见的质押处理逻辑不一样，我们通常的质押处理逻辑是直接将代币质押到一个合约中，然后直接在该合约计算奖励和份额。但是GMX平台的质押进行了三层的传递。\r\n三者分别代表不同的追踪合约\r\nstakeGMXTracker是基础治理代币GMX的追踪合约\r\nbonusGmxTracker是奖励代币或激励代币的追踪合约\r\nfeeGmxTracker 是费用相关的奖励的追踪合约\r\n但是这三者其实都是RewardTracker的实现\r\n让我们看看它的源码\r\n**stakeGMX**\r\n\r\n```js\r\nfunction _stakeGmx(address _fundingAccount, address _account, address _token, uint256 _amount) private {\r\n        require(_amount \u003e 0, \"RewardRouter: invalid _amount\");\r\n        // 将 _amount 的 GMX 代币从 _fundingAccount 质押到 stakedGmxTracker 合约中，为 _account 用户质押\r\n        IRewardTracker(stakedGmxTracker).stakeForAccount(_fundingAccount, _account, _token, _amount);\r\n        // 将 _amount 的 stakedGmxTracker 代币（由上一操作质押的结果）质押到 bonusGmxTracker 合约中，为 _account 用户质押。\r\n        IRewardTracker(bonusGmxTracker).stakeForAccount(_account, _account, stakedGmxTracker, _amount);\r\n        // 将 _amount 的 bonusGmxTracker 代币 (由上一操作质押的结果) 质押到 bofeeGmxTracker 合约中，为 _account 用户质押。\r\n        IRewardTracker(feeGmxTracker).stakeForAccount(_account, _account, bonusGmxTracker, _amount);\r\n\r\n        emit StakeGmx(_account, _amount);\r\n    }\r\n```\r\n逻辑很清晰，就是三层的传递\r\n我们来看看，RewardTracker里面的实现\r\n\r\n```js\r\nfunction stakeForAccount(address _fundingAccount, address _account, address _depositToken, uint256 _amount) external override nonReentrant {\r\n  // 校验发送者的资格      \r\n  _validateHandler();\r\n  // 进行质押保存      \r\n  _stake(_fundingAccount, _account, _depositToken, _amount);\r\n    \r\n}\r\n```\r\n\r\n```js\r\nfunction _stake(address _fundingAccount, address _account, address _depositToken, uint256 _amount) private {\r\n        \r\n  require(_amount \u003e 0, \"RewardTracker: invalid _amount\");\r\n        \r\n  require(isDepositToken[_depositToken], \"RewardTracker: invalid _depositToken\");\r\n\r\n  // 将代币转移到当前合约中      \r\n  IERC20(_depositToken).safeTransferFrom(_fundingAccount, address(this), _amount);\r\n\r\n  // 更新奖励状态   \r\n  _updateRewards(_account);\r\n\r\n  // 然后增加stakeAmounts      \r\n  stakedAmounts[_account] = stakedAmounts[_account].add(_amount);\r\n  // 增加 depositBalances   \r\n  depositBalances[_account][_depositToken] = depositBalances[_account][_depositToken].add(_amount);\r\n  // 增加总存款数      \r\n  totalDepositSupply[_depositToken] = totalDepositSupply[_depositToken].add(_amount);\r\n\r\n  // mint amount对应的数量      \r\n  _mint(_account, _amount);\r\n    }\r\n```\r\n\r\n```js\r\nfunction _updateRewards(address _account) private {\r\n        uint256 blockReward = IRewardDistributor(distributor).distribute();\r\n        //  质押代币的总量\r\n        uint256 supply = totalSupply;  \r\n\r\n        // 更新每个代币的累积奖励\r\n        uint256 _cumulativeRewardPerToken = cumulativeRewardPerToken;\r\n        if (supply \u003e 0 \u0026\u0026 blockReward \u003e 0) {\r\n            _cumulativeRewardPerToken = _cumulativeRewardPerToken.add(blockReward.mul(PRECISION).div(supply));\r\n            cumulativeRewardPerToken = _cumulativeRewardPerToken;\r\n        }\r\n\r\n        // cumulativeRewardPerToken can only increase\r\n        // so if cumulativeRewardPerToken is zero, it means there are no rewards yet\r\n        if (_cumulativeRewardPerToken == 0) {\r\n            return;\r\n        }\r\n\r\n        if (_account != address(0)) {\r\n            // 是用户的质押金额\r\n            uint256 stakedAmount = stakedAmounts[_account];\r\n            // 是用户根据他们的质押金额和累积奖励的变化计算出的奖励。\r\n            uint256 accountReward = stakedAmount.mul(_cumulativeRewardPerToken.sub(previousCumulatedRewardPerToken[_account])).div(PRECISION);\r\n            // 更新用户的可领取奖励 claimableReward。\r\n            uint256 _claimableReward = claimableReward[_account].add(accountReward);\r\n\r\n            claimableReward[_account] = _claimableReward;\r\n            previousCumulatedRewardPerToken[_account] = _cumulativeRewardPerToken;\r\n            // 如果用户有可领取的奖励且他们的质押金额大于零，则计算用户的累计奖励\r\n            if (_claimableReward \u003e 0 \u0026\u0026 stakedAmounts[_account] \u003e 0) {\r\n                uint256 nextCumulativeReward = cumulativeRewards[_account].add(accountReward);\r\n                // 更新账户的平均质押金额\r\n                averageStakedAmounts[_account] = averageStakedAmounts[_account].mul(cumulativeRewards[_account]).div(nextCumulativeReward)\r\n                    .add(stakedAmount.mul(accountReward).div(nextCumulativeReward));\r\n                // 更新累积奖励金额\r\n                cumulativeRewards[_account] = nextCumulativeReward;\r\n            }\r\n        }\r\n    }\r\n```\r\n可以看到实际的质押逻辑，和普通的质押没有什么差别，都是代币转移，更新奖励状态，更新质押,存款数量。在此基础上增加了_mint 自身的操作。\r\n看到这里，大家一定会有疑问，为什么好端端的要抽象成三层呢，好处是啥？ \r\n接下来我们结合着另外一个场景来看一下\r\n**_compoundGmx方法**\r\n\r\n```js\r\nfunction _compoundGmx(address _account) private {\r\n        // 从stakedGmxTracker提取出esGmx，可以对esGmx进行单独质押 \r\n        uint256 esGmxAmount = IRewardTracker(stakedGmxTracker).claimForAccount(_account, _account);\r\n        if (esGmxAmount \u003e 0) {\r\n            _stakeGmx(_account, _account, esGmx, esGmxAmount);\r\n        }\r\n        // 从bonusGmxTracker 中提取出 bnGmx   \r\n        uint256 bnGmxAmount = IRewardTracker(bonusGmxTracker).claimForAccount(_account, _account);\r\n        if (bnGmxAmount \u003e 0) {\r\n            // 将bnGmxAmount 作为奖励份额，质押到feeGmxTracker，（它自身不能被解锁）  \r\n            IRewardTracker(feeGmxTracker).stakeForAccount(_account, _account, bnGmx, bnGmxAmount);\r\n        }\r\n    }\r\n```\r\n从这一步上，我们就可以看出它设计的精妙了，esGmx作为它的奖励代币，可以进行二次质押。bnGmx作为额外奖励，不能进行解锁，也不能进行交换，但是可以增加代币的奖励份额。因此只需要stake到feeGmxTracker里面即可。这样大大增强了代码的可读性已经扩展性\r\n\r\n接下来我们看一下解质押的逻辑，刚好和质押的流程相反\r\n**解质押流程**\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/09/BEpkg4Ji66dc19b889f80.png)\r\n可以看到流程完全想法，调整了一下。 同时也可以看到单独对bnGmx进行了处理\r\n结合着源码一起来看\r\n\r\n```js\r\nfunction _unstakeGmx(address _account, address _token, uint256 _amount) private {\r\n        require(_amount \u003e 0, \"RewardRouter: invalid _amount\");\r\n\r\n        uint256 balance = IRewardTracker(stakedGmxTracker).stakedAmounts(_account);\r\n        // 从 feeGmxTracker 中解质押 _amount 的 GMX，并转移到 bonusGmxTracker。\r\n        IRewardTracker(feeGmxTracker).unstakeForAccount(_account, bonusGmxTracker, _amount, _account);\r\n        // 从 bonusGmxTracker 中解质押 _amount 的GMX，并转移到 stakedGmxTracker\r\n        IRewardTracker(bonusGmxTracker).unstakeForAccount(_account, stakedGmxTracker, _amount, _account);\r\n        // 从 stakedGmxTracker 中解质押 _amount 的GMX，并转移到 _token 中\r\n        IRewardTracker(stakedGmxTracker).unstakeForAccount(_account, _token, _amount, _account);\r\n\r\n        // 领取 bonusGmxTracker 中的奖励（bnGmx），如果有奖励则将这些奖励在 feeGmxTracker 中重新质押。\r\n        uint256 bnGmxAmount = IRewardTracker(bonusGmxTracker).claimForAccount(_account, _account);\r\n        if (bnGmxAmount \u003e 0) {\r\n            IRewardTracker(feeGmxTracker).stakeForAccount(_account, _account, bnGmx, bnGmxAmount);\r\n        }\r\n\r\n        // 计算需要减少的 bnGmx 数量，解质押相应的 bnGmx 数量并进行销毁，以反映减少的质押量。\r\n        uint256 stakedBnGmx = IRewardTracker(feeGmxTracker).depositBalances(_account, bnGmx);\r\n        if (stakedBnGmx \u003e 0) {\r\n            uint256 reductionAmount = stakedBnGmx.mul(_amount).div(balance);\r\n            IRewardTracker(feeGmxTracker).unstakeForAccount(_account, bnGmx, reductionAmount, _account);\r\n            IMintable(bnGmx).burn(_account, reductionAmount);\r\n        }\r\n\r\n        emit UnstakeGmx(_account, _amount);\r\n    }\r\n```\r\n第一步 校验解质押的数量必须大于0\r\n第二步  从 feeGmxTracker 中解质押 _amount 的 GMX，并转移到 bonusGmxTracker\r\n第三步  从 bonusGmxTracker 中解质押 _amount 的GMX，并转移到 stakedGmxTracker\r\n第四步  从 stakedGmxTracker 中解质押 _amount 的GMX，并转移到 _token 中\r\n第五步  领取 bonusGmxTracker 中的奖励（bnGmx），如果有奖励则将这些奖励在 feeGmxTracker 中重新质押。\r\n第六步  计算需要减少的 bnGmx 数量，解质押相应的 bnGmx 数量并进行销毁，以反映减少的质押量。\r\n整体上的逻辑很清晰\r\n接下来，看一下unstakeForAccount逻辑\r\n\r\n```js\r\nfunction unstakeForAccount(address _account, address _depositToken, uint256 _amount, address _receiver) external override nonReentrant {\r\n        _validateHandler();\r\n        _unstake(_account, _depositToken, _amount, _receiver);\r\n    }\r\n```\r\n核心处理依然在_unstake里面\r\n\r\n```js\r\nfunction _unstake(address _account, address _depositToken, uint256 _amount, address _receiver) private {\r\n        require(_amount \u003e 0, \"RewardTracker: invalid _amount\");\r\n        require(isDepositToken[_depositToken], \"RewardTracker: invalid _depositToken\");\r\n        // 调用 _updateRewards 方法来更新 _account 的奖励状态。这是因为解质押可能影响到用户的奖励计算，需要确保奖励数据的最新性。\r\n        _updateRewards(_account);\r\n        \r\n        uint256 stakedAmount = stakedAmounts[_account];\r\n        // 确保解质押的数量不超过用户的当前质押量。\r\n        require(stakedAmounts[_account] \u003e= _amount, \"RewardTracker: _amount exceeds stakedAmount\");\r\n        // 从用户的质押量中减去解质押的数量。\r\n        stakedAmounts[_account] = stakedAmount.sub(_amount);\r\n\r\n        uint256 depositBalance = depositBalances[_account][_depositToken];\r\n        // 确保用户的存款余额能大于解质押的两\r\n        require(depositBalance \u003e= _amount, \"RewardTracker: _amount exceeds depositBalance\");\r\n        depositBalances[_account][_depositToken] = depositBalance.sub(_amount);\r\n        totalDepositSupply[_depositToken] = totalDepositSupply[_depositToken].sub(_amount);\r\n\r\n        // 销毁_amount数量的份额\r\n        _burn(_account, _amount);\r\n        // 进行代币转移\r\n        IERC20(_depositToken).safeTransfer(_receiver, _amount);\r\n    }\r\n```\r\n第一步 依然校验数字 和 代币的合法性\r\n第二步 调整奖励状态\r\n第三步 校验确保解质押的数量不超过用户的当前质押量。\r\n第四步 确保用户的存款余额能大于解质押的量\r\n第五步 减少总存款量\r\n第六步 销毁_amount数量的份额\r\n第七步 进行代币转移","title":"GMX 源码解析五，质押和解质押逻辑"},"history":null,"timestamp":1725700715,"version":1}