A ZapIn call should invest the asset supplied by the caller.
It should never let someone provide an unrelated ERC20 while the contract silently spends native WEMIX already retained as goodwill and backing affiliate liabilities.
That was possible in WeswapZapIn.
A permissionless attacker could call ZapIn with:
An arbitrary ERC20 as the input token
A real WWEMIX liquidity pair as the destination
WWEMIX as _swapTarget
Zero native WEMIX in msg.value
The arbitrary ERC20 was transferred into the Zap contract.
But it did not fund the liquidity operation.
Instead, the privileged WWEMIX branch inside _fillQuote wrapped native WEMIX from WeswapZapIn itself.
That branch also returned before the normal approved-target check.
The newly minted WWEMIX then passed through the real Weswap swap and liquidity path, and the resulting LP tokens were transferred to the attacker.
The proof showed 9.9 WEMIX leaving the Zap contract, 9.9 WWEMIX being minted, a positive LP balance appearing for the attacker, and the arbitrary ERC20 remaining unused inside WeswapZapIn.
The same native WEMIX backed a recorded affiliate liability.
After the drain, the liability remained 10 WEMIX while the native backing fell to 0.1 WEMIX, causing affiliate withdrawal to revert.
I submitted this report as Major through the WEMIX bug bounty program hosted on CertiK Skynet.
The program classified it as Low and paid a $100 bounty.
That classification does not match the demonstrated impact.
This was a permissionless conversion of contract-held native WEMIX into attacker-owned LP tokens, followed by undercollateralization of a recorded liability.
The complete public report and proof of concept are available in the GitHub Gist.
The asset-source invariant
A ZapIn operation converts one user-supplied asset into liquidity.
The fundamental invariant should be:
Every asset spent during a ZapIn call must be attributable to the value supplied for that call.
For native WEMIX input:
The caller supplies msg.value
The Zap wraps or swaps that value
The resulting assets fund liquidity
For ERC20 input:
The caller transfers the ERC20
The Zap swaps that ERC20
The resulting assets fund liquidity
The vulnerable flow broke that invariant.
The attacker supplied one asset.
The contract spent another.
How WeswapZapIn accumulated native WEMIX legitimately
The exploit did not rely on forcing native currency into the contract.
A normal native ZapIn could leave WEMIX behind through the goodwill mechanism.
For native input, _pullTokens calculated goodwill and returned only the remaining amount for investment:
if (token == address(0)) {
require(
msg.value > 0,
"No WEMIX sent"
);
totalGoodwillPortion =
_subtractGoodwill(
WEMIXAddress,
msg.value,
affiliate,
enableGoodwill
);
return
msg.value
-
totalGoodwillPortion;
}
The deducted goodwill remained as native WEMIX in WeswapZapIn.
The PoC performed a normal native ZapIn with:
Input
1000 WEMIX
Goodwill
100 basis points
Retained native WEMIX
10 WEMIX
This was normal protocol behavior, not an artificial funding assumption.
The retained WEMIX also backed an affiliate claim
With an approved affiliate and affiliateSplit = 100, _subtractGoodwill recorded the full goodwill amount as an affiliate balance:
uint256 affiliatePortion =
(
totalGoodwillPortion
*
affiliateSplit
)
/
100;
affiliateBalance[
affiliate
][
token
] =
affiliateBalance[
affiliate
][
token
]
+
affiliatePortion;
totalAffiliateBalance[
token
] =
totalAffiliateBalance[
token
]
+
affiliatePortion;
After the normal ZapIn, the state was:
Native WEMIX held by WeswapZapIn
10 WEMIX
affiliateBalance[affiliate][WEMIX]
10 WEMIX
totalAffiliateBalance[WEMIX]
10 WEMIX
The contract-held WEMIX was therefore not merely idle surplus.
It backed a recorded liability.
The attacker controlled the input token and swap target
The public ZapIn function accepted both:
_FromTokenContractAddress
_swapTarget
The attacker selected:
_FromTokenContractAddress
Arbitrary ERC20
_swapTarget
WWEMIX
The arbitrary ERC20 was not token0 or token1 of the target WWEMIX/TokenX pair.
That forced _performZapIn into _fillQuote.
For ERC20 input, _pullTokens transferred the attacker’s tokens into WeswapZapIn:
IERC20(token)
.safeTransferFrom(
msg.sender,
address(this),
amount
);
The exploit transaction sent:
msg.value = 0
The arbitrary ERC20 arrived in the Zap contract.
It remained there after the exploit.
The vulnerable WWEMIX shortcut
The root cause was the first branch in _fillQuote:
if (
_swapTarget
==
wwemixTokenAddress
) {
IWWEMIX(
wwemixTokenAddress
)
.deposit{
value: _amount
}();
return (
_amount,
wwemixTokenAddress
);
}
This shortcut made three unsafe assumptions.
It did not verify native input
The branch did not require:
_fromTokenAddress
==
address(0)
An ERC20-funded call could therefore activate native wrapping.
It spent the contract’s current native balance
The call:
deposit{
value: _amount
}()
sent WEMIX from address(this).
The attacker supplied zero native WEMIX in the exploit transaction.
The value came from the contract’s pre-existing balance.
It bypassed the target allowlist
The normal path later checked:
require(
approvedTargets[
_swapTarget
],
"Target not Authorized"
);
The WWEMIX branch returned before that check.
The PoC confirmed:
approvedTargets[WWEMIX] = false
and the exploit still succeeded.
A control using another unapproved target reverted with:
Target not Authorized
The WWEMIX shortcut bypassed the same authorization rule that protected the general swap path.
Native WEMIX became WWEMIX owned by the Zap
WWEMIX.deposit minted wrapped tokens to msg.sender:
function deposit()
public
payable
{
_mint(
msg.sender,
msg.value
);
emit Deposit(
msg.sender,
msg.value
);
}
During the exploit, msg.sender was WeswapZapIn.
The contract received WWEMIX minted from its own native balance.
That WWEMIX became the intermediate asset used by the rest of the ZapIn flow.
The real Weswap path converted it into attacker-owned LP
The minted WWEMIX entered _swapIntermediate.
Part of it was swapped into the second token of the real target pair.
The resulting assets were passed to _weDeposit, which called the real Weswap router:
(
uint256 amountA,
uint256 amountB,
uint256 LP
) =
weswapRouter
.addLiquidity(
_ToWepoolToken0,
_ToWepoolToken1,
token0Bought,
token1Bought,
1,
1,
address(this),
deadline
);
The LP tokens were first minted to WeswapZapIn.
The public ZapIn function then transferred them to msg.sender.
That completed the value flow:
Native WEMIX held by WeswapZapIn
Wrapped into WWEMIX
Swapped and added as liquidity
Converted into LP
Transferred to the attacker
The attacker’s arbitrary ERC20 did not fund this path.
The concrete proof-of-concept result
The full local Hardhat proof used:
The real WeswapZapIn contract
The real WeswapRouter
The real WeswapFactory
A real WeswapPair created through the factory
The actual WWEMIX contract behavior
Helper ERC20 tokens only as local assets
The observed transition was:
Native WEMIX before exploit
10 WEMIX
Exploit msg.value
0 WEMIX
Native WEMIX after exploit
0.1 WEMIX
Native WEMIX removed
9.9 WEMIX
WWEMIX total supply increase
9.9 WEMIX
Attacker LP balance
Positive
Arbitrary ERC20 left in WeswapZapIn
Full exploit input amount
The transaction succeeded even though:
The attacker supplied no native WEMIX
The arbitrary ERC20 was not in the target pair
No useful pair existed for that arbitrary ERC20
WWEMIX was not an approved target
This is a direct unauthorized transfer of economic value.
The affiliate liability became undercollateralized
The exploit did not reduce the recorded affiliate balances.
Afterward:
Recorded affiliate WEMIX liability
10 WEMIX
Remaining native backing
0.1 WEMIX
The affiliate withdrawal path attempted to send the full recorded amount:
Address.sendValue(
payable(msg.sender),
tokenBal
);
The call reverted because the contract no longer had enough native WEMIX.
The same exploit therefore caused:
Attacker-owned LP funded by contract WEMIX
Loss of native WEMIX held by WeswapZapIn
Undercollateralized affiliate accounting
Failed affiliate withdrawal
Direct EOA funding was not part of the exploit
ZapBase rejected direct native transfers from an EOA:
receive()
external
payable
{
require(
msg.sender
!=
tx.origin,
"Do not send WEMIX directly"
);
emit Receive(
msg.sender,
msg.value
);
}
The PoC tested this as a clean control, and the direct send reverted.
The exploitable balance came primarily from the legitimate native ZapIn and goodwill flow.
That matters because the report does not depend on accidental or forced funding.
The contract could naturally hold the asset during intended operation.
The complete attack path
The exploit required only normal protocol state:
1. WeswapZapIn holds native WEMIX from goodwill accounting
2. A real WWEMIX pair exists
3. The attacker owns or creates an arbitrary ERC20
4. The attacker approves WeswapZapIn to transfer it
5. The attacker calls ZapIn with that ERC20
6. The ERC20 is not part of the target pair
7. The attacker sets _swapTarget to WWEMIX
8. The attacker sends msg.value = 0
9. _pullTokens transfers the ERC20 into WeswapZapIn
10. _performZapIn calls _fillQuote
11. The WWEMIX shortcut wraps the Zap contract’s native WEMIX
12. The shortcut returns before the approved-target check
13. The real Weswap path creates liquidity
14. LP tokens are transferred to the attacker
15. The arbitrary ERC20 remains unused in the Zap
16. Affiliate liabilities remain recorded without sufficient backing
The attacker did not need:
Owner privileges
Affiliate status
Fee-whitelist membership
An approved swap target
A fake router
A fake pair
A fake WWEMIX implementation
Direct storage writes
External RPC control
Why this is Major, not Low
A Low classification would fit a harmless edge case, a cosmetic discrepancy, dust-only impact, or a revert without material loss.
This report demonstrated a complete economic exploit.
The attacker received real value
The attacker ended with LP tokens minted through the genuine Weswap liquidity path.
Those LP tokens were funded by WEMIX that had already belonged to the Zap contract.
The attacker supplied none of the drained asset
The exploit transaction sent zero native WEMIX.
The provided ERC20 remained unused in the contract.
The asset leaving the protocol was not the asset supplied by the attacker.
The attack was permissionless
Any caller could choose the input token and WWEMIX target.
No administrative or governance privilege was required.
The shortcut itself bypassed the approved-target check.
The vulnerable balance arose naturally
The proof created the native balance through the intended ZapIn goodwill mechanism.
The attack did not rely on forced WEMIX, an artificial test balance, or an owner mistake.
The contract became unable to honor a recorded liability
The affiliate accounting remained at 10 WEMIX.
The backing fell to 0.1 WEMIX.
Affiliate withdrawal reverted.
The impact was therefore not limited to temporary contract balance movement.
The PoC executed the full financial path
The proof did not stop at the vulnerable deposit call.
It continued through:
WWEMIX minting
Real pair interaction
Real router liquidity addition
LP minting
LP transfer to the attacker
Affiliate withdrawal failure
That is material direct loss.
It is not Low impact.
Why the $100 outcome understates the evidence
The final Low classification produced a $100 bounty.
That is the program’s recorded decision.
It does not alter the proof:
9.9 native WEMIX left the Zap
9.9 WWEMIX was minted
The attacker received LP tokens
The attacker sent zero native WEMIX
The arbitrary ERC20 remained unused
The 10 WEMIX liability remained
Only 0.1 WEMIX remained as backing
Affiliate withdrawal reverted
Severity should follow the strongest demonstrated impact.
Here, that impact was permissionless extraction of contract-held native WEMIX into an attacker-owned liquidity position.
That supports Major severity.
Why I describe the WEMIX handling as systematic downgrading
Severity disagreements are normal in bug bounty programs.
My concern is the recurring result across my own valid WEMIX submissions.
Reports supported by reproducible proofs and concrete protocol impact were repeatedly assigned substantially lower final severities.
This report was submitted as Major.
The PoC demonstrated direct asset extraction, LP minted to the attacker, an allowlist bypass, and an undercollateralized affiliate liability.
It was classified as Low.
I cannot infer intent from that decision.
I can document the repeated pattern and compare the classifications with the technical evidence.
Calling it systematic downgrading describes that recurring outcome across my submissions without claiming a motive.
A technically convincing Low justification would need to explain why the following do not constitute material loss:
Permissionless removal of native WEMIX
Attacker-owned LP funded by that WEMIX
Zero native input from the attacker
Target authorization bypass
Recorded affiliate claims left without backing
Without addressing those facts, the Low classification does not match the demonstrated state transition.
Primary correction
The WWEMIX shortcut must require genuine native input:
if (
_swapTarget
==
wwemixTokenAddress
) {
require(
_fromTokenAddress
==
address(0),
"WWEMIX shortcut requires native input"
);
IWWEMIX(
wwemixTokenAddress
)
.deposit{
value: _amount
}();
return (
_amount,
wwemixTokenAddress
);
}
This binds the native wrapping behavior to the correct source asset.
Bind native spending to the current call
The contract should also enforce:
Native value wrapped during this ZapIn
<=
Native value supplied and attributed to this ZapIn
Pre-existing contract balance must never be treated as caller input.
A branch should not be allowed to spend native WEMIX merely because address(this).balance is sufficient.
Apply authorization before shortcuts
Target validation should happen before target-specific execution.
A fast path must not return before the normal authorization check unless it has an equally strict dedicated validation path.
Isolate affiliate reserves
The contract should distinguish:
Current-call native input
Owner-withdrawable surplus
Reserved affiliate WEMIX
A useful reserve invariant is:
Spendable native WEMIX
=
Native balance
-
Recorded affiliate liabilities
Unrelated user operations should never consume reserved affiliate backing.
Regression tests
A complete fix should verify:
ERC20 input cannot activate native WWEMIX wrapping
msg.value = 0cannot mint WWEMIX from pre-existing balanceUnapproved targets cannot bypass authorization through early returns
Arbitrary ERC20 input cannot mint LP funded by contract WEMIX
Legitimate native ZapIn still works
Legitimate ERC20 ZapIn spends only the supplied ERC20
Affiliate liabilities remain fully backed
Affiliate withdrawal succeeds after unrelated ZapIn activity
Direct EOA native sends remain rejected
The real pair and router path remain functional
Reserved WEMIX cannot be treated as current-call input
Failed calls leave contract and affiliate balances unchanged
The central postcondition is:
No ZapIn may spend native WEMIX that was not supplied and attributed to that call.
Broader lessons
Contract balance is not caller input
A payable contract may hold native currency for fees, refunds, affiliates, or previous users.
Availability does not imply ownership by the current caller.
Fast paths need full validation
The WWEMIX shortcut bypassed the source check and the target allowlist.
Optimized branches must preserve the same security properties as the general path.
Input identity must match value identity
An ERC20-funded call must not activate native-asset behavior simply because the caller selected a wrapper contract.
Liabilities require reserve isolation
A recorded affiliate balance is a claim against contract assets.
Its backing must not remain freely spendable by unrelated operations.
Severity follows the final value destination
The bug began as a missing source check.
It ended with contract WEMIX converted into attacker-owned LP.
Impact analysis must follow the value through the complete path.
Conclusion
WeswapZapIn allowed a caller to provide an arbitrary ERC20 while selecting WWEMIX as _swapTarget.
The ERC20 was transferred into the contract but did not fund the operation.
The WWEMIX shortcut wrapped _amount using native WEMIX already held by the Zap and returned before the approved-target check.
The resulting WWEMIX passed through the real Weswap swap and liquidity path.
The attacker received the LP tokens.
The proof demonstrated:
Zero native WEMIX supplied by the attacker
9.9 native WEMIX removed from WeswapZapIn
9.9 WWEMIX minted
Positive attacker LP balance
Arbitrary ERC20 left unused
10 WEMIX affiliate liability still recorded
0.1 WEMIX remaining as backing
Affiliate withdrawal reverting
The program classified the report as Low and paid $100.

The demonstrated impact was Major.
A permissionless caller converted contract-held native WEMIX into attacker-owned LP while leaving recorded liabilities undercollateralized.
That is direct financial loss caused by a broken asset-source boundary.
Top comments (0)