This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0xd10faa5b33ddb501b1dc6430896c966048271f2510ff9ed681dd6d510c5df9f60xb6333e994fd02a9255e794c177efbdeb1fe779c7Base0x833589fcd6edb6e08f4c7c32d4f71b54bda02913Base0x4200000000000000000000000000000000000006Base0xb33ff54b9f7242ef1593d2c9bcd8f9df46c77935Base0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1bBase0xcbb7c0000ab88b473b1f5afd9ef808440eed33bfBase0x940181a94a35a4569e4529a3cdfb74e38fd98631Base0xecac9c5f704e954931349da37f60e39f515c11c1On Base (chainid 8453, block 25431001), an unprivileged EOA 0x4015d786e33c1842c3e4d27792098e4a3612fc0e invokes an attacker-controlled contract 0x22a7da241a39f189a8aec269a6f11a238b6086fc.
This contract calls the public OdosLimitOrderRouter.isValidSigImpl function multiple times with carefully crafted EIP‑6492–style signatures.
Each call causes OdosLimitOrderRouter (0xb6333e994fd02a9255e794c177efbdeb1fe779c7) to execute arbitrary ERC‑20 transfer calls from its own address, draining its entire balances in several ERC‑20 tokens directly to the attacker EOA.
At a high level, the root cause is that the router inherits UniversalSigValidator and exposes isValidSigImpl(address,bytes32,bytes,bool) as a public function. When called with EIP‑6492‑encoded signatures and allowSideEffects = true, this function performs attacker‑controlled external calls from the router (via create2Factory.call(factoryCalldata)) without access control or side‑effect restrictions, and then uses the Identity precompile to satisfy the ERC‑1271 magic value. This bypasses the router’s intended owner/liquidator‑only withdrawal mechanisms and allows full draining of router‑held funds in a single transaction.
OdosLimitOrderRouter design
OdosLimitOrderRouter is an EIP‑712–based limit order router that can hold ERC‑20 balances on‑chain.0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22Base0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452Base0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42BaseswapRouterFunds and transferRouterFunds to move router‑held funds, with an additional privileged liquidatorAddress allowed to call swapRouterFunds.SignatureValidator and UniversalSigValidator
The router inherits SignatureValidator, which itself extends UniversalSigValidator, an implementation of EIP‑6492 and ERC‑1271 designed to support counterfactual smart wallets.
The key function is:
function isValidSigImpl(
address _signer,
bytes32 _hash,
bytes calldata _signature,
bool allowSideEffects
) public returns (bool)
(From verified router project source, UniversalSigValidator implementation.)
This function is intended as a generic helper for validating signatures, including counterfactual deployments.
For normal EIP‑1271 order validation, the router uses isValidSig(account, orderHash, signature), which internally calls isValidSigImpl with allowSideEffects = false. In that path, any side effects during counterfactual validation are reverted so that signature checks cannot accidentally change state.
However, the raw isValidSigImpl entrypoint with allowSideEffects = true remains publicly callable on the router.
EIP‑6492 side‑effectful behavior
Inside UniversalSigValidator.isValidSigImpl, when a signature is marked as counterfactual via the EIP‑6492 detection suffix, the implementation:
Detects the suffix constant.
Decodes the signature into (create2Factory, factoryCalldata, sigToValidate).
If address(_signer).code.length == 0, it executes:
(bool success, bytes memory err) = create2Factory.call(factoryCalldata);
from the caller’s context.
On OdosLimitOrderRouter, this call executes with msg.sender = address(this) (the router), meaning any factoryCalldata can be used to perform arbitrary calls from the router’s address, including ERC‑20 transfer operations of router‑held balances.
After this call, isValidSigImpl proceeds to an ERC‑1271 check: if _signer is a contract (or treated as such in the logic), it calls:
IERC1271Wallet(_signer).isValidSignature(_hash, sigToValidate)
and returns true if the magic value 0x1626ba7e (ERC1271_SUCCESS) is observed.
Public exposure of isValidSigImpl on router
isValidSigImpl as a public function via inheritance from UniversalSigValidator._signer, _hash, _signature, and allowSideEffects, and it is not restricted by any access control or scoped to limit which contracts may be called via EIP‑6492.UniversalSigValidator.isValidSigImpl is exposed as a public function on OdosLimitOrderRouter. When an attacker invokes it with:
(create2Factory, factoryCalldata) pair encoded inside the signature, andallowSideEffects = true,the router executes an arbitrary external call using create2Factory.call(factoryCalldata) from the router’s address, with no access control or limitation on side effects.
By setting create2Factory to ERC‑20 token contracts and encoding transfer calls that move the router’s entire token balance to the attacker EOA, the adversary can drain router‑held balances. The ERC‑1271 check is satisfied by using the Identity precompile as _signer, which echoes input bytes such that the first four bytes equal the ERC‑1271 success magic value.
In the verified router source (src/Contract.sol in the cloned project for 0xb6333e994f...779c7), the UniversalSigValidator implementation is:
contract UniversalSigValidator {
bytes32 private constant ERC6492_DETECTION_SUFFIX =
0x6492649264926492649264926492649264926492649264926492649264926492;
bytes4 private constant ERC1271_SUCCESS = 0x1626ba7e;
function isValidSigImpl(
address _signer,
bytes32 _hash,
bytes calldata _signature,
bool allowSideEffects
) public returns (bool) {
uint256 contractCodeLen = address(_signer).code.length;
bytes memory sigToValidate;
bool isCounterfactual = _signature.length >= 32
&& bytes32(_signature[_signature.length-32:_signature.length]) == ERC6492_DETECTION_SUFFIX;
if (isCounterfactual) {
address create2Factory;
bytes memory factoryCalldata;
(create2Factory, factoryCalldata, sigToValidate) =
abi.decode(_signature[0:_signature.length-32], (address, bytes, bytes));
if (contractCodeLen == 0) {
(bool success, bytes memory err) = create2Factory.call(factoryCalldata);
if (!success) revert ERC6492DeployFailed(err);
}
} else {
sigToValidate = _signature;
}
if (isCounterfactual || contractCodeLen > 0) {
try IERC1271Wallet(_signer).isValidSignature(_hash, sigToValidate)
returns (bytes4 magicValue) {
bool isValid = magicValue == ERC1271_SUCCESS;
if (contractCodeLen == 0 && isCounterfactual && !allowSideEffects) {
assembly {
mstore(0, isValid)
revert(31, 1)
}
}
return isValid;
} catch (bytes memory err) { revert ERC1271Revert(err); }
}
// fallback to ecrecover ...
}
}
Caption: Core part of UniversalSigValidator.isValidSigImpl, showing the EIP‑6492 suffix detection, decoding into (create2Factory, factoryCalldata, sigToValidate), the side‑effectful create2Factory.call(factoryCalldata) when contractCodeLen == 0, and the ERC‑1271 check using ERC1271_SUCCESS. This code runs in the context of the caller (here, OdosLimitOrderRouter).
Origin: Verified OdosLimitOrderRouter project source (src/Contract.sol).
Key observations:
isCounterfactual is true and _signer has no bytecode (contractCodeLen == 0), the function unconditionally executes create2Factory.call(factoryCalldata) and only checks that it does not revert.create2Factory must be a specific deployment factory, orfactoryCalldata must contain deployment bytecode or be side‑effect‑free.transfer encoded in factoryCalldata will transfer tokens held by the router.The router itself inherits SignatureValidator, which wraps isValidSigImpl for normal EIP‑1271 validation:
contract SignatureValidator is UniversalSigValidator {
// ...
function _getOrderOwnerOrRevert(
bytes32 orderHash,
bytes calldata encodedSignature,
SignatureValidationMethod validationMethod
) internal returns (address account) {
if (validationMethod == SignatureValidationMethod.EIP712) {
account = ECDSA.recover(orderHash, encodedSignature);
} else if (validationMethod == SignatureValidationMethod.EIP1271) {
assembly {
account := shr(96, calldataload(encodedSignature.offset))
}
bytes calldata signature = encodedSignature[20:];
if (!isValidSig(account, orderHash, signature)) {
revert InvalidEip1271Signature(orderHash, account, signature);
}
} else { /* PreSign path */ }
}
}
Here, isValidSig(account, orderHash, signature) calls isValidSigImpl with allowSideEffects = false and uses a try/catch mechanism to undo side effects for counterfactual signatures. This path is reasonably safe when only used internally.
However, because OdosLimitOrderRouter inherits UniversalSigValidator directly, it exposes isValidSigImpl as a public function with an explicit allowSideEffects parameter. An attacker can skip the router’s order flow and directly call:
OdosLimitOrderRouter.isValidSigImpl(
signer, // chosen by attacker
hash, // can be 0x00, not enforced
signature,// attacker-crafted EIP-6492 payload
true // allowSideEffects
);
The root cause is therefore a design‑level misuse of a side‑effectful signature validation helper:
UniversalSigValidator.isValidSigImpl is designed to be used carefully, often off‑chain or with restricted parameters, because EIP‑6492 allows it to execute arbitrary calls during validation.OdosLimitOrderRouter is a fund‑holding contract that must not allow arbitrary users to execute calls from its context.isValidSigImpl publicly with allowSideEffects = true, the router unintentionally grants any caller the ability to execute arbitrary calls from its own address.0x0000000000000000000000000000000000000004) as _signer, the attacker ensures that the subsequent ERC‑1271 check always returns the expected magic value 0x1626ba7e, making isValidSigImpl return true even after executing the malicious call.OdosLimitOrderRouter (0xb6333e994fd02a9255e794c177efbdeb1fe779c7) on Base:
SignatureValidator → UniversalSigValidator.isValidSigImpl(address,bytes32,bytes,bool) as a public function.UniversalSigValidator.isValidSigImpl implementation:
src/Contract.sol of the verified router project.(create2Factory, factoryCalldata, sigToValidate) and performing create2Factory.call(factoryCalldata) from the caller’s context.For the exploit to succeed, the following conditions must hold (all verified in on‑chain trace and metadata):
Router holds token balances:
OdosLimitOrderRouter holds non‑zero balances of multiple ERC‑20 tokens at or before block 25431001, including:
0x833589fcd6edb6e08f4c7c32d4f71b54bda029130x42000000000000000000000000000000000000060xb33ff54b9f7242ef1593d2c9bcd8f9df46c779350x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf0x940181a94a35a4569e4529a3cdfb74e38fd986310xecac9c5f704e954931349da37f60e39f515c11c10x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec220xc1cba3fcea344f92d9239c08c0568f6f2f0ee4520x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42These balances are confirmed via balanceOf calls in the exploit trace.
Router bytecode and ABI correspond to the verified project:
0xb6333e...779c7 matches the verified OdosLimitOrderRouter implementation that:
UniversalSigValidator.isValidSigImpl as a public function in its ABI.Attacker can craft EIP‑6492 signatures:
(create2Factory, factoryCalldata) pairs invoke ERC‑20 transfer from the router’s address to the attacker EOA for each token.isValidSigImpl treats the signature as counterfactual and executes create2Factory.call(factoryCalldata).Identity precompile behavior on Base:
0x0000000000000000000000000000000000000004, which returns its calldata unmodified._signer = Identity and crafts sigToValidate such that:
0x1626ba7e (ERC1271_SUCCESS).isValidSigImpl returns true.No side effects in signature verification:
Signature verification must not allow attackers to cause arbitrary state changes. Here, isValidSigImpl allows arbitrary external calls under attacker control when allowSideEffects = true.
Least privilege and access control:
Movement of router‑held funds should be restricted to owner/liquidator via swapRouterFunds and transferRouterFunds. The public isValidSigImpl path bypasses these controls completely.
Separation of concerns:
A powerful, side‑effectful validation helper intended for controlled environments (EIP‑6492) is reused inside a fund‑holding router without constraining its possible side effects, turning it into an arbitrary call primitive.
The adversary executes a single transaction on Base where:
OdosLimitOrderRouter.isValidSigImpl repeatedly with carefully crafted EIP‑6492‑style signatures and allowSideEffects = true.create2Factory.call(factoryCalldata).true.This sequence drains all router‑held balances in the targeted ERC‑20 tokens in one transaction.
Attacker EOA
0x4015d786e33c1842c3e4d27792098e4a3612fc0e0xd10faa5b33ddb501b1dc6430896c966048271f2510ff9ed681dd6d510c5df9f6.from = OdosLimitOrderRouter and to = 0x4015...fc0e in the trace.Attacker Contract
0x22a7da241a39f189a8aec269a6f11a238b6086fc0x22A7dA241A39F189a8Aec269A6F11A238B6086fc::exploit(...).OdosLimitOrderRouter::isValidSigImpl calls that drain router‑held funds by targeting different ERC‑20 token contracts in each call.OdosLimitOrderRouter
0xb6333e994fd02a9255e794c177efbdeb1fe779c7Router‑held token contracts (victim assets)
0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 (verified project).0x4200000000000000000000000000000000000006.0xb33ff54b9f7242ef1593d2c9bcd8f9df46c77935 (labelled Token in trace).0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b.0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf.0x940181a94a35a4569e4529a3cdfb74e38fd98631.0xecac9c5f704e954931349da37f60e39f515c11c1.0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22.0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452 (verified project).0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42 (verified project).Transaction details
0xd10faa5b33ddb501b1dc6430896c966048271f2510ff9ed681dd6d510c5df9f625431001other (direct arbitrary call to router’s public isValidSigImpl via attacker contract).Trace‑backed call flow
A representative segment from the collected trace:
0x22A7dA241A39F189a8Aec269A6F11A238B6086fc::exploit(
OdosLimitOrderRouter: [0xB6333E994Fd02a9255E794C177EfBDEB1FE779C7],
[0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, 0x4200..., ...]
)
├─ OdosLimitOrderRouter::isValidSigImpl(
│ Identity: [0x0000000000000000000000000000000000000004],
│ 0x000...000, // _hash = 0
│ 0x000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913
│ 0000000000000000000000000000000000000000000000000000000000000060
│ ... a9059cbb (transfer) ... 4015d786e33c1842c3e4d27792098e4a3612fc0e
│ ... amount ...
│ ... 649264926492... (EIP-6492 suffix),
│ true
│ )
│ ├─ FiatTokenProxy::fallback(...)
│ │ ├─ FiatTokenV2_2::transfer(
│ │ │ from: OdosLimitOrderRouter,
│ │ │ to: 0x4015D786e33c1842c3e4d27792098e4A3612Fc0e,
│ │ │ value: 15578334373
│ │ │ ) [delegatecall]
│ │ └─ emit Transfer(from = router, to = attacker, ...)
│ ├─ PRECOMPILES::identity(...) [staticcall]
│ │ └─ returns bytes starting with 0x1626ba7e
│ └─ returns true
Caption: Excerpt from the seed transaction trace showing the attacker contract calling OdosLimitOrderRouter::isValidSigImpl with _signer = Identity, _hash = 0x0, and a crafted EIP‑6492 signature whose decoded factoryCalldata triggers FiatTokenProxy::fallback/FiatTokenV2_2::transfer from the router to the attacker. The Identity precompile then returns data starting with 0x1626ba7e, satisfying the ERC‑1271 magic value.
Origin: Seed transaction trace (trace.cast.log) for 0xd10faa5b...5df9f6.
Similar calls appear for each affected token (FiatTokenProxy‑style stablecoins, WETH9, OptimismMintableERC20, other ERC‑20s), always with:
_signer = 0x0000000000000000000000000000000000000004 (Identity precompile)._hash = 0x00.signature encoding:
create2Factory.transfer(routerBalance, attackerEOA) as factoryCalldata.allowSideEffects = true.For each such call, the router’s entire balance in the targeted token is transferred to the attacker EOA, and isValidSigImpl returns true.
The exploit drains all router‑held balances for the targeted ERC‑20 tokens. Quantified amounts from the analysis:
FiatTokenProxy‑style stablecoin (0x833589fcd6edb6e08f4c7c32d4f71b54bda02913)
15,578,334,373 units transferred from OdosLimitOrderRouter to 0x4015d7...fc0e.198,830,527 units transferred in a separate call within the same transaction.WETH9 (0x4200000000000000000000000000000000000006)
2.261323351186171128 WETH transferred from OdosLimitOrderRouter to 0x4015d7...fc0e.Multiple additional ERC‑20 tokens held by the router
81,182,355,184,994,926,311,507 units of 0xb33ff54b9f7242ef1593d2c9bcd8f9df46c77935.1,514,424,244,715,040,557,606 units of 0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b.2,343,323 units of 0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf.2,134,216,454,655,905,106,108 units of 0x940181a94a35a4569e4529a3cdfb74e38fd98631.576,319 units of 0xecac9c5f704e954931349da37f60e39f515c11c1.0.144206352825325002e18 units of 0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22.0.122592242770994685e18 units of 0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452.0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42.All of these transfers have:
from = OdosLimitOrderRouter (0xb6333e...779c7),to = attacker EOA (0x4015d7...fc0e),Router fund depletion:
After the transaction, OdosLimitOrderRouter’s balances in the listed ERC‑20 tokens are reduced to zero (or to the residual post‑transfer amounts determined by the exploit payloads), representing a full depletion of router‑held funds for those assets.
Attacker profit:
The attacker’s portfolio gains the full set of drained ERC‑20 balances. The analysis notes:
0.000015877492 ETH‑equivalent (from the native balance delta).Loss characterization:
These funds are held by the router on behalf of its users or protocol operations. The exploit bypasses all designed withdrawal and routing logic, directly transferring assets from the router to the attacker in a single step.
pre_state_sigma_B)The pre‑state σ_B is defined as the Base chain state immediately before transaction 0xd10faa5b33ddb501b1dc6430896c966048271f2510ff9ed681dd6d510c5df9f6 in block 25431001, where:
OdosLimitOrderRouter (0xb6333e994fd02a9255e794c177efbdeb1fe779c7) holds non‑zero balances of the ERC‑20 tokens listed above (stablecoins, WETH9, and several other ERC‑20s).UniversalSigValidator.isValidSigImpl(address,bytes32,bytes,bool).This pre‑state is supported by:
transaction_sequence_b)The ACT opportunity focuses on a single adversary‑crafted transaction:
18453 (Base)0xd10faa5b33ddb501b1dc6430896c966048271f2510ff9ed681dd6d510c5df9f6adversary-craftedInclusion feasibility:
0x4015d7...fc0e) sends a standard type‑2 transaction on Base to the attacker contract 0x22a7da241a39f189a8aec269a6f11a238b6086fc.OdosLimitOrderRouter.isValidSigImpl function with attacker‑chosen arguments (router address, token addresses, EIP‑6492‑encoded signature payloads, Identity precompile as signer).This confirms that, given pre‑state σ_B, any party could have submitted a similar transaction to realize the same exploit and profit.
exploit_predicate)profit0x4015d786e33c1842c3e4d27792098e4a3612fc0e0.000015877492 ETH‑equivalent in gas, inferred from the native balance delta.The exploit predicate is thus satisfied: given σ_B, executing this transaction increases the attacker’s net value in terms of the chosen reference asset (USD).
[1] Seed transaction metadata
Human‑readable metadata for the seed transaction 0xd10faa5b33ddb501b1dc6430896c966048271f2510ff9ed681dd6d510c5df9f6 on Base (block, gas, participants, and environment).
[2] Execution trace for exploit transaction
Full cast run -vvvv‑style trace for the exploit transaction, showing:
0x22A7dA...::exploit(...) calling OdosLimitOrderRouter::isValidSigImpl.balanceOf and transfer calls.from = router and to = attacker for each affected ERC‑20.[3] Verified OdosLimitOrderRouter and UniversalSigValidator source
The project cloned for 0xb6333e994fd02a9255e794c177efbdeb1fe779c7, containing:
UniversalSigValidator.isValidSigImpl.SignatureValidator and its _getOrderOwnerOrRevert logic.OdosLimitOrderRouter implementation with owner/liquidator‑only fund movement functions.[4] Verified FiatTokenProxy‑style token project (0x60a3e35c...db1adb42)
Contains FiatTokenProxy and underlying token implementation used in parts of the exploit.
[5] Verified FiatTokenProxy project (0x833589fc...9bda02913)
Provides the stablecoin implementation from which the router’s large stablecoin balances are drained.
[6] Verified OssifiableProxy‑based token project (0xc1cba3fc...f0ee452)
Confirms the behavior of the OssifiableProxy‑style token involved in the exploit and supports the trace‑observed transfers from the router to the attacker.
These references collectively provide the on‑chain code, transaction trace, and metadata needed to fully reconstruct and validate the vulnerability, exploit path, and resulting losses.