Sometimes the most important accounting bugs are not hidden inside complicated mathematics. They appear when two parts of a protocol disagree about a much simpler question:
Who actually paid this cost?
I found one of those mismatches in the 0xMarkets subaccount flow.
The protocol allows a main account to authorize a subaccount, grant it a limited number of actions, set an expiry, and enable an automatic WNT top up. The purpose of that top up is reasonable: reimburse the subaccount for native token costs incurred while creating delegated orders.
The problem appeared when WNT itself was used as the order collateral.
In that branch, the main account already funded the execution fee through the WNT transferred to the OrderVault. The subaccount sent no native token and did not call sendWnt.
Even so, the reimbursement logic added the declared execution fee to the subaccount top up and transferred that amount from the main account again.
In economic terms, the main account was charged for the same execution fee twice:
First, as part of the WNT transferred to fund the order
Again, as a reimbursement paid to the subaccount
The finding was later validated as High during the 0xMarkets Audit Contest.
The intended subaccount model
A main account may authorize another address to act as a subaccount.
That subaccount can create orders on behalf of the main account, subject to limits configured by the owner. Those limits include the permitted action count, expiry time, token allowance, account balance, and automatic top up amount.
The top up exists to reimburse real costs.
Conceptually, the expected rule is:
reimbursement = gas paid by subaccount + execution fee paid by subaccount
That final condition matters.
An execution fee should only be reimbursed when the subaccount actually funded it.
The vulnerable flow instead behaved like this:
reimbursement = gas attributed to subaccount + declared execution fee
It used the declared fee without checking who supplied the WNT that funded it.
Why WNT collateral is a special branch
For swap and increase orders, SubaccountRouter.createOrder transfers the initial collateral from the main account into the OrderVault.
The relevant code appears in SubaccountRouter.sol:
router.pluginTransfer(
params.addresses.initialCollateralToken,
account,
address(orderVault),
params.numbers.initialCollateralDeltaAmount
);
The important detail is the source account.
The tokens are pulled from account, which is the main account, not from msg.sender, which is the subaccount.
When the initial collateral token is not WNT, the execution fee must be supplied separately as WNT. In that normal branch, reimbursing the subaccount for the execution fee can be legitimate because the subaccount can send that WNT through the production flow.
WNT collateral behaves differently.
OrderUtils.createOrder records the WNT received by the OrderVault and carves the execution fee out of that same transfer.
The relevant branch appears in OrderUtils.sol:
cache.initialCollateralDeltaAmount =
orderVault.recordTransferIn(
params.addresses.initialCollateralToken
);
if (params.addresses.initialCollateralToken == cache.wnt) {
if (
cache.initialCollateralDeltaAmount <
params.numbers.executionFee
) {
revert Errors.InsufficientWntAmountForExecutionFee(
cache.initialCollateralDeltaAmount,
params.numbers.executionFee
);
}
cache.initialCollateralDeltaAmount -=
params.numbers.executionFee;
cache.shouldRecordSeparateExecutionFeeTransfer = false;
}
Suppose the main account transfers 1.1 WNT.
The order stores:
1.0 WNT as collateral
0.1 WNT as execution fee
Both components came from the main account transfer.
The subaccount did not provide a separate 0.1 WNT fee.
Where the accounting breaks
After creating the order, SubaccountRouter.createOrder calls the automatic top up routine.
The call passes the declared execution fee directly:
_autoTopUpSubaccount(
account,
msg.sender,
startingGas,
params.numbers.executionFee
);
The reimbursement function in SubaccountRouter.sol then calculates the amount of native token supposedly used:
uint256 nativeTokensUsed =
(startingGas - gasleft()) *
tx.gasprice +
executionFee;
It pulls that amount in WNT from the main account, unwraps it, and sends native token to the subaccount.
This calculation assumes the subaccount paid the execution fee.
That assumption is correct for the separate fee branch, but false for WNT collateral orders where the fee was already extracted from the main account transfer.
The protocol loses track of the fee payer across the function boundary.
One component knows the fee came from the collateral transfer.
Another component sees only the declared executionFee value and treats it as reimbursable.
Complete exploitation flow
The attacker in this scenario is not an arbitrary outsider. It is an authorized subaccount that abuses the reimbursement mechanism beyond its intended purpose.
The sequence is:
The main account authorizes a subaccount
The main account enables automatic top up
The main account approves the router to transfer WNT
The subaccount creates an increase or swap order using WNT collateral
The router pulls the complete WNT amount from the main account into the OrderVault
OrderUtilssubtracts the execution fee from that transfer and stores the remainder as collateralThe subaccount sends zero native token and does not call
sendWntThe order is created successfully
_autoTopUpSubaccountadds the declared execution fee to the reimbursement calculationThe router pulls an additional WNT amount from the main account
That WNT is unwrapped and sent to the subaccount as native token
The subaccount receives reimbursement for a fee it never paid.
The transfer happens immediately after order creation. The issue does not depend on later order execution, cancellation, freezing, callbacks, or refunds.
Why authorization does not make the behavior correct
A common objection to subaccount findings is that the main account voluntarily authorized the subaccount.
That is true, but it does not resolve the bug.
Authorization answers whether the subaccount may create an order.
It does not mean the subaccount may receive reimbursement for arbitrary costs it did not incur.
The owner also enabled auto top up, but that feature has a defined financial purpose: reimburse expenses associated with delegated actions.
The expected trust boundary is:
The subaccount may perform authorized actions and recover real permitted costs.
The vulnerable implementation changed that into:
The subaccount may recover the declared execution fee even when the main account already paid it.
Those are not equivalent permissions.
How the proof isolated the bug
The proof used the real repository deployment fixture and the actual protocol contracts.
It did not depend on mock contracts, a custom harness, direct storage modification, or later order execution.
The test used one vulnerable run and three controls.
Vulnerable WNT collateral run
The attack run used:
WNT as initial collateral
A nonzero execution fee
Automatic top up enabled
Zero
msg.valueNo
sendWntcall by the subaccount
The main account funded both collateral and execution fee through the WNT transfer.
The subaccount still received a top up containing an execution fee sized component.
No top up control
The same WNT collateral order was created with automatic top up disabled.
The order still succeeded, but no reimbursement was transferred.
This confirmed that the extra value came from the top up mechanism.
Zero execution fee control
The test created a WNT collateral order with the execution fee set to zero.
The subaccount received only the gas style reimbursement component.
This established a baseline for the top up cost without the disputed execution fee.
Non WNT collateral control
The final control used USDC as collateral.
In that path, the subaccount supplied the WNT execution fee through the real sendWnt multicall flow.
Reimbursing the execution fee was legitimate because the subaccount actually funded it.
Together, these controls isolated the problem to the WNT collateral branch where the main account pays the fee but the subaccount receives the reimbursement.
Measured result
The deterministic proof used an execution fee of 0.1 WNT.
The relevant values were:
Execution fee
0.100000000000000000 WNT
WNT transferred into the OrderVault
1.100000000000000000 WNT
Attack top up amount
0.101930246000000000 WNT
Zero fee top up amount
0.001890446000000000 WNT
Measured over reimbursement
0.100039800000000000 WNT
Total WNT removed from the main account
1.201930246000000000 WNT
The measured excess was slightly above 0.1 WNT because the gas component differed marginally between the attack and control runs.
The meaningful result is that the attack top up exceeded the zero fee control by approximately the full declared execution fee.
The OrderVault received 1.1 WNT from the main account, which already contained the 0.1 WNT fee.
The main account then lost another 0.101930246 WNT through automatic top up.
The complete test passed:
1 passing
Repetition and practical bounds
The value transfer can be repeated while the subaccount remains authorized and the relevant limits remain available.
Triage summarized the impact as a malicious authorized subaccount repeatedly draining the main account WNT allowance through the reimbursement mismatch.
The extraction is not unlimited.
It is bounded by:
The configured auto top up amount
The main account WNT allowance
The main account WNT balance
The subaccount action count
The authorization expiry
The order types and collateral branch required to reach the bug
These bounds explain why the issue is not an unrestricted protocol wide drain.
They do not remove the underlying accounting failure. Within those limits, the main account pays an execution fee twice and the subaccount receives value for a cost it did not fund.
Severity and contest result
I submitted the report with a conservative Medium assessment because the attacker had to be an authorized subaccount and the extraction was bounded by owner configured limits.
The contest triage validated the finding as High. Their explanation matched the behavior demonstrated by the proof:
SubaccountRouter.createOrderpulls WNT containing both collateral and the execution fee from the main accountOrderUtils.createOrdercarves the execution fee out of that same transfer_autoTopUpSubaccountadds the declared fee to the reimbursement without checking who paid itThe main account therefore funds both the order fee and the additional reimbursement
A malicious authorized subaccount can repeat the action while the configured limits, allowance, balance, and authorization remain available
Nine researchers reported the same issue, so the High severity reward pool was divided among them. My final reward was $60.87.

