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.
| Contract | Address | Deployed |
|---|---|---|
| AdagBills, the bill book the app uses | 0xaf6C47ae3e2ccD2Cd829Dd8a1DcCb7a7665c08cB (Sourcify) | 0xad01...64b1, block 22,859,681, 26 September 2026, 2,227,984 gas, 0.044560 USDC |
| AdagGuard, the loan guard | 0x9A3F3eE50Ae108124C7Cf54a1b68c14fe5800806 (Sourcify) | 0xa969...1e1c, block 22,859,780, 26 September 2026, 1,778,869 gas, 0.035577 USDC |
| AdagBills, first deployment, kept as history | 0x6F2199e0A04e5e8ba67c89C467b6DF168b01137E (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).
Arc, Morpho, Chainlink and Safe contracts Adag uses
The two Morpho markets
| Market | Id | Liquidates at |
|---|---|---|
| USDC lent against cirBTC | 0xc2db905f...225815d | 86% |
| EURC lent against cirBTC | 0x6ea1ea96...137daf4 | 86% |
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:
| Step | Transaction | Gas | Fee |
|---|---|---|---|
| Supplier writes bill #1: 1.00 USDC, reference ADAG-PROOF-0001 | 0x87b4...035e | 196,806 | 0.004133 USDC |
| Payer pledges 0.00002943 cirBTC, borrows 1.00 USDC and pays bill #1 through Memo, in one signature | 0x7dba...3ad0 | 394,110 | 0.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.
| Step | Transaction | Gas | Fee |
|---|---|---|---|
| Payer sets the rule and approves 1 USDC, in one signature | 0x80bb...1ceb | 180,883 | 0.003799 USDC |
A second wallet calls protect: repaid 0.464348 USDC, 39.07% to 30.00% | 0xb54f...9880 | 211,485 | 0.004441 USDC |
The same wallet calls protect again at the same price: repaid 0 | 0x60f9...8a37 | 112,339 | 0.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
| Step | Transaction | Gas | Fee |
|---|---|---|---|
| Payer sends the supplier 0.05 USDC for its own fees | 0x6b42...c069 | 73,938 | 0.001553 USDC |
| Supplier writes bill #1: 1.00 USDC, reference ADAG-PROOF-0001 | 0x13fb...f888 | 196,734 | 0.004131 USDC |
| Payer pledges 0.00003133 cirBTC, borrows 1.00 USDC and pays bill #1 through Memo, in one signature | 0x6987...2292 | 406,167 | 0.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:
| Suite | Target | Block | Result |
|---|---|---|---|
attack.mjs | AdagBills | 22,863,576 | 23 of 23 |
attack.mjs | AdagBills, first deployment | 22,863,531 | 18 of 18 |
guard-attack.mjs | AdagGuard | 22,863,638 | 14 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.
| Function | Who calls it | What it does |
|---|---|---|
createBill(address currency, uint256 amount, uint64 due, bytes ref) returns (uint256 id) | A supplier | Writes an Open bill payable to the caller. Emits BillCreated |
voidBill(uint256 id) | The bill's supplier | Cancels an Open bill. Emits BillVoided |
pay(uint256 id) | A payer through Memo in a Multicall3From batch, or a Safe directly | Refuses 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 Safe | Records the caller's own Morpho position in both markets and the block. Moves no tokens. Emits Enrolled |
| View | What 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 |
| Event | Meaning |
|---|---|
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.
| Function | Who calls it | What it does |
|---|---|---|
setRule(bytes32 marketId, uint64 triggerWad, uint64 targetWad, uint64 expiry) | The borrower | Stores the caller's own rule and adds the caller to the list of rule holders. Emits RuleSet |
clearRule(bytes32 marketId) | The borrower | Deletes the caller's rule; never reads Morpho, so it always works. Emits RuleCleared |
protect(address borrower, bytes32 marketId) returns (uint256 repaid) | Anyone | At 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 |
| View | What 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 |
| Event | Meaning |
|---|---|
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.