Reproduced Exploit
Lucidly — addLiquidity solves supply with the wrong balances
addLiquidity calls _calculateSupply with _prevSupply, _virtualBalanceProdFinal, _virtualBalanceSumFinal, and true. The supply argument should be the running supply, the balances should be the fee-adjusted ones, and the round-up flag should be _prevSupply == 0.
Chain
Other
Category
logic
Date
Jul 2024
Source
AuditVault
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, Balances (native / ERC-20 / NFT), 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. Reproduction of a public audit finding curated by AuditVault — the original finding: 36348-h-08-wrong-usage-of-variables-for-calculatesupply-pashov-au. Standalone Foundry PoC and full write-up: 36348-h-08-wrong-usage-of-variables-for-calculatesupply-pashov-au_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/wrong-condition · vuln/logic/state-update
Reproduction: a self-contained Foundry PoC that compiles and runs offline with only
forge-std— no fork and no RPC. Trace: output.txt. PoC: test/36348-h-08-wrong-usage-of-variables-for-calculatesupply-pashov-au_exp.sol.
Key info#
| Impact | HIGH — the first add on an empty pool reverts, and a later add of 100 and 100 mints 200.000000000000020218 LP instead of 179.999999999999942197 LP |
| Protocol | Lucidly Pool.addLiquidity |
| Vulnerable code | The first _calculateSupply call passes _prevSupply, the final virtual balances, and true |
| Bug class | Wrong arguments to the supply update |
| Finding | Pashov Audit Group — Lucidly, H-08 · id 36348 |
| Report | Lucidly security review |
| Source | AuditVault finding |
| Status | The second call in the function is the correct one. Reproduced here as a local synthetic. |
| Compiler | solc 0.8.27, cancun, via IR |
TL;DR#
addLiquidity calls _calculateSupply with _prevSupply, _virtualBalanceProdFinal, _virtualBalanceSumFinal, and true. The supply argument should be the running supply, the balances should be the fee-adjusted ones, and the round-up flag should be _prevSupply == 0.
On an empty pool _prevSupply is 0. _calculateSupply reverts on that zero, so the first add mints 0 LP.
After a seed of 100 and 100, supply is 200.000000000000020000 LP. Adding another 100 and 100 should mint 179.999999999999942197 LP once the 10 percent fee is removed from the credited balances. The buggy call mints 200.000000000000020218 LP. The excess is 20.000000000000078021 LP. A full withdrawal of that LP quotes 100.000000000000000054 of token0. The fair withdrawal quotes 94.736842105263136896.
run() does not warp. Weights are equal and rates are 1, so the product of a balanced pool is 1. Amplification is 100e18.
The vulnerable code#
// @> VULN: _prevSupply, the final balances, and true — not the fee-adjusted supply
(nextSupply, prodFinal) = _calculateSupply(2, prevSupply, AMPLIFICATION, prodFinal, sumFinal, true);
_calculateSupply starts with require(supply_ != 0), which is the zero check the report points at. The fixed call passes prevSupply == 0 ? sum : prevSupply and prevSupply == 0 as the round-up flag, and it uses the balances after the fee.
Root cause#
The first call was written with the arguments of the later fee mint: previous supply, final balances, and round up. Those are the wrong inputs for the LP minted to the depositor. The zero previous supply also makes the first deposit revert.
Preconditions#
- Two assets, equal weights, rate 1, amplification
100e18, fee 10 percent. - The empty-pool add is attempted before any seed.
- The mis-mint is measured after a seed of 100 and 100.
- No timestamp change happens inside
run().
Attack walkthrough#
addLiquidity(100, 100)on the empty pool._prevSupplyis 0. The call reverts. Minted LP is 0.- Seed 100 and 100. Supply becomes 200.000000000000020000 LP, minted to the genesis LP.
previewFair(100, 100)returns 179.999999999999942197 LP.- The buggy
addLiquidity(100, 100)mints 200.000000000000020218 LP to the depositor. quoteWithdrawof that LP returns 100.000000000000000054 of token0, against a fair quote of 94.736842105263136896.
Control: the fixed pool's first addLiquidity(100, 100) mints a non-zero supply. A second add mints strictly less than 200 LP, because the 10 percent fee is not given to the depositor.
Diagrams#
Impact#
The pool cannot take its first deposit through addLiquidity. After it has supply, a depositor of 100 and 100 is minted 20.000000000000078021 LP more than the fee-adjusted mint, and a withdrawal of that LP returns the deposit without leaving the 10 percent fee in the pool.
Remediation#
Pass the running supply, the fee-adjusted product and sum, and _prevSupply == 0 into the first _calculateSupply. That is the call the report says is already correct on the second use.
Sources#
- AuditVault finding 36348
- Pashov Lucidly security review, H-08
- Audited tree: lucidlyfi/lucidly-core-v1 Pool.sol
Taxonomy#
severity/highvuln/logic/wrong-conditionvuln/logic/state-update
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 36348-h-08-wrong-usage-of-variables-for-calculatesupply-pashov-au_exp (evm-hack-registry mirror).
- AuditVault finding: 36348-h-08-wrong-usage-of-variables-for-calculatesupply-pashov-au.
Alerts & third-party analyses
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.