Steadefi

Steadefi
DeFiHardhatFoundryOracle
35,000 USDC
View results
Submission Details
Severity: high
Valid

Vault stuck on "Deposit" status during processDepositCancellation flow

Summary

If the "processDepositCancellation" flow is entered, a malicious contract user with no receive function can get the vault stuck on "Deposit" status

Vulnerability Details

If the vault token deposited by malicious user is native, and the deposit flow ends with the GMX keeper calling "processDepositCancellation()" function on the vault, it is trivial to get the status of the vault stuck on "Deposit". The probability of entering the "processDepositCancellation" flow can be maximized by the user by entering the minimum slippage allowed by the vault and spamming the vault with deposits until the slippage is too tight to pass. Other potential avenues for entering "processDepositCancellation()" flow include setting the slippage too high, or if there isn’t enough available supply to buy into GM

This can be seen in the "processDepositCancellation()" function detailed in the GMXDeposit library:

function processDepositCancellation(
GMXTypes.Store storage self
) external {
GMXChecks.beforeProcessDepositCancellationChecks(self);
// Repay borrowed assets
GMXManager.repay(
self,
self.depositCache.borrowParams.borrowTokenAAmt,
self.depositCache.borrowParams.borrowTokenBAmt
);
// Return user's deposited asset
// If native token is being withdrawn, we convert wrapped to native
if (self.depositCache.depositParams.token == address(self.WNT)) {
self.WNT.withdraw(self.WNT.balanceOf(address(this)));
(bool success, ) = self.depositCache.user.call{value: address(this).balance}("");
require(success, "Transfer failed.");
} else {
// Transfer requested withdraw asset to user
IERC20(self.depositCache.depositParams.token).safeTransfer(
self.depositCache.user,
self.depositCache.depositParams.amt
);
}
self.status = GMXTypes.Status.Open;
emit DepositCancelled(self.depositCache.user);
}

https://github.com/Cyfrin/2023-10-SteadeFi/blob/0f909e2f0917cb9ad02986f631d622376510abec/contracts/strategy/gmx/GMXDeposit.sol#L193-L222

PoC

diff --git a/test/gmx/local/GMXDepositTest.t.sol b/test/gmx/local/GMXDepositTest.t.sol
index 872610a..36b994e 100644
--- a/test/gmx/local/GMXDepositTest.t.sol
+++ b/test/gmx/local/GMXDepositTest.t.sol
@@ -136,6 +136,27 @@ contract GMXDepositTest is GMXMockVaultSetup, GMXTestHelper, TestUtils {
assertEq(uint256(vault.store().status), 0);
}
+ function test_processDepositCancelMaliciousRevert() external {
+ MaliciousUser maliciousUser = new MaliciousUser();
+ deal(address(maliciousUser), 10 ether);
+ deal(address(WETH), address(maliciousUser), 10 ether);
+ vm.startPrank(address(maliciousUser));
+
+ _createNativeDeposit(address(WETH), 1e18, 0, SLIPPAGE, EXECUTION_FEE);
+
+ vm.expectRevert();
+ // cancel deposit
+ mockExchangeRouter.cancelDeposit(
+ address(WETH),
+ address(USDC),
+ address(vault),
+ address(callback)
+ );
+
+ // status should stay at deposit
+ assertEq(uint256(vault.store().status), 1); // Deposit status
+ }
+
function test_processDepositFailure() external {
vm.startPrank(user1);
@@ -348,3 +369,8 @@ contract GMXDepositTest is GMXMockVaultSetup, GMXTestHelper, TestUtils {
}
+
+
+contract MaliciousUser {
+ // In reality contains approve ERC20 function and deposit function
+}

Impact

Vault status will be stuck on "Deposit" and vault won't be able to accept any new deposits, withdrawals, rebalances or compounds

Recommendations

Pay native depositors back in WETH instead of ETH. It simplifies the code and removes the attack vector

diff --git a/contracts/strategy/gmx/GMXDeposit.sol b/contracts/strategy/gmx/GMXDeposit.sol
index 1b28c3b..270e55f 100644
--- a/contracts/strategy/gmx/GMXDeposit.sol
+++ b/contracts/strategy/gmx/GMXDeposit.sol
@@ -203,18 +203,11 @@ library GMXDeposit {
);
// Return user's deposited asset
- // If native token is being withdrawn, we convert wrapped to native
- if (self.depositCache.depositParams.token == address(self.WNT)) {
- self.WNT.withdraw(self.WNT.balanceOf(address(this)));
- (bool success, ) = self.depositCache.user.call{value: address(this).balance}("");
- require(success, "Transfer failed.");
- } else {
- // Transfer requested withdraw asset to user
- IERC20(self.depositCache.depositParams.token).safeTransfer(
- self.depositCache.user,
- self.depositCache.depositParams.amt
- );
- }
+ // Transfer requested withdraw asset to user
+ IERC20(self.depositCache.depositParams.token).safeTransfer(
+ self.depositCache.user,
+ self.depositCache.depositParams.amt
+ );
self.status = GMXTypes.Status.Open;
Updates

Lead Judging Commences

hans Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

DOS by rejecting native token

Impact: High Likelihood: High An attacker can repeatedly force the protocol to get stuck in a not-open status. This can happen on both deposit, withdraw callback for both successful execution and failures. Will group all similar issues.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.