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

tx.origin Misuse in setPerpVault Function

Summary

  • The setPerpVault function in the GmxProxy contract uses tx.origin to check the caller's ownership.

  • This causes the contract to incorrectly reject transactions where the owner calls the function through an intermediary contract.

Severity: High

Likelihood: Medium to High

Impact: High

Affected Line of Code:

https://github.com/CodeHawks-Contests/2025-02-gamma/blob/main/contracts/GmxProxy.sol#L346-L357

require(tx.origin == owner(), "not owner");

Vulnerability Details

  • The function setPerpVault uses tx.origin to verify the caller’s ownership.

  • tx.origin refers to the original sender of the transaction, which could cause mismatches if the function is called via an intermediary contract.

  • This makes it impossible for the owner to call the function through intermediary contracts, even though they are the original sender.

  • An attacker can exploit this by manipulating the call chain to bypass the ownership check.

Impact

  • Using tx.origin instead of msg.sender causes incorrect ownership checks and prevents the owner from interacting through intermediaries.

  • This could be exploited by attackers to bypass ownership control, potentially manipulating contract state or funds.

  • It compromises the security of the contract by allowing unauthorized access.

Likelihood Explanation:

  • Likelihood: Medium to High

    • tx.origin misuse is a common vulnerability in Solidity.

    • If the contract interacts with other contracts (e.g., proxies), attackers can easily exploit this issue by crafting transactions through intermediaries.

  • The test cases demonstrated this issue, showing that even the owner was prevented from executing the function through an intermediary contract.

Proof of Concept:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "forge-std/Test.sol";
import "../contracts/GmxProxy.sol";
contract GmxProxyTest is Test {
GmxProxy public gmxProxy;
address public owner;
address public attacker;
function setUp() public {
// Define owner and attacker addresses
owner = address(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496); // Owner's address (known)
attacker = address(0x456); // Attacker's address (arbitrary)
// Deploy the GmxProxy contract
gmxProxy = new GmxProxy();
// Initialize the contract with the correct parameters (9 arguments)
gmxProxy.initialize(
address(0), // _orderHandler
address(0), // _liquidationHandler
address(0), // _adlHandler
address(0), // _gExchangeRouter
address(0), // _gmxRouter
address(0), // _dataStore
address(0), // _orderVault
address(0), // _gmxReader
address(0) // _referralStorage
);
// Ensure the owner is correctly initialized
address currentOwner = gmxProxy.owner();
console.log("Current owner after initialization: ", currentOwner); // Debug output
// Verify the owner is correctly set
assertEq(currentOwner, owner, "Owner is not correctly set during initialization.");
}
// Test: Owner calls setPerpVault
function testContractCallFromOwner() public {
// Output to check who is the current owner
console.log("Current owner: ", gmxProxy.owner());
// Ensure the owner is correctly set
assertEq(gmxProxy.owner(), owner, "Owner is not correctly set.");
// Simulate the call with tx.origin being the owner
console.log("Owner is calling setPerpVault directly");
// Directly call setPerpVault from the owner (no prank here)
gmxProxy.setPerpVault(owner, address(this)); // Owner is calling setPerpVault directly
}
// Test: Non-owner (attacker) tries to call setPerpVault
function testContractCallFromNonOwner() public {
// Output to check who is the current owner
console.log("Current owner: ", gmxProxy.owner());
// Ensure the owner is correctly set
assertEq(gmxProxy.owner(), owner, "Owner is not correctly set.");
// Simulate the call with tx.origin being the attacker
console.log("Non-owner (attacker) is trying to call setPerpVault");
// Use vm.startPrank to simulate the attacker's call
vm.startPrank(attacker);
// Attempt to call setPerpVault from the attacker
try gmxProxy.setPerpVault(owner, address(this)) {
// If no error is thrown, the test should fail, because non-owner shouldn't be able to setPerpVault
revert("Attacker was able to call setPerpVault, which should not happen");
} catch Error(string memory reason) {
// This is expected: we expect the revert "not owner" from the require statement
console.log("Error caught as expected: ", reason);
}
// End the prank
vm.stopPrank();
}
}
  • Test output showing the failure of the contract when the owner calls setPerpVault through vm.startPrank:

    [FAIL: revert: not owner] testContractCallFromOwner()
    Logs:
    Current owner after initialization: 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496
    Owner is calling setPerpVault directly

Tools Used

Manual

Recommendations

  • Fix: Replace tx.origin with msg.sender in the setPerpVault function.

    Fixed Code Snippet:

    function setPerpVault(address _owner, address _vault) external {
    require(msg.sender == owner(), "not owner"); // Use msg.sender instead of tx.origin
    // Rest of the logic
    }
  • By using msg.sender, the contract will correctly verify the immediate caller’s identity, even when intermediaries are involved.

Updates

Lead Judging Commences

n0kto Lead Judge 10 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!