Reproduced Exploit
TTSwap Market: permissionless `initGood` + attacker-controlled ERC20 skews AMM accounting via `buyGood`/`payGood`
1. Attacker CREATE-deploys a factory (nonce 0) that spawns a child exploit and a fake ERC20 good (spoofed transfer/balanceOf). 2. Child flash-loans 8 WETH from Balancer, then calls permissionless initGood(WETH, packed, fake, goodConfig, …) so the market seeds a new good pair using attacker-chosen q…
Loss
5.098626217469779632 ETH (exact PoC / on-chain profit after 8 WETH Balancer repay)
Chain
Ethereum
Category
logic
Date
Apr 2026
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, 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. Original research reproduction (not from DeFiHackLabs). Standalone Foundry PoC, offline
anvil_state.json, and full write-up: 2026-04-TTSwapMarket_exp in theevm-hack-registrymirror.
Vulnerability classes: vuln/logic/incorrect-initialization · vuln/logic/missing-check · vuln/oracle/price-manipulation
Reproduction: the PoC compiles & runs in an isolated Foundry project at this project folder. The fork is served offline from the bundled
anvil_state.json(local anvil replays Ethereum state at block24991800), so no public RPC is required. Full verbose trace: output.txt. Verified market implementation: src_TTSwap_Market.sol. Proxy: TTSwap_Market_Proxy. Swap math library: L_Good.sol.
Key info#
| Loss | 5.098626217469779632 ETH (exact PoC / on-chain profit after 8 WETH Balancer repay) |
| Vulnerable contract | TTSwap Market proxy — 0x5C70a413fcb7ea8c8D478D06F31f8963cE4EE635 |
| Implementation | 0x97904142f8d4dfa3e54011cf5dc19dbcaf5dd6a6 |
| Fake good token (attacker CREATE) | 0xEefFdB5fCED045EFbaa7293f2D2Eff5EcfBE9A26 |
| Attacker EOA | 0x12494E12A20267c52c445765e9c204CDa7e7A02e |
| Attack factory | 0x5d5Fc1C6156F8c5EEA8E7FE2649507F0CE6bFc1B (CREATE, nonce 0) |
| Child exploit | 0x91Ae7aDeA5153229910B5C379A8C7561aD70B144 |
| Balancer Vault (FL) | 0xBA12222222228d8Ba445958a75a0704d566BF2C8 |
| Attack tx | 0x43a0161de17f23e8767e736cce39c87bcc4de5e6504a23c98899427f760ee343 |
| Chain / block / date | Ethereum mainnet / fork 24991800 (attack in 24991801) / 2026-04-30 |
| Bug class | Permissionless market initGood with attacker-controlled ERC20 + value/quantity AMM seeding → mispriced buyGood/payGood inventory drain |
Naming / TesseraSwap (important)#
This is not TesseraSwap.
| TTSwap Market (this entry) | TesseraSwap (2026-05-TesseraSwap_exp) | |
|---|---|---|
| Chain | Ethereum | Base |
| Victim | TTSwap Market singleton AMM | Tessera treasury swap router |
| Bug | Market init + buy/pay accounting | CEI: pay output before collect input |
| Profit | ~5.1 ETH | ~13k USDC / loop campaign |
ExVul alert: status/2049788573554229599.
TL;DR#
- Attacker CREATE-deploys a factory (nonce 0) that spawns a child exploit and a fake ERC20 good (spoofed
transfer/balanceOf). - Child flash-loans 8 WETH from Balancer, then calls permissionless
initGood(WETH, packed, fake, goodConfig, …)so the market seeds a new good pair using attacker-chosen quantity and WETH value deposit. - Because the fake token fabricates successful transfers of arbitrary quantities, internal
currentState/investState(value vs quantity) can be skewed relative to real inventory of existing goods. - Repeated
buyGood/payGoodagainst market inventory extract USDT, USDC, WBTC, BNB, SOL, and residual WETH at broken rates; tokens are swapped to WETH on Uni v2/v3, the 8 WETH loan is repaid, and 5.098626217469779632 ETH is kept.
Root cause#
Permissionless initGood with attacker-controlled ERC20#
initGood is not admin-gated (guardedEntry reentrancy only). Any caller may register a new good by supplying:
_valuegood— an existing value good (WETH)_erc20address— the new "normal" good (attacker CREATE fake token)_initialpacked uint256 — amount0 = normal qty, amount1 = value-good qty_goodConfig— fee/K parameters for the new good
// sources/.../src_TTSwap_Market.sol (verified impl)
function initGood(
address _valuegood,
uint256 _initial,
address _erc20address,
uint256 _goodConfig,
bytes calldata _normaldata,
bytes calldata _valuedata,
address _trader,
bytes calldata signature
) external payable override guardedEntry msgValue returns (bool) {
_checkTrader(_trader);
// min qty check only — no token allowlist / no real-supply verification
if (_initial.amount0() < 500000 || _initial.amount0() > 2 ** 109)
revert TTSwapError(36);
...
_erc20address.transferFrom(msg.sender, msg.sender, _initial.amount0(), _normaldata);
_valuegood.transferFrom(msg.sender, msg.sender, _initial.amount1(), _valuedata);
...
goods[_valuegood].investGood(_initial.amount1(), investResult, 100);
goods[_erc20address].init(
toTTSwapUINT256(investResult.investValue, _initial.amount0()),
_goodConfig
);
}
The fake ERC20 returns success for any transferFrom and reports inflated balances. Real WETH still moves into the market, but the normal-good quantity side is attacker-chosen fiction.
AMM state seeds from that init#
L_Good.init sets both current quantity and invest value from the packed _init:
// L_Good.sol
self.currentState = toTTSwapUINT256(_init.amount1(), _init.amount1()); // qty
self.investState = toTTSwapUINT256(_init.amount1(), _init.amount0()); // value
Subsequent good1Swap / good2Swap price trades from current_quantity and current_value:
ΔV = (K * V * Δa) / (K * Q + Δa * 10000) // good1Swap input
Δb = (K * Q * ΔV) / (K * V + ΔV * 10000) // good2Swap output
With skewed (V, Q) on the fake good (and paired value-good depth changes from the WETH invest), buyGood / payGood emit real market inventory against the synthetic good at rates the attacker controls.
Why Balancer 8 WETH#
Flash capital funds the value-good leg of initGood and intermediate swap inventory. After draining multi-token market balances and routing them to WETH, the loan is repaid and residual native ETH is profit.
Attack path#
Funds (historical)#
| Flow | Amount |
|---|---|
| Balancer FL | 8 WETH |
| Market multi-asset drain → WETH legs | USDT 3,595.52 · USDC 1,851.03 · WBTC 0.025 · BNB 1.32 · SOL 7.029 · residual WETH |
| Net to attacker EOA | 5.098626217469779632 ETH |
PoC#
CREATE replay (primary)#
test/TTSwapMarket_exp.sol replays the full historical CREATE initcode at fork block 24991800 (attacker nonce forced to 0 → factory 0x5d5Fc1…). Constructor tree performs FL + init + drain + repay.
Playground synthetic#
test/2026-04-TTSwapMarket.sol wraps the same initcode in attack() and forwards ETH to owner.
# Offline (bundled anvil_state.json)
bash _shared/run_poc.sh 2026-04-TTSwapMarket_exp -vv
Exact assert: profit == 5_098_626_217_469_779_632 wei ETH.
Mitigation notes#
- Do not allow untrusted ERC20s as goods without a registry/allowlist or deposit verification against real
balanceOfdeltas (and preferably transfer tax / fee-on-transfer guards). - Gate
initGood(or require a bonded stake / admin/oracle-approved listing) so attacker-controlled tokens cannot seed AMM (V, Q) state. - After any transferFrom into the market, reconcile balance deltas rather than trusting the caller-supplied quantity parameter.
- Cap
goodConfigparameters and reject configs that create extreme V/Q ratios relative to observed external prices.
References#
- ExVul: https://x.com/exvulsec/status/2049788573554229599
- Attack tx: https://etherscan.io/tx/0x43a0161de17f23e8767e736cce39c87bcc4de5e6504a23c98899427f760ee343
- Market proxy: https://etherscan.io/address/0x5c70a413fcb7ea8c8d478d06f31f8963ce4ee635
- Impl: https://etherscan.io/address/0x97904142f8d4dfa3e54011cf5dc19dbcaf5dd6a6#code
- Upstream (similar surface): https://github.com/tt-swap/ttswap-core-v1
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 2026-04-TTSwapMarket_exp (evm-hack-registry mirror).
- Attack transaction: view on explorer.
Alerts & third-party analyses
- Original alert / thread: post on X.
- Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
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.