Reproduced Exploit

INIT Capital — Order creator can update tokenOut to arbitrary token

1. createOrder enforces tokenOut ∈ {baseAsset, quoteAsset}. 2. updateOrder rewrites order.tokenOut with no such check. 3. Executors commonly pre-approve the hook for many tokens to fill diverse orders. 4. HARM: creator front-runs fillOrder, sets tokenOut to a high-value approved token,

Jan 2024Otherlogic3 min read

Chain

Other

Category

logic

Date

Jan 2024

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 30258-h-02-orders-creator-can-update-tokenout-to-arbitrary-token-c. Standalone Foundry PoC and full write-up: 30258-h-02-orders-creator-can-update-tokenout-to-arbitrary-token-c_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/order-management · impact/mev/frontrun · impact/loss-of-funds/direct-drain

Reproduction: a self-contained Foundry PoC that compiles & runs in an isolated project with only forge-std — no fork, no RPC, no anvil_state. Full trace: output.txt. PoC: test/30258-h-02-orders-creator-can-update-tokenout-to-arbitrary-token-c_exp.sol.


Key info#

ImpactHIGH — order creator can front-run fillOrder and rewrite tokenOut to any high-value token the executor has approved the hook to spend
ProtocolINIT Capital — MarginTradingHook stop-loss / take-profit orders
Vulnerable codeMarginTradingHook.updateOrder — assigns order.tokenOut = _tokenOut with no base/quote validation
Bug classMissing invariant re-check on update that exists at create time
Findingcode4rena — INIT Capital invitational, 2024-01 · #30258 · reporter said
Report2024-01-init-capital-invitational
SourceAuditVault
StatusAudit finding — confirmed by INIT. Reproduced as a standalone local PoC.
Compiler^0.8.24 (PoC)

Sibling finding from the same invitational: #30257 (updateOrder access control), #30259 (limitPrice front-run).


TL;DR#

  1. createOrder enforces tokenOut ∈ {baseAsset, quoteAsset}.
  2. updateOrder rewrites order.tokenOut with no such check.
  3. Executors commonly pre-approve the hook for many tokens to fill diverse orders.
  4. HARM: creator front-runs fillOrder, sets tokenOut to a high-value approved token, fillOrder transferFroms that token from the executor to the creator.

The vulnerable code#

SOLIDITY
function updateOrder(
    uint _posId,
    uint _orderId,
    uint _triggerPrice_e36,
    address _tokenOut,
    uint _limitPrice_e36,
    uint _collAmt
) external {
    _require(_collAmt != 0, Errors.ZERO_VALUE);
    Order storage order = __orders[_orderId];
    _require(order.status == OrderStatus.Active, Errors.INVALID_INPUT);
    uint initPosId = initPosIds[msg.sender][_posId];
    _require(initPosId != 0, Errors.POSITION_NOT_FOUND);
    MarginPos memory marginPos = __marginPositions[initPosId];
    uint collAmt = IPosManager(POS_MANAGER).getCollAmt(initPosId, marginPos.collPool);
    _require(_collAmt <= collAmt, Errors.INPUT_TOO_HIGH);
    // @> MISSING: _require(_tokenOut == marginPos.baseAsset || _tokenOut == marginPos.quoteAsset, ...);

    order.triggerPrice_e36 = _triggerPrice_e36;
    order.limitPrice_e36 = _limitPrice_e36;
    order.collAmt = _collAmt;
    order.tokenOut = _tokenOut; // @> VULN
}

Fix:

DIFF
+       _require(_tokenOut == marginPos.baseAsset || _tokenOut == marginPos.quoteAsset, Errors.INVALID_INPUT);
        order.tokenOut = _tokenOut;

Root cause#

Create-time invariants on order shape are not re-enforced on update. tokenOut is trusted by fillOrder as the asset the executor must pay; mutating it after creation changes which approved allowance is burned.


Preconditions#

  • Executor has approved the hook for a high-value token outside the position's pair (common for multi-order bots).
  • Order is Active and creator can call updateOrder (own position).
  • fillOrder is pending in the mempool (or creator can time the rewrite).

Attack walkthrough#

  1. Creator opens a WETH/USDC position and creates a valid order with tokenOut = USDC.
  2. Executor holds and approves both USDC and WBTC to the hook.
  3. Creator front-runs: updateOrder(..., tokenOut = WBTC, ...).
  4. Executor's fillOrder pulls amtOut of WBTC to the creator.
  5. Executor's USDC is untouched; WBTC is stolen.

Diagrams#

flowchart LR A["createOrder tokenOut=USDC"] --> B["executor approves USDC+WBTC"] B --> C["updateOrder tokenOut=WBTC"] C --> D["fillOrder transferFrom WBTC"] D --> E["creator receives WBTC"]

Impact#

Direct theft of executor inventory whenever the hook holds multi-token approvals. Severity raised to High in contest: multi-token executor approvals are common practice.


Taxonomy#

  • severity/high
  • impact/mev/frontrun
  • genome: wrong-condition, frontrun, specific-token-type, variant, frontrun-exposure
  • sector/lending
  • platform/code4rena

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.