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

Phishing Attack via tx.origin in setPerpVault within GmxProxy

Summary

The GmxProxy contract uses tx.origin for access control in its setPerpVault() function:

function setPerpVault(address _perpVault, address market) external {
require(tx.origin == owner(), "not owner"); // <-- Uses tx.origin
...
}

This is insecure because an attacker can deploy a malicious contract and trick the genuine owner into calling setPerpVault() through it, thereby impersonating the owner. Since tx.origin remains the actual owner’s externally owned account (EOA), the malicious contract’s call will pass the require(tx.origin == owner(), ...) check. As a result, the attacker can set the perpVault to any address of their choosing, potentially hijacking the contract’s functionality.

Vulnerability Details

  • tx.origin vs. msg.sender: In Solidity, tx.origin is the original EOA that initiated the transaction, while msg.sender is the immediate caller. Access control using tx.origin is widely discouraged because any intermediate contract can forward the call, retaining the same tx.origin.

  • Phishing scenario: A malicious dApp or contract can trick the real owner into calling a function on this malicious contract, which in turn calls setPerpVault() on GmxProxy. The require(tx.origin == owner()) check succeeds because the real owner is still the tx.origin, even though the immediate caller (msg.sender) is the malicious contract.

Impact

By passing the ownership check, the attacker can:

  1. Set perpVault to a malicious address they control.

  2. Potentially disrupt or redirect the contract’s main functionality, especially if perpVault is critical for handling positions, tokens, or settlement logic.

  3. Carry out further privilege escalation or malicious transactions using the new perpVault.

The result can be a loss of control over the contract’s core operations, which is a severe security breach.

Proof of Concept (PoC)

Below is a minimal example showing how an attacker could abuse tx.origin by deploying a deceptive contract named GmxPr0xy (notice the zero in place of the letter “o”) that forwards calls to GmxProxy.setPerpVault(). The real owner is tricked into calling the malicious function, causing tx.origin to be the legitimate owner’s address but msg.sender to be the malicious contract.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
// Part of the legitimate contract's interface that we need
interface IGmxProxy {
function setPerpVault(address _perpVault, address market) external;
function owner() external view returns (address);
}
// Original GmxProxy snippet (vulnerable portion):
//
// function setPerpVault(address _perpVault, address market) external {
// require(tx.origin == owner(), "not owner");
// ...
// }
contract GmxPr0xy { // Notice the similar but slightly altered name: 'GmxPr0xy'
IGmxProxy public target;
address public attacker;
// Deploy with the address of the legitimate GmxProxy
constructor(address _gmxProxy) {
target = IGmxProxy(_gmxProxy);
attacker = msg.sender;
}
// Trick the real owner into calling this function (e.g., via a phishing UI)
// The malicious contract calls the real setPerpVault() under the hood
function maliciousSetPerpVault(address _maliciousVault, address _market) external {
// As soon as the real owner calls this function,
// tx.origin == realOwner, but msg.sender == this malicious contract.
// The require(tx.origin == owner()) in target.setPerpVault() will pass,
// because target.owner() is the real owner's address.
target.setPerpVault(_maliciousVault, _market);
}
}

Attack Steps

  1. Deployment: Attacker deploys GmxPr0xy (the phishing contract) with the legitimate GmxProxy address in the constructor.

  2. Phishing: Attacker lures the legitimate owner to call maliciousSetPerpVault() on GmxPr0xy, under false pretenses (e.g., a “claim rewards” button on a malicious dApp).

  3. Execution: When the owner sends the transaction, tx.origin is owner, but msg.sender is GmxPr0xy. Inside setPerpVault(), the check require(tx.origin == owner(), "not owner") succeeds. This lets the phishing contract forcibly set perpVault to any address the attacker wants.

  4. Result: The attacker has effectively hijacked the GmxProxy by redirecting or controlling its core functionality, likely enabling theft of funds or manipulation of positions.

Tools Used

Manual review.

Recommendations

Replace tx.origin with msg.sender:
Always use msg.sender for access control checks. For instance:

function setPerpVault(address _perpVault, address market) external {
require(msg.sender == owner(), "not owner"); // Safe: use msg.sender
...
}

This ensures only the direct caller (the real user or the contract they’re interacting with) can pass the ownership check, preventing a malicious contract from impersonating the owner.

Updates

Lead Judging Commences

n0kto Lead Judge 7 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.