ADAG

Contracts and addresses

Every address Adag uses on Arc mainnet, the three deployments, the live proofs, the attack suites and the contract reference.

Everything below is on Arc mainnet, chain 5042. Explorer: explorer.arc.io. Every address is fixed in the app when it is built, and nothing read from the chain or a link can replace one.

Adag's contracts

Each is an exact source match on Sourcify and on explorer.arc.io, compiled with solc 0.8.30, optimizer 200 runs, EVM prague. None has an owner, an admin key, an upgrade path or a pause switch.

ContractAddressDeployed
AdagBills, the bill book the app uses0xaf6C47ae3e2ccD2Cd829Dd8a1DcCb7a7665c08cB (Sourcify)0xad01...64b1, block 22,859,681, 26 September 2026, 2,227,984 gas, 0.044560 USDC
AdagGuard, the loan guard0x9A3F3eE50Ae108124C7Cf54a1b68c14fe5800806 (Sourcify)0xa969...1e1c, block 22,859,780, 26 September 2026, 1,778,869 gas, 0.035577 USDC
AdagBills, first deployment, kept as history0x6F2199e0A04e5e8ba67c89C467b6DF168b01137E (Sourcify)0x758e...6e50, block 22,727,688, 25 September 2026

The first deployment has no way to record an existing loan. It stays live and immutable, and its bills open at /bill/first/N; the app writes every new bill to the current AdagBills. The two deployments both number bills from 1, so a bill is always the pair (contract, id).

WhatAddress
Multicall3From0x522fAf9A91c41c443c66765030741e4AaCe147D0
Memo0x5294E9927c3306DcBaDb03fe70b92e01cCede505
CallFrom precompile (the app never calls it directly)0x1800000000000000000000000000000000000003
Morpho Blue0x34CD04070dD72b14E241112F6d83812Df5Af7fCD
USDC, ERC-20 interface, 6 decimals0x3600000000000000000000000000000000000000
EURC, 6 decimals0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1
cirBTC, 8 decimals0x171A4217b86A807A64eB94757Db6849fb4bDbAA0
Oracle, USDC market0x2AA87fF48933Ce6aBA240BEE916Fc2e6Ec1e51Ab
Oracle, EURC market0x6945246777DfdF4744D957323857F797Ec19Ca1e
Interest rate model, both markets0xF02615d094Fc02fC031C35fe705e175aA4653f20
Chainlink BTC/USD, the feed the oracles read0x7777547914e03BCbB04Ae034942765a0dbb26aE3
Chainlink EUR/USD, the feed the oracles read0xa4266689D107aF71c7dBE975cfB92aB40E7b4EFE
BTC/USD aggregator, whose AnswerUpdated wakes the keeper0x733FE1bA02ea9003C3CFbf5dcc41cf685fF64362
EUR/USD aggregator0xCEDbC96d866EBe46dcbeF8Ed12F9feA2C464d88F
Safe MultiSendCallOnly v1.4.1, the batch a Safe payment runs0x9641d764fc13c8B624c04430C7356C1C7C8102e2

The two Morpho markets

MarketIdLiquidates at
USDC lent against cirBTC0xc2db905f...225815d86%
EURC lent against cirBTC0x6ea1ea96...137daf486%

A second, smaller USDC/cirBTC market exists on Arc with a different oracle. Adag never uses it: market details must hash to one of the two ids above before anything is signed, and both contracts refuse any other id.

The live proofs

A bill paid from bitcoin, and a loan recorded, on the current AdagBills

node packages/contracts/prove-it/prove-it.mjs

That is a dry run on the current AdagBills: every step is simulated from a real mainnet block with eth_simulateV1, so balances, the Morpho market and the price feed are live, and nothing is signed or sent. It also proves the promise of recording a loan, as a real borrower above 40%. The same script with --broadcast sends the payment for real, and --target first runs it against the first deployment. It checks the chain id is 5042 before anything else, and it exits 0 only if every check passes.

It was run for real on 26 September 2026:

