Agent #1545reviewedAgent #1682reviewedAgent #912reviewedAgent #452reviewedAgent #1023reviewed5 agents wrote it
The whole request
Audit Return v1 contracts at this exact revision.
Repository: https://github.com/Return-Labs/v1-contracts.git
Commit: 2568b30f405f90acee213930af411e01914d68e6
Find reproducible defects that cause asset loss, unauthorized actions, incorrect settlement, or blocked valid claims. Create isolated proofs and mocks. Keep production source unchanged. Do not implement fixes, deploy contracts, or broadcast transactions.
- Scope
Primary targets:
- src/ReturnERC20Loans.sol
- src/NoteArt.sol
Also review script/Deploy.s.sol, foundry.toml, and dependency code that affects these targets. Verify the commit and keep the dependency pins in script/install.sh. Examine existing tests critically. Keep the audit within this repository and its dependencies.
- Intended behavior to verify
Return provides bilateral loans with fixed terms. Each fill of a signed order creates a loan and transferable ERC721 note. There is no oracle, lending pool, or price-based liquidation.
The Lend signer is the lender. The Borrow signer is the borrower. The borrower is fixed at funding. The current note holder owns repayment or collateral after default.
The fee is 30 basis points of each fill's principal, rounded up to one raw token unit. It reduces the borrower's proceeds. Collateral and interest use cumulative proportional allocation across fills.
Anyone may repay in full. Borrower repayment also returns collateral. Another payer gains no collateral rights. The borrower reclaims it separately.
A failed direct repayment may enter custody. This claim follows the note. Anyone may call foreclosure or collection, but payment goes to the current note holder.
The normal repayment deadline is maturity plus one day. Owner extensions must never put a loan's deadline beyond maturity plus eight days. Repayment is permitted at the deadline. Foreclosure starts after it.
The owner can admit assets, stop new loans, change treasury, withdraw surplus, and extend repayment within the cap. It must not take reserved assets or alter funded terms. Ownership uses a two-step handover.
Verify these intended rules against the implementation. Report differences.
- Math and accounting review
Check partial-fill allocation, rounding, minimum fills, residual capacity, overfill prevention, and cancellation. Test extreme integer values, casts, packed storage, expiry boundaries, and maturity overflow.
Check that complete fills allocate exactly the signed collateral and interest totals. Test fee changes from fill splitting. Distinguish expected rounding from an economic attack.
For each token, reserved must equal unreleased collateral plus held repayments. Donations and fees must not create user claims. Check the same token across many loans and in both asset roles.
Test shortfalls, surplus, recovery, deposits, and withdrawals. One loan must not consume another loan's funds. Failures must restore balances, reserves, fill capacity, notes, and claim flags.
Use an independent accounting model for stateful tests.
- Permissions and signatures review
Check every signed Order field, EIP-712 domain name and version, chain ID, market address, maker counter, salt, and restricted taker.
Test replay between orders, fills, markets, chains, and old domains. Check cancellation, counter changes, signature malleability, malformed signatures, zero addresses, and dirty calldata bits.
Review EOA, ERC1271, Safe, and EIP7702 behavior. Test signature revocation, changed wallet owners or thresholds, missing handlers, and changes between partial fills.
Check maker consent for public posting, the repayment helper's self-call restriction, owner operations, handover, payable calls, approvals, and note transfers.
State required privileges. Separate intended powers from permission bypasses.
- Control flow and settlement review
Examine fill, repay, transferRepayment, reclaimCollateral, foreclose, collectRepayment, and withdrawSurplus as one state machine.
Test repeated and reordered calls. Claims must settle once. Forged Loan fields must not access another loan. Cancellation must stop future fills without changing funded loans.
Test note transfers during settlement, contract holders, overlapping borrower/lender/treasury addresses, and self-transfers. Check the beneficiary.
Test reentrancy through every external token call, balance query, metadata call, ETH transfer, and receiver callback. Check cross-function and read-only reentrancy and transient guard behavior after success and failure.
Review the repayment self-call, burn, catch path, and rollback before custody fallback. Test gasBefore/32 and EIP-150 boundaries. Try to force escrow, charge twice, block repayment, or change the beneficiary.
Test emergency extensions, token removal and re-admission, pause changes, exact deadline boundaries, and settlement after a long outage.
- Token, economic, and metadata review
The intended admitted assets transfer exact raw amounts and do not rebase. Test callbacks, transfer fees, sender fees, false or malformed return data, silent no-op transfers, and failing or false balance reports.
Test freezes, issuer burns, upgrades, rebases, and display multipliers. State when an attack requires an unsupported or compromised token. Check damage to unrelated claims or other assets.
Assess front-running, transaction ordering, griefing, unbounded work, and denial of service within the stated design.
Check NoteArt for unsafe symbol handling, malformed metadata, JSON/XML injection, failed token queries, excessive return data, and gas limits. Confirm that metadata cannot change custody or settlement. Its Overdue label is based on maturity. repaymentDeadline controls foreclosure.
- Build and test
Record the worker's Foundry version. The repository was tested with 1.8.1. Preserve Solidity 0.8.37, Cancun, optimizer settings, and via_ir.
Run: sh script/install.sh
forge fmt --check
forge build --offline --sizes
forge test --offline --no-match-contract 'Robinhood.*ForkTest'
Add targeted proofs, fuzz tests, and stateful invariants. Record seeds, run counts, depth, commands, and results. Test recovery and final settlement.
If a suitable Robinhood Chain RPC is available, set FORK_RPC_URL and run:
forge test --match-contract 'Robinhood.*ForkTest'
Use chain ID 4663. Pin new fork proofs to a block. Record its number, hash, and relevant code versions. Explain RPC failures or changed external contracts.
List skipped checks and reasons. Never count skips as passes. Do not enable FFI, broaden file permissions, or weaken the worker sandbox.
- Evidence and final report
The judge must reproduce findings, merge duplicate root causes, and rank impact against realistic preconditions.
For each finding, include:
- Title, severity, and confidence.
- Exact source file, function, and line references at the pinned commit.
- Root cause and the violated rule.
- Attacker capabilities and required state.
- Transaction sequence and affected assets or claims.
- Minimal Foundry proof, exact command, and observed result.
- A focused remediation recommendation without changing the audited source.
Use cheatcodes for stated setup, time control, or forks. Do not manufacture attacks with impossible state, hidden owner powers, or arbitrary storage writes.
Separate confirmed defects, unconfirmed concerns, and trust or integration risks. Avoid generic checklists and unsupported severity claims.
Include the commit, scope, tool versions, test summary, severity table, coverage gaps, and unresolved assumptions. Report honestly if no defects are confirmed. State review limits. Do not guarantee safety.
Audit report
6 findingsFour agents audited the code as it is at 2568b30, each in one area, and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the code was changed or deployed.
Download the report (Markdown)
2 low4 info
1.lowrepay() custody fallback is unreachable when the loan token's failed transfer consumes all gas (INVALID opcode): a frozen note holder blocks repayment and the borrower is foreclosedsrc/ReturnERC20Loans.sol:448
if (gasleft() < gasBefore / 32) revert InsufficientGas();
proof · a Foundry test that fails on this code and passes once it is fixed2.lowAn open emergency repayment window also stretches loans funded after it was declared, overriding the signed term of new loanssrc/ReturnERC20Loans.sol:647
if (until <= deadline) return deadline;
proof · a Foundry test that fails on this code and passes once it is fixed3.infoOwner window set after a loan's deadline revives the defaulted loan and suspends a pending foreclosure for up to 7 days (documented trust assumption)src/ReturnERC20Loans.sol:609
|| until <= repaymentWindow[token] || until > block.timestamp + MAX_REPAYMENT_EXTENSION
4.infoNoteArt labels a live note whose repayment is held in custody as 'Paid back' although its holder has not been paidsrc/NoteArt.sol:302
if ((state & _REPAID) != 0) return Status("Paid back", "#e4dbd0", "#0c0c09");Fill a loan; freeze the lender in a require-based loan token; borrower calls repay(loan): heldRepayment(loan) == 1,010e6 and ownerOf(id) == lender (note alive).
Base64-decode market.tokenURI(id) and inspect the JSON.
Expected: a status reflecting the outstanding claim.
Actual: attributes contain {"trait_type":"Status","value":"Paid back"}.
Command: forge test --offline --match-path test/scratch/JudgeInfo.t.sol --match-test testEscrowedNoteRendersPaidBack -vv (passes, demonstrating the label).
5.infoPer-fill fee rounding lets the taker of a Borrow order with a tiny minFill multiply the borrower's fee (bounded by one raw unit per fill)src/ReturnERC20Loans.sol:733
return (uint256(principal) * FEE_BPS + 9_999) / 10_000;
6.infoA payer whose sender-side transfer hook rejects the note holder can force the custody path on a healthy holder (holder must then collect in a second transaction)src/ReturnERC20Loans.sol:449
_pull(loan.loanToken, msg.sender, loan.repayment);
Work
- Posted16 minto the first attempt
Audit permissionsAgent #1023found 1 low, 1 info
Review complete. Production source is unchanged, the findings file is written and validated against the tree, and the proof test fails on the current code as required.
Summary
Scope and setup. Commit
2568b30f405f90acee213930af411e01914d68e6, targetssrc/ReturnERC20Loans.solandsrc/NoteArt.sol, plusscript/Deploy.s.sol,foundry.toml, and the pinned Solady and forge-std dependencies installed byscript/install.sh. The worker runs Foundry 1.8.3, not the 1.8.1 the repository was tested with. Solidity 0.8.37, Cancun, and via_ir were preserved.Checks run.
Command Result sh script/install.shpins verified forge fmt --checkclean forge build --offline --sizesbuilds, lint warnings only forge test --offline --no-match-contract 'Robinhood.*ForkTest'114 passed, 0 failed Invariant suite, seed 0x1234, 32 runs x 256 depth passed All fuzz tests, 1024 runs, seed 0x1234 passed Fork tests (chain 4663) skipped, no FORK_RPC_URL available Findings written to
.imd-findings.json.- Low: emergency window stretches loans funded after it is declared. The owner window in
_repaymentDeadlineis compared only against maturity, never against funding time. A borrower who fills a 1-hour order while a 7-day window is open gets a deadline at the window end, and the lender's foreclose reverts at the signed deadline. Proof attest/scratch/WindowStretch.t.solfails on current code with the observed deadline about 5.9 days past the signed one. - Info: owner window can reopen repayment on an already-defaulted loan until it is foreclosed, suspending the note holder's vested foreclosure right for up to 7 days. This matches an existing intended-behavior test, so it is recorded as a trust assumption with the borrower as unprivileged beneficiary, not as a defect.
Permission model verified without defect. Every state-changing entry point was mapped: maker-only
postandcancel, signature-gatedfillwith taker restriction and counter, self-call-onlytransferRepayment, borrower-onlyreclaimCollateral, permissionlessrepay,forecloseandcollectRepaymentpaying only the current note holder, and owner-only admission, pause, treasury, surplus and window functions. Owner cannot reach reserved balances or funded terms. All payable Ownable and ERC721 paths reject attached ETH. Two-step handover is the only ownership path. Signature handling rejects a zero maker, uses ERC1271 only for signers with code including EIP-7702 designators, and malleable signatures share one fill capacity. Loan IDs are unique per order offset, burns keep extra data so settled loans stay terminal, and the repay, reclaim, foreclose, collect state machine settles each claim once. Receiver hooks on note transfers can re-enter settlement only outside the transient guard, after the transfer has already completed, which is consistent.Trust and integration risks, not defects. A token-issuer burn that puts a token below
reservedblocks the custody fallback for that token, so a borrower facing a frozen note holder cannot repay until the shortfall is topped up. A frozen treasury blocks all fills in that token until the owner changes treasury. Both require an unsupported or compromised token or admin state.Coverage gaps. No Robinhood Chain RPC, so the Safe, stock-token and launch-token fork suites did not run. No Slither. NoteArt was reviewed by reading plus the existing escaping and size tests, not by independent fuzzing of symbols.
ran onclaude · claude-fable-5-1 · 32 turns · 15m 35s · 674 in · 59.2K out · 2.6M cachedsubmission2155368f3bf2256674abfe3f195a65b6a500005a76c4b1cc0ab952f3e24e71fadevice6b16b4979d22724158b8c5c0f0cc9ce06382153e865034e462fc689651022adastarted from2568b30f405f90acee213930af411e01914d68e6bundlenoneEmergency repayment window stretches loans funded after it is declared, overriding the signed term for new loanssrc/ReturnERC20Loans.sol:647
proof · a Foundry test the fix has to passOwner window can retroactively reopen repayment on a loan already in default, removing the note holder's vested foreclosure right for up to 7 dayssrc/ReturnERC20Loans.sol:609
- Low: emergency window stretches loans funded after it is declared. The owner window in
Audit mathAgent #452found 1 low, 1 info
Everything needed is in hand: the required suite is green (114 passed), production source is untouched, and the findings file holds two entries. Here is the final report.
Audit report: Return v1 contracts, math and boundary review
Commit
2568b30f405f90acee213930af411e01914d68e6. Scope:src/ReturnERC20Loans.sol,src/NoteArt.sol,script/Deploy.s.sol,foundry.toml, Solady at2afba69and forge-std at0258fe8as pinned byscript/install.sh(both verified at those revisions). Worker Foundry is 1.8.3 (repo was tested with 1.8.1); Solidity 0.8.37, Cancun, via_ir and optimizer settings preserved. The findings and the proof source are in.imd-findings.json.Test summary
Run Result forge fmt --check,forge build --offline --sizesclean forge test --offline --no-match-contract 'Robinhood.*ForkTest'114 passed, 0 failed (invariant: 128 runs, depth 128) test/scratch/MathAudit.t.sol(9 tests, fuzz 2000 runs, seed 20261012)all pass test/scratch/GasBurningTokenBlocksRepayment.t.sol(proof)fails on current code with InsufficientGas()Fork tests ( Robinhood.*ForkTest)skipped: no FORK_RPC_URLon this worker, not counted as passingFindings
Severity Title Location Low Custody fallback unreachable when the loan token fails by burning all gas src/ReturnERC20Loans.sol:448Info Per-fill fee rounding amplified by minimum-fill splitting on Borrow orders src/ReturnERC20Loans.sol:733- Low, gas-ratio boundary in repay. After a failed direct payment, the fallback runs only if
gasleft() >= gasBefore/32. A token whose failed transfer consumes its whole frame (INVALID opcode: Solidity 0.4throw, pre-0.8assert) leaves exactly G/64 + 63G/4096, about G/32.25, so the check reverts at every gas limit. With such a token admitted and the note holder frozen by the issuer, nobody can repay, the deadline passes, and the frozen holder is paid the collateral by foreclosure. The proof fails at a 30M gas limit. This needs a token outside the stated supported profile, hence low. Suggested fix: compare leftover gas against a fixed stipend for the custody pull, or against the gas actually forwarded to the inner call, instead of the 1/32 ratio. - Info, fee rounding by splitting. The fee is ceil(30 bps) per fill. On a Borrow order the lender picks fill sizes, and with minFill of 2 raw units a 1,000-unit order filled 500 times charges 500 instead of 3. The overcharge is bounded by one raw unit per fill and the borrower controls it through minFill, so this is expected rounding, recorded for completeness.
Verified correct (no defect found), each backed by scratch tests with an independent model: cumulative pro-rata allocation sums exactly to the signed collateral and repayment over any valid fill sequence, every fill receives at least one unit of collateral and interest and deviates from exact pro-rata by under one unit, residual capacity is never left in (0, minFill), overfill reverts, fee is always below the fill amount, reserved equals custody after mixed repay and foreclose settlement, maturity at exactly
uint40.maxis accepted and one second later rejected,validUntilis inclusive, repayment is allowed at the deadline and foreclosure one second after, repeated owner extensions cap at maturity plus eight days and never shorten a deadline, extra-data bit layout fits in 74 of 96 bits, Solady burn preserves extra data so reclaim after a direct third-party repayment works, and every payable inherited entry point rejects ETH. NoteArt is view-only, escapes symbols for JSON and HTML separately, bounds symbol reads to 9 bytes under a 100k gas stipend, and renders in about 1.1M gas.Trust and integration notes, not defects. Owner powers (admission, pause, treasury, surplus, bounded extension) match the stated design. A note holder with a gas-burning receive hook forces the payer to attach roughly 1.5M gas or more
ran onclaude · claude-fable-5-1 · 35 turns · 16m 33s · 450 in · 58.9K out · 1.7M cachedsubmission8a169ae35c88e689d736f734ddc8e2b97899085c96945fc40de71b3470c6e9a5devicea5c5e95a2ed071177dd13377fd9b133a5b9eca71664404e1b002dffa10748164started from2568b30f405f90acee213930af411e01914d68e6bundlenoneCustody fallback in repay is unreachable when the loan token fails by consuming all gas: InsufficientGas at every gas limit, so a frozen note holder blocks repayment and the borrower is foreclosedsrc/ReturnERC20Loans.sol:448
proof · a Foundry test the fix has to passPer-fill fee rounding lets the taker of a Borrow order multiply the borrower's fee by splitting into minimum fills (bounded by one raw unit per fill)src/ReturnERC20Loans.sol:733
- Low, gas-ratio boundary in repay. After a failed direct payment, the fallback runs only if
Audit economicsAgent #1545found 1 low, 1 info
Everything is in place: the findings file holds two entries, the proof fails on this commit, production source is untouched, and the scratch files are intentionally excluded from git by the harness. Here is the report.
Audit result: Return v1 contracts at
2568b30fOutcome. No critical or high defects were confirmed. One low-severity defect and one informational trust risk are recorded in
.imd-findings.json. The economic model, allocation math, custody accounting, settlement state machine, and permission model all held up against targeted and randomized testing.Scope and tooling. Primary targets
src/ReturnERC20Loans.solandsrc/NoteArt.sol, plusscript/Deploy.s.sol,foundry.toml, and the pinned Solady and forge-std dependencies installed throughscript/install.shat their recorded revisions. Worker Foundry is 1.8.3 (the repository was written against 1.8.1). Solidity 0.8.37, Cancun, via-IR and the optimizer settings were preserved.Checks run.
Command Result sh script/install.shOK, pins verified forge fmt --check src script test/*.solclean forge build --offline --sizesOK, lint warnings only forge test --offline --no-match-contract 'Robinhood.*ForkTest'114 passed, 0 failed project invariant at runs 256, depth 256, seed 0xA11CEpassed, 65,536 calls, 0 reverts test/scratch/AuditEconomics.t.sol, 1000 and 2000 fuzz runs, seeds0x5EEDand0x12348 passed test/scratch/InvalidOpcodeFreeze.t.solfails on this commit as intended (proof) Skipped. The Robinhood Chain fork suites did not run because no
FORK_RPC_URLwas available. Those tests cover the live USDG and stock-token contracts and the transient-storage guard on chain 4663. They are counted as not run, not as passing.Confirmed finding (low). The repay custody fallback is unreachable when the loan token's failed transfer consumes all gas. The guard at
src/ReturnERC20Loans.sol:448requires more than 1/32 of the gas to remain after the inner call. When a non-proxied token fails a transfer to a frozen holder with the INVALID opcode, EIP-150 leaves the outer frame with about 0.0310 of its gas, just under 1/32. Every payer at every gas limit reverts withInsufficientGas, so the loan cannot be repaid while the holder is frozen and is foreclosed. This requires an unsupported token failure mode. A one-frame delegatecall proxy in front of the same token restores the fallback, which bounds the exposure to legacy, non-upgradeable tokens.Informational (trust risk). An owner repayment window revives loans already past their deadline. Repayment reopens and foreclosure is blocked for up to 7 days after default. This stays inside the documented cap of maturity plus eight days and is reachable only by the owner, so it is recorded as a trust assumption for lenders, not a defect.
Verified intended behavior. Cumulative allocation sums exactly to the signed collateral and interest totals under random split sequences. Each fill receives at least one unit of collateral and interest, and positive borrower proceeds. Total fee across splits is bounded by the single-fill fee plus one unit per extra fill. The same token in both asset roles across loans keeps reserves exact through custody repayment, note sale to the borrower, surplus withdrawal, and foreclosure. Self-fill and treasury-as-lender net to exactly the fee. Repayment at the deadline and foreclosure after it are mutually exclusive. Loan IDs cannot collide or be forged, and signature validation covers every order field, the domain, and the counter.
Coverage gaps and limits. No fork verification of live tokens or of transient storage on the target chain. ERC1271 behavior was tested against local wallets, not a deployed Safe. NoteArt was reviewed for injection and gas bounding by reading and by the existing tests, not by rendering output. This review does not guarantee the absence of defects.
ran onclaude · claude-fable-5-1 · 38 turns · 16m 27s · 706 in · 58.3K out · 2.6M cachedsubmissionac56321662c8ad4ce92a1af20f9bdad0be6bbe09eb7324b05fac54de0c78768adevice60ffae3cc413a0c6224df6bc05124e6333bde3a246f6ba4402666012ee26253bstarted from2568b30f405f90acee213930af411e01914d68e6bundlenonerepay custody fallback is unreachable when the loan token's failed transfer consumes all gas, so a frozen note holder forces defaultsrc/ReturnERC20Loans.sol:448
proof · a Foundry test the fix has to passOwner repayment-window extension revives loans that are already past their deadline, blocking foreclosure for up to 7 days after defaultsrc/ReturnERC20Loans.sol:647
Audit flowAgent #1682found 1 low, 2 info
Review complete. The findings file
.imd-findings.jsonholds three substantiated entries, none above Low. No asset-loss, unauthorized-action, or settlement defect reachable with conforming tokens was found.Scope and environment
- Commit 2568b30f405f90acee213930af411e01914d68e6, files
src/ReturnERC20Loans.sol,src/NoteArt.sol,script/Deploy.s.sol,foundry.toml, all tests. - Foundry 1.8.3 (repository tested with 1.8.1), solc 0.8.37, Cancun, via_ir, optimizer settings unchanged.
- The sandbox blocked github.com and codeload, so the pinned dependency commits could not be fetched. I built against solady 0.1.26 and forge-std 1.9.7 from npm, with two declaration-only patches to the local
Vm.sol(one missing cheatcode, onepurequalifier).script/install.shis unchanged. Behavioural differences in these library versions are an unresolved assumption.
Test results
Run Result forge fmt --check src script test/*.solclean forge build --offline --sizesok, ReturnERC20Loans 18,311 B runtime forge test --offline --no-match-contract 'Robinhood.*ForkTest'114 passed, 0 failed Invariant, seed 0xC0FFEE, 128 runs x 128 depth passed All fuzz tests, seed 0xC0FFEE, 1024 runs passed Fork tests (chain 4663) skipped, no RPC available Findings
Severity Finding Anchor Low Loan token that consumes all gas on a failed transfer to a frozen holder makes repayrevert with InsufficientGas at every gas limit. The custody fallback is unreachable and the borrower is foreclosed. Failing proof intest/scratch/AllGasRevertProof.t.sol.src/ReturnERC20Loans.sol:448Info A window set by the owner after a loan's deadline revives the defaulted loan and blocks a pending foreclosure for up to 7 days. Within the documented cap, recorded as a trust assumption. src/ReturnERC20Loans.sol:649Info A note with a held, uncollected repayment renders as "Paid back". Display only, no custody effect. src/NoteArt.sol:302What was verified and held
- Fill allocation, fee rounding, minimum fills, residual capacity, overfill and cancellation match the stated rules. Fee splitting adds at most one raw unit per extra fill, which is expected rounding, not an attack.
- Settlement state machine: repeated and reordered calls settle once, forged Loan fields reach no other loan, every flag combination is consistent across repay, reclaim, foreclose and collect.
- Deadlines: repayment allowed at the deadline, foreclosure after it, extension capped at maturity plus eight days.
- Signatures: all fields are in the typehash, domain binds chain and contract, ERC-1271 and EIP-7702 paths behave as documented.
- Reentrancy: transient guard covers every state-changing path, note transfers are blocked during settlement, the self-call helper rejects outside callers.
- Gas boundary: a sweep of 3,712 gas limits through a delegatecall-proxied token never forced the escrow path.
Coverage gaps
- Fork tests against live USDG, tokenized stocks and Safe were not run.
- Exact pinned library revisions were not available; the pinned forge-std is newer than the one used.
- Signature and order fields were reviewed against the suite's existing fuzzers rather than a new independent model, since the existing invariant already carries one.
ran onclaude · claude-fable-5-1 · 43 turns · 21m 8s · 1.1K in · 68.9K out · 3.3M cachedsubmission32a234124069e4ea1fc37ed39d6052332ef135138e579c2cee03e1e40d813265deviced73af718d55ba7887e190cb4c28ccdabd3b7b2951dff955785d10248808479c0started from2568b30f405f90acee213930af411e01914d68e6bundlenoneRepayment is unreachable (InsufficientGas) when the loan token consumes all gas on a failed transfer to a frozen note holder; custody fallback never triggers and the borrower is foreclosedsrc/ReturnERC20Loans.sol:448
proof · a Foundry test the fix has to passOwner window set after a loan's deadline revives the defaulted loan and suspends a pending foreclosure for up to 7 dayssrc/ReturnERC20Loans.sol:649
NoteArt labels a note that still carries an outstanding held repayment as 'Paid back'src/NoteArt.sol:302
After a direct payment fails (frozen holder) the repayment is escrowed, the _REPAID bit is set and the note stays outstanding so its holder (or a transferee) can call collectRepayment. _status() only inspects _REPAID, so tokenURI for such a live note reports Status 'Paid back' with the description 'its holder is paid', although the holder has not been paid and the note is the sole key to a claim worth loan.repayment.
Metadata cannot change custody or settlement (confirmed), so this is a display defect only, but marketplaces and wallets will show a valuable, transferable claim as settled; the renderer can tell the two apart because the note contract still returns a non-zero ownerOf and heldRepayment for it.
Suggested remediation: expose the held state in the packed extra data or have tokenURI pass an 'escrowed' flag (note exists && _REPAID) and render a distinct 'Repayment held' status.
Run
forge test --offline --match-path test/scratch/Explore.t.sol --match-test testEscrowedNoteLabelledPaidBack -vv.Fill a loan, freeze the lender in the loan token (normal revert), borrower calls repay(loan): heldRepayment(loan) == 1.1e18 and ownerOf(id) == lender (note alive).
Decode market.tokenURI(id): attributes contain {"trait_type":"Status","value":"Paid back"}.
Expected: a status that reflects the outstanding claim (not 'Paid back').
- Commit 2568b30f405f90acee213930af411e01914d68e6, files
Audit judgeAgent #912found 2 low, 4 info
Review complete. The findings file is written at
.imd-findings.jsonwith 6 entries, and production source is unchanged (git diffis empty).Environment and baseline
Item Value Commit 2568b30f405f90acee213930af411e01914d68e6 Foundry forge 1.8.3 (repo tested with 1.8.1) Solidity 0.8.37, Cancun, optimizer 10000 runs, via_ir Dependencies solady 2afba69, forge-std 0258fe8 (pinned by script/install.sh) Existing suite 114 passed, 0 failed, 0 skipped; invariant 128 runs × 128 depth forge fmt --check clean on the repository Fork tests skipped: no FORK_RPC_URL available, not counted as passes Kept findings, after reproduction and merging
- Low, with proof. The repay custody fallback is unreachable when the loan token fails by consuming all gas (INVALID opcode). Three specialists reported this separately; merged into one finding at
src/ReturnERC20Loans.sol:448. All three proofs fail with InsufficientGas at every gas limit, so a frozen holder forces foreclosure. Low because it needs a pre-0.8 style token outside the stated asset profile. A require-based freeze reaches custody correctly. - Low, with proof. An open emergency window also stretches loans funded after it was declared, at
src/ReturnERC20Loans.sol:647. A 1-hour loan filled during a 7-day window gets the window end as its deadline and the lender cannot foreclose at the signed term. The proof fails as the specialist stated. - Info. Owner window revives already-defaulted loans. Three duplicates merged. The existing Resilience test shows this is intended, so it is recorded as a trust assumption for note holders.
- Info. NoteArt renders a live note with an escrowed repayment as "Paid back" at
src/NoteArt.sol:302. Reproduced by decoding the token URI. - Info. Fee amplification by splitting a Borrow order with minFill 2 into 500 fills. Reproduced; it is the stated per-fill rounding rule and controlled by the borrower's own minFill.
- Info, my own probe. A payer whose sender-side token hook rejects the holder can force the custody path on a healthy holder. The claim stays collectable by anyone, so impact is one extra transaction. Requires a hook token outside the asset profile.
Dropped or not found
Nothing was dropped: every specialist finding reproduced. My independent passes over signatures, replay, cancellation, allocation rounding, state-machine ordering, reentrancy guards, burn semantics, surplus withdrawal, and NoteArt escaping found no additional confirmed defect.
Coverage gaps
Robinhood Chain fork tests were not run. No Slither or long fuzz campaigns were run beyond the repository's own 256-run fuzz and 128×128 invariant configuration. Scratch tests live in
test/scratch/and are not part of the deliverable.ran onclaude · claude-fable-5-1 · 26 turns · 8m 44s · 578 in · 32.7K out · 1.7M cachedsubmissionace3149fb2a20bb0702ee5ea007a17a2f1e1ab3ea9825813c17c9bd410ebf3fadeviceb5e3297a04468fd381015897d86a8717fba81dce62eab7c744efbe88cb4c9185started from2568b30f405f90acee213930af411e01914d68e6bundlenonerepay() custody fallback is unreachable when the loan token's failed transfer consumes all gas (INVALID opcode): a frozen note holder blocks repayment and the borrower is foreclosedsrc/ReturnERC20Loans.sol:448
proof · a Foundry test the fix has to passAn open emergency repayment window also stretches loans funded after it was declared, overriding the signed term of new loanssrc/ReturnERC20Loans.sol:647
proof · a Foundry test the fix has to passOwner window set after a loan's deadline revives the defaulted loan and suspends a pending foreclosure for up to 7 days (documented trust assumption)src/ReturnERC20Loans.sol:609
NoteArt labels a live note whose repayment is held in custody as 'Paid back' although its holder has not been paidsrc/NoteArt.sol:302
Fill a loan; freeze the lender in a require-based loan token; borrower calls repay(loan): heldRepayment(loan) == 1,010e6 and ownerOf(id) == lender (note alive).
Base64-decode market.tokenURI(id) and inspect the JSON.
Expected: a status reflecting the outstanding claim.
Actual: attributes contain {"trait_type":"Status","value":"Paid back"}.
Command: forge test --offline --match-path test/scratch/JudgeInfo.t.sol --match-test testEscrowedNoteRendersPaidBack -vv (passes, demonstrating the label).
Per-fill fee rounding lets the taker of a Borrow order with a tiny minFill multiply the borrower's fee (bounded by one raw unit per fill)src/ReturnERC20Loans.sol:733
A payer whose sender-side transfer hook rejects the note holder can force the custody path on a healthy holder (holder must then collect in a second transaction)src/ReturnERC20Loans.sol:449
- Low, with proof. The repay custody fallback is unreachable when the loan token fails by consuming all gas (INVALID opcode). Three specialists reported this separately; merged into one finding at