Credit delegation / re-lending. 16 in-scope contracts, ~3,250 lines, three leads opened, three closed, zero submissions. Everything below is a non-finding: code that looked wrong and isn't.
Small enough to read completely in one pass, actively maintained (last commit three days before the review), and built on a primitive that isn't a fork of anything — delegated credit that gets re-lent. New primitives are where logic bugs survive, because automated tooling has no pattern for them and prior auditors have no muscle memory.
_convertBaseToCollateralHypothesis: the function converts a base-asset amount into collateral units with no ceiling, so a user could be credited more collateral than they actually hold.
Why it looked real: a comment in the code itself warns that the result must be clamped against the user's collateral — a warning like that usually means somebody was worried, and worries are not always acted on.
Closed: both implementations do clamp. The Euler and the Aave V3 collateral vaults
each take the min against the user's actual balance before use. The comment
documents a rule that is, in fact, followed in every call site.
skim()Hypothesis: skim() computes
balance - totalAssetsDepositedOrReserved. If the accounted total ever exceeds the
real balance, that subtraction reverts — or, in an unchecked block, wraps.
Closed: the onlyBorrowerAndNotExtLiquidated modifier is the invariant. It
is exactly the condition under which balance ≥ totalAssetsDepositedOrReserved is
guaranteed to hold; the one state where the accounted total can exceed the balance — after an
external liquidation — is the state the modifier excludes.
Hypothesis: this was the good one. After an external liquidation,
totalAssetsDepositedOrReserved is deliberately not updated — the protocol
needs the stale value to detect that the liquidation happened at all. So the stale, inflated
figure feeds _maxRelease in the three-way collateral split. Inflated input to a
distribution function is a real bug shape: over-release to one party at another's expense.
Closed: _maxRelease never reaches the transfer unclamped.
splitCollateralAfterExtLiq binds the release to the live balance:
Math.min(_collateralBalance - userCollateral, _maxRelease)
// EulerCollateralVault.sol:251
However inflated _maxRelease gets, the payout is capped by collateral that
actually exists.
How it was verified, not just read: a real fork test against mainnet block
23,600,528, no mocking. The protocol's two existing fuzz tests (1,000 runs each)
preserve full equality in the split for every value of _maxRelease, and the
end-to-end external-liquidation scenario
(test_e_handleExternalLiquidation_case00) passes.
Zero findings. Zero submissions. Twyne's own repository guidelines ask reviewers to be transparent about non-issues rather than file them, and that is the correct instruction — a bounty queue full of speculative reports is a tax on the team and buys the reporter nothing. The value of this page is the opposite one: these three paths are now documented as checked, with citations, so neither I nor anyone reading this needs to spend the hours again.
Reviewed 2026-08-24 against 0xTwyne/twyne-contracts-v1. All testing on a local fork. Not affiliated with Twyne.