Skip to content

[PROPOSAL] RFP-014 — Antumbra Liquidation: Increasing-Discount Collateral Auctions #187

Open

Description

@edenbd1

RFP ID

RFP-014 — Liquidation & Auction Engine

Your Project Name

Antumbra Liquidation: Increasing-Discount Collateral Auctions

Team or Organization Name

Antumbra

Primary Contact

Telegram https://t.me/edenbdn or edenbaud1@gmail.com. Either reaches both of us within a day.

Team Members

Eden Baudin — engineer, privacy & ZK

  • GitHub: https://github.com/edenbd1
  • X: https://x.com/edenbdn
  • Telegram: https://t.me/edenbdn
  • LinkedIn: https://www.linkedin.com/in/edenbdn/
  • Role: LEZ/SPEL program, on-chain integer arithmetic, public/private account
    model, Logos Core module and Basecamp mini-app, CI and reproducible evidence.
  • λPrize LP-0005 — Private Balance Attestation: won and merged. A SPEL
    program on LEZ, accepted into this programme's own repository.
  • Since then, on LEZ v0.3 and Basecamp 0.3: RFP-017's vesting program and
    Accord, a private M-of-N multisig, both deployed and driven on the public LEZ
    testnet, and Logos Forum (λPrize LP-0026), a serverless forum for Basecamp on
    Logos Delivery and Storage.
  • 15+ hackathon wins. Shipping a working system against a deadline, which is
    the shape of a milestone.
  • Ex-DevRel in privacy, across RWA and DeFi.
  • Incubated at the Berkeley Blockchain Xcelerator.
  • Former president of a blockchain association.

Nada Ahmadi — engineer, DeFi & AMM

  • GitHub: https://github.com/nadaamd
  • LinkedIn: https://www.linkedin.com/in/nada-ahmadi/
  • Role: AMM pricing and precision-safe integer math, differential backtesting
    against real on-chain data, sale and trading front-end, wallet integration.
  • Podium finish, Uniswap track, ETHGlobal Lisbon 2026 — Umbra, an autonomous
    circuit breaker for DeFi positions: it risk-scores a pool and evacuates funds
    through an on-chain Uniswap v3 swap, backtested over 48,000 real swaps.
  • Full-stack developer at XRPL Commons.
  • Member of DeVinci Blockchain, ESILV.

Project Summary

Requested: $55,000 over 12 weeks, all-in — $43,000 of milestone payouts
to us, paid on acceptance with nothing in advance, and $12,000 which is the
external auditor's own fee rather than ours.

The short version. The auction house this RFP specifies is already running
on the public LEZ testnet: a lot opened, partial fills bought at an escalating
discount, the lot exhausted and closed short, two configurations side by side,
and the instructions the program refused; and since 2026-10-17, a second
deployment where three fresh signers liquidate, a shortfall goes to a debt
auction, and a stale feed pauses only its own type. Driving it is also how we found two
defects in our own engine, which are fixed. The first build's accounts that
showed them were on the v0.2.4 testnet and went with its reset of 2026-09-30,
so section 6 records them by hash and value. The rest of this document is how the other pieces
attach, and what we found in the specification while building the first one.
Every transaction below is listed in
DEPLOYMENTS.md.

This part is deployed, not proposed. The facts, the one command that
re-checks all of them, and a film of the earlier v0.2.4 session:

Program antumbra_auction, live on the public LEZ testnet v0.3 at ADLHPytqfLnzM2hfoJUx6ZXPLwFo6o4RmqXWRJGETMJ7
ImageID dc3618e80b2645906b70c5aee9c6e1be50d985fc00f031c3dc03e0ad9fd9384f, the reproducible Docker build
Lifecycle run 2026-10-15, blocks 15515 to 15691: 8 applied, 12 refused as required, 26 checks, 0 failures (evidence/v03/)
Re-checked 2026-10-16, from the committed manifest against https://testnet.lez.logos.co: 26 checks pass, 0 fail
Liquidation build F1, F4, R2, R4 and R5 on chain: ImageID 72c22843…, a second deployment at J3TXJsr7U31F8G9YEVFCebeeCjd7Ro7f8t4KU4eB3iMy, run 2026-10-17 in blocks 28,593 to 28,857: 26 applied, 11 refused as required, 22 state checks, 0 failures; the verifier re-checks it at 52 checks, 0 failures (evidence/v03/liquidation/)
Next testnet Redeployed and the verifier re-run on testnet 0.4 within days of its launch
Source edenbd1/antumbra-auction — public, MIT OR Apache-2.0
Demo a film of the v0.2.4 session, recorded 2026-08-23 (v0.2.4, before the 2026-09-30 reset; historical): the ImageID and deploy transaction it shows, and the two accounts it fetches, went with the reset, and the command it runs is now scripts/verify-onchain-v024.sh. The v0.3 run is re-checked by the command below
$ git clone https://github.com/edenbd1/antumbra-auction && cd antumbra-auction
$ ./scripts/verify-onchain.sh --manifest evidence/v03/testnet.tsv

scripts/verify-onchain.sh is the
whole of it, and CI runs the suite in release —
the profile that sets overflow-checks = true, because a debug run exercises
different arithmetic from the one that ships.

Nothing above needs our cooperation to check. The script talks to the public
sequencer and reads the ImageID out of the program's header account on chain,
and CI rebuilds the committed binary byte for byte in RISC Zero's pinned Docker
builder, so the ImageID on chain is the one a clean clone produces. It counts
its own checks rather than printing a number we typed. On v0.3 a refused
instruction is included in a block and charged, so what makes it a refusal is
that it changed nothing: the script hashes the final state of every account the
run touched and compares it with what the run recorded, and one control is a
random hash that must not resolve. A check that only ever says yes is not a
check.

But a prototype is not the argument, and we will not pretend it is. The
argument is that the platform risk in this RFP is already spent for us. λPrize
LP-0005 was merged into logos-co/lambda-prize after review, and three more
submissions are open. The parts of RFP-014 that are new-platform risk for anyone
starting now — SPEL program layout, the RISC0 guest build, chained-call token
movement, Basecamp module packaging, standalone-sequencer CI — are things we have
already done on this exact stack and had reviewed. The auction on chain is
evidence of that, not a substitute for it.

