H-08. Yield in trove is lost when closing a strategy vault

Submitted by Cosine, pontifex. Selected submission by: Cosine.

Relevant GitHub Links

Summary

The funds in the trove contract are not claimed during the emergency close flow and can not be claimed in a normal way during this situation, because of a status change. Therefore, all the acquired yield will be lost.

Vulnerability Details

When users deposit, or withdraw tokens, all acquired yield from GMX is sent to the trove contract:
plain text
function deposit( GMXTypes.Store storage self, GMXTypes.DepositParams memory dp, bool isNative ) external { // Sweep any tokenA/B in vault to the temporary trove for future compouding and to prevent // it from being considered as part of depositor's assets if (self.tokenA.balanceOf(address(this)) > 0) { self.tokenA.safeTransfer(self.trove, self.tokenA.balanceOf(address(this))); } if (self.tokenB.balanceOf(address(this)) > 0) { self.tokenB.safeTransfer(self.trove, self.tokenB.balanceOf(address(this))); } ... }
plain text
function withdraw( GMXTypes.Store storage self, GMXTypes.WithdrawParams memory wp ) external { // Sweep any tokenA/B in vault to the temporary trove for future compouding and to prevent // it from being considered as part of withdrawers assets if (self.tokenA.balanceOf(address(this)) > 0) { self.tokenA.safeTransfer(self.trove, self.tokenA.balanceOf(address(this))); } if (self.tokenB.balanceOf(address(this)) > 0) { self.tokenB.safeTransfer(self.trove, self.tokenB.balanceOf(address(this))); } ... }
The only way in the system to claim these yield is the compound function, which calls the beforeCompoundChecks function:
plain text
function compound( GMXTypes.Store storage self, GMXTypes.CompoundParams memory cp ) external { // Transfer any tokenA/B from trove to vault if (self.tokenA.balanceOf(address(self.trove)) > 0) { self.tokenA.safeTransferFrom( address(self.trove), address(this), self.tokenA.balanceOf(address(self.trove)) ); } if (self.tokenB.balanceOf(address(self.trove)) > 0) { self.tokenB.safeTransferFrom( address(self.trove), address(this), self.tokenB.balanceOf(address(self.trove)) ); } ... GMXChecks.beforeCompoundChecks(self); ... }
This function reverts if the current status of the system is not Open or Compound_Failed:
plain text
function beforeCompoundChecks( GMXTypes.Store storage self ) external view { if ( self.status != GMXTypes.Status.Open && self.status != GMXTypes.Status.Compound_Failed ) revert Errors.NotAllowedInCurrentVaultStatus(); ... }
As the emergency close flow updates this status to Paused and later to Closed, calling compound will revert:
plain text
function emergencyPause( GMXTypes.Store storage self ) external { self.refundee = payable(msg.sender); GMXTypes.RemoveLiquidityParams memory _rlp; // Remove all of the vault's LP tokens _rlp.lpAmt = self.lpToken.balanceOf(address(this)); _rlp.executionFee = msg.value; GMXManager.removeLiquidity( self, _rlp ); self.status = GMXTypes.Status.Paused; emit EmergencyPause(); }
plain text
function emergencyClose( GMXTypes.Store storage self, uint256 deadline ) external { GMXChecks.beforeEmergencyCloseChecks(self); // Repay all borrowed assets; 1e18 == 100% shareRatio to repay GMXTypes.RepayParams memory _rp; ( _rp.repayTokenAAmt, _rp.repayTokenBAmt ) = GMXManager.calcRepay(self, 1e18); ( bool _swapNeeded, address _tokenFrom, address _tokenTo, uint256 _tokenToAmt ) = GMXManager.calcSwapForRepay(self, _rp); if (_swapNeeded) { ISwap.SwapParams memory _sp; _sp.tokenIn = _tokenFrom; _sp.tokenOut = _tokenTo; _sp.amountIn = IERC20(_tokenFrom).balanceOf(address(this)); _sp.amountOut = _tokenToAmt; _sp.slippage = self.minSlippage; _sp.deadline = deadline; GMXManager.swapTokensForExactTokens(self, _sp); } GMXManager.repay( self, _rp.repayTokenAAmt, _rp.repayTokenBAmt ); self.status = GMXTypes.Status.Closed; emit EmergencyClose( _rp.repayTokenAAmt, _rp.repayTokenBAmt ); }
And as we can see during these process the funds inside the trove contract are never claimed and as the strategy vault is the only address that can claim the funds of the trove, all funds are gone.
plain text
contract GMXTrove { /* ==================== STATE VARIABLES ==================== */ // Address of the vault this trove handler is for IGMXVault public vault; /* ====================== CONSTRUCTOR ====================== */ /** * @notice Initialize trove contract with associated vault address * @param _vault Address of vault */ constructor (address _vault) { vault = IGMXVault(_vault); GMXTypes.Store memory _store = vault.store(); // Set token approvals for this trove's vault contract _store.tokenA.approve(address(vault), type(uint256).max); _store.tokenB.approve(address(vault), type(uint256).max); } }

Impact

If a strategy vault is closed, all funds in the trove are lost.

Tools Used

Manual Review

Recommendations

Transfer the funds inside the trove into the vault during the emergency close process.