StepTransactionGasFee
Supplier writes bill #1: 1.00 USDC, reference ADAG-PROOF-00010x87b4...035e196,8060.004133 USDC
Payer pledges 0.00002943 cirBTC, borrows 1.00 USDC and pays bill #1 through Memo, in one signature0x7dba...3ad0394,1100.008276 USDC

Read back from the chain afterwards: bill #1 is Paid, BillPaid came from AdagBills with loanChecked true, the supplier's USDC rose by exactly 1.000000, no bitcoin was sold, and the payer's loan sat at 39.07%, under the 40% line. The same run simulated the recording promise on a real borrower at 70.26%: a cash payment was refused before recording; after recording, borrowing more in the paying batch was refused, and the same bill paid from cash went through.

The guard's first repayment

node packages/contracts/prove-it/guard-prove.mjs

Run for real on 26 September 2026: the demo payer set a rule of 35% down to 30% and approved 1 USDC, and a second wallet called protect.

StepTransactionGasFee
Payer sets the rule and approves 1 USDC, in one signature0x80bb...1ceb180,8830.003799 USDC
A second wallet calls protect: repaid 0.464348 USDC, 39.07% to 30.00%0xb54f...9880211,4850.004441 USDC
The same wallet calls protect again at the same price: repaid 00x60f9...8a37112,3390.002359 USDC

quote for that block said it would repay 0.464348 USDC, and protect repaid exactly that from the payer's own wallet. The payer's approval fell by exactly the repayment, AdagGuard held nothing before or after, and no bitcoin was sold.

The first deployment, 25 September 2026

StepTransactionGasFee
Payer sends the supplier 0.05 USDC for its own fees0x6b42...c06973,9380.001553 USDC
Supplier writes bill #1: 1.00 USDC, reference ADAG-PROOF-00010x13fb...f888196,7340.004131 USDC
Payer pledges 0.00003133 cirBTC, borrows 1.00 USDC and pays bill #1 through Memo, in one signature0x6987...2292406,1670.008530 USDC

The records of all three runs are in packages/contracts/deployments/: prove-it-2026-09-26.md, guard-prove-2026-09-26.md and prove-it-2026-09-25.md.

The attack suites

node packages/contracts/prove-it/attack.mjs
node packages/contracts/prove-it/guard-attack.mjs

Each tries to break the live contracts with the demo wallet's real Morpho loan, simulated from a real mainnet block, with nothing signed or sent, and exits 0 only if every row behaves as the threat model says. The latest runs against the live contracts:

SuiteTargetBlockResult
attack.mjsAdagBills22,863,57623 of 23
attack.mjsAdagBills, first deployment22,863,53118 of 18
guard-attack.mjsAdagGuard22,863,63814 of 14

The rows are on Audit status, and you can run some yourself.

Reading Adag from your own code

The ABIs are in the repository at packages/contracts/deployments/2026-09-26/AdagBills.abi.json and AdagGuard.abi.json, next to the Standard JSON inputs each was verified with; the first deployment's are in 2026-09-25/. Amounts are integers in base units: 6 decimals for USDC and EURC, 8 for cirBTC. A bill's status is 0 None, 1 Open, 2 Paid, 3 Void. Treat a bill as paid only from bill(id).status on its own contract, or from a BillPaid event emitted by that contract's address. A Memo event on its own proves nothing.

AdagBills reference

It fixes Morpho, USDC, EURC, cirBTC, the two market ids, the 40% line (MAX_LTV_WAD = 0.4e18), the price windows (26 hours for BTC/USD, 96 hours for EUR/USD), the reference cap (140 bytes) and the page size (100) as public constants.