And the specification's dependency section is wrong in both directions. We
checked each dependency against the live runtime rather than against its prize
tracker, and two of the four are the opposite of what the document says:

Dependency What RFP-014 says What the chain says today Effect on this work
LP-0012 events Listed under "now delivered on LEZ, so they no longer gate this RFP" getTransactionReceipt, getEvents and getLogs all answer Method not found. Asserted on every run of our v0.2.4 verify script, and checked again on the v0.3 testnet on 2026-10-16 A real blocker moved out of the blocker list. Liquidator discovery must poll account state. We price that, and design for it
LP-0013 token authorities Listed as a hard blocker — "a token custodian … requires the transfer-authority primitives in LP-0013, currently open" A program-owned holding account already custodies and pays out on the public testnet without it — antumbra_vesting funds an escrow through a chained call to the native transfer program, and refuses any funding account that program does not own. LP-0013 gates only the case where the payer is neither the signer nor the program Not a blocker. Custody is designed to the credit-open / debit-owner rule, and swaps to the official primitive when it lands
Price feed (RFP-019 / RFP-020) "RFP-020 delivers external price feeds; RFP-019 is an alternative once a DEX is live" RFP-020 is awarded with 0 of 6 milestones delivered. RFP-019 is closed with no milestone issues at all The one dependency that is genuinely missing. A reference source ships behind the same interface, so the engine works before either lands and swaps in one file after
RFP-001 / RFP-002 authorities "closed (candidate picked) and the libraries are in development" 2 of 3 milestones delivered each. The only entry in that section that is accurate today Built behind a thin local adapter matching the published surface; final conformance is a swap, and we say so

The pattern underneath: that section tracks issue status, not runtime
capability
, and closed does not mean delivered. It is the reason the
milestone plan below never gates a payment on another team's undelivered work,
and the reason every dependency in it sits behind a trait we can satisfy
ourselves until the official one arrives.

A liquidation engine is an arithmetic problem wearing a protocol. Almost
everything that goes wrong with one goes wrong in a quotient — and the quotient
is the part that can be proven before anyone is paid. So we wrote it first.
edenbd1/antumbra-auction is
public, MIT OR Apache-2.0, 41 tests green, #![forbid(unsafe_code)], no
dependencies at all, and overflow-checks = true on the release profile —
because a debug run exercises different arithmetic from the one that ships.

It also found that this RFP's own named reference cannot be transcribed.
Section 1 below shows why, with the transcription in the repository so the claim
is a test result rather than a paragraph.

Technical Approach

1. The reference this RFP names does not compute on the platform it targets

The RFP points at Reflexer's IncreasingDiscountCollateralAuctionHouse and links
the GEB source. That code is Solidity: every intermediate is a uint256, which
holds 1.16e77. A SPEL program on LEZ computes in u128, which stops at 3.4e38.

Two of the reference's helpers overflow before any auction logic runs.

function rdivide(uint256 x, uint256 y) internal pure returns (uint256 z) {
    z = multiply(x, RAY) / y;          // RAY = 1e27
}

The product is taken first. A collateral price quoted in WAD is about 1e18 for a
unit priced at 1, so x · RAY is about 1e45 — seven orders of magnitude past
u128::MAX. The exact ceiling is u128::MAX / RAY, which is 3.4 × 10¹¹; as
a WAD price that is 0.00000034. Every price from a third of a millionth
upwards is out of reach.
This is not an edge case reached by an extreme input.

And rpower, which compounds the discount per second by exponentiation-by-
squaring in ray scale, squares a number near 1e27. The first squaring is 1e54
— sixteen orders past the ceiling, before the exponent is even read.

Both are transcribed in a reference module and exercised by
tests at ordinary values, so this is executable. One test deliberately gives the transcription
numbers small enough to compute and asserts that it does: a transcription that
failed everywhere would prove nothing about the width.

The fix is not a bigger integer. The quotient always fits; only the product
does not. It is taken in 256 bits and divided back in one step with nothing
intermediate stored, reusing the widening multiply and restoring division whose
differential vectors and zkVM cycle counts already run in CI in
edenbd1/antumbra-lez. The discount schedule is linear in the multiplier rather than compounded,
because compounding is what forces rpower. It is monotone, reaches its maximum
at the deadline and stays there, and costs one mul_div instead of a loop whose
length is the age of the auction. A stepwise or exponential curve tabulates into
the same shape; what cannot be done is compounding a 1e27 rate in 128 bits.

2. The direction of that quotient is a withdrawal, not a rounding

collateral_out = bid · WAD / discounted_price

This has to round down. F3 requires partial fills, which hands the bidder
control of how many times the quotient is rounded. Priced at 2000 with no
discount, 2000 base units of coin buy exactly one base unit of collateral:

the same 2000 units, spent differently collateral received
one bid of 2000 1
2000 bids of 1, rounding down 0
2000 bids of 1, rounding up 2000

Splitting the bid multiplies the collateral by the number of fills. Run to
completion, the bidder takes everything that was seized for one two-thousandth of
its value. R6's invariant — seized collateral equals collateral in auction —
is asserted directly over 600 awkward partial fills against an escalating
discount, and again on chain after every fill.

3. Health is a comparison of two products, and neither of them fits

Deciding a position is undercollateralised compares collateral · price against
debt · liquidation_ratio. A vault holding a million units of a collateral
priced at 2000 wants 2e45 on the left.

This is the worst of the three, because it fails quietly: with overflow checks
off the product wraps and a healthy position is declared liquidatable.
Neither product is needed — only the comparison. It is decided in 256 bits
without materialising either side, and the test checks it against real
multiplication everywhere a real multiplication exists, across the range either
side of 2⁶⁴.

Staleness is a state, not an error. R2 pauses liquidation for a collateral
type whose feed is stale and R4 says only that type pauses, so health returns
Paused rather than failing. An engine that treated a stale feed as an error
would take the whole system down for one bad oracle, which is what R5 forbids.
The age is a saturating subtraction, so a feed timestamped in the future does not
wrap into a very large age and pause everything.

4. A three percent minimum improvement is zero percent below thirty-four units

F4 and F5 want a debt auction and a surplus auction, and both guard
themselves with a percentage: a challenger must beat the standing value by beg.
Written in integers that guard evaporates as the numbers shrink. On the surplus
side 1 · 1.03 floors to 1 and 33 · 1.03 floors to 33 — the unchanged bid
clears its own threshold.
A bidder re-posts the standing value, the expiry
resets, and the auction stays open for free while the system carries the bad debt
it exists to clear.

