Skip to content

English Auction Smart Contract

The English Auction contract (contracts/auction/src/auction.asm) implements an ascending-price auction on the Minichain VM. Bidders compete by placing ever-higher bids; when the timer expires the highest bidder wins and the seller can withdraw the proceeds.

PropertyValue
Sourcecontracts/auction/src/auction.asm
Compiled size~726 bytes
Storage slots7 fixed + 1 per bidder
Gas (init)< 500 000

RoleDescription
SellerDeploys the contract. Receives the winning bid after the auction ends.
BidderSends value > 0 to place a bid. If outbid, their funds are credited to a refund ledger and can be withdrawn at any time.

All values are stored as u64 in bytes 24–31 (big-endian) of a 32-byte storage slot.

SlotNameDescription
0sellerAddress identifier of the auction creator
1highest_bidCurrent highest bid amount
2highest_bidderAddress identifier of the current leader
3end_timeUnix timestamp when bidding closes (deploy_time + 3600)
4reserve_priceMinimum acceptable bid — hard-coded to 100
5ended1 if the auction has been finalized, else 0
6min_bid_incrementMinimum amount by which a new bid must exceed the current highest — hard-coded to 10
10 + kpending_returns[k]Refund ledger. k = caller_id mod 2^56 gives each bidder a unique slot

The contract uses CALLVALUE and the caller identity to determine which function to execute:

┌──────────────────────┐
│ Any Transaction │
└──────────┬───────────┘
┌─────▼─────┐
│ Initialised?│──No──▶ init()
└─────┬──────┘
│ Yes
┌─────▼──────────┐
│ Time expired & │──Yes──▶ endAuction() (auto-finalize)
│ ended == 0? │ then continue…
└─────┬───────────┘
│ No
┌─────▼─────────┐
│ CALLVALUE > 0? │──Yes──▶ bid()
└─────┬──────────┘
│ No
┌─────▼──────────────┐
│ caller == seller? │──Yes──▶ seller_withdraw()
└─────┬───────────────┘
│ No
withdraw()

Triggered automatically on the very first transaction (when slot 0 == 0).

ActionDetail
Store sellerslot[0] = caller
Set end timeslot[3] = timestamp + 3600
Set reserve priceslot[4] = 100
Set min incrementslot[6] = 10
LogEmits LOG seller_id

Called when CALLVALUE > 0.

Preconditions (reverts if any fail):

  1. ended == 0 — auction is still active
  2. CALLVALUE >= reserve_price (100)
  3. CALLVALUE >= highest_bid + min_bid_increment

Effects:

  • Previous highest_bidder’s amount is added to their pending_returns slot
  • highest_bid = CALLVALUE
  • highest_bidder = caller
  • Logs: [previous_bidder_id, new_bid, new_bidder_id] (when outbidding) or [new_bid, new_bidder_id] (first bid)

Called when CALLVALUE == 0 and caller != seller.

Preconditions:

  • pending_returns[caller] > 0 (reverts if nothing to withdraw)

Effects:

  • Reads the caller’s pending return amount
  • Zeros the pending return slot
  • Logs: [amount, caller_id]

seller_withdraw() – Seller Claims Proceeds

Section titled “seller_withdraw() – Seller Claims Proceeds”

Called when CALLVALUE == 0 and caller == seller.

Preconditions:

  • ended == 1 — auction must have been finalized (reverts otherwise)
  • highest_bid > 0 — there must be proceeds (reverts on double withdraw)

Effects:

  • Reads highest_bid
  • Sets highest_bid = 0 (prevents double-claim)
  • Logs: [winning_bid, seller_id]

Called automatically at the start of every transaction when timestamp > end_time and ended == 0.

Effects:

  • Sets ended = 1
  • Logs: [highest_bid, highest_bidder_id]

RegisterPurpose
R0Storage keys (scratch)
R1Storage values / loaded data (scratch)
R2Comparisons & arithmetic (scratch)
R3Comparison results / flags (scratch)
R4CALLER — sender address identifier
R5CALLVALUE — bid amount
R6Jump targets
R7TIMESTAMP — current block timestamp
R8Pending returns key/value
R9-R11Scratch

