Security

Security review and resolutions

We put our own deployed contract through an adversarial review. It found two critical flaws. This page publishes all of them, the change that closes each one, and the test that fails if it ever comes back.

Status: published, not in service. The enforcement contract is deployed and source-verified on Base mainnet, but no policy is installed and it holds no funds. It stays that way until an external audit by a named firm. An AI-assisted review is a pre-audit, not an audit.

Why this page exists

A credit bureau's only real asset is being trustworthy about its own numbers. A team that quietly ships an unenforced limit and calls it enforcement is not one you want underwriting anything. So when a review of our first deployment found that the limit was not actually binding, the choice was to publish it rather than patch quietly.

Nothing was ever at risk. No policy had been installed and no capital sat behind the contract — which is precisely why we deployed it out of service first.

The finding that mattered

Version one recorded a draw amount against the credit limit, then executed a call whose calldata the borrower supplied. The contract never hashed or decoded that calldata. An account could book one dollar and move ten thousand. The number was bookkeeping, not a constraint.

Worse, because the available-headroom figure clamped at zero, a draw of zero always passed the check — so even an agent with a zero limit held an unrestricted execute trampoline.

Version two removes the borrower's calldata entirely. The manager constructs the ERC-20 transfer itself from (asset, recipient), reads balanceOf(account) before and after the callback, and reverts unless exactly the booked amount moved.

Every finding

IDFindingFix in v2Regression test
C1
Critical
draw booked amount but executed caller-supplied calldata, so an account could book $1 and move $10,000draw no longer accepts calldata. It takes (asset, recipient), builds the transfer itself, and measures balanceOf(account) either side of the callback. A mismatch reverts SpendMismatch.test_C1_AccountCannotMoveMoreThanBooked
C2
Critical
amount == 0 always passed the limit check, making a live policy an unrestricted execute trampolineExplicit if (amount == 0) revert ZeroAmount(). A zero limit now grants no authority at all.test_C2_ZeroLimitCannotDrawAnything
H1
High
repay was pure storage math callable by the borrower, outside the reentrancy guard — free debt erasureSplit in two. repayWithAsset pulls tokens via transferFrom to a settlement sink and only then reduces the book. settle is onlyBureau and logged.test_H1_AccountCannotSettleItsOwnDebt
H2
High
A non-zero keystore commitment outranked signature-checked terms, authorising draws with no bureau signatureThe keystore path is removed entirely. The only route to a commitment is an EIP-712-checked install.test_H2_NoKeystoreSurfaceRemains
M1
Medium
_accrue advanced the clock even when truncation booked zero, so poking every few seconds stopped interest foreverThe clock only advances when something was actually booked. Zero-amount settlement is rejected.test_M1_ClockDoesNotAdvanceWhenNothingAccrues
M2
Medium
No nonce: a revoked signature could be replayed to restore an old limit, and the bureau could not push a tighter onenonce added to the signed struct, incremented on install, revoke and default. installPolicyFor and revokeFromBureau let the bureau re-underwrite without the borrower.test_M2_RevokedSignatureCannotBeReplayed
M3
Medium
Two hot keys with instant, unbounded power; markDefault needed no debtSigner rotation behind a 2-day timelock. Ownership is two-step. markDefault requires outstanding obligation.test_M3_BureauSignerRotationIsTimelocked
L1
Low
Repaid reported the requested amount, overstating settlement on overpay_reduce returns the applied figure; Settled emits that.test_L1_SettledEventReportsAppliedNotRequested
L2
Low
accruedAt as uint64 truncates in 2106 and can brick a policyStored as uint256.covered by every accrual test
L3
Low
Reentrancy guard covered only drawnonReentrant on every state-changing entry point; the account callback is treated as hostile.test_L3_ReentrantDrawDuringCallbackBlocked
I1
Info
Interest truncates toward the borrowerUnchanged and intentional. Documented.test_InterestMatchesHandCalculation
I2
Info
Epoch bump leaves commitments and debt in placeUnchanged and intentional: a bump freezes borrowing without forgiving or stranding debt.test_EpochBumpDoesNotEraseDebt

Properties now enforced rather than assumed

Verification

Suite      73 tests passing  (69 unit + 4 Base Sepolia fork)
Coverage   98.95% lines · 97.67% statements · 86.96% branches · 96.97% functions
Size       runtime 9,947 B  (40% of the EIP-170 ceiling)
Deploy     ~2.96M gas  (~$0.10 on Base)

The fork tests run against live Base state and the real Circle USDC contract, through an ERC-4337-shaped account that performs its own transfers.

Signature acceptance, proven against mainnet

Offline signature recovery only proves we signed something. To prove the deployed contract accepts our terms and rejects tampering, we simulate the real installPolicy call against live mainnet state using terms fetched from the live bureau endpoint:

accepted honest terms, correct account
rejected credit limit inflated 1000× — BadSignature()
rejected honest terms presented by a different account — BadSignature()

The signature is bound to both the value and the account, on the contract as deployed.

Still open before real money moves

  1. External audit by a named firm. This resolution set answers an AI-assisted review, which is a pre-audit.
  2. Bureau signer to an HSM. It is currently a hot key, and it holds no funds by design — it signs, it never transacts.
  3. Owner to a multisig. Ownership currently sits with a dedicated cold account that is deliberately kept off every internet-facing host; a multisig is still the right end state.
  4. Liquidation remains off-chain. markDefault records the event; recovery is a business process.

Addresses

Network     Base mainnet (chainId 8453)
Manager v2  0x4c2d505f9c76aF780573b6451c2D20eEe76bbc78   in force
Verified    Sourcify exact match on creation and runtime bytecode
Superseded  0x6668f85DEB0f49d048ceC07D9aa059dD23BF9A36   epoch bumped, all its terms void

View v2 on Basescan · Verified source on Sourcify · Scoring methodology

Review conducted 2026-08-24. Resolutions deployed the same day. Bumping the superseded contract's epoch permanently voids every set of terms ever issued for it.