The fix is not a larger percentage. The improvement must move by at least one
base unit whatever the percentage works out to, and the threshold must round
against the challenger — ceiling on the growing side, floor on the shrinking one.
Two lines, and a test over every size from 1 to 200 that fails without either.

The spec licenses a stand-in for the token these auctions trade against —
"fully implemented and tested (using a mock token), but parameterized by a
configurable token address"
— so the token is an address they never dereference,
and everything in them is real arithmetic over real state.

5. What is on chain today, and what it demonstrates

One program on v0.3 for the auction house, with two configurations under it; the liquidation build (F1, F4, R2, R4, R5) is a second deployment, J3TXJsr7…, whose run is in the summary table and the requirements table. A 2% discount at open
growing to 20% across the window; ten base units of collateral, priced at 2000,
raising 20000.

On the v0.3 testnet, 2026-10-15 (a 120-second window rather than an hour, so the
run fits the testnet's pace; the second bid of 2000 comes 60 s in, and buys
1123595505617977528 against 1020408163265306122 at open, 10.11% more):

transaction shows
start_auction 82283f8e…a32192d6 the lot opened
buy_collateral at open f29c4a7e…a18c774 a partial fill, F3
buy_collateral 60 s later 25a76e95…6433e2e more collateral for the same bid, F2
the bid that exhausts the lot 6c2e4a12…6c85ab the price re-struck, the lot closed, a shortfall booked
a second config, a target met exactly defa399f…5927e6 F7 and R3: 5% at open reaching 30% over 60 s, a bid of 5000 clamped to the 2000 left to raise

The v0.2.4 run below is kept as history (v0.2.4, before the 2026-09-30 reset; historical): a 2% to 20% schedule over an hour, and two programs, the first build and the fixed one. Its transactions went with the reset, so its links no longer resolve.

transaction shows
start_auction 53004d4c…bde1fa8e the lot opened
buy_collateral at t=0 66fe3871…639fec60 a partial fill, F3
buy_collateral at t=1800 559ef1a1…27df8a74 10.11% more collateral for the same bid, F2
a bid of 16000 on a lot worth 12569.59 3a5941a2…35190b9d the price re-struck, the lot exhausted, and a shortfall booked
a second config, a second auction 0871332b…835d147f F7 and R3

The v0.3 auction account BNa27Uup1PA5WLHkcd9ZojLZCn3XbZo2REY7TvrQUFSH
afterwards, read from the public sequencer on 2026-10-16 by anyone, and matching
what the library computes digit for digit (the v0.2.4 run left the same six
figures):

collateral_seized     10000000000000000000
collateral_sold       10000000000000000000
collateral_left                          0     sold + left == seized, which is R6
raised            16569594129786746160000
to_raise          20000000000000000000000
settled                                  1

R6 is checkable without us. The invariant is not an assertion you have to
trust; it is two counters in a public account that a reader can add up.

And the auction closed 3430.41 short of its target, and said so. That is the
input F4's debt auction exists to consume, and it is the thing the next section
is about.

What it refused, which is what the successes mean anything against. Twelve
instructions on v0.3: a config id used twice; a schedule that cannot be
evaluated; an opening by someone who is not the config's admin; an opening with a
schedule its config does not hold; an empty lot; the same auction opened twice; a
bid below the minimum; a bid dated before a fill already honoured; a settle on an
auction neither sold out nor funded; a settle on one already settled; a bid after
settlement; and a bid on an auction that does not exist. On v0.3 each one is
included in a block and charged, and changes nothing: every one is recorded by
hash, and the four accounts the run touched hash today to exactly what the run
left, so a refusal that had moved anything would show.

6. The two defects we found in our own engine, and where they were fetched

This is the part a prototype buys that a design document cannot. Both came out
of reading a live account on the v0.2.4 testnet rather than out of a review. We
left the first build deployed on purpose so the evidence stayed fetchable rather
than described; the reset of 2026-09-30 took it with everything else, and the
account below reads empty today (checked 2026-10-16). What follows is the record
(v0.2.4, before the 2026-09-30 reset; historical).

A bid larger than the lot was charged in full. quote clamped the collateral
handed out to what the lot still held, and went on charging a bid that had been
priced against a larger lot. The bidder overpaid — and worse, the auction
recorded raised == to_raise and reported itself fully raised while the
collateral it had actually sold was worth much less. A shortfall that is never
booked is one F4's debt auction never hears about, and F4 is a hard requirement
of this RFP.

We submitted that bid deliberately, so the defect has a public address:

Transaction (v0.2.4, gone with the 2026-09-30 reset) e46fb1ac…77555ae2, block 20376
Account 9xMhmHEsJXX18f4rBSwFkRKA9kwE2tQBEnKSCXpdK2yB
What it read until the reset raised = to_raise = 5000·10¹⁸, fully raised
What the config beside it allowed the schedule never sells above 95% of market
So the whole lot was worth at most 4750, at any moment of its life

Two public reads, no cooperation from us, and the account was internally
inconsistent. The fix re-strikes the price when it is the lot that ran out
rather than the target: the bidder pays for the collateral they receive, rounded
towards the auction, capped at the bid it replaces. Two named tests fail without
it, and on v0.3 the fix is on chain: the bid of 16000 that exhausts the lot,
6c2e4a12…6c85ab,
is re-struck to 12569.59 and the shortfall is booked in the account above.

And the on-chain R6 assertion was a tautology. The first build checked
left + (seized - left) != seized in
the guest program.
That is an identity: it holds for every value
of every field, including the ones a leak would produce, and it was never once
going to fire. An invariant needs two quantities that are maintained separately
or there is nothing to compare, so AuctionState now carries collateral_sold,
moved independently of collateral_left, on v0.3 as on v0.2.4. That is also
what makes R6 checkable from the account rather than only inside our proof.

Neither of these is in the RFP's own list of things to watch for, and neither is
findable by reading a specification. They are findable by deploying, driving, and
then reading back what the chain actually holds — which is what our milestone
gates ask for, and why they ask for it.

7. Two measurements that change the design, not the marketing

