DEV Community

Daniel
Daniel

Posted on

How an Arbitrary ERC20 Drained Native WEMIX From WeswapZapIn

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For ERC20 input:

The caller transfers the ERC20

The Zap swaps that ERC20

The resulting assets fund liquidity
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

After the normal ZapIn, the state was:

Native WEMIX held by WeswapZapIn
10 WEMIX

affiliateBalance[affiliate][WEMIX]
10 WEMIX

totalAffiliateBalance[WEMIX]
10 WEMIX
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The attacker selected:

_FromTokenContractAddress
Arbitrary ERC20

_swapTarget
WWEMIX
Enter fullscreen mode Exit fullscreen mode

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
    );
Enter fullscreen mode Exit fullscreen mode

The exploit transaction sent:

msg.value = 0
Enter fullscreen mode Exit fullscreen mode

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
    );
}
Enter fullscreen mode Exit fullscreen mode

This shortcut made three unsafe assumptions.

It did not verify native input

The branch did not require:

_fromTokenAddress
    ==
address(0)
Enter fullscreen mode Exit fullscreen mode

An ERC20-funded call could therefore activate native wrapping.

It spent the contract’s current native balance

The call:

deposit{
    value: _amount
}()
Enter fullscreen mode Exit fullscreen mode

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"
);
Enter fullscreen mode Exit fullscreen mode

The WWEMIX branch returned before that check.

The PoC confirmed:

approvedTargets[WWEMIX] = false
Enter fullscreen mode Exit fullscreen mode

and the exploit still succeeded.

A control using another unapproved target reverted with:

Target not Authorized
Enter fullscreen mode Exit fullscreen mode

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
    );
}
Enter fullscreen mode Exit fullscreen mode

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
        );
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The affiliate withdrawal path attempted to send the full recorded amount:

Address.sendValue(
    payable(msg.sender),
    tokenBal
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A useful reserve invariant is:

Spendable native WEMIX
=
Native balance
-
Recorded affiliate liabilities
Enter fullscreen mode Exit fullscreen mode

Unrelated user operations should never consume reserved affiliate backing.

Regression tests

A complete fix should verify:

  1. ERC20 input cannot activate native WWEMIX wrapping

  2. msg.value = 0 cannot mint WWEMIX from pre-existing balance

  3. Unapproved targets cannot bypass authorization through early returns

  4. Arbitrary ERC20 input cannot mint LP funded by contract WEMIX

  5. Legitimate native ZapIn still works

  6. Legitimate ERC20 ZapIn spends only the supplied ERC20

  7. Affiliate liabilities remain fully backed

  8. Affiliate withdrawal succeeds after unrelated ZapIn activity

  9. Direct EOA native sends remain rejected

  10. The real pair and router path remain functional

  11. Reserved WEMIX cannot be treated as current-call input

  12. 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
Enter fullscreen mode Exit fullscreen mode

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)