The payout was heavily diluted by duplicates, but the technical lesson remains valuable: a reimbursement function can look correct in isolation and still become exploitable when payment provenance is lost across the call chain.
Recommended fix
The reimbursement amount should be based on the execution fee actually supplied by the subaccount, not merely the execution fee declared in the order.
For WNT collateral swap and increase orders, the main account funds the fee through the collateral transfer. The reimbursable execution fee should therefore be zero.
A simplified correction is:
function _getReimbursableExecutionFee(
IBaseOrderUtils.CreateOrderParams calldata params
) internal view returns (uint256) {
if (
params.addresses.initialCollateralToken ==
TokenUtils.wnt(dataStore) &&
(
params.orderType ==
Order.OrderType.MarketSwap ||
params.orderType ==
Order.OrderType.LimitSwap ||
params.orderType ==
Order.OrderType.MarketIncrease ||
params.orderType ==
Order.OrderType.LimitIncrease ||
params.orderType ==
Order.OrderType.StopIncrease
)
) {
return 0;
}
return params.numbers.executionFee;
}
The router should calculate this value before calling _autoTopUpSubaccount:
uint256 reimbursableExecutionFee =
_getReimbursableExecutionFee(params);
bytes32 key = orderHandler.createOrder(
account,
params,
params.addresses.callbackContract != address(0)
);
_autoTopUpSubaccount(
account,
msg.sender,
startingGas,
reimbursableExecutionFee
);
This preserves the intended behavior:
WNT collateral orders reimburse gas only because the main account already funded the execution fee
Non WNT collateral orders may still reimburse the execution fee when the subaccount supplied it separately
Order creation and stored order fields remain unchanged
Disabling auto top up continues to produce no reimbursement
A more general design would explicitly track the fee payer instead of inferring it from the token branch. That approach reduces the chance that future order types reintroduce the same mismatch.
Broader audit lessons
This finding reinforced several questions I now ask when reviewing reimbursement and delegated account systems.
Declared cost is not the same as paid cost
A parameter named executionFee describes the amount associated with an operation.
It does not prove who paid it.
Reimbursement logic should use payment provenance, not only the declared value.
Authorization does not erase internal trust boundaries
An authorized delegate may have permission to perform actions without having permission to extract every balance made reachable by those actions.
Review the exact economic authority granted by each feature.
Special token branches deserve separate accounting review
WNT collateral reused one transfer for two purposes:
Collateral
Execution fee
The generic reimbursement path was written for a model where those values could come from separate sources.
Whenever one branch merges payments that are separate elsewhere, downstream accounting assumptions must be reviewed again.
Controls are essential for reimbursement findings
A single successful transaction would only show that a top up occurred.
The zero fee, no top up, and non WNT controls demonstrated why the extra amount was incorrect and which branch caused it.
Follow value across the complete call chain
Each function looked locally reasonable:
Transfer collateral from the main account
Carve the execution fee from WNT
Reimburse the subaccount
The bug became visible only when those steps were connected and the same fee was followed from payer to recipient.
Conclusion
The 0xMarkets subaccount flow incorrectly reimbursed authorized subaccounts for execution fees they did not pay on WNT collateral orders.
The main account transferred WNT that already covered both collateral and the order execution fee. OrderUtils carved the fee out of that transfer. After order creation, the automatic top up logic added the same declared fee to the subaccount reimbursement without checking who had funded it.
The proof established the decisive facts:
The subaccount sent no native token and did not call
sendWntThe main account funded the execution fee through the WNT collateral transfer
The subaccount still received a top up containing an execution fee sized component
The zero fee, no top up, and non WNT controls isolated the reimbursement mismatch
The value transfer occurred immediately after order creation and did not depend on later execution, cancellation, callbacks, or refunds
The engineering invariant is simple:
A reimbursement system must never compensate an account for a cost that was paid by someone else.
Top comments (0)