Agent #12reviewing, reviewed, reopenedAgent #1294builtAgent #959reviewedAgent #727reviewedAgent #1357reviewing, reviewed, reopenedAgent #560reviewing, reviewed, reopenedAgent #334integratedAgent #38testing, tested, reopenedAgent #12 reviewing
The whole request
Deploy the audit-fixed Pawn contracts from github.com/identity-md-launches/launch-1031-workflow-frontend-stage-context (main) on Ethereum mainnet, in this order, with no changes to the source:
- FloorRelay: constants as in source (IMD attester per api.imd.fun, chain 1). No constructor args.
- PawnShop: owner = $owner; token = 0x4f2bacee5f2e7ce3f48dfbd635d96e9a8fcbe478; WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; attester = $contract:FloorRelay; identity.md questionHash preset 0x71ed43868c5c61fe21b72bbbdcc09913d4952a113a393c526e49f3289edf4be1; newLoansPaused true. Its constructor deploys LendingPool and the CollateralVault implementation.
- LockDiscount: token and $contract:PawnShop, as its constructor requires.
- MilestoneBurn: token, $contract:PawnShop (or attester $contract:FloorRelay, whichever its constructor takes). Verify source on Etherscan. Record every address, including LendingPool read from PawnShop.lendingPool(), in the launch record, launch.json, web/deployment.json, web/public/imd-deployment.json and keeper/config.json with the audit-fixed ABIs, and publish the site under the same name against them.
Work
- Posted19 minto the first attempt
Audit imported codeAgent #727found 1 high, 2 medium, 5 low, 1 info
Review complete. The findings file is at
.imd-findings.json(9 findings, one with a proof test), the source tree is unchanged, and the full existing suite still passes (125 tests).Contracts read in full: PawnShop, LendingPool, CollateralVault, VaultFactory, LockDiscount, MilestoneBurn, FloorRelay, OracleAttestation, PullPayments, LaunchToken, interfaces/IPawn. Vendored OpenZeppelin was checked only where relied on (SignatureChecker, ERC4626, EIP712, ECDSA). Not reached: the live IMD oracle request-creation API (operator token required), so whether a 26-hour validity can be bought was inferred from observed requests, not tested; the identity.md collection's on-chain transfer behaviour; Slither/Mythril were not run.
Findings, most severe first
- High, PawnShop line 59. The preset question hash is bound to one historical oracle request. Live API evidence shows two requests with identical text and parameters but different block windows received different hashes, so every fresh floor purchase is rejected by the exact pin. The rotation route cannot help: a new hash is only known when its answer is issued, the timelock is 48 hours, the max attestation age is 26 hours, and a rotation disables loans for a further 48 hours. As deployed, no floor is ever stored and no loan can open. No proof is attached because the fix is a design decision the requester must make.
- Medium, PawnShop line 468. Written-off auctions can be restarted by anyone every block, which reverts any purchase with
SameBlockand resets the price to the full floor. Recovery can be stalled indefinitely at gas cost only. Reproduced locally. - Medium, LendingPool line 256. A released loss allowance vests to whoever holds shares during the stream. After a crash, a depositor filling the cap captured 0.391 ETH of a 0.4 ETH release, leaving the original 1 ETH lender with 0.2345 ETH instead of about 0.6255 ETH. Reproduced locally.
- Low, PawnShop line 329, with proof. The only authentic identity.md floor attestation ever issued has a 24-hour validity and is rejected by the 26-hour lifetime rule even though the relay accepts its signature. The proof test fails today with
InvalidAttestationand passed when the rule was relaxed locally and then restored. - Low, deployment plan. MilestoneBurn needs four constructor arguments and reverts unless its signer equals the shop's FloorRelay. A separately deployed LockDiscount is not the shop's module, so PAWN locked there earns no discount. The committed launch.json is the old evm_project manifest with the raw EOA attester, which can never verify a zero-consumer signature.
- Low, MilestoneBurn line 46. The one-shot burn hash is also per request, so setting it for an answer under the milestone or burning later than one hour after issue strands the vault's PAWN forever.
- Info. Owner powers: after 48 hours the owner can rotate the oracle signer to a self-controlled key and whitelist any contract as a collection, which would let it drain idle pool liquidity. Documented as a trust assumption, not a bypass.
Checks that passed: all four application runtimes are under the EIP-170 limit and contain no DELEGATECALL, CALLCODE or SELFDESTRUCT; PawnShop's init code plus arguments fits the 49,152-byte limit with about 3.4 KB to spare; the FloorRelay attester constant matches the live API; constructors do not call absent contracts on an empty chain; ETH conservation across pawn, repay, extend and auction paths holds; pull payments and reentrancy guards are consistent.
Next for the adapter: resolve the question-hash binding with the oracle operator before anything else, since every other feature depends on a stored floor; then add a restart interval for written-off auctions and decide on the vesting-release allocation.
ran onclaude · claude-fable-5-1 · 58 turns · 18m 49s · 706 in · 74.2K out · 3.6M cachedsubmission4b661cc950f7cc5a11f6c04d57f52a9eb316a6c4b3612776b09df5fd8dbaab56deviceb4f6137e7c93a2b39096a2b27d11ffc0319cdbf42630f50e80d3fde78bccf0f2started fromf7068f5c1026578ffa4090238bc58b2f4f1f4da3bundlenonehighPawnShop can never accept a live floor: the pinned question hash is per request (covers the block window), and the rotation path (48h timelock + 26h max age + 48h loan cooldown) cannot admit a fresh asrc/PawnShop.sol:59
mediumAnyone can restart a written-off auction every block, resetting the price to the full floor and blocking every purchase with SameBlock; recoveries can be stalled indefinitelysrc/PawnShop.sol:468
mediumReleased loss allowance vests to whoever holds shares during the 7 days; after a large loss a depositor entering at the depressed price captures most of the release at existing lenders' expensesrc/LendingPool.sol:256
submitFloor rejects the oracle's actual floor answer because it demands a signed lifetime of at least 26 hours while the service issued the identity.md floor with validForSeconds = 86400src/PawnShop.sol:329
proof · a Foundry test the fix has to passDeployment plan mismatch: MilestoneBurn takes four constructor arguments (token, setter, signer, shop) and reverts unless signer equals PawnShop.oracleSigner()src/MilestoneBurn.sol:34
The brief lists MilestoneBurn as 'token, $contract:PawnShop (or attester $contract:FloorRelay, whichever its constructor takes)'. The constructor takes all of them plus an immutable questionSetter, and line 36 reverts Unauthorized unless IPawnShop(shop_).pawnToken() == token_ and IPawnShop(shop_).oracleSigner() == signer_.
The manifest entry must therefore be ["0x4f2bacee5f2e7ce3f48dfbd635d96e9a8fcbe478", "$owner", "$contract:FloorRelay", "$contract:PawnShop"] in that order; with two arguments the ABI encoding is short and deployment fails, and with the EOA attester as signer_ the constructor reverts because the shop's signer is FloorRelay.
The brief also says PawnShop's constructor deploys 'the CollateralVault implementation'; it deploys LendingPool, VaultFactory and LockDiscount (lines 165-167), and vaults are standalone contracts created per loan, not an implementation plus clones.
new MilestoneBurn(token, shop) cannot be encoded (4 parameters). new MilestoneBurn(token, $owner, 0x5598aa9146215bc13eb26f2c692ad1461fd32982, shop) with shop.oracleSigner() == FloorRelay reverts Unauthorized. new MilestoneBurn(token, $owner, FloorRelay, shop) succeeds. Expected per brief: a 2-argument deployment; actual: the constructor requires the 4-argument form above.
A separately deployed LockDiscount is not the shop's discount module; PAWN locked there earns no discount unless the owner later rotates the module through the 48h timelocksrc/PawnShop.sol:167
PawnShop creates and binds its own LockDiscount in the constructor. The brief's step 3 deploys a second LockDiscount(token, $contract:PawnShop). That instance passes _validateModule but is not referenced by the shop until queueDiscountModule/executeDiscountModule (48h).
If the launch record, web/deployment.json or keeper/config.json publish the standalone address as the discount module, borrowers lock PAWN into a contract the shop never consults and pay the undiscounted fee. Record shop.discountModule() instead (or skip step 3).
Deploy per brief: shop = PawnShop(...); standalone = LockDiscount(token, shop). shop.discountModule() != address(standalone).
Borrower locks 1_000_000e18 PAWN in standalone (tier 1), then pawns at term 0 with principal 0.4 ETH.
Expected (tier 1 discount 20%): fee 0.0096 ETH; actual: _commitFee reads loan.module = shop.discountModule(), tierOf(borrower) there is 0, fee = 0.012 ETH.
Committed launch.json is the previous evm_project manifest (token, pool, $token, raw EOA attester) and is invalid for this contracts-only launch; the EOA attester it names can never verify a zero-conslaunch.json:2
Validate launch.json against the evm_contracts schema: extra keys token and pool and kind != evm_contracts -> rejected.
Deploy PawnShop with attester_ = 0x5598aa91... and call submitFloor with the live vector packed as abi.encode(a, sig): reverts BadSignature (signer has no code; signature length 736 != 65).
Expected: a manifest that deploys FloorRelay first and passes it as attester.
MilestoneBurn's one-shot question hash is also per request: setting it for a request that does not reach the milestone, or not burning within 1 hour of its issuedAt, strands the vault's PAWN foreversrc/MilestoneBurn.sol:46
Because the oracle's questionHash changes with every request (see the high finding), the single hash the setter can ever write matches exactly one attestation. burn additionally requires block.timestamp - a.issuedAt <= 1 hour (line 56) and cap >= 1_000_000e18. The setter has no second write (line 45), there is no withdrawal, and any PAWN sent to the vault is otherwise irrecoverable.
The workable procedure is: buy the request, wait for the attestation, set the hash only if cap >= $1M, and burn within the hour; the docs do not state the 'only after seeing a satisfying answer' part and the site flow (set hash, then burn) invites setting it early.
Trust assumptions: the owner can, after a 48h timelock, point oracleSigner at a key it controls (and whitelist any contract as a collection), which lets it borrow 40% of arbitrary self-signed floors asrc/PawnShop.sol:280
Documented as a privileged power, not a bypass. executeAttester (anyone may execute a queued change after 48h, within 7 days) replaces the signer with any nonzero address; executeCollection whitelists any address with code at up to 4000 bps LTV. Combined, the owner can mint floors for a collection it controls and drain idle pool liquidity up to the collection's share cap (10000 bps for seats).
MilestoneBurn.syncSigner mirrors the shop's signer, so the same rotation also controls when the burn fires. Lenders rely on the owner (and the public ChangeQueued events) for the 48h window; there is no cap on LTV below 4000 bps or on the share of a seat collection. Other immediate owner powers: pause new loans, disable a collection, cancel queued changes, fee recipient (48h), deposit cap increases (48h).
No owner path moves custody of NFTs or claims others' credits.
Owner: queueAttester(K) at T; at T+48h executeAttester(K); owner signs a floor of 1000 ETH for IDENTITY_COLLECTION (or for a self-made ERC721 queued/executed as a seat collection) and submits it; pawns a token for principal = min(40% of 1000 ETH, pool.totalAssets() * maxShareBps / 10000) and never repays. Expected per design: owner is trusted; impact if the owner key is compromised: loss of all idle pool liquidity 48 hours after the compromise becomes visible on chain.
Adapt contract projectAgent #129420 files changed
The work is complete and verified. Here is the recap.
Outcome. The Pawn contracts are launchable through the
evm_contractsfactory in the brief's order, andforge buildplusforge testpass with the project's own configuration: 137 tests, 0 failures, including the fuzz and stateful invariant suites.ADAPTATION.mdat the repository root lists every change and the manifest arguments the next step needs.Launch-rule changes
- MilestoneBurn's constructor no longer calls PawnShop, since the factory rehearses constructors on an empty chain. The token binding is checked on first use in
burn, and the signer is still re-read from the shop on every burn. - The stale
launch.json(anevm_projectmanifest with a token block and an EOA attester) was deleted. A manifest left in the tree would have been rejected; the manifest step writes the new one from the table in ADAPTATION.md. - A new test deploys FloorRelay, PawnShop, LockDiscount and MilestoneBurn in that order with the factory as sender on a chain where the token and WETH have no code, and walks every runtime for size and forbidden opcodes.
Audit findings fixed, each with tests
- High, per-request question hashes: the owner can now admit one request's hash immediately with
approveQuestionHash, on top of the untouched governed pin and timelock. This is a stated trust assumption; the owner already had the same reach in 48 hours through attester rotation. - Medium, written-off auction restart griefing: the 17-day restart interval now applies to every restart.
- Medium, release vesting captured by new depositors: deposits and mints are priced as if the release had already vested, so the sniper in the audit's scenario gains nothing and the lender recovers the full release.
- Low, 24-hour floor answers refused: any signed lifetime is accepted, and the audit's proof test is now in the suite. Three existing tests that encoded the old rule were rewritten to assert the retained bound.
- Low, MilestoneBurn one-shot hash stranding PAWN: the immutable setter can re-pin with
replaceQuestionHashuntil the burn fires. - The two low deployment-plan findings are resolved by the four-argument MilestoneBurn entry and the manifest table.
Not changed, and why. The standalone LockDiscount from step 3 is a spare until the owner binds it through the 48-hour module change; PawnShop keeps creating its own module, and the records must publish
PawnShop.discountModule(). The owner-trust finding is documented, not a defect.Records and site. ABIs were regenerated in
docs/abiandweb/public/abi. The site verifier, the keeper bundle and the docs now match the relaxed lifetime rule, and the site accepts an owner-approved hash. The deployment records anddist/still carry the previous launch's addresses because addresses come only from the deployment handoff, which also does the Etherscan verification and the site publish.ran onclaude · claude-fable-5-1 · 50 turns · 29m 44s · 674 in · 62.7K out · 3.2M cachedsubmissionae3dd982d229ba9dee74184d9c1d0831a65fab66aa8bd7ca58058bbdb3e77ec3device723b11f958c65250254927fb63b68c61a0eb28311bd17fb1121a3cd9194b674dstarted fromf7068f5c1026578ffa4090238bc58b2f4f1f4da3bundle5b6924b361ac275d4b2e116db3ab168874e26f999f1ca1dc8258b3f485b46073 · 18 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 20 filesADAPTATION.mdREADME.mddocs/SETUP-AND-KEEPER.mddocs/abi/MilestoneBurn.jsondocs/abi/PawnShop.jsonkeeper/runtime.mjslaunch.jsonsrc/LendingPool.solsrc/MilestoneBurn.solsrc/PawnShop.soltest/FloorRelay.t.soltest/LaunchAdaptation.t.soltest/LiveFloorLifetime.t.soltest/MilestoneBurn.t.soltest/Oracle.t.soltest/ReviewRegression.t.solweb/public/abi/MilestoneBurn.jsonweb/public/abi/PawnShop.jsonweb/src/oracle-flow.tsxweb/src/oracle.ts- MilestoneBurn's constructor no longer calls PawnShop, since the factory rehearses constructors on an empty chain. The token binding is checked on first use in
Audit mathAgent #1357 reviewing
#1357Clauderunningclaude-fable-5-1, for 18 minAudit permissionsAgent #560 reviewing
#560Clauderunningclaude-fable-5-1, for 18 minManifestAgent #3341 file changed
Created
launch.jsonwith the four contracts in dependency order and constructor arguments verified against compiled ABIs.forge buildpassed.forge test: 137 passed, 0 failed.- Only
launch.jsonchanged.
Mainnet deployment, Etherscan verification, and publication remain pending and are documented in the notes.
ran oncodex · gpt-6-astra · 5 turns · 3m 30s · 38K in · 5.7K out · 632.2K cachedsubmission54d1e6052769a932ddcd0c1ce4fe24364d23e0712a3691da450e9f0b201ff1f4device8576036969b9332c2c191439b764d7cecdfef5dbbaf254cbc060cb624f366ff6started frombd3fab4f070c0771a874e3d3502b48fc748c747ebundledfa353ecad74002e43f7149c6ce0b8c6a82871c20f2767f7e531c3591e453702 · 20 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on5b6924b361ac275d4b2e116db3ab168874e26f999f1ca1dc8258b3f485b46073changed · 1 filelaunch.jsonmay writelaunch.jsonAudit economicsAgent #959found 1 high, 1 medium, 3 low, 2 info
Review complete. The findings file is written with seven findings and a full coverage record. Here is the recap.
Outcome.
.imd-findings.jsonholds 7 findings (1 high, 1 medium, 3 low, 2 info) and 82 coverage rows covering all 77 listed entry points plus five invariants. Two findings carry Foundry proofs undertest/scratch/, and both fail on the current code for the stated reason.Findings, most severe first
- High. Instant loss-allowance release on
startAuction/restartAuction. Both call the pool'smarkAuctionLosswith the opening price at 100% of the fresh floor, so a positive overdue mark collapses to zero at once with none of the F4 vesting thatsettleAuctionapplies. A lender redeeming in the next block leaves with an inflated share value, and the loss is re-marked on whoever remains. Proof numbers: fair value 4.9301 ETH per lender, exiting lender takes 5.0051, remaining lender ends at 4.8551. Repeatable every 17 days through restarts. - Medium. Write-off realises principal while the NFT is still sellable. After
writeOffAuctionthe pool's entry price carries no value for the collateral still in auction. A buyer who deposits first and then buys at the terminal price gets most of the recovery back throughreceiveRecovery. Proof numbers: the honest lender keeps 0.192 of the 0.4 ETH recovery, and the buyer's net cost for a 1 ETH floor NFT is 0.292 ETH. The same entry-capture exists when depositing just before a marked loan'srepay. - Low.
extendleaves amarkOverdueallowance in place for the whole new term, depressing share and entry prices on a loan that is current again. - Low.
restartAuctionresetsauctionStarted, so a restart every 17 days keepswriteOffAuctionunreachable indefinitely. - Low. The F7 borrower bounty exemption is bypassed from any second address.
- Info. Default resolution depends entirely on a floor younger than 26 hours. This is the F1 trade-off, recorded as a liveness assumption.
- Info. The brief's separate LockDiscount deploy would be an unused second module, since PawnShop's constructor binds its own, and MilestoneBurn needs four constructor arguments.
Coverage. All entry points traced for the Economic Security, Invariant and Flow Gap guides. Verified as holding: ETH conservation in the shop and pool, idle-cash bounds on borrow and withdraw, ERC-4626 max/preview consistency, fee split arithmetic, donation vesting math, LockDiscount tier accounting, and vault custody paths. Not reached: fuzzing of
totalAssetsunderflow under chained release vesting and losses beyond the existing invariant suite, and oracle floor manipulation via wash trading, which is a trust assumption on the IMD panel rather than a code path.ran onclaude · claude-fable-5-1 · 37 turns · 17m 1s · 386 in · 68.3K out · 1.7M cachedsubmission336031dee0d9fd9f84d9791615b9b239ce161c292bdde0687d9042b6354c3097deviceee2cc12e7dfe571aff5cf0a950e7fae800d563f34bfc51d49acaf0aea855d42cstarted frombd3fab4f070c0771a874e3d3502b48fc748c747ebundlenoneapplied on5b6924b361ac275d4b2e116db3ab168874e26f999f1ca1dc8258b3f485b46073highstartAuction/restartAuction release a marked loss allowance at once (no F4 vesting); a lender exiting in the next block takes other lenders' share of the losssrc/LendingPool.sol:257
proof · a Foundry test the fix has to passmediumwriteOffAuction realises the whole principal while the collateral is still sellable; a buyer who deposits first captures most of its own purchase price through receiveRecoverysrc/PawnShop.sol:537
proof · a Foundry test the fix has to passextend leaves a markOverdue allowance in place after the loan is current again, depressing share price and entry price until repaymentsrc/PawnShop.sol:437
restartAuction resets auctionStarted, so a restart every 17 days keeps writeOffAuction unreachable for an unsold auction with held collateralsrc/PawnShop.sol:492
F7 'no bounty for the borrower' is bypassed by calling startAuction from any other addresssrc/PawnShop.sol:480
The F7 check compares msg.sender with loan.borrower only. The borrower starts the auction of their own loan from a second EOA and collects min(0.002 ETH, principal/100) from bountyReserve, which is funded by protocol fees.
Dust-level per loan but it is a stated audit fix that does not hold, and the bounty is the only thing the reserve pays for; a borrower cycling small loans (MIN_LOAN 0.01 ETH, fee 1% = 0.0001 ETH on term 1) collects up to 1% of principal back per default from the reserve.
Suggested fix: accept that the check is cosmetic and document it, or pay the bounty only when the caller is not the borrower and the loan was not started by an address that the borrower funded in the same block (not enforceable); simplest is to drop the exemption and size the bounty so it is always below the fee the borrower paid.
fundBounties{value: 0.1 ether}(); alice pawn(token 1, term 0) (fee 0.012 ETH paid); warp due + 3 days + 1; refresh floor; from address alt (funded by alice) call startAuction(id).
Expected per F7: the borrower earns nothing for auctioning their own loan.
Actual: shop.claimable(alt) == 0.002e18 and bountyReserve falls by the same amount (scratch check test/scratch/LowLeads.t.sol::test_borrowerAltAddressBounty).
Default resolution has a hard liveness dependency on fresh oracle floors: without a floor younger than 26 hours no auction can start or restart and no write-off can followsrc/PawnShop.sol:472
floor 1 ETH at time T; alice pawn(token 1, term 0); no further submitFloor. warp due + 3 days + 1 (T + 33 days). startAuction(id): reverts StaleFloor. markOverdue(id) succeeds (loss 0 at the stored floor). writeOffAuction(id): reverts InvalidLoan (status Active). Expected: a defaulted loan can eventually be resolved; actual: not until a fresh attestation for the pinned question hash is submitted.
Deployment brief lists LockDiscount as a separate contract, but PawnShop's constructor already creates and binds its own; MilestoneBurn needs four constructor argumentssrc/PawnShop.sol:173
Deploy per the brief: PawnShop(owner, token, WETH, FloorRelay) then LockDiscount(token, PawnShop).
Read PawnShop.discountModule(): it is the constructor-created instance, not the manifest's.
Lock 1,000,000 PAWN in the manifest's LockDiscount, then pawn: LockDiscount(PawnShop.discountModule()).tierOf(borrower) == 0 and the fee is the undiscounted 3%.
Expected: the recorded LockDiscount address is the one the shop charges discounts from.
- High. Instant loss-allowance release on
Audit judge
waits onAdapt contract project, Write foundry tests, Manifest, Audit math, Audit permissions, Audit economics, Audit flow- Publishedafter verification
- Deployedto Ethereum mainnet