A contract can have no runtime code and still already be executing as a contract.
That construction window is enough to bypass one of CROSS DEX V3's maker restrictions.
The router tries to reject contract accounts with:
function _checkAccountCode(address account) private view {
if (whitelistedCodeAccounts.contains(account)) return;
if (account.code.length != 0) {
revert RouterContractAccountBlocked(account);
}
}
During a constructor, however:
address.code.length == 0
A malicious maker can therefore submit a sell order while it is still being deployed. After deployment, the same address has runtime code and can reject native CROSS in receive().
That becomes exploitable on native CROSS quote pairs because CROSS WETH automatically unwraps payouts to non pair recipients.
When an honest buyer matches the malicious sell order, the maker payout becomes a native CROSS transfer. The maker rejects it, the WETH transfer reverts, and the buyer's entire matching transaction rolls back.
The malicious order remains live with the same amount, so later buyers can hit the same failure again.
I reported this finding as Medium through the CertiK SkyShield CROSS bug bounty program. It earned $109.40.

The demonstrated impact is localized order book griefing, not theft or global DEX unavailability.
The constructor bypass creates a valid contract owned order
Order submission uses the router's checkSubmit path, which eventually calls _checkAccountCode(_msgSender()).
The restriction works for contracts that already have runtime code. The PoC confirms that with a post deployment control.
The problem is the constructor case.
The PoC uses CREATE2 so the attacker's future maker address is known before deployment. The attacker approves that predicted address to spend the required BASE tokens.
Then RejectMaker is deployed.
Inside its constructor, it:
pulls BASE from the attacker
approves the router
submits a sell limit order
At that moment the router sees no runtime code and accepts the maker.
After deployment, the contract has:
receive() external payable {
revert RejectNative();
}
The order itself remains a normal stored sell order owned by that now deployed contract.
This matters because matching does not later revalidate whether the owner can safely receive the configured payout.
Native CROSS settlement turns the maker into a revert point
When an honest buyer crosses a sell order, PairImplV3 pays the maker in the pair's QUOTE token:
function _exchangeSellOrder(
uint256 orderId,
address _owner,
uint256 amount,
uint256 fee
) private {
if (fee == 0) {
QUOTE.safeTransfer(_owner, amount);
} else {
uint256 value = amount - fee;
QUOTE.safeTransfer(_owner, value);
}
}
For a normal ERC20 quote asset, the malicious contract can receive the token and matching succeeds.
Native CROSS pairs are different because QUOTE is CROSS WETH.
Its transfer path automatically unwraps tokens sent to non pair recipients:
function _update(
address from,
address to,
uint256 value
) internal override {
super._update(from, to, value);
if (to != address(0)) {
if (!IRouterV3(ROUTER).isPair(to)) {
_burn(to, value);
payable(to).sendValue(value);
}
}
}
The maker is not a registered pair.
So what looks like a WETH payout becomes:
WETH transfer
→ burn
→ native CROSS transfer
The malicious maker rejects that native transfer.
sendValue reverts, which propagates through the WETH transfer and through the buyer's match.
The failed match does not consume the malicious order
Because the buyer transaction reverts atomically, the attempted trade does not partially complete.
The PoC verifies:
buyer receives BASE
0
maker receives native CROSS
0
buyer match
REVERTED
malicious order remains live
YES
order amount unchanged
YES
That persistence is what makes the issue a repeatable liveness problem rather than an isolated failed trade.
While the order remains in the book, later matching attempts that reach it can encounter the same revert.
Four controls isolate the exact cause
The Foundry PoC contains four tests:
| Test | What it proves |
|---|---|
testBug |
A constructor submitted maker order is accepted, an honest native CROSS match reverts, and the order remains unchanged. |
testEOA |
The same native CROSS path succeeds with an EOA maker. |
testToken |
The same malicious contract works when QUOTE is a normal ERC20. |
testBlock |
A contract submitting after deployment is correctly rejected by the router. |
The reproduced run completed with:
4 passed
0 failed
0 skipped
These controls rule out the main false positives.
The issue is not that native pair matching is generally broken.
It is not that contract makers inherently break every quote asset.
And it is not that the router completely fails to block deployed contracts.
The failure requires this specific chain:
constructor submission
→ contract maker stored
→ native CROSS quote
→ WETH auto unwrap
→ rejecting receive()
→ settlement revert
→ full rollback
→ order remains live
Why this is Medium, not Low
The demonstrated impact is griefing and matching liveness failure, not theft.
That limitation is real and is why I do not classify the issue as High or Critical. But it does not reduce the finding to an informational or minor implementation quirk.
The downgrade rationale identifies several constraints:
the order must be submitted during construction
the pair must use CROSS on the maker payout side
the attacker must lock their own BASE into the malicious order
the impact is localized rather than protocol wide
Those are valid exploit preconditions and they should be included in the severity analysis.
They do not, however, change what happens once the malicious order has been accepted.
The PoC reaches a deterministic state:
constructor submitted order accepted
→ contract finishes deployment
→ maker rejects native CROSS
→ honest buyer reaches the order
→ CROSS WETH auto unwraps the maker payout
→ receive() reverts
→ buyer matching transaction reverts
→ the order and its amount roll back unchanged
Because the malicious order survives the failed transaction, the failure is repeatable. A later matching attempt that reaches the same order can hit the same revert again.
The attack requires attacker supplied inventory
The attacker must fund the sell order with BASE.
That is a meaningful constraint and it makes the attack less attractive than a zero cost denial of service.
The article should not hide that fact.
But requiring the attacker to place real inventory into a valid looking maker order does not eliminate the impact on honest takers. The protocol accepts that order into the book, and once settlement reaches it, the buyer cannot complete the match because the protocol's own payout path invokes a hostile native receive hook.
The report does not need to assume that the attacker's locked BASE is stolen, lost, or recoverable through some unproven path. The relevant demonstrated fact is narrower: the funded order exists, remains live after the failed match, and can continue to poison settlement while it remains in the book.
The taker failure is caused by protocol settlement
The honest buyer is not directly choosing to send native CROSS to the maker.
PairImplV3 pays the sell maker through QUOTE.safeTransfer, and CROSS WETH itself converts that transfer into native CROSS for a non pair recipient.
There is no recovery path around the reverting sendValue inside the matching transaction shown by the PoC. The revert propagates and rolls the transaction back.
That makes the failure part of the normal in scope matching path, not merely a malicious contract reverting an unrelated call.
Localized does not mean negligible
The downgrade correctly notes that the issue does not compromise protocol wide solvency, ownership, or accounting.
I agree.
The PoC also does not demonstrate:
direct theft
unauthorized minting
permanent user fund loss
full DEX unavailability
Those limitations are exactly why the report was submitted as Medium griefing.
But the affected path is still a real user facing trading path. An order accepted by the router can become persistently unmatchable on a native CROSS pair and make honest buyer transactions revert whenever matching reaches it.
That is more than a cosmetic inconsistency or a best practice observation.
The contract restriction is intentionally enforced by the router
CROSS explicitly blocks non whitelisted accounts that already have runtime code:
if (account.code.length != 0) {
revert RouterContractAccountBlocked(account);
}
The PoC confirms this policy with testBlock.
The constructor path defeats that restriction only because runtime code has not been installed yet. The accepted address later becomes exactly the type of deployed contract the router normally refuses as a maker.
That bypass would still be incomplete as a report if no downstream effect existed.
Here there is one: the stored contract maker can make the native payout revert and abort honest matching.
The controls support Medium griefing
The four tests isolate the impact:
post deployment contract submission blocked
YES
constructor submitted contract order accepted
YES
EOA maker on native CROSS succeeds
YES
same malicious maker on non native quote succeeds
YES
native CROSS match against malicious maker reverts
YES
buyer receives BASE
NO
maker receives native CROSS
NO
malicious order remains live after revert
YES
order amount remains unchanged
YES
The issue is therefore conditional and narrow, but it is also concrete and repeatable.
That maps cleanly to the report's original framing:
Smart Contract Medium: Griefing
I therefore consider Medium technically better supported by the demonstrated behavior than Low.
The downgrade's prerequisites reduce exploitability and blast radius. They do not turn a repeatable failure of honest matching against an accepted order into a non impactful implementation detail.
Fix the settlement dependency
The most direct fix is to prevent arbitrary contract receive hooks from controlling DEX matching.
The report recommends auto unwrapping WETH only for EOAs.
Contract recipients should receive WETH instead and explicitly unwrap it later if they can safely accept native CROSS.
Conceptually:
if (
!IRouterV3(ROUTER).isPair(to)
&& to.code.length == 0
) {
_burn(to, value);
payable(to).sendValue(value);
}
A separate explicit withdraw() path can let contract recipients unwrap voluntarily.
That changes the security property from:
maker receive() decides
whether matching succeeds
to:
matching completes in WETH
maker decides later
whether to unwrap
The router's account restriction should also not assume that code.length == 0 proves an address will permanently behave like an EOA.
But the settlement fix is more important because it removes the ability of an untrusted recipient hook to revert an otherwise valid match.
Broader audit lesson
This finding comes from combining two behaviors that look manageable on their own:
constructor:
code.length == 0
and:
WETH transfer:
automatic native unwrap
The vulnerability appears only when the entire lifecycle is followed:
maker validation
→ constructor order
→ stored owner
→ later matching
→ quote payout
→ auto unwrap
→ receive() revert
→ transaction rollback
→ order survives
That is the broader lesson.
If a protocol validates an account only when state is created, ask whether the account can behave differently when that state is consumed later.
And if settlement sends native value to an arbitrary recipient, ask whether that recipient can make unrelated users' transactions fail.
Conclusion
CROSS DEX V3 tries to block contract makers by checking account.code.length.
A constructor can bypass that restriction because runtime code is still absent.
Using CREATE2, the PoC funds a predicted maker address and submits a sell order from inside the maker's constructor. After deployment, the same maker rejects native CROSS.
When an honest buyer later matches that order on a native CROSS quote pair, CROSS WETH automatically unwraps the maker payout into native CROSS.
The maker rejects the transfer.
The entire buyer match reverts, and the malicious order remains live with the same amount.
The core invariant is:
A maker's payout behavior should not be able to make honest taker matching revert.
A stored order should be safe to settle even if its owner is a contract with hostile native receive behavior.
That repeatable, user facing matching failure is why I classify the issue as Medium rather than Low.

Top comments (0)