A refusal leaves no receipt, on either runtime. On v0.2.4 a refused
instruction cost a full proof, 430 s against 62 s for a fill, and surfaced only
as "Transaction not found in preconfigured amount of blocks" (v0.2.4, before
the 2026-09-30 reset; historical). On v0.3 a refused public transaction is
included in a block and charged its fee, and changes nothing, while
getTransactionReceipt, getEvents and getLogs still answer Method not found (checked against the public testnet on 2026-10-16). So a liquidator bot
still cannot probe by trying: every rejected attempt is a fee paid with no
diagnostic, and telling it from a fill means reading the account back. It must
read account state before submitting, which is precisely what the RFP believed
LP-0012's events had made unnecessary; our v0.3 CLI already pre-checks a bid, a
settle and an opening before sending. That is a design constraint. It is
measured in M0 and paid for in M5, rather than discovered in month three.

The LEZ explorer's own page for the bid that exhausted the lot, on v0.2.4

That is the explorer's own page, not ours, taken before the reset (v0.2.4,
before the 2026-09-30 reset; historical). The Program ID on it is
38ab5886…, the v0.2.4 ImageID, base58-encoded. Its v0.3 counterpart is live
today: the explorer's page for
6c2e4a12…6c85ab
names program ADLHPyt…, the header account that names ImageID dc3618e8… at
the top of this proposal.

A program's id was its code; on v0.3 it is not. On v0.2.4 the ProgramId was
the ImageID of the guest, so two deployers of byte-identical code got the same
program id (v0.2.4, before the 2026-09-30 reset; historical). On v0.3 a program
is addressed by the header account it is deployed at, and the header names the
ImageID: the same build, ImageID dc3618e8…, sits at header B31q8iMw… on our
local sequencer and at ADLHPyt… on the public testnet. F7's "Each product
deploys its own configured instance of the program"
can therefore now be read
literally. We still propose a distinct config account per product under one
shared deployment, because that is what makes the RFP's own rationale, shared
audits and shared liquidator tooling, true rather than aspirational; a separate
deployment of the same audited binary stays available to any product that wants
one. Two configurations under different admins are live under one program today.

8. Every missing dependency sits behind a trait with a working default

The rule is uniform and it is what keeps this proposal's schedule ours: nothing
here waits on another award.

  • Price — a PriceSource returning a value and an observation time. A
    reference implementation ships in-repo: an on-chain observation buffer with a
    time-weighted read, which is the consumer side RFP-014 actually owns. RFP-020's
    adaptor or RFP-019's TWAP substitutes at the trait boundary with no consumer
    change. Staleness and per-collateral pause are ours either way.
  • Host CDP — a CollateralSource with seize and cancel_debt. A minimal
    reference host in-repo implements it and is documented as the conformance
    target. A stand-in written against a published interface is a conformance test
    of that interface; it is not a claim that the eventual counterparty behaves as
    assumed, and the README says so.
  • Protocol token — a configurable address, exactly as F4 and F5 direct.
  • Admin and freeze — a thin adapter matching RFP-001's and RFP-002's
    published surface, which are at 2 of 3 milestones. Final conformance is a swap
    once their third lands, disclosed as such rather than assumed away.
  • Events — there is no shim for a capability the runtime does not have.
    Discovery polls account state, the cost of that is published, and we
    re-check the event methods on every testnet version; if getEvents ever
    answers, we adopt it.

9. Time is an argument, so it is guarded

now is supplied by the caller. A caller who can rewind it re-prices a fill at an
earlier, shallower discount, so every instruction that reads the clock refuses a
now below one already honoured. The refusal is on chain above. On v0.3 the
runtime bounds it as well: a plan names the block timestamp it is valid at, so a
bid or a settle at now is valid only once the block timestamp reaches now,
and an opening only within two minutes of it. A fill therefore cannot be priced
at a discount the auction has not reached, and the monotonic guard stays as the
cross-check.

10. Soft requirements

Three are listed, and one of them is the invariant this proposal has already
spent a section on. All three are in scope.

Multi-oracle redundancy with a stale fallback — in scope, in M2. The
PriceSource trait already returns a value and an observation time, so a
redundant source is a second implementation behind the same boundary plus a
policy that chooses between them: the primary while it is fresh, the secondary
when it is not, Paused when neither is. That is a small amount of code on top
of what R2 and R4 already oblige us to write, and it is the difference between
one bad oracle pausing a collateral type and one bad oracle being routed around.

Formal verification of the auction invariants — in scope, in M6, and we have
already found the version of it that does not work.
The RFP names exactly the
property this engine was built around: collateral seized equals collateral in
auction
, and debt shortfalls are correctly propagated to the debt auction
house
. Our first build asserted the first of those as
left + (seized - left) != seized, which is an identity — true of every number
there is, incapable of ever firing. The current build carries two counters moved
independently, and their sum is checkable from a public account by anyone.
Formal verification is the next step up from that rather than a new direction:
the properties are already written as executable assertions, and M6 discharges
them under a model checker instead of only over test vectors. We will report
which properties are proven and which are merely tested, because a proposal
that says "formally verified" without saying of what is the same tautology
written in prose.

A liquidator bot SDK with monitoring and automated triggers — in scope, in
M5.
This is where the cost of a refusal stops being trivia. A bot that probes
by trying pays a fee per rejected attempt on v0.3 (seven minutes and a full
proof on v0.2.4) and learns nothing from it, so the SDK's monitor reads account state and submits only what
the arithmetic says will be accepted. The reference liquidator in M4 is that
loop, and its polling cost at a realistic position count is one of the two
numbers M0 exists to measure.

And what we are not proposing. The RFP's Out of Scope list — flash-loan
liquidations, batching aggregators, insurance funds, multi-collateral atomic
liquidations — is absent from this plan on purpose. Nothing in the design
forecloses them and none of them is billed here.

11. Requirement traceability

Every numbered hard requirement, what meets it, and how you can check.

