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 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.