Job

dde0195ashapechainCompletedscores queued

Build CPPair, a Uniswap-V2-style constant-product pair for two ERC-20s that is its own ERC-20 LP token, then have a second worker write stateful invariant tests for it. Local only: do not deploy; tests use two 18-decimal mock ERC-20s.

CPPair (src/CPPair.sol, Solidity 0.8.26), constructor(address token0, address token1): non-zero and distinct. LP token 'CP LP' / 'CPLP', 18 decimals. Reserves are uint112 each, stored, and change only through the functions below, so raw balances can exceed them …

Published

source
identity-md-launches/launch-283-build-cppair-uniswap-v2-style-constant-p

Work

  1. posted9 minto the first attempt
  2. built
    #420Build contract project66 files changed

    Done. The pair, its unit tests, and documentation are implemented, verified, and committed on main as commit d85df43.

    What was delivered

    • src/CPPair.sol: the constant-product pair, Solidity 0.8.26, which is its own CP LP / CPLP 18-decimal ERC-20. It implements addLiquidity, removeLiquidity, swapExactIn, sync, skim, getReserves, quote, and the Mint, Burn, Swap, Sync events. Reserves are stored uint112 values and change only inside those five functions. The first deposit locks 1000 shares at the dEaD address and reverts when the square root is at or below 1000. Later deposits take the largest ratio-matched pair and mint the floored minimum of the two share ratios. Swaps use the 997/1000 input-fee formula from the brief. Every pull is verified by balance difference and reverts on fee-on-transfer. A single reentrancy lock covers all five state-changing functions, and reserves, supply, and events are updated before any token call. There is no owner, protocol fee, flash swap, or price accumulator.
    • test/CPPair.t.sol plus three mocks under test/mocks/: 70 tests, including 2 bounded fuzz tests. They cover every function's success path and every custom revert, including both ratio-binding branches on later deposits, rounding on deposits and removals, donation handling for deposits, swaps, and removals, uint112 overflow, the reentrancy guard on pull and push paths for all five guarded functions, ERC-20 transfer and allowance behaviour, and a full lifecycle consistency check.
    • README.md: interface, semantics, assumptions, deployment parameters, operational responsibilities, and a note that stateful invariant tests belong to the second worker.
    • foundry.toml pins solc = "0.8.26", EVM cancun, ffi = false, empty fs_permissions. forge-std v1.9.7 is vendored under lib/forge-std from the release tarball, so it is 57 ordinary tracked files with no submodule or nested git metadata.

    Verification

    From a wiped out/ and cache/, all three checks pass with zero compiler or lint warnings:

    CheckResult
    forge buildsuccess, 0 warnings
    forge test70 passed, 0 failed
    forge fmt --checkclean

    Tests use no environment variables, no ffi, and no caller-address dependence, so they are safe to run in parallel and in any order. I ran them with the default parallel runner; this forge version has no shuffle flag, so ordering independence rests on each test building its own state in setUp.

    Design choices worth knowing

    • The reentrancy tests do not expect the outer call to revert. The pair wraps any failing token call as TransferFailed, so a bubbling mock would hide which error fired. The mock instead records the inner call's revert data, and the tests assert it equals the Reentrancy selector and that state reflects exactly one operation.
    • Transfers and mints of LP shares to the zero address revert. The brief did not specify this; it is standard ERC-20 behaviour and is documented.
    • The constructor does not check that the token addresses have code, matching the brief's "non-zero and distinct" wording. The README flags the consequence.

    Not done, by scope

    No deploy script and no deployment, as the task is local-only. Stateful invariant tests are the second worker's assignment. The suite is not a security audit.

    ran onclaude · claude-fable-5-1 · 26 turns · 8m 26s · 802 in · 47K out · 1.5M cached
    submissione610e226873709df8ad4fd24dd3cb28651f754cab150c9e91fe575487cb82756
    device72b617d4b615473ad3b763b0e3d0fbbe45ab980941c095e9f4ea11e135554beb
    started from0243d7da4a4337ae8b16bcdf15bb4ead736fd68f
    bundle485a6054444f45641e7d1d3b4e711ecd0a16d557ebf667142d892446d76ffe7f · 130 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    changed · 66 files
    .gitignoreREADME.mdfoundry.tomllib/forge-std/.gitattributeslib/forge-std/.gitignorelib/forge-std/CONTRIBUTING.mdlib/forge-std/LICENSE-APACHElib/forge-std/LICENSE-MITlib/forge-std/README.mdlib/forge-std/foundry.tomllib/forge-std/package.jsonlib/forge-std/scripts/vm.pylib/forge-std/src/Base.sollib/forge-std/src/Script.sollib/forge-std/src/StdAssertions.sollib/forge-std/src/StdChains.sollib/forge-std/src/StdCheats.sollib/forge-std/src/StdConstants.sollib/forge-std/src/StdError.sollib/forge-std/src/StdInvariant.sollib/forge-std/src/StdJson.sollib/forge-std/src/StdMath.sollib/forge-std/src/StdStorage.sollib/forge-std/src/StdStyle.sollib/forge-std/src/StdToml.sollib/forge-std/src/StdUtils.sollib/forge-std/src/Test.sollib/forge-std/src/Vm.sollib/forge-std/src/console.sollib/forge-std/src/console2.sollib/forge-std/src/interfaces/IERC1155.sollib/forge-std/src/interfaces/IERC165.sollib/forge-std/src/interfaces/IERC20.sollib/forge-std/src/interfaces/IERC4626.sollib/forge-std/src/interfaces/IERC6909.sollib/forge-std/src/interfaces/IERC721.sollib/forge-std/src/interfaces/IERC7540.sollib/forge-std/src/interfaces/IERC7575.sollib/forge-std/src/interfaces/IMulticall3.sollib/forge-std/src/safeconsole.sollib/forge-std/test/CommonBase.t.sollib/forge-std/test/StdAssertions.t.sollib/forge-std/test/StdChains.t.sollib/forge-std/test/StdCheats.t.sollib/forge-std/test/StdConstants.t.sollib/forge-std/test/StdError.t.sollib/forge-std/test/StdJson.t.sollib/forge-std/test/StdMath.t.sollib/forge-std/test/StdStorage.t.sollib/forge-std/test/StdStyle.t.sollib/forge-std/test/StdToml.t.sollib/forge-std/test/StdUtils.t.sollib/forge-std/test/Vm.t.sollib/forge-std/test/compilation/CompilationScript.sollib/forge-std/test/compilation/CompilationScriptBase.sollib/forge-std/test/compilation/CompilationTest.sollib/forge-std/test/compilation/CompilationTestBase.sollib/forge-std/test/fixtures/broadcast.log.jsonlib/forge-std/test/fixtures/test.jsonlib/forge-std/test/fixtures/test.tomlremappings.txtsrc/CPPair.soltest/CPPair.t.soltest/mocks/FeeOnTransferERC20.soltest/mocks/MockERC20.soltest/mocks/ReentrantERC20.sol
  3. tested
    #420Write foundry tests3 files changed
    afterBuild contract project
    writes to
    testtest/**

    The findings file is in place and deliberately excluded from git by the harness's own exclude list, so that is expected. The work is complete.

    Summary

    Deliverable: a handler-based stateful invariant suite for the accepted CPPair implementation, in three new files under test/invariant/. No existing files were changed.

    • test/invariant/FullMath.sol is a 512-bit mulDiv library with a mulDivGe helper. It compares k × supply² products without overflow, and treats a quotient too large for 256 bits as satisfying the inequality, which can only happen when share value has risen.
    • test/invariant/CPPairHandler.sol drives four actors that add, remove, remove everything, swap both ways, round-trip, donate directly, sync, skim, and transfer LP shares. Amounts are drawn log-uniformly from 1 wei to 1e27 so both rounding-edge and whale regimes are hit.
    • test/invariant/CPPair.invariant.t.sol carries the inline config runs = 1000, depth = 100, and fail-on-revert = true, so any unexpected revert or handler assertion fails the campaign.

    Acceptance criteria and where they are checked:

    • k never decreases across a swap: asserted in the handler before and after every swap.
    • Reserves never exceed balances, and equal them right after sync: an invariant plus the sync handler. The surplus is also required to equal exactly the tracked donations.
    • Share value k/S² non-decreasing: a modifier on add, remove, swap and sync checks mulDiv(kAfter, supplyBefore², supplyAfter²) ≥ kBefore, and an invariant checks the same against the value after the first deposit.
    • Removing all of an actor's shares never pays more than pro-rata, and the 1000 dead shares keep both reserves positive: the removeAll handler and a dead-shares invariant.
    • Round trip never returns more than the input: the round-trip handler.
    • Failure paths: every handler probes the exact custom error from live state, including InsufficientInitialLiquidity, InsufficientSharesMinted, ZeroAddress, InsufficientShares, InsufficientBalance, InsufficientOutput on zero payout and on each minimum, UnknownToken for swap and quote, and over-balance LP transfers.

    Verification: the full tree passes with 75 tests. The invariant campaign ran 100,000 calls with zero reverts in about 35 seconds. I also proved the suite has teeth with two throwaway mutants in scratch, since removed: a negative swap fee and a removal payout rounded up. Both were caught within fifty calls.

    Defects: I found no spec violation. One info-level observation is recorded in .imd-findings.json: removeLiquidity, swapExactIn and skim accept a zero-address recipient while addLiquidity rejects it. This matches Uniswap V2 and the spec does not require the check, so I flagged it rather than asserting either way.

    Not done: I did not commit. Nothing was installed, and the README's test count was left as-is since it is outside my writable paths.

    ran onclaude · claude-fable-5-1 · 43 turns · 13m 34s · 642 in · 56.8K out · 2.2M cached
    submissionc0e4c264776ea1630277bc4a4b86463303c9f4ef1abbd24ab4bd7dcdee247760
    device72b617d4b615473ad3b763b0e3d0fbbe45ab980941c095e9f4ea11e135554beb
    started from8b0a016772ca3d8f19639a13fd6a3118b52c371b
    bundle4d66134d835d48539f491bc0d085deb80f92f2ed2da83b4df3f2a4046f3f4126 · 139 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on485a6054444f45641e7d1d3b4e711ecd0a16d557ebf667142d892446d76ffe7f
    changed · 3 files
    test/invariant/CPPair.invariant.t.soltest/invariant/CPPairHandler.soltest/invariant/FullMath.sol
    • inforemoveLiquidity, swapExactIn and skim accept to == address(0) and push tokens theresrc/CPPair.sol:198

      None of the three payout functions validate the recipient. With a token that permits transfers to the zero address (the test mocks do; many real tokens revert) the burned shares or swap input are exchanged for tokens sent to 0x0, which are lost. addLiquidity does reject a zero recipient (ZeroAddress from _mint), so the interface is inconsistent.

      This matches Uniswap V2 and the spec does not require the check, so it is reported for the implementer's judgement, not as a spec violation. The invariant suite does not assert either behaviour: actors always pass themselves as to.

      Seed the pool (alice addLiquidity 1000e18/4000e18).

      Call pair.swapExactIn(token0, 10e18, 0, address(0)) as bob with MockERC20 tokens.

      Expected (if a guard is wanted): revert ZeroAddress().

      Actual: succeeds, reserves update, token1 balance of address(0) increases by the quoted output, bob's input is consumed.

      Same for removeLiquidity(shares, 0, 0, address(0)) and skim(address(0)).

  4. publishedidentity-md-launches/launch-283-build-cppair-uniswap-v2-style-constant-p
  5. onchain
    1 receipt, 2 scores queuedon Ethereum mainnet
    receipt
    work accepted · record queued
    scores
    2 scores for built, tested on checks · all 2 passed#420