# Requirement (abridged) How it is met Evidence Milestone
F1 Anyone can trigger liquidation on any undercollateralized position. No whi… Permissionless liquidate: no whitelist, incentive paid from the seized lot Done on chain (v0.3): three fresh signers, none holding a balance, each liquidate one position (f3f6bc1a…, 1e079815…, acf1fb83…, blocks 28,698 to 28,711); a safe position and a second liquidation of the same one refused (testnet.tsv) M2
F2 Seized collateral is sold via auctions where the discount to market price … Increasing discount, schedule per config, early termination on target or empty lot Done on chain (v0.3): the same bid of 2000 bought 10.11% more 60 s into a 120 s window; the lot exhausted and the auction closed itself M1
F3 Auctions support buying partial collateral lots, preventing large position… Partial fills, clamped to what is left to raise and left to sell Done on chain (v0.3), including the fill where the lot ran out before the target and the price had to be re-struck M1
F4 When collateral auctions don't raise enough to cover debt plus penalty, th… Debt auction over a configurable token address; buffer absorbs first, freeze beyond it Debt auction done on chain (v0.3): a lot that ran out short, its shortfall booked by anyone (wrong amount and a second booking refused), bids that do not improve by 3% refused, settled to the best bid after its window (7482fd77…, block 28,813). The buffer and the freeze fallback are M3 M3
F5 When the protocol accumulates excess stablecoin above a threshold, surplus… Surplus auction with burn, same configurable address; accrual is real penalties Threshold crossing and burn e2e; accrual-only branch shown as default M3
F6 Auctions follow strict state transitions with no reversals. All state chan… Strict transitions; settled is terminal Done on chain (v0.3): settle-early, settle-twice and bid-after-settle all refused, and the auction account hashes today to what the run left M1
F7 The system integrates with its host CDP protocol via a well-defined interf… One program, one config account per product. On v0.3 a separate deployment of the same binary is also possible, and stays available Done on chain (v0.3): two configs under different admins, an auction under each, each on its own account M1
U1 Build the program using the SPEL framework, which generates the IDL and cl… SPEL program; IDL and CLI generated from it Done on v0.2.4 (before the 2026-09-30 reset; historical): idl/antumbra_auction.idl.json is committed and drove the CLI. SPEL has no v0.3 support yet, so the v0.3 program is written on lee_core and its CLI drives every instruction on the public testnet M1
U2 Provide a Logos mini-app GUI with local build instructions, downloadable a… Basecamp module loaded from a git repo, reading live auction state Load verified against Basecamp's own Qt, with a deliberate failing control M5
U3 Provide a CLI that covers core functionality of the program. The CLI may h… CLI covering every instruction Transcript of each against the public testnet M5
U4 Tools display undercollateralized positions with estimated liquidator ince… Position list with estimated incentive and a trigger; discovery polls state, because the runtime has no events e2e against the reference host; polling cost published M5
U5 Active auctions display collateral type, amount, current discount, raised … Collateral, amount, current discount, raised against target, time remaining; preview is the same mul_div the program runs Preview and fill diffed and asserted equal M5
U6 Provide an SDK that can be used to build Logos modules for interacting wit… SDK with the deshield-interact-reshield path Driven twice already on antumbra-lez, with distinct ephemerals M5
R1 Auction operations are non-reentrant. State transitions are ordered to pre… One instruction, one pre-state, one post-state; transitions ordered Re-entrant chained call attempted and refused; every transition pair enumerated M1
R2 Liquidation uses time-weighted or confidence-adjusted prices, not raw spot… Time-weighted read over an observation buffer; stale pauses that type Done on chain (v0.3): liquidation reads the time-weighted price (1588.24 when the run liquidated, against a new print of 1000; that a print weighs nothing when it lands is a unit test); a feed left to go stale on chain pauses its type (b91ba757… refused, block 28,825), and liquidates again once it resumes (9c874512…) M2
R3 Multiple liquidations and auctions can occur simultaneously without interf… Each auction is its own PDA; concurrency is isolation, not a lock Done on chain (v0.3): two auctions, each its own account, each closed on its own figures; fills overlapping in time are the M1 gate M1
R4 If a price feed fails for one collateral type, only that type's liquidatio… Feeds map to collateral configs, so a failure is scoped to one Done on chain (v0.3): with type A stale, a type B position liquidated in the next blocks (9b4cd452…, block 28,836) M2
R5 Other collateral types and non-liquidation operations continue unaffected. Non-liquidation operations are not gated on any feed Done on chain (v0.3): a bid on type A's open auction lands while A is paused (eb1f6ebf…, block 28,842) M2
R6 Critical invariants: seized collateral equals collateral in auction; debt … Seized equals sold plus left, from two counters moved separately, asserted after every fill and readable off the account Checkable on chain by anyone, and over 600 partial fills off it M1
P1 Liquidation transactions complete within one block. One instruction, no multi-transaction choreography Inclusion block against submission block, measured M2
P2 The system supports many concurrent auctions without exceeding compute lim… Cost flat in the number of live auctions: each is its own account Measured on v0.3: a partial fill is 54,058 cycles, 0.54% of the 10M gas cap, a re-struck fill 55,481, 0.55%, and the heaviest instruction, liquidate, 98,382, 0.98%; CI re-measures the committed binary and fails if the table drifts M4
P3 Bid transactions complete within one block. One instruction per bid On v0.3 every applied bid in the run is one transaction in one block (blocks 15641, 15647, 15660, 15686). Timing measured on v0.2.4 (before the 2026-09-30 reset; historical): a fill proved in 62 s and landed in the next block. Re-measured on the current testnet in M0 M1
S1 The program is deployed and tested on LEZ devnet/testnet. Deployed and driven on the public testnet Done on v0.3: header ADLHPyt… naming ImageID dc3618e8…, lifecycle driven 2026-10-15 in blocks 15515 to 15691, re-checked 2026-10-16 at 26 checks, 0 failures. The v0.2.4 deployment (ImageID 38ab5886…, deploy ac43ac1a…, block 20377) went with the 2026-09-30 reset; historical. Committed: redeployed and the verifier re-run on testnet 0.4 within days of its launch M1
S2 End-to-end integration tests run against a LEZ sequencer (standalone mode)… e2e against a standalone sequencer with RISC0_DEV_MODE=0, in CI The job runs; a skipped step counts as not run and fails the build M6
S3 CI must be green on the default branch. Green on the default branch Green today on the auction crate; badge in the README M6
S4 Every hard requirement in Functionality, Usability, Reliability, and Perfo… One named test per hard requirement, in a coverage matrix The matrix, with rows marked where a stand-in is the counterparty M6
S5 A README documents end-to-end usage: deployment steps, program addresses, … README with deployment steps, addresses and CLI walkthrough Partly done: DEPLOYMENTS.md already carries addresses and the recipe M6
Pr1 The mini-app and SDK must support both direct public account interaction a… The operation handle can only be consumed by reshielding A test asserting the refusal to drop it unreshielded M5
Pr2 When using the private account path, the SDK must validate that the target… Target validated as shielded before submission, explicit typed error Public target rejected, private control passing in the same run M5
Pr3 The ephemeral public account created during the deshield step must never b… A fresh ephemeral per operation, published Done twice already: two distinct ephemerals, no prior history, never reused M5