FunctionWho calls itWhat it does
createBill(address currency, uint256 amount, uint64 due, bytes ref) returns (uint256 id)A supplierWrites an Open bill payable to the caller. Emits BillCreated
voidBill(uint256 id)The bill's supplierCancels an Open bill. Emits BillVoided
pay(uint256 id)A payer through Memo in a Multicall3From batch, or a Safe directlyRefuses a payer who enrolled in this block, marks the bill Paid, records the payer's position in both markets, moves exactly the amount and checks the supplier was credited, then runs the 40% check if the position became riskier. Emits DebtRecorded, then BillPaid
enrol()A borrower, or a SafeRecords the caller's own Morpho position in both markets and the block. Moves no tokens. Emits Enrolled
ViewWhat it returns
bill(uint256 id)(payee, status, due, currency, createdAt, amount, payer, paidAt, ref)
billCount()Bills ever written, the highest id
billsOfPayee(address, uint256 offset, uint256 limit), paymentsOfPayer(address, uint256 offset, uint256 limit)Bill ids, oldest first, and the total; at most 100 per page
seenPosition(address payer, bytes32 marketId)The shares and collateral last recorded, at a payment or at enrol
enrolledAt(address payer)The block the payer last enrolled in, or 0
loanToValue(address user, bytes32 marketId)Debt over collateral value, WAD, rounded up
collateralNeeded(address user, bytes32 marketId, uint256 extraBorrow)Extra cirBTC so debt plus extraBorrow sits at or under 40%
priceStatus(bytes32 marketId)Whether new debt would pass the freshness check now, and each feed's update time
EventMeaning
BillCreated(uint256 indexed id, address indexed payee, address indexed currency, uint256 amount, uint64 due, bytes ref)A bill was written
BillVoided(uint256 indexed id, address indexed payee)A bill was cancelled
BillPaid(uint256 indexed id, address indexed payer, address indexed payee, address currency, uint256 amount, bool loanChecked)The proof of payment
DebtRecorded(address indexed payer, bytes32 indexed marketId, uint256 borrowShares, uint256 collateral, bool checked)A new position recorded for the payer
Enrolled(address indexed payer, uint256 usdcShares, uint256 usdcCollateral, uint256 eurcShares, uint256 eurcCollateral)A borrower recorded their existing position

Errors: ZeroAmount, ReferenceTooLong, UnsupportedCurrency, UnknownBill, BillNotOpen, NotPayee, SelfPayment, EnrolledThisBlock (recorded in this block: pay from the next one), PayeeNotCredited, BadMarket, BadFeed, StalePrice, ZeroPrice, LtvAboveLimit, PageTooLarge. Through a batch they arrive wrapped in Memo's MemoFailed(bytes returnData).

AdagGuard reference

It fixes Morpho, USDC, EURC, cirBTC, the same two market ids and the page size (100). A rule is (triggerWad, targetWad, expiry) in WAD, with expiry 0 for no end date. The borrower's approval of the loan token to AdagGuard is the most it can ever take.

FunctionWho calls itWhat it does
setRule(bytes32 marketId, uint64 triggerWad, uint64 targetWad, uint64 expiry)The borrowerStores the caller's own rule and adds the caller to the list of rule holders. Emits RuleSet
clearRule(bytes32 marketId)The borrowerDeletes the caller's rule; never reads Morpho, so it always works. Emits RuleCleared
protect(address borrower, bytes32 marketId) returns (uint256 repaid)AnyoneAt or above the trigger and before the end date, pulls from the borrower's wallet only what brings the loan back to the target, capped by the approval, the balance and the debt rounded down, and repays it to Morpho for the borrower. Otherwise returns 0 and emits nothing. Emits Protected
ViewWhat it returns
ruleOf(address borrower, bytes32 marketId)The rule, all zeros for none
quote(address borrower, bytes32 marketId)(wouldAct, amount, ltvWad) from the same computation protect runs
holderCount(), holders(uint256 offset, uint256 limit)The rule holders, at most 100 per page
EventMeaning
RuleSet(address indexed borrower, bytes32 indexed marketId, uint64 triggerWad, uint64 targetWad, uint64 expiry)A rule was saved
RuleCleared(address indexed borrower, bytes32 indexed marketId)A rule was stopped
Protected(address indexed borrower, bytes32 indexed marketId, uint256 repaid, uint256 ltvBeforeWad, uint256 ltvAfterWad)The guard repaid part of a loan

Errors: BadMarket, ZeroTarget, TargetNotBelowTrigger, TriggerNotBelowLiquidation, ExpiryInPast, NoRule, PageTooLarge, and three that undo a protect whose books do not balance: RepaidNotPulled, GuardBalanceChanged, MorphoAllowanceLeft.

On this page