These values are hard-coded in the assembly source and can be modified before deployment:

ConstantValueLineDescription
RESERVE_PRICE100LOADI R1, 100Minimum acceptable first bid
MIN_BID_INCREMENT10LOADI R1, 10Minimum bid-over-bid increase
AUCTION_DURATION3600LOADI R2, 3600Seconds from deployment to end
PENDING_RETURNS_BASE10.const PENDING_RETURNS_BASE 10First dynamic storage slot

Terminal window
# Alice deploys the auction contract
minichain deploy --from @alice --source contracts/auction/src/auction.asm --gas-limit 500000

Representative gas costs measured by integration tests:

OperationApproximate Gas
init()~170 000
bid() (first bid)~130 000
bid() (outbid)~210 000
withdraw()~120 000
seller_withdraw()~140 000

All operations stay well under the 5 000 000 gas limit used in tests.


The contract is exercised by 16 integration tests in crates/vm/tests/auction_test.rs:

#TestWhat it verifies
1test_init_sets_storageSeller, reserve price, end time, min increment are correctly stored
2test_first_bid_validA bid ≥ reserve is accepted; highest_bid and highest_bidder update
3test_bid_below_reserve_revertsBid < 100 reverts
4test_bid_below_min_increment_revertsBid within 10 of current highest reverts
5test_outbid_credits_pending_returnsPrevious bidder’s funds move to pending_returns
6test_withdraw_by_outbid_bidderOutbid bidder successfully withdraws; slot zeroed
7test_withdraw_nothing_revertsWithdraw with 0 pending reverts
8test_double_withdraw_revertsSecond withdraw after claiming reverts
9test_auto_finalize_on_time_expiryTransaction after end_time sets ended = 1
10test_bid_after_ended_revertsBidding after finalization reverts
11test_seller_withdraw_after_endSeller claims winning bid; highest_bid cleared
12test_seller_double_withdraw_revertsSecond seller withdraw reverts
13test_seller_withdraw_before_end_revertsSeller cannot withdraw while auction is active
14test_multiple_bids_accumulate_pending_returnsThree competing bidders’ refunds recorded correctly
15test_full_happy_pathEnd-to-end: init → bids → withdraw → seller claim
16test_gas_usage_is_boundedInit uses > 0 and < 500 000 gas

Run all tests:

Terminal window
cargo test --test auction_test -p minichain-vm

ConcernMitigation
Re-entrancyPull-over-push: bidders withdraw explicitly; no callbacks during bid
Double withdrawPending slot zeroed before logging; seller’s highest_bid zeroed after read
Bid after endended flag checked at the top of bid(); auto-finalize runs before dispatch
Integer overflowAll arithmetic uses 64-bit registers; practical bid ranges are well within u64
Seller self-biddingNot prevented — the seller can bid, but gains no advantage since they already own the item
No bidsIf no valid bids occur, highest_bid == 0 and the seller withdraw reverts (nothing to claim)

Seller deploys Bidder A bids 150
───────────── ─────────────────
┌──────────┐ time < end ┌───────────────┐
│ init() │ ───────────────▶ │ bid(150) │
│ seller=S │ │ highest=150 │
│ end=T+1h │ │ bidder=A │
└──────────┘ └───────┬────────┘
Bidder B bids 250 │ Bidder A outbid
───────────────── │ pending_returns[A]=150
┌───────────────┐ │
│ bid(250) │◀───────┘
│ highest=250 │
│ bidder=B │
└───────┬────────┘
time > end │ auto-finalize
─────────── │ ended=1
┌───────▼────────┐
│ endAuction() │
│ ended = 1 │
└───────┬────────┘
┌──────────────┼──────────────┐
▼ ▼ ▼
┌────────────┐ ┌──────────────┐ ┌──────────┐
│ A.withdraw │ │ seller_with- │ │ B wins! │
│ gets 150 │ │ draw → 250 │ │ (item) │
└────────────┘ └──────────────┘ └──────────┘