That is all 30 numbered hard requirements.

Milestones, Payout and Timeline

Sequential and gated. No upfront payment on any milestone, including M0:
each is invoiced once it meets its stated gate, and Logos never pays ahead of
delivered work. Durations are calendar weeks for the two of us working
together, and they sum to the 12 the RFP asks for.

# Deliverable Effort Payout
M0 Cost baseline and the two spikes that could change the design 1 wk $3,500
M1 The auction house, hardened 2 wk $7,000
M2 Liquidation, prices, and failure isolation 2 wk $7,000
M3 Debt and surplus auctions, and the buffer 1.5 wk $5,000
M4 The host interface, and a reference host 1 wk $3,500
M5 SDK, CLI, mini-app, and the privacy path 1.5 wk $5,500
M6 CI, tests, docs, and the testnet bundle 1 wk $3,500
M7 External audit coordination and remediation 2 wk $8,000
Eight milestones, paid on acceptance $43,000

M0 — Cost baseline and the two spikes that could change the design. 1 week,
$3,500. Measure the zkVM cycle cost of a fill, a liquidation and a settle
under the executor, tagged to the testnet version they were taken against,
because the per-transaction budget moves between releases. Two go/no-go spikes:
whether a chained call can seize from a host and initialise auction state in one
transaction, and what a liquidator's polling loop costs at a realistic position
count now that events do not exist. The external audit slot is also booked in
this week — firms are scheduled months out, and an audit milestone that starts
looking for a firm in week 11 is an audit milestone that slips. Gate: cost
numbers delivered and reviewed with Logos; both spikes answered in writing,
including if the answer is unwelcome; audit engagement confirmed in writing.

M0 shapes how we build; it does not decide whether the engine ships.

M1 — The auction house, hardened. 2 weeks, $7,000. What is on chain
today plus the missing lifecycle: reserve price, minimum bid, deadline handling,
and the re-entrancy and transition-pair tests in full. Gate: every F2, F3, F6,
F7, R1, R3 and R6 test green, and the lifecycle re-driven on testnet with each
refusal recorded by hash — which on this runtime is the only trace one leaves.

M2 — Liquidation, prices, and failure isolation. 2 weeks, $7,000. The
permissionless trigger and its incentive; the PriceSource trait with an
in-repo time-weighted reference; staleness pausing one collateral type while the
rest continues. Gate: three unrelated signers each liquidate on testnet; a feed
allowed to go stale on chain pauses its type only, with an open auction of that
type still accepting bids in the same run.
That run is already on the public
testnet (2026-10-17, 52 checks, 0 failures); M2 pays for the PriceSource
trait as the boundary RFP-020's adaptor plugs into, and the incentive and staleness parameters a host sets.

M3 — Debt and surplus auctions, and the buffer. 1.5 weeks, $5,000. Both
houses over a configurable token address, the surplus buffer, and the freeze
fallback when bad debt exceeds it. Gate: the shortfall path e2e against a mock
token; the token-unset branch demonstrated as the default configuration; the
minimum-improvement rule tested at the sizes where a percentage rounds to
nothing.

M4 — The host interface, and a reference host. 1 week, $3,500. The
CollateralSource interface, a minimal in-repo host implementing it as the
conformance target, and the published cycle table. Gate: seizure driven through
the interface on testnet; cycle costs published and shown flat in the number of
live auctions rather than asserted to be.

M5 — SDK, CLI, mini-app, and the privacy path. 1.5 weeks, $5,500. The
SDK with a non-skippable reshield and shielded-target validation; the CLI; the
Basecamp module reading live state; the position list and the bid preview.
Gate: the module loads in Basecamp with a deliberately failing control beside
it; a public reshield target rejected by name with a private control passing in
the same run; preview and fill diffed equal.

M6 — CI, tests, docs, and the testnet bundle. 1 week, $3,500. e2e
against a standalone sequencer at RISC0_DEV_MODE=0 in CI, the coverage matrix
with one named test per hard requirement, and a README a reviewer can follow
without us. Gate: CI green on the default branch with the e2e job actually
running — a skipped step counts as not run — and the published addresses aged
past the explorer's indexing lag before we call it done.

M7 — External audit coordination and remediation. 2 weeks, $8,000. The
firm booked in M0 reviews the frozen tree. This figure is our work: scoping
the review, coordinating the firm, remediating what it finds and re-verifying
afterwards. The auditor's own fee is a separate itemised line, to be scoped
with Logos.
A review by a firm of the standard an engine holding seized
collateral needs costs an order of magnitude more than any milestone in this
plan; folding it into one of them would put a number in this proposal that could
not survive being checked, which is the opposite of what the rest of it is for.
Priced at the same weekly rate as the other milestones here. The engine
custodies seized collateral, and the proposal template asks for an audit
milestone where a project handles funds. Gate: report delivered, every finding
remediated or accepted in writing with the reason, re-verification passed.

On the number. $55,000 all-in: $43,000 of milestone payouts to us,
plus the external auditor's own fee of $12,000 — money that goes to the
firm and never reaches us, itemised here so the figure is a budget and not a
starting position. We propose Trail of Bits, Zellic, NCC Group or another firm
with a Rust and zkVM practice, mutually agreed with Logos; the scope is the
arithmetic core, the escrow path and the guest program, and a broader engagement
is a separate decision with a separate number rather than a surprise. It is not
under-scoping. The auction arithmetic, the SPEL program and the deploy-and-drive
path are already built and already paid for by us, which is what an experience
dividend looks like from the buyer's side. Between us we have shipped four
programs on this stack, one of them merged into this programme's own repository,
so the ramp-up another team prices into month one is not in this plan.

