Guarantees and limits
Five properties this contract makes unconditional, each proved against the deployed source, and the three bounded exceptions, each with its bound stated exactly. Read this page before depositing.
Most documentation of a custody contract asserts safety and leaves the reader to find the exceptions in the source. This page does the opposite. The guarantees below are real and they are absolute, and they are worth more, not less, for sitting beside an honest account of what they do not cover.
Every claim cites the function that enforces it. The source is published, and any deployment the house publishes must be an exact byte match to it, at 0 differing bytes, metadata included. The verification steps are there to run first; every other claim here is worthless if the code at a published address is not the code below.
The guarantees#
-
The house can never move your free balance#
Not to itself, not to a consignor, not to a recovery address, not under any finding, default, suspension or emergency. There is no function that transfers unlocked collateral out of the contract, and none can be added.
Proof. The contract makes exactly two outbound token transfers in its entire surface.
claimWithdrawtransfers tomsg.sender: the owner of the balance, and nobody else.settletransfers totreasury, animmutable, and revertsInsufficientLockwhenlockOf[bidder][lot] == 0. There is no third transfer.lockandreleasemove no tokens at all; they only move a number between the free and locked columns of the same bidder's own balance. -
Your exit cannot be blocked, paused, or delayed by anyone#
Once the timelock has run, claiming is a transaction you sign that reads nothing the house controls. There is no state the contract can be put into (paused, settler revoked, guardian hostile, servers gone) in which a ripened claim fails.
Solidity function claimWithdraw() external { uint256 amount = pendingWithdrawalOf[msg.sender]; if (amount == 0) revert NothingPending(); if (block.timestamp < withdrawableAt[msg.sender]) revert TooEarly(...); pendingWithdrawalOf[msg.sender] = 0; withdrawableAt[msg.sender] = 0; balanceOf[msg.sender] -= amount; emit WithdrawClaimed(msg.sender, amount, balanceOf[msg.sender]); if (!IERC20Minimal(token).transfer(msg.sender, amount)) revert TransferFailed(); }Proof. Read the signature:
external, and nonotPausedmodifier. Read the body: no reference tosettler,guardianorpaused. The same is true ofrequestWithdrawandcancelWithdraw. The guardian's brake is deliberately built to freeze the house's side of settlement while leaving every bidder-side exit open. -
A settlement has exactly one possible destination, fixed at deployment#
Even a fully compromised house key cannot send your collateral to an attacker. It can only push it to the address the contract was born with, an address the house already controls, which turns theft into misappropriation and makes the loss recoverable in the ordinary legal way rather than gone.
Solidity address public immutable treasury; // ... if (!IERC20Minimal(token).transfer(treasury, amount)) revert TransferFailed();Proof.
treasuryisimmutable: written once in the constructor, no setter, not storage.settletakes no destination argument. And the separation is held across rotations, not just at deployment:acceptSettlerandacceptGuardianboth revertRolesMustDifferwhen the caller is the treasury, because a constructor check alone is walked past by two ordinary, fully authorised role rotations. -
There is no upgrade path and no admin override#
No proxy, no
delegatecall, no implementation slot, noselfdestruct, no owner, no pausable-upgradeable base, and no function that changes a rule. The bytecode at that address is the whole of the agreement and it cannot be edited under you. A change of rules is a new contract and a public migration.Proof. The constants are compile-time:
VERSION,MAX_LOCK_AGEandSETTLE_DELAYareconstant;token,treasuryandwithdrawDelayareimmutable. The only mutable state the roles can touch issettler,guardianandpaused, each through a named two-step transfer or the brake. Search the verified source fordelegatecallandselfdestruct: neither appears. -
Encumbrance is bounded, and the release is permissionless#
No lock can hold your money for ever, and freeing an overdue lock does not require the house to cooperate, be online, or exist.
Solidity /// @notice Release a lock that has stood past MAX_LOCK_AGE. Callable /// by ANYONE, deliberately. Not pausable. function expireLock(address bidder, bytes32 lot) external { ... }Proof.
expireLockhas no access modifier and nonotPaused. Any address may call it against any lock whoselockExpiresAthas passed, which is 30 days after the lock was created or last raised. If the guardian has calledrevokeSettler, the age requirement is waived entirely and every lock becomes expirable at once, because with no settler there is nobody who could ever settle or release it and holding the bidder to a timer would be punishment for the house's incident.
A sixth, about the roles#
The guardian, the incident key, cannot appoint a settler. Not "cannot appoint itself": cannot appoint at all. transferSettler is onlySettler, and the power is absent from the guardian rather than guarded by a check.
That is deliberate, and the reasoning is worth repeating because it is the kind of thing usually got wrong. A check like "the new settler may not be the guardian's address" compares addresses. Separation of duties is about parties. A guardian holding a second address proposes it, accepts from it, and now holds both roles, a pause-only key that can encumber every bidder's free balance and push locked collateral to the treasury. No on-chain check can tell that address apart from an honest new settler key. So the power is not there to abuse.
The limits#
Three. None of them is a bug, and each is the cost of a property the system needs. Each has a bound, and the bound is a number rather than a reassurance.
1. The house can encumber your free balance#
This is what collateral is. When you take the lead, the house locks your maximum plus its fee, and it does not ask you at the moment it does so, because your bid was the authorisation. But it means a balance sitting free in the escrow is not equivalent to a balance in your own wallet: the house's key can make it unwithdrawable by locking it against a lot.
A compromised settler key can do this too, against invented lot ids, to any bidder with a balance in the contract.
- Bound
- Any individual lock dies 30 days after it was created or last raised, after which anyone may expire it.
- Bound
- One continuous encumbrance episode is capped at roughly 60 days. After 30 days of unbroken encumbrance the contract refuses to add any more (
EncumbranceDue): not under a fresh lot id, not by raising an existing lock, not by drawing on a pending withdrawal. The last lock creatable is created an instant before that, and runs its own 30 days. - Bound
- Episodes cannot be chained. A due bidder clock may only restart after the total has reached zero and the bidder has been free for
2 × withdrawDelay + SETTLE_DELAY, which is 2 hours 2 minutes here: a real chance to request, wait, claim, and survive transaction ordering. - Not bounded by
- Lot ids. The settler holds 2256 of them and rotating through them buys nothing, because the episode clock is keyed to the bidder and does not look at the lot at all.
What you can do about it. Withdraw what you are not actively bidding with. Free balance in your own wallet cannot be locked by anybody.
2. A requested withdrawal is not a guaranteed exit#
Until you claim, the tokens are still in the contract, and a lock may draw them back into collateral. This includes a request that has fully ripened and is sitting unclaimed: lock draws on pendingWithdrawalOf without consulting withdrawableAt, so a matured request can be pulled back, up to all of it.
This is the deliberate correction of the contract's worst historical bug. In version 2 the lock simply reverted in this case, so a bidder could take the lead, immediately request a withdrawal, and stand with zero collateral behind a live bid while the deposit walked out an hour later. Bid, push the price, never pay. Drawing the shortfall back is what makes the collateral real.
- Cost
- It genuinely increases the settler's reach: money you have asked to leave can be re-encumbered.
- Bound
- The same two clocks as limit 1: 30 days per lock, ~60 days per episode, and the episode cannot restart without a real exit window.
- Bound
- Once a lock has been expired, no lock against you may rise for
2 × withdrawDelay + SETTLE_DELAY. Two delays, not one: a withdrawal is two steps a delay apart, so a one-delay window is exactly not enough and loses to the settler on block ordering. The extraSETTLE_DELAYis the margin after the latest possible claim ripens. - Visible
- The contract emits
WithdrawReduced(bidder, lot, amount, stillPending)when it happens. You can see the moment, not infer it.
What you can do about it. Claim. Do not leave a ripened withdrawal sitting. The claim is one transaction and nobody can stop it.
3. A pause freezes locked balance, never free balance#
The guardian's brake stops deposit, lock, settle and release. Because release is inside the brake, a locked balance is not reachable while the pause is on: an outbid bidder whose lock the settler has not yet released waits.
release is inside the brake on purpose. It is the settler's function, and the server answers a reverted settle by releasing the winner's collateral, so a brake that left release outside would, in an incident, hand every in-flight winner their money back with the invoice unpaid. Inside the brake the settlement state freezes rather than unwinding. A bidder loses nothing they could rely on, because release was never a path they could take: expireLock is, and that one is not pausable.
- Not affected
- Free balance, always.
requestWithdraw,claimWithdraw,cancelWithdrawandexpireLockare all outside the brake. - Bound
- Standing locks keep draining during a pause.
expireLockis not pausable, so 30 days is the ceiling on how long a pause can hold any lock. - Bound
revokeSettlermakes every lock immediately expirable, collapsing that wait to zero. It is terminal, no settler can ever be appointed again, and it also pauses, because a deposit into an escrow that can never lock, settle or release is a trap.- Cannot happen
- A revoked escrow cannot be unpaused.
unpauserevertsSettlerRevokedwhensettler == address(0).
The worst case, stated in one paragraph#
The thief can lock every bidder's free balance against lot ids they invent, and, no sooner than two minutes after each lock, settle those amounts to the Minthouse treasury. They cannot send a cent to an address of their own, because no function accepts a destination. They cannot reach a balance that is already out of the contract. They cannot stop a claim that has ripened. The guardian ends it with pause or, terminally, revokeSettler, which makes every lock expirable at once. What a bidder is exposed to, precisely, is: collateral misappropriated to an address the auction house already controls, and an encumbrance of at most one episode.
The full analysis of each key, each role, and each attacker is Threat model.
What is not claimed#
So that nobody has to guess at the edges of the page above:
- Not non-custodial. The contract holds customer funds and the house operates a key against it. The house says so in the contract's own header.
- Not audited to exhaustion. Two independent audits, a ten-lens panel, 161 Foundry and 934 Node tests, and a clean Slither triage, but no formal proof of the full state machine, no bug bounty currently running, and no insurance on balances.
- Not proof against your own key loss. Balances are keyed to the depositing address and cannot be reassigned. That is the same property that stops the house doing it to you.
- Not private. Your lock, and therefore your maximum bid, is public and permanent.
- Not instant, in a disaster. The exit from a locked balance is unconditional and needs nobody's cooperation, but it can take up to 30 days.