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:
function deposit(
GMXTypes.Store storage self,
GMXTypes.DepositParams memory dp,
bool isNative
) external {
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)));
}
...
}
function withdraw(
GMXTypes.Store storage self,
GMXTypes.WithdrawParams memory wp
) external {
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:
function compound(
GMXTypes.Store storage self,
GMXTypes.CompoundParams memory cp
) external {
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:
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:
function emergencyPause(
GMXTypes.Store storage self
) external {
self.refundee = payable(msg.sender);
GMXTypes.RemoveLiquidityParams memory _rlp;
_rlp.lpAmt = self.lpToken.balanceOf(address(this));
_rlp.executionFee = msg.value;
GMXManager.removeLiquidity(
self,
_rlp
);
self.status = GMXTypes.Status.Paused;
emit EmergencyPause();
}
function emergencyClose(
GMXTypes.Store storage self,
uint256 deadline
) external {
GMXChecks.beforeEmergencyCloseChecks(self);
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.
contract GMXTrove {
IGMXVault public vault;
* @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();
_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.