On the schedule. The RFP estimates 10–12 weeks. The durations above are
effort, not calendar; run strictly sequentially they sum to 12 weeks, and
that is the number we commit to — the top of the range rather than the middle,
because each milestone here is gated on evidence produced by the one before it
rather than on a hand-off. The critical path is M0 → M1 → M2 → M3 → M6 → M7,
which is 9.5 weeks.
M4's host interface and M5's SDK, CLI and mini-app sit on
the second track from the end of M1 — the role split declared above rather than
a scheduling convenience — and they are inside the twelve only because we would
rather gate their payment behind the engine being right than run them early for
the calendar. M7's audit is a third party's diary, not ours, which is why the
firm is booked in M0. We flag rather than hide the one dependency we do not
control: M2's price work and M4's host work are built against in-repo
references, so neither slips if RFP-019, RFP-020 or a CDP host lands late — but
the conformance swap to whichever Logos selects is post-delivery work, and it
sits in the Post-Delivery Plan rather than being smuggled into a gate here.

Total Requested Budget (USD)

$55,000

Relevant Experience

We have already shipped on this exact stack, in this programme, and it is
public and checkable.

λPrize LP-0005 — Private Token Balance Attestation. Won, and merged into
logos-co/lambda-prize.

Since then, on LEZ v0.3 and Basecamp 0.3, all public:

  • RFP-017's vesting program, https://github.com/edenbd1/antumbra-lez: a v0.3
    program with custody through program PDAs, private claims into shielded
    accounts, a Basecamp module, and an end-to-end run on the public testnet that
    a script re-checks against the sequencer from a clean clone.

  • Logos Forum, λPrize LP-0026 (Solution: LP-0026 - Logos Forum lambda-prize#168),
    https://github.com/edenbd1/lp-0026-forum: a serverless forum for Basecamp on
    Logos Delivery and Storage, with its own module catalog.

  • Accord, https://github.com/edenbd1/lez-accord: an M-of-N multisig with a
    private vault, deployed on the public testnet, its approvals collected over
    the Logos chat module.

    Between them these cover the surface this RFP asks for: LEZ programs and
    IDLs, PDAs and account models, the shielded/public account boundary,
    privacy-preserving transactions, Logos Core modules and Basecamp mini-apps,
    CLIs, and end-to-end runs against a real LEZ sequencer.

The math for this RFP is already written, public, and running in CI.
https://github.com/edenbd1/antumbra-lez — MIT OR Apache-2.0, 50 tests green.

The CI is worth thirty seconds of your time, because it checks the claims and
not only the tests:

  • the suite runs in release, since that profile sets
    overflow-checks = true — a debug run exercises different arithmetic from the
    one that ships, which is how a wrapping bug reaches production past a green
    build;
  • the committed differential vectors are regenerated and diffed against
    their Python generators, because a committed oracle's real failure mode is
    not being wrong, it is being edited to make a failing test pass;
  • and the zkVM cycle table is re-measured on GitHub's own runners under the
    RISC0 executor and compared against the published document, so a figure we
    quote here and a figure a reader reproduces cannot disagree.

That last job is the one to look at:
https://github.com/edenbd1/antumbra-lez/actions/workflows/ci.yml — green,
including the executor run, which means every cycle count in this proposal is
reproducible by someone who has never spoken to us.

Eden Baudin — privacy and RWA engineer, ex-DevRel in privacy computing
across RWA and DeFi: the side of the table that finds out which document is
wrong and which SDK breaks under a real integration. 15+ hackathon wins, which
is the same shape as a milestone: a working system against a fixed date.
Incubated at the Berkeley Blockchain Xcelerator.

Nada Ahmadi — DeFi and AMM engineer. Podium finish on the Uniswap track at
ETHGlobal Lisbon 2026 with Umbra (https://github.com/nadaamd/umbra), an
autonomous circuit breaker that risk-scores a pool and evacuates a position
through an on-chain Uniswap v3 swap, backtested over 48,000 real swaps —
directly the AMM-pricing and differential-backtesting work this RFP needs.
Full-stack developer at XRPL Commons. Member of DeVinci Blockchain, ESILV.

How this maps to the recommended team profile. The RFP lists six
competencies. Here is where each already exists rather than being promised.

The RFP asks for Where it is, and how you can check
DeFi protocol design (liquidation systems, auction mechanisms) The auction house above, deployed and driven — plus the finding that the RFP's own named reference cannot be transcribed on this platform
Solana or SVM program development LEZ's account model is SVM-shaped — PDAs, ATAs, IDLs, program-owned state — and SPEL is its Anchor. Four programs written against it, one merged after review
Auction theory and mechanism design src/lib.rs and src/settlement.rs — the discount schedule, the partial-fill rounding proof, and the minimum-improvement rule that stops working at small sizes
Oracle integration and price feed security src/liquidation.rs — staleness as a state rather than an error, isolated per collateral type, with the age measured so a future timestamp cannot pause everything
Writing and running on-chain tests 41 tests green with no dependencies, plus an on-chain check script whose controls are twelve refused instructions pinned by final-state hashes and a random hash that must not resolve; it counts them itself rather than quoting a number here
Front-end development for DeFi applications Nada Ahmadi — Umbra, a Uniswap-track podium at ETHGlobal Lisbon 2026, and full-stack work at XRPL Commons

Post-Delivery Plan

Six months of support, included, with no ongoing cut. No revenue share, no
protocol-fee percentage, no retainer. Logos gets a maintained primitive rather
than a commercial dependency.

The three swaps we have designed for, and will perform. Each is a trait
boundary today, so each is a small change rather than a rewrite: the price source
to whichever of RFP-019 or RFP-020 Logos selects; the admin and freeze adapters
to RFP-001's and RFP-002's libraries when their third milestones land; and
custody to LP-0013's transfer authorities if a deployment ever needs a payer that
is neither the signer nor the program. We will do these within the support
window at no additional cost.

If the runtime gains events, we adopt them. Our v0.2.4 verify script
asserted on every run that getTransactionReceipt, getEvents and getLogs
answer Method not found, and on the v0.3 testnet they still did on 2026-10-16.
The day that stops being true, we replace the polling
discovery path with a subscription — which is a better liquidator bot and a
cheaper one.

Version churn is ours. LEZ is pre-mainnet and its runtime will move. Cycle
figures are published tagged to the testnet version they were measured against,
and keeping the engine current across upgrades during the support window is part
of this commitment rather than a change request. That starts with testnet 0.4:
we redeploy and re-run the verifier there within days of its launch.

Beyond six months, we are glad to keep maintaining it; if Logos wants a
longer operating commitment with an SLA, we will scope and price one separately
rather than promise a response time we have not staffed.

Permissions and Consent

  • I/We confirm Logos may contact me using the primary contact information provided above for follow-ups and next steps.
  • I/We consent to Logos using information from this proposal publicly such as blogs case studies social posts or analytical reporting. Redactions can be requested at any time.

Program Requirements

  • I/We have read and agree to the Logos RFP Terms and Conditions and I understand that no Grant is awarded and no right to payment arises unless a Grant Agreement is executed.
  • I/We understand that RFP specifications are proposals rather than instructions, that Logos makes no representations as to their legal or regulatory treatment, and that we are responsible for assessing what we build, deploy or operate and for complying with the laws that apply to us.
  • I/We understand this project must be open-sourced under the MIT and Apache 2.0 Licenses unless explicitly approved otherwise.
  • I/We are prepared to deliver milestone-based outcomes.

Activity

  1. edenbd1 commented on Aug 23, 2026

    @edenbd1
    Author

    Demo, recorded against the public LEZ testnet

    antumbra-014-liquidation-auction.mp4

    Nothing here is mocked or replayed from a file: every command is really executed
    in the session you see, and every figure on screen is derived from account data
    fetched during it rather than from numbers we already knew. Every check the film
    performs re-runs from a clean clone with one command:

    git clone https://github.com/edenbd1/antumbra-auction && cd antumbra-auction
    ./scripts/verify-onchain.sh

    The middle of the film is the part worth the two minutes. It fetches two
    accounts on the same public chain: the first build of this engine, which reports
    itself fully raised on a lot its own configuration could never have sold for
    that much, and the build that fixes it, which closes short and books the
    shortfall. Two public reads, no cooperation from us.

    Program antumbra_auction
    ImageID 38ab5886d20784c8322d1c6a3595cc8a666568946acb63248bac8ecf1149bd57
    Deploy transaction ac43ac1a…07a75b
    Source https://github.com/edenbd1/antumbra-auction

    Subtitles are burned into the picture, so the film is followable with the sound
    off.

  2. edenbd1 commented on Oct 15, 2026

    @edenbd1
    Author

    Update: ported to LEZ v0.3 and running on the public testnet.

    The public testnet was reset to v0.3 on 2026-09-30 and moved to sequencer v0.3.1 on 2026-10-12, so the v0.2.4 transactions this proposal cites no longer resolve. antumbra_auction is now rewritten on v0.3's plan-and-apply model and deployed again:

    • Program ADLHPytqfLnzM2hfoJUx6ZXPLwFo6o4RmqXWRJGETMJ7, ImageID dc3618e8…: the committed binary, a Docker build that CI rebuilds from source byte for byte (DEPLOYMENTS.md).
    • The lifecycle on the public testnet (2026-10-15): two configurations side by side, an auction opened by its config's admin with its schedule checked against the config, bids filled at the discount the chain's clock has reached (the same bid buys more collateral 60 s later), the target met with the last bid clamped, and every refusal the RFP asks for: 8 transactions applied, 12 refused exactly where they must be, 11 state checks (evidence/v03/testnet.tsv).
    • Re-check it yourself, with curl and python3 only: ./scripts/verify-onchain.sh --manifest evidence/v03/testnet.tsv --rpc https://testnet.lez.logos.co gives 26 checks pass, 0 fail (output).
    • What v0.3 changed in the design: a bid can no longer name a time the chain has not reached, so nobody can price a fill at a deeper discount than the schedule has reached; configs and auctions live in separate seed spaces; four new refusal codes (8011 to 8014). The arithmetic crate, its 41 tests and its differential vectors are unchanged; 15 host tests and 16 tests of the committed binary through lee::V03State reproduce the v0.2.4 run to the base unit.

    What changed and why: docs/deploy-v03.md.

  3. edenbd1 commented on Oct 17, 2026

    @edenbd1
    Author

    Update: liquidation, the time-weighted feed and the debt auction run on the public testnet (F1, F4, R2, R4, R5).

    The build that adds them (ImageID 72c22843aa2c128f2e580ed7d96a478e6184b9d734dd09abe29199c1c4c1a433) is a second, immutable deployment at J3TXJsr7U31F8G9YEVFCebeeCjd7Ro7f8t4KU4eB3iMy. ADLHPytq… and its evidence stay as they are. The run took blocks 28,593 to 28,857 on 2026-10-17:

    Requirement On chain
    F1 permissionless trigger Three fresh signers each liquidate one position. None of them holds a balance. Transactions: f3f6bc1a56d58f8b…, 1e07981582dfcdb1…, acf1fb8347b381f2…. Liquidating a safe position is refused, and so is a second liquidation of the same position.
    F4 debt auction A lot that ran out short has its shortfall booked by anyone. A wrong amount and a second booking are both refused. Bids that improve by less than 3% are refused. The auction settles to the best bid once its window closes (7482fd7711bf9aff…).
    R2 time-weighted price, staleness The run liquidated at the time-weighted price of 1588.24, against a new print of 1000. Once type A's feed is stale, liquidating an A position is refused (b91ba757561b8bb4…). When the feed resumes, the same position is liquidated (9c874512f7f5b590…).
    R4 failure scoped to one type While A is paused, a type B position is liquidated (9b4cd45265ff82e2…).
    R5 other operations continue A bid on type A's open auction lands while A is paused (eb1f6ebf82f1f6a1…).

    Totals: 26 transactions applied, 11 refused as required, 22 state checks read back, 0 failures. Anyone can re-check the run:

    ./scripts/verify-onchain.sh --manifest evidence/v03/liquidation/testnet.tsv

    It passes 52 checks with 0 failures. One refused transaction lost its hash index in a block rewrite, so getTransaction returns null for it. The verifier finds it in block 28,718 by scanning for its hash. Changing that hash makes the check fail. Manifest, transcript and the full scenario are in evidence/v03/liquidation/.

    The proposal body is updated to match. It now carries these rows and the earlier v0.3 corrections, and the cycle table has been re-measured on this build. Prices and milestones are unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

RFP-014Proposals for RFP-014proposalProposal submitted for an RFP

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions