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,
Chain
Other
Category
logic
Date
Jan 2024
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
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.
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-registrymirror.
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, noanvil_state. Full trace: output.txt. PoC: test/30258-h-02-orders-creator-can-update-tokenout-to-arbitrary-token-c_exp.sol.
Key info#
| Impact | HIGH — order creator can front-run fillOrder and rewrite tokenOut to any high-value token the executor has approved the hook to spend |
| Protocol | INIT Capital — MarginTradingHook stop-loss / take-profit orders |
| Vulnerable code | MarginTradingHook.updateOrder — assigns order.tokenOut = _tokenOut with no base/quote validation |
| Bug class | Missing invariant re-check on update that exists at create time |
| Finding | code4rena — INIT Capital invitational, 2024-01 · #30258 · reporter said |
| Report | 2024-01-init-capital-invitational |
| Source | AuditVault |
| Status | Audit 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#
createOrderenforcestokenOut ∈ {baseAsset, quoteAsset}.updateOrderrewritesorder.tokenOutwith no such check.- Executors commonly pre-approve the hook for many tokens to fill diverse orders.
- 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#
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:
+ _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#
- Creator opens a WETH/USDC position and creates a valid order with
tokenOut = USDC. - Executor holds and approves both USDC and WBTC to the hook.
- Creator front-runs:
updateOrder(..., tokenOut = WBTC, ...). - Executor's fillOrder pulls
amtOutof WBTC to the creator. - Executor's USDC is untouched; WBTC is stolen.
Diagrams#
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/highimpact/mev/frontrungenome: wrong-condition,frontrun,specific-token-type,variant,frontrun-exposuresector/lendingplatform/code4rena
Sources#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/30258-h-02-orders-creator-can-update-tokenout-to-arbitrary-token-c.md
- Report: https://code4rena.com/reports/2024-01-init-capital-invitational
- Vulnerable source:
code-423n4/2024-01-init-capital-invitational@ main —contracts/hook/MarginTradingHook.sol#updateOrder(L504–L526)
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 30258-h-02-orders-creator-can-update-tokenout-to-arbitrary-token-c_exp (evm-hack-registry mirror).
- AuditVault finding: 30258-h-02-orders-creator-can-update-tokenout-to-arbitrary-token-c.
Alerts & third-party analyses
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.