Development
This chapter presents the technical development of the work in four integrated blocks: the problem and the threat model (§4.1), the proposed solution (§4.2), the specification and modelling of the system (§4.3) and the concrete implementation (§4.4), closing with the consolidation of the guarantees obtained by the architecture (§4.5).
4.1 Problem and Threat Model
Before designing the solution, this section fixes what is being protected and from whom: it recaps the problem (§4.1.1), defines the adversary model (§4.1.2) and the privacy objectives required (§4.1.3), delimits the role of the TSN in the trust model (§4.1.4), qualitatively maps the leakage by auction state (§4.1.5) and declares the limitations of the model (§4.1.6).
4.1.1 Recapitulation of the problem
Chapter 3 closed by pointing out a specific gap: as of the date of this work, no documented academic implementation of a private Vickrey auction on Fhenix CoFHE, with formal analysis of information leakage, existed. The following sections of this chapter deliver that implementation. Before that, however, it is necessary to fix precisely what "privacy" means in the context of this mechanism. This section formally defines the adversary considered, the secrecy objectives required, the role of the Threshold Services Network (TSN) in the trust model and the declared limits of what the system protects. The corresponding quantitative analysis, with a measure of residual uncertainty about the losing bids, is presented in Chapter 5.
4.1.2 Adversary model
A conservative adversary model is adopted, close to what the private-auction literature usually calls an honest-but-curious adversary (one who follows the mechanism's protocol but tries to infer as much as possible from what it observes) at the platform level, combined with a malicious bidder (who may deviate from the protocol) at the application level. In concrete terms, the adversary is treated as a single entity (representing, without loss of generality, a collusion among multiple actors) with the capabilities described below.
As to what it observes, the adversary has full access to the public blockchain. That includes: every transaction circulating through the mempool (the public waiting area through which transactions pass before confirmation, defined in §1.1), every confirmed block, the contract's entire storage state, every event emitted, all calldata (the portion of the transaction that carries the function call's arguments), and every wallet address that signs transactions. In practical terms, it has permission equivalent to any query possible through a public block explorer.
As to what it controls, the adversary sends its own transactions with any content, at any moment, with any gas priority. It can operate one or several wallets simultaneously and coordinate with validators to influence transaction ordering within a block, as described in §1.2.2. It does not control, however, other people's transactions before they are propagated.
As to the objectives the adversary pursues, three are directly relevant to this work. First, it tries to infer the value of each individual bid submitted by other participants, above all the losing bids. Second, it tries to infer the winner's specific bid beyond what is necessarily revealed by the mechanism (that is, beyond the second price). Third, it tries to manipulate the closing price or the identity of the winner by means of its own strategic transactions, such as frontrunning, shill bidding (bids placed by the seller themselves to inflate the price artificially) or repeated submissions.
There is also what the adversary does not control, and those suppositions are the explicit premises of the model. In particular, it is assumed that at least t out of N TSN operators cooperate honestly: that is, they do not reveal the decryption key to the adversary and do not decrypt ciphertexts beyond those the contract authorises through FHE.allowPublic. It is also assumed that the underlying network (Arbitrum Sepolia, in the case of the implementation evaluated) is available, that is, valid transactions eventually make it into some block. The analysis of complete censorship scenarios, in which validators refuse to include transactions, is out of scope (see §4.1.6).
4.1.3 Privacy objectives required
Under the adversary model above, the system must preserve four properties simultaneously, listed in decreasing order of strength.
The first, and most central, is the permanent secrecy of the losing bids. For any participant who did not win the auction, the exact value of their bid must remain inaccessible to the adversary at all times, including after the auction has closed. This property is the fundamental economic reason that justifies adopting Fully Homomorphic Encryption as the cryptographic infrastructure of the mechanism, as discussed in §1.2.5 and §3.5.
The second is the partial secrecy of the winner's bid. The winner is necessarily identified by the mechanism, and the second-highest bid is necessarily published (it is the price to pay). But the winner's specific bid, in full, must remain hidden. It may be arbitrarily higher than the second price, and that slack must remain confidential.
The third is minimum necessary public knowledge. The Vickrey mechanism, by its nature, requires that two values be made public at the end: the identity of the winner (so that the item may be delivered to them) and the second-highest bid (so that this amount may be charged to them). No other individual bid data needs to be revealed, and none will be.
The fourth is the preservation of the truthfulness property intrinsic to the Vickrey mechanism, described in §2.5.5. Truthfulness depends on the bids remaining effectively sealed, which in turn depends on the three previous properties. Under the adversary considered, if the first three are guaranteed, the fourth follows automatically.
4.1.4 The role of the TSN in the trust model
The TSN is the articulation point between two regimes that seem incompatible in an FHE-based auction: full confidentiality during the computation and selective revelation at the end. Without it, either the bids would remain encrypted forever (and the mechanism could not deliver the item to the winner), or it would be necessary to trust a centralised operator to hold the decryption key (regressing to the trusted-auctioneer model that one set out to avoid). The TSN resolves that tension through threshold Multi-Party Computation (MPC, defined in §2.3.4), as described in §2.3.4.
The trust premise of the TSN is, formally, the following: there exist at least t operators out of the N in the network who behave honestly, that is, who decrypt only those ciphertexts that the Solidity contract has explicitly authorised through FHE.allowPublic. That premise is qualitatively analogous to the one in Proof of Stake validation systems (where an honest majority of validators is assumed) and to that of the whole threshold cryptography family. It is not a trivial premise, but it is the kind of premise that the distributed cryptography literature considers standard and that Fhenix operates as the basis of its commercial product (FHENIX, 2026b).
What the TSN guarantees, under that premise, is strong. First, no ciphertext is decryptable unless the Solidity contract has issued the corresponding authorisation. Second, authorised decryptions only produce a result when the quorum cooperates, and each result comes accompanied by a collective signature that the contract verifies before accepting it (via FHE.verifyDecryptResult). Third, the decryption key is never reconstructed at a single point in the network; it exists only in distributed shares.
What the TSN does not guarantee, and which must be said explicitly, is equally important to the model. It does not prevent operators from refusing to take part in a legitimate decryption (a situation in which the auction remains in a pending state until the quorum cooperates). It does not control the latency of the decryption round trip, which depends on the load and availability of the operators. And it depends, ultimately, on the economic and technical operation of Fhenix itself as a network; a scenario of permanent shutdown of the TSN would make the handles (short references to the encrypted data, which the contract stores in place of the ciphertexts) previously encrypted under its keys undecryptable forever, although confidentiality would still be preserved.
4.1.5 Qualitative leakage analysis by auction state
Combining the adversary model (§4.1.2), the privacy objectives (§4.1.3) and the role of the TSN (§4.1.4), it is possible to map, for each auction state, exactly what becomes visible and what remains hidden. The contract models four states, as described in §4.2.6: Active, SettlementRequested, Settled and Cancelled. Table 2 summarises the qualitative leakage in each of them; in it appears the collateral: the security deposit in ETH that each bidder hands over when submitting a bid, with a uniform value defined publicly by the seller at auction creation and returned at the end of the cycle (in full to the losers; minus the second price for the winner), as detailed in §4.2.5.
| State | Visible to the adversary | Hidden from the adversary |
|---|---|---|
Active | Number of bids submitted, bidder addresses, uniform collateral value, timestamp of each bid, seller identity, state | Value of each individual bid, partial identity of the winner, partial value of the highest and second-highest bids |
SettlementRequested | Everything visible in Active; plus two ciphertext hashes (winnerCtHash and secondPriceCtHash) marked as publicly decryptable | The plaintext values of the winner and of the second price (until the TSN responds); the values of all losing bids |
Settled | Everything visible in SettlementRequested; plus the identity of the winner (address) and the second price in plaintext, both verified via FHE.verifyDecryptResult | The winner's specific bid (it is known only to be greater than or equal to the second price); the values of all losing bids |
Cancelled | Number of bids (typically zero, since cancellation requires totalBids == 0), addresses, status | Not applicable in practice (no bids submitted) |
Table 2: Adversary capabilities by auction state.
Four observations about the table deserve emphasis. First, the adversary learns the bidders' identities from the outset. This is an unavoidable consequence of the bid transaction being signed by a pseudonymous but public wallet. Advanced mechanisms to conceal that information as well (for example, stealth addresses) are out of scope for this work (see §4.1.6).
Second, the adversary learns the number of bids but not their values, throughout the active period. This is an especially useful property in competitive auctions: the adversary knows how many competitors are taking part in the auction, but does not know how to price their own bid in order to beat them.
Third, the losing bids are never revealed, at any moment. They remain stored in the CoFHE coprocessor's state as opaque ciphertexts, and no subsequent transaction marks them as decryptable. Even the winner, if they wish to know by how much their bid exceeded the second price, can only inspect their own handle by means of a specific permit, as discussed in §2.3.5. The losing bids of the others remain encrypted indefinitely.
Fourth, the winner's bid is partially revealed. It is known, after Settled, that it is greater than or equal to the second price (since the winner is the highest bid) and less than or equal to the collateral deposited (since the contract caps the bid at the collateral by means of the operation described in §4.2.5). The interval [secondPrice, collateralAmount] is the space of residual uncertainty about the winner's bid, and Chapter 5 derives that interval in closed form and illustrates it with a numerical example.
4.1.6 Declared limitations of the model
Four classes of leakage are admittedly outside what the system protects. Recognising them explicitly is part of the methodological commitment of this work.
The first is temporal ordering leakage. The contract resolves value ties by order of arrival, which means that, in the event of a tie at the highest bid, the first address to submit it is declared the winner, as detailed in §4.2.4. This introduces an observable asymmetry: the adversary learns, indirectly, that the declared winner is the first to arrive with the winning value (in the case of a tie). Broader temporal analysis, including submission patterns as strategic signalling, is not treated as a relevant attack in this model.
The second is the winner's identity, revealed by design. The Vickrey mechanism requires the item to be delivered to the winner, which requires knowing publicly who they are. Mechanisms for further concealing the winner (for example, allocating the item to a stealth address derived from the public address) are possible in principle, but constitute an extension outside the scope of this work and are recorded as future work in Chapter 6.
The third is the public upper bound on bids given by the collateral. Each bidder deposits exactly the collateralAmount in plaintext ETH when submitting a bid, and that amount is publicly defined by the seller at auction creation. The adversary therefore knows that every bid is less than or equal to that cap. The choice of a uniform collateral (everyone deposits the same, rather than each choosing their own) mitigates individual leakage: instead of learning each bidder's own cap, the adversary learns only the common cap imposed by the seller. But the cap itself is public, and that reduces the inference space over the bids. This is an intrinsic limitation of on-chain auctions without additional credit mechanisms; it is discussed in more depth in §4.2.5.
The fourth is censorship at the consensus level. Validators may refuse to include a specific bid transaction, delaying or preventing its entry into the auction. Private mempools such as Flashbots Protect, discussed in §1.2.2, partially mitigate that vector, but complete defence against censorship is out of scope. The model in this work assumes reasonable liveness of the underlying network (the property that the network keeps processing and including transactions, without stalling indefinitely).
With those limitations recognised, the system guarantees, under the declared premises, exactly what was listed in §4.1.3: permanent secrecy of the losing bids, partial secrecy of the winner's bid, minimum public knowledge necessary to the mechanism, and truthfulness preserved. The technical solution that enables those guarantees is presented next.
4.2 Proposed Solution
This section presents the solution in layers: the overview of the architecture (§4.2.1), the exclusion problem that distinguishes second price from first price (§4.2.2), the incremental top-2 pattern that solves it at constant cost (§4.2.3), the deterministic handling of ties (§4.2.4), the defensive design decisions (§4.2.5) and the execution flow with the state machine (§4.2.6).
4.2.1 Overview of the architecture
The proposed solution is organised into four components that exchange messages in a well-defined topology. On the user's side there is the off-chain client, running in the browser. It is responsible for encrypting the participant's bids before anything is sent to the network, authenticating the participant through the wallet and orchestrating the retrieval of the decrypted result at the end. On the public network side there is the Solidity contract, named ConfidentialVickreyAuction, hosted on a public blockchain compatible with the Ethereum Virtual Machine (the EVM, as per §2.1.3); it holds the auction state, receives the encrypted bids, homomorphically computes the winner and the second price, and governs the mechanism's state machine. On the cryptographic infrastructure side there is the CoFHE coprocessor, which stores the actual ciphertexts and executes the homomorphic operations requested by the contract. And there is the Threshold Services Network (TSN), which jointly holds the decryption key and activates it, under authorisation from the contract, upon cooperation of a minimum quorum of operators. Each of these pieces was characterised in detail in Chapter 2 (§2.3.4 and §2.3.5); here the focus is on how they articulate.
Figure 1 summarises the topology. The client sends the contract an encrypted structure InEuint64, containing the bid pre-encrypted as homomorphic ciphertext of type euint64. The contract stores only short references to those ciphertexts (called handles) and delegates the actual encrypted computation to the CoFHE coprocessor, which operates on its own network outside the blockchain. At the end of the auction, the contract marks the handles of interest (that of the winner and that of the second price) as publicly decryptable and the TSN responds, upon the client's request, with the plaintext values accompanied by a collective signature, obtaining from the coprocessor the ciphertexts whose decryption the contract authorised. The client publishes those values back on the blockchain, and the contract verifies that the plaintext values correspond to the original encrypted handles by means of the FHE.verifyDecryptResult function. The on-chain verification guarantees that the client cannot invent arbitrary results, without needing the oracle callback (an external service that delivers data to the contract) adopted by Zama's fhEVM (HINDI, 2024).
Figure 1. Topology of the solution.
This topology has three properties that are attractive from the point of view of this work. First, it is client-orchestrated: the contract is passive between the settlement request and its finalisation, which reduces the coupling between on-chain and off-chain to a declarative two-step pattern. Second, it is modular: the CoFHE coprocessor is external to the host blockchain, which allows the same contract to be deployed on different EVM networks (Ethereum Sepolia, Arbitrum Sepolia, Base Sepolia) without modification. Third, it is verifiable on-chain: no plaintext value is accepted by the contract without a TSN signature over the corresponding ciphertext, which makes trust in the client irrelevant to the correctness of the mechanism.
4.2.2 The exclusion problem
The central property of the Vickrey auction, as established in §2.5.4, is that the winner (the highest bid) pays the value of the second-highest bid. Computing that rule in plaintext is trivial: sort the vector of bids in decreasing order, assign the item to the first and the price to the second. In FHE, however, this operation faces a structural difficulty, which the literature calls the exclusion problem.
The difficulty can be stated directly: to identify the second-highest value, one must first exclude the winner from the set of candidates. In plaintext, that exclusion is done by comparing each value with the maximum already computed and discarding whatever matches. In FHE, however, one cannot look at the values: both the bid and the maximum are ciphertexts; the comparison bid_i == max produces an encrypted Boolean (ebool), not an observable decision; and taking a conditional action on that Boolean (discarding the bid or keeping it) must also occur in the encrypted domain.
The standard solution in the literature, described in Faruukku (2026) in the context of Zama's fhEVM, is to build a homomorphic mask: for each bid b_i, compute isWinner_i = FHE.eq(b_i, max) (an encrypted ebool indicating whether the bid equals the maximum) and then maskedBid_i = FHE.select(isWinner_i, 0, b_i). The FHE.select operation (an encrypted ternary operator) replaces the winning bid with zero, without the contract having to know which bid that winner was. Applying FHE.max over the masked vector, the original second-highest value emerges.
This solution has three practical limitations for an on-chain implementation. First, it requires storing every encrypted bid throughout the auction, so that the vector can be revisited in the settlement phase; the storage cost in FHE is high, since each euint64 consumes a handle that occupies a storage slot in the contract. Second, it performs the second-price computation entirely in the settlement phase, at O(N) complexity in the number of bids; each iteration performs one FHE.eq and one FHE.select operation, both expensive (each triggers a TFHE bootstrapping, as per §2.3.1). Third, it requires explicit handling of ties at the top: if two bids equal the maximum, the mask zeroes both, and the second-highest erroneously falls to the third real value. Faruukku's (2026) VeilBid solves that last limitation with a deterministic first-match pattern, using FHE.and and FHE.not over exclusion flags accumulated iteratively, which adds still more comparisons per bid during settlement.
4.2.3 The incremental top-2 pattern
The solution adopted in this work starts from a simple observation: it is not necessary to keep every bid individually in order to compute the second-highest value. It suffices to maintain, throughout the auction, two aggregate ciphertexts: the current highest bid (highestBid) and the current second-highest bid (secondHighestBid). With each new bid that arrives, those two values are updated in a single pass, without revisiting the history. The algorithm is exactly the one that would be used to maintain the top-2 of a stream of plaintext values, with the difference that every operation is homomorphic.
The pseudocode is as follows. For each new bid b submitted by bidder X (Listing 1):
higherThanFirst = FHE.gt(b, highestBid)
higherThanSecond = FHE.gt(b, secondHighestBid)
newSecond = FHE.select(
higherThanFirst,
highestBid, # the old first drops to second
FHE.select(higherThanSecond, b, secondHighestBid) # or b becomes the new second
)
newFirst = FHE.select(higherThanFirst, b, highestBid)
newWinner = FHE.select(higherThanFirst, X, highestBidder)
highestBid = newFirst
secondHighestBid = newSecond
highestBidder = newWinnerListing 1: Pseudocode of the incremental top-2 pattern executed on every bid.
The real implementation in Solidity, in the relevant excerpt of the ConfidentialVickreyAuction.sol contract, reproduces that pattern literally (Listing 2):
ebool isHigherThanFirst = FHE.gt(bidAmount, auction.highestBid);
ebool isHigherThanSecond = FHE.gt(bidAmount, auction.secondHighestBid);
euint64 newSecond = FHE.select(
isHigherThanFirst,
auction.highestBid,
FHE.select(isHigherThanSecond, bidAmount, auction.secondHighestBid)
);
euint64 newHighest = FHE.select(isHigherThanFirst, bidAmount, auction.highestBid);
eaddress newHighestBidder = FHE.select(
isHigherThanFirst,
FHE.asEaddress(msg.sender),
auction.highestBidder
);Listing 2: Homomorphic update of the top-2 aggregates in the contract's bid function.
This design differs from the iterative-mask pattern along three measurable dimensions. First, the cost per bid is constant with respect to N: each bid executes exactly two FHE.gt comparisons and four FHE.select operations, regardless of how many bids have already been submitted. In the iterative mask, the settlement cost grows linearly in N (one FHE.eq and one FHE.select per bid, inside a loop). Second, the on-chain storage per auction is constant: only three encrypted handles are persisted (highestBid, secondHighestBid, highestBidder), instead of N individual handles. Third, the settlement phase becomes trivial: since the second price is already computed by the time the auction closes, the requestSettlement function merely marks the corresponding handle as publicly decryptable; no additional comparison is necessary. Chapter 5 quantifies the impact of these three properties on gas and latency.
4.2.4 Deterministic handling of ties
An important property of the incremental top-2 algorithm, arising from the use of FHE.gt (strictly greater) rather than FHE.gte (greater than or equal), is that ties between the new bid and the current highestBid preserve the previous winner. Consider the scenario in which bidder X already holds the current highest bid and bidder Y submits a bid of the same value. Since FHE.gt(b_Y, highestBid) = FHE.gt(highestBid, highestBid) = false, the FHE.select keeps the state: highestBid remains the previous value, and highestBidder remains X. Y's bid drops to second, since it is strictly greater than the previous secondHighestBid.
This property solves the handling of ties without any additional structure. VeilBid, which operates on the iterative mask, needs an accumulated exclusion flag to guarantee that only the first participant to arrive with the maximum value is counted as the winner; that adds FHE.and and FHE.not operations during settlement. In the incremental top-2 pattern, the strict inequality of FHE.gt does the same job for free, and the resulting rule coincides with the classical tie-breaking rule in auction theory: in the event of a tie at the highest bid, whoever arrived first wins (KRISHNA, 2009).
The property goes beyond the simple tie. Consider a scenario with three bidders equal to the maximum value (a "triple pile-up"): X bid first, then Y, then Z. After X's bid, the state is (highestBid = v, secondHighestBid = 0). After Y's bid, FHE.gt(v, v) = false at the first level, so Y does not replace X; but FHE.gt(v, 0) = true at the second level, so secondHighestBid becomes v (Y's bid). After Z's bid, the first level again rejects (keeping X), and the second level also rejects because FHE.gt(v, v) = false. The final state is (highestBid = v, secondHighestBid = v, highestBidder = X). The winner is X (the first to arrive), and they pay v (the tied value), exactly as the classical Vickrey rule prescribes in the presence of ties (§2.5.4). This scenario is verified empirically in the contract's tests (§4.4.5), in the triple tie at the top case.
The interaction of tie handling with the second-price computation also deserves attention. In a scenario with a single highest bid (X bid v_max) and two bidders tied in second place (Y and Z both bid v_2 < v_max), the algorithm produces highestBid = v_max, secondHighestBid = v_2, and the winner X pays v_2. That behaviour also coincides with classical Vickrey, and is covered by the tie at the second place test.
4.2.5 Defensive design decisions
The lean formulation of the incremental top-2 algorithm is the heart of the solution, but the final implementation must face four additional vectors that appeared during development and were resolved by explicit design decisions. Each decision is discussed below, with the rationale for the problem attacked, the alternative adopted and the trade-off implied.
The first is the homomorphic cap of the bid at the collateral value. Without that safeguard, a malicious bidder could submit an encrypted bid of arbitrarily high value (for example, close to the uint64 maximum), depositing only the minimum collateral defined by the seller. That bid would win the auction without the contract being able to detect it (all values are encrypted), and in the finalisation phase the computed second price could be greater than the winner's available collateral, making payment impossible. The contract would be stuck in SettlementRequested indefinitely, with the other bidders' collateral locked. This is a clear griefing vector. The solution adopted is to intercept the application in the bid function itself and truncate the encrypted value to the collateral (Listing 3):
euint64 cap = FHE.asEuint64(uint64(auction.collateralAmount));
ebool tooHigh = FHE.gt(bidAmount, cap);
bidAmount = FHE.select(tooHigh, cap, bidAmount);Listing 3: Homomorphic truncation of the bid to the collateral cap.
The bidder does not notice the truncation (the operation is homomorphic), but the property established is strong: for any auction and any bidder, the stored bid is less than or equal to the collateral deposited. This truncation has an implication for truthfulness when a participant's true value exceeds the collateral, discussed in Chapter 5 (§5.6). The cost is three additional FHE operations per bid (one FHE.asEuint64, one FHE.gt and one FHE.select), whose impact on gas is discussed in Chapter 5.
The second decision is the prohibition on the seller taking part as a bidder in their own auction. In mechanism theory, that practice is called shill bidding and breaks the truthfulness property: the seller can artificially inflate the closing price by means of their own bids, knowing that, if they win their own auction, they will pay themselves (KRISHNA, 2009). Since the seller's identity is recorded in plaintext at auction creation, the protection is direct: the bid function reverts explicitly if msg.sender == auction.seller. That check is done in plaintext, with no additional FHE cost. The bug in this work's implementation that motivated this protection was discovered during the first test on a public network, in which the seller inadvertently took part in their own auction and their collateral became accounted for and stuck in the contract; Chapter 5 details the incident as evidence of the value of testing on a public testnet.
The third decision is the requirement of at least three distinct bidders for the auction to be finalisable, expressed by the constant MIN_BIDDERS = 3 in the contract. That requirement is a defence against auctions with only two participants in which both bid the same value: in that scenario, the second price revealed coincides with the highest price, and the adversary learns that there was in fact a tie. With three or more bidders, the second price may or may not coincide with the highest, and the adversary cannot distinguish those cases without knowing other bids. This decision follows the pattern adopted by Faruukku (2026) in VeilBid, and is discussed in Chapter 6 as a point of extensibility (auctions with fewer participants would be possible through an alternative tie-breaking mechanism).
The fourth decision is the pull-pattern payment model for withdraw. Instead of the contract automatically transferring ETH to the winner, to the seller and to the losers during finalisation, the finalizeSettlement function merely records the result, and each participant (including the seller) actively calls withdraw to take out the balance owed to them. This decision follows the canonical security recommendation in Solidity (OpenZeppelin, 2024): minimising automatic transfers reduces the attack surface for reentrancy1 and eliminates the possibility that a single participant's transfer failure stalls the finalisation of the entire auction. The cost is one additional transaction per participant (instead of everything being resolved in finalizeSettlement), but since each participant pays their own withdrawal gas, the aggregate gas cost stays distributed.
4.2.6 Execution flow and state machine
The life cycle of an auction is modelled by a four-node state machine, defined in the contract by enum Status { Active, SettlementRequested, Settled, Cancelled } and illustrated in Figure 2. The valid transitions are as follows:
Figure 2. Auction state machine (introductory version).
The execution flow of an auction instance traverses that state machine in two distinct stages, corresponding to the architectural pattern of Fhenix CoFHE described in §2.3.5: the auction executes in an active phase, in which only encrypted operations occur, and reveals in a final phase, in which two (and only two) values are made public. The separation of those two phases is what allows the system to guarantee the privacy properties stated in §4.1.3 under the adversary model of §4.1.2.
The active phase begins with the creation of the auction by the seller, via createAuction, and ends when the configured deadline arrives. During that phase, any non-seller address may call bid, submitting an encrypted bid and depositing exactly the collateral required. Each bid call incrementally updates the top-2 aggregates (as per §4.2.3), applies the homomorphic cap (as per §4.2.5), and records the bidder in the internal array of participants. No decryption occurs, and no bid is revealed. The contract remains in the Active state throughout that phase.
The final phase begins when any address (seller or bidder) calls requestSettlement after the deadline. That call transitions the state to SettlementRequested, marks two specific handles as publicly decryptable (those of highestBidder and secondHighestBid) through FHE.allowPublic, and stores the respective ciphertext hashes in public variables of the contract, as detailed in §2.3.5. From that moment on, any off-chain client may query the TSN, retrieve the plaintext values accompanied by a collective signature, and submit them back on-chain via finalizeSettlement. The contract verifies the signatures with FHE.verifyDecryptResult, records the decrypted values in public fields and transitions to Settled. That transition occurs only once per auction: the function requires the SettlementRequested state and consumes it by writing Settled, so that a second call reverts. The other participants do not trigger it; after Settled, losers, winner and seller individually withdraw their balances via withdraw, under the pull model described in §4.2.5. This flow is drawn in greater detail in the Sequence Diagrams presented in §4.3.6.
The Cancelled state is an alternative route restricted to the case in which the auction is created but receives no bids. Only the seller may call cancelAuction, and only while totalBids == 0 (that restriction is part of the contract). The function exists to prevent abandoned auctions from occupying auctionIds permanently.
After Settled or Cancelled, the auction is terminal: no further transition is possible, and the state remains recorded for auditing. The withdraw functions operate independently of the main state machine, following the pull-pattern model described in §4.2.5: each eligible participant (winner, seller, losers) withdraws the amount owed in a separate transaction, in any order, at any moment after finalisation.
4.3 Specification and Modelling
§4.2 presented the idea of the solution and the algorithm at its core. This section formalises what the system must do and how its pieces are organised, before §4.4 presents the details with code examples. The route is the following: the Functional and Non-Functional Requirements (§4.3.1), the Actors and Use Cases (§4.3.2), the data model and the state maintained by the contract (§4.3.3), the public interface and its preconditions (§4.3.4), the catalogue of Fully Homomorphic Encryption operations employed (§4.3.5), the Sequence Diagrams of the two main flows (§4.3.6) and the formal state machine of the auction (§4.3.7).
4.3.1 Requirements
The requirements of the system derive directly from two sources already established in this chapter. The privacy objectives and the threat model of §4.1 give rise to the secrecy and verifiability demands; the technical solution of §4.2 gives rise to the demands on the behaviour of the mechanism. The classical distinction between Functional Requirements (FR), which describe what the system does, and Non-Functional Requirements (NFR), which describe the qualities it must exhibit, is adopted. Each requirement is anchored to the section that motivates it, so that traceability between problem, solution and specification remains explicit.
Table 3 lists the Functional Requirements.
| ID | Functional Requirement | Origin |
|---|---|---|
| FR-01 | Allow a seller to create an auction defining name, item identifier, uniform collateral and bidding time window. | §4.2.6 |
| FR-02 | Allow any non-seller address to submit a single encrypted bid, depositing exactly the collateral required. | §4.2.6 |
| FR-03 | Homomorphically truncate every bid to the collateral value, without revealing the bid or the occurrence of the truncation. | §4.2.5 |
| FR-04 | Prevent the seller from taking part as a bidder in their own auction. | §4.2.5 |
| FR-05 | Maintain, incrementally and encrypted, the highest bid, the second-highest bid and the identity of the highest bidder. | §4.2.3 |
| FR-06 | Resolve ties at the highest bid by order of arrival, deterministically and without additional structure. | §4.2.4 |
| FR-07 | Require a minimum of three distinct bidders for the auction to be finalisable. | §4.2.5 |
| FR-08 | After the deadline expires, mark the winner and the second price as publicly decryptable by the TSN. | §4.2.6 |
| FR-09 | Finalise the auction only after on-chain verification of the decryption proofs issued by the TSN. | §4.2.1, §4.2.6 |
| FR-10 | Settle payments by pull pattern: the seller withdraws the second price, the winner withdraws the change from the collateral and the losers withdraw the collateral in full. | §4.2.5 |
| FR-11 | Allow the cancellation of an auction that received no bids. | §4.2.6 |
| FR-12 | Expose public queries of the auction state and of the settlement result. | §4.2.1 |
Table 3: Functional Requirements of the system.
Two requirements deserve comment for concentrating the originality of the solution. FR-05 is the direct translation of the incremental top-2 pattern of §4.2.3: instead of storing the N bids, the contract keeps only three encrypted aggregates, updated at every bid. FR-09 is what makes trust in the off-chain client irrelevant to correctness: no plaintext value enters the contract state without the TSN's collective signature over the corresponding ciphertext being verified on the blockchain itself.
Table 4 lists the Non-Functional Requirements. Each of them is a cross-cutting quality, required throughout the auction's life cycle, rather than an isolated function.
| ID | Non-Functional Requirement | Origin |
|---|---|---|
| NFR-01 | Permanent secrecy of the losing bids, in all auction states, including after closing. | §4.1.3 |
| NFR-02 | Partial secrecy of the winner's bid: only that it is greater than or equal to the second price is revealed. | §4.1.3 |
| NFR-03 | Verifiability: no plaintext value is accepted by the contract without a TSN signature over the corresponding ciphertext. | §4.2.1 |
| NFR-04 | Resistance to griefing: no single participant can stall the finalisation of the auction. | §4.2.5 |
| NFR-05 | Bid cost constant with respect to the number of participants (O(1) per bid) and constant storage per auction. | §4.2.3 |
| NFR-06 | Portability: the contract must be deployable on any EVM-compatible network where CoFHE is active, without code changes. | §4.2.1 |
| NFR-07 | Resistance to reentrancy in ETH transfers, ensured by the checks-effects-interactions pattern and by pull-based withdrawal. | §4.2.5 |
| NFR-08 | Resistance to quantum attacks, inherited from the TFHE cryptographic basis. | §2.4.3 |
| NFR-09 | Compatibility with Solidity 0.8.25 (with evmVersion cancun) and with the @fhenixprotocol/cofhe-contracts library. | §4.4 |
Table 4: Non-Functional Requirements of the system.
NFR-08 is what connects this work to the post-quantum cryptography discussion of §2.4: since the secrecy of the bids rests on TFHE, and TFHE is considered resistant to quantum attacks (§2.4.3), the confidentiality of the losing bids is robust not only against classical adversaries, but also against an eventual adversary equipped with quantum computing capable of breaking conventional asymmetric cryptography. That is a desirable property in auctions of long-lived assets, in which a losing bid should not be revealable years later (the threat that §2.4.4 describes as harvest now, decrypt later).
4.3.2 Actors and Use Cases
The system involves two human Actors and two external systems. The human Actors are the seller, who creates the auction and offers the item, and the bidder, who submits an encrypted bid and disputes the item. The external systems are the CoFHE coprocessor, which stores the ciphertexts and executes the homomorphic operations, and the Threshold Services Network (TSN), which decrypts under authorisation the two values revealed at the end. The off-chain client running in the browser is not an actor in the mechanism sense: it is the instrument through which the bidder encrypts the bid and through which any participant queries the TSN, as characterised in §4.2.1.
Figure 3 presents the Use Case Diagram. The requestSettlement and finalizeSettlement cases are permissionless: any address may trigger them provided the preconditions are satisfied (the deadline elapsed, in the first case, and possession of the TSN proofs, in the second). The diagram expresses that openness through an actor generalisation: Seller and Bidder inherit from the generic User actor, to which the permissionless cases (requestSettlement, finalizeSettlement) and the withdrawal (withdraw) are associated; §4.3.4 details the preconditions of each operation. That openness is deliberate: it guarantees that the auction can be finalised even if the winner refuses to cooperate, meeting the robustness requirement discussed in §4.1.4.
Figure 3. Use Case Diagram of the contract.
Each use case corresponds to an external function of the contract, with the following intent: createAuction instantiates a new auction; bid submits an encrypted bid with collateral; requestSettlement closes the active phase and marks the handles for decryption; finalizeSettlement publishes the verified result; withdraw performs the withdrawal owed to each participant; and cancelAuction discards an empty auction. §4.3.4 specifies each one in detail.
4.3.3 Data model and contract state
The contract state is organised around a central structure, Auction, indexed by a sequential numeric identifier (auctionId). Each auction instance is an entry in the auctions mapping. The structure groups four categories of data, which are worth distinguishing because they have distinct privacy natures.
The first category is the public plaintext metadata: the auction name, the seller's address, the item identifier, the uniform collateral value, the start and end timestamps, the current state (Status) and the bid counter (totalBids). These fields are deliberately public; none of them carries strategic information about the bids.
The second category is the encrypted state, kept as three handles: highestBid and secondHighestBid, of type euint64, and highestBidder, of type eaddress. These are exactly the three aggregates of the incremental top-2 pattern (§4.2.3). As discussed in §2.3.2, these fields do not store the ciphertexts themselves, but only handles: short references (of 32 bytes) that point to the actual ciphertexts, which live in the CoFHE coprocessor outside the blockchain. Table 5 summarises these three fields.
| Field | Type | Initialisation | Updated in |
|---|---|---|---|
highestBid | euint64 | FHE.asEuint64(0) | bid (top-2) |
secondHighestBid | euint64 | FHE.asEuint64(0) | bid (top-2) |
highestBidder | eaddress | FHE.asEaddress(address(0)) | bid (argmax: the address associated with the highest bid) |
Table 5: Encrypted fields of the auction state.
The third category is the decryption result, filled in only on the transition to Settled: the winner's address (decryptedWinner) and the second price (decryptedSecondPrice), both in plaintext. Alongside them, two ciphertext hashes (winnerCtHash and secondPriceCtHash) are captured at the moment of requestSettlement, so that the off-chain client knows which handles to submit to the TSN. The capture of those hashes is done by the eaddress.unwrap and euint64.unwrap operations, which extract the 32-byte reference from an encrypted handle.
The fourth category is the auxiliary accounting mappings, kept outside the Auction structure because they are indexed by (auction, address) pairs: collateral records how much each bidder deposited (always equal to collateralAmount, by construction); hasBid prevents double bidding; and hasWithdrawn prevents double withdrawal. To those is added the constant MIN_BIDDERS, fixed at three, which materialises FR-07.
There is also a derived structure, AuctionView, which mirrors the public metadata of Auction without the encrypted fields. It is the return type of the getAuction query function: since encrypted handles have no useful representation for a client that merely inspects the state, the view omits them, exposing only what is public and readable. That separation between the storage structure and the query structure is a modelling decision that keeps the read interface clean and avoids exposing handles in contexts where they have no utility.
4.3.4 Public interface of the contract
The contract interface is composed of six state-changing functions and four query (read-only) functions. Table 6 specifies the state functions: for each one, who may call it, the preconditions which, if violated, revert the transaction, and the main effect on the state.
| Function | Who may call | Preconditions (revert if violated) | Main effect |
|---|---|---|---|
createAuction | any address (becomes the seller) | name of 1 to 32 characters; endTime > startTime; endTime > now; 0 < collateral ≤ uint64 max | creates the auction in Active; initialises the encrypted top-2 at zero |
bid | any non-seller | state Active; within the time window; msg.value == collateral; not having bid yet; not being the seller | applies the homomorphic cap; updates the encrypted top-2; records the bidder |
requestSettlement | any address | state Active; after endTime; totalBids ≥ MIN_BIDDERS | transitions to SettlementRequested; applies allowPublic; writes the ctHashes |
finalizeSettlement | any address (holding the proofs) | state SettlementRequested; valid proofs; winner with sufficient collateral | writes the winner and the second price; transitions to Settled |
withdraw | eligible participant | state Settled or Cancelled; not having withdrawn yet; being eligible | transfers the ETH owed (pull model) |
cancelAuction | the seller only | state Active; totalBids == 0 | transitions to Cancelled |
Table 6: Interface of the state functions.
The four query functions complete the interface: getAuction returns the AuctionView (public metadata); getSettlementResult returns the winner and the second price, but only after Settled; getSettlementCtHashes returns the two ctHashes for the client to feed the TSN, available from SettlementRequested onwards; and getBidderEncryptedBid returns an encrypted handle, of deliberately limited usefulness (since only the top-2 aggregates are retained, and not the individual bids, this function exists mainly for symmetry with the interface of the official PoC that served as the starting point).
The robustness of the interface is sustained by a set of seventeen custom errors, which make every failure diagnosable. They cover violations of state (AuctionNotActive, AuctionNotEnded, AuctionEnded, SettlementNotRequested), of authorisation (NotSeller, NotEligible, SellerCannotBid), of uniqueness (AlreadyBid, AlreadyWithdrawn), of parameters (InvalidTimeRange, WrongCollateral, NameRequired, NameTooLong, InsufficientBidders), of cryptographic verification (InvalidDecryptionProof) and of settlement (TransferFailed, WinnerCannotPay). The use of custom errors, rather than text messages, is also a gas-economy choice, since each error is identified by a four-byte selector.
4.3.5 Catalogue of FHE operations
The solution employs a lean subset of the primitives offered by Fhenix's FHE.sol library, characterised at a high level in §2.3.3. Table 7 catalogues each operation used, where it appears and for what purpose. The restraint of that set is intentional: since every comparison or selection operation is expensive in FHE (each one involves the TFHE machinery described in §2.3.1), minimising the variety and the number of operations is a cost requirement, formalised in NFR-05.
| Operation | Role | Where it is used | Purpose |
|---|---|---|---|
FHE.asEuint64 | imports or creates an encrypted value | createAuction, bid | converts the client's InEuint64 into a euint64; creates the encrypted constants (initial zero, cap ceiling) |
FHE.asEaddress | encrypts an address | createAuction, bid | encrypts the bidder's address to compose the winner's argmax |
FHE.gt | encrypted strict comparison | bid | decides the cap (bid > ceiling) and the top-2 (bid > highest, bid > second) |
FHE.select | encrypted ternary operator | bid | applies the cap, updates the top-2 and selects the winner without decrypting |
FHE.allowThis | ACL authorisation | createAuction, bid | allows the contract itself to reuse the handle in future calls |
FHE.allow | ACL authorisation | bid | allows the bidder to inspect their own encrypted bid |
FHE.allowPublic | ACL authorisation | requestSettlement | allows the TSN to decrypt the winner and the second price |
FHE.verifyDecryptResult | on-chain verification | finalizeSettlement | confirms that the plaintext value corresponds to the ciphertext, with the TSN proof |
euint64.unwrap / eaddress.unwrap | handle extraction | requestSettlement | extracts the 32-byte ctHash for the client to feed the TSN |
Table 7: Catalogue of the FHE operations employed.
One modelling point that is usually a source of error deserves emphasis: the access control list (ACL) over the handles. In Fhenix CoFHE, an encrypted handle cannot be operated on freely; the contract must have explicit authorisation to use it. Three operations govern that authorisation, in three distinct scopes. FHE.allowThis grants the contract itself the right to reuse a handle in future transactions, and that is why each new highestBid, secondHighestBid and highestBidder must be re-authorised at every bid (without that, the next bid call would not be able to operate on the aggregates). FHE.allow grants a specific address the right to decrypt a handle, used so that the bidder can inspect their own bid. And FHE.allowPublic makes the handle decryptable by anyone through the TSN, reserved exclusively for the two values that the Vickrey mechanism must reveal. That gradation of scopes (contract, address, public) is what operationalises, at the platform level, the privacy objectives of §4.1.3.
4.3.6 Sequence Diagrams
The two central flows of the auction, the bid and the settlement, traverse the four components of the architecture in well-defined orders. Modelling them as Sequence Diagrams makes explicit who talks to whom, in what order, and where each encrypted operation occurs.
Figure 4 shows the flow of a bid. The essential point is that encryption occurs entirely in the client, before any data leaves the browser: the plaintext value of the bid never travels across the network. The contract receives only the already-encrypted InEuint64 structure, validates the plaintext preconditions (state, deadline, collateral, anti-shill, double bid) and delegates to CoFHE both the homomorphic cap and the top-2 update.
Figure 4. Sequence Diagram of the bid flow.
Figure 5 shows the settlement flow, which materialises Fhenix's client-orchestrated two-step pattern described in §2.3.5. After the deadline, requestSettlement marks the two handles of interest as publicly decryptable and emits the ctHashes. The off-chain client queries the TSN, receives the plaintext values accompanied by proofs, and submits them back via finalizeSettlement. The contract then verifies the proofs with FHE.verifyDecryptResult before accepting any value. Payments do not occur in that flow: each participant claims them afterwards, via withdraw, under the pull model (§4.2.5).
Figure 5. Sequence Diagram of the settlement flow.
4.3.7 Formal state machine
§4.2.6 already introduced, in textual form, the auction's state machine. This section presents it completely and formally, with Figure 6 and the corresponding transition table. The machine has four states (Active, SettlementRequested, Settled, Cancelled) and is deterministic: each transition is triggered by exactly one external function, and each external function only transitions the state from a fixed origin, under an explicit guard.
Figure 6. Formal state machine of the auction, with the transition guards.
Table 8 enumerates the transitions with their guards. The guards are exactly the preconditions already specified in §4.3.4, reorganised here from the perspective of the state transition each one protects.
| Origin | Transition | Guard | Destination |
|---|---|---|---|
| (initial) | createAuction() | valid parameters (name, deadlines, collateral) | Active |
Active | requestSettlement() | block.timestamp ≥ endTime and totalBids ≥ MIN_BIDDERS | SettlementRequested |
Active | cancelAuction() | msg.sender == seller and totalBids == 0 | Cancelled |
SettlementRequested | finalizeSettlement() | valid proofs (verifyDecryptResult) and solvent winner | Settled |
Settled | (terminal) | no outgoing transition | Settled |
Cancelled | (terminal) | no outgoing transition | Cancelled |
Table 8: Transitions of the state machine and their guards.
Two properties of this machine matter for the analysis in the following chapter. The first is that there is no return transition: once the active phase has closed, bids can no longer be accepted, which eliminates the possibility of an adversary reopening the auction after observing the revealed second price. The second is that the cancellation path is strictly restricted to empty auctions (totalBids == 0), which prevents a disgruntled seller from cancelling an already contested auction in order to suppress an unfavourable result. Both properties reinforce, at the modelling level, the integrity guarantees that §4.1 demands of the mechanism. withdraw, since it operates outside the main state machine, is discussed in §4.4 together with the implementation of the payment model.
The modelling presented in this section fixes the contract between what the solution promises and what the implementation delivers. §4.4 describes how each of those elements was realised in Solidity and in TypeScript, and reports the results of the test suite that verifies the implementation's adherence to that specification.
4.4 Implementation
This section reports how the specification of §4.3 was realised in executable code. It describes the technologies and the execution environment (§4.4.1), the Solidity contract (§4.4.2), client-side encryption and the frontend (§4.4.3), the deployment on a public testnet and the end-to-end demonstration (§4.4.4) and, finally, the structure of the test suite that verifies the implementation's adherence to the specification (§4.4.5). The cost and latency measurements produced by this implementation are not presented here, but in Chapter 5, dedicated to the evaluation; this section deals with how the system was built, not with how much it costs.
4.4.1 Technologies and execution environment
The project is organised as a monorepo (a single repository that gathers, versioned together, several related code packages) managed by pnpm workspaces, with two independent packages. The hardhat package contains the Solidity contract, the deployment scripts and the test suite; the nextjs package contains the web interface. That separation allows the contract to be developed, tested and deployed without any dependency on the frontend, and vice versa. All the source code described in this section (contract, frontend, deployment scripts and test suite) is publicly available in the project repository, at https://github.com/thiagorochatr/fhe-vickrey-auction, meeting the third-party reproducibility objective assumed by the work. Table 9 lists the main technology components and their versions.
| Category | Technology | Version |
|---|---|---|
| Contract | Solidity2 (with evmVersion cancun) | 0.8.25 |
| FHE library | @fhenixprotocol/cofhe-contracts3 | 0.1.3 |
| FHE client (browser) | @cofhe/sdk4 | 0.5.1 |
| Test plugin and mock | @cofhe/hardhat-plugin, @cofhe/mock-contracts5 | 0.5.1 |
| Contract framework | Hardhat6 | 2.22 |
| Ethereum library (scripts) | ethers7 | 6 |
| Frontend | Next.js8 | 16 |
| Web3 (frontend) | wagmi9, viem10, RainbowKit11 | 2.x |
| State (frontend) | Zustand12 | 5 |
| Styling | Tailwind CSS13, daisyUI14 | 3, 4 |
Table 9: Main technology components of the implementation.
The implementation supports two execution environments, as anticipated in §2.3. The first is the local mock, provided by @cofhe/mock-contracts through @cofhe/hardhat-plugin. In that environment, the homomorphic operations are simulated in memory, which makes it possible to run the entire test suite without a network and without real gas cost, and without depending on the availability of the Threshold Services Network. The second is the public testnet Arbitrum Sepolia, where CoFHE is active and the encrypted computation actually occurs in the coprocessor. The development discipline adopted was to write and stabilise the tests on the mock first, and only then promote the contract to the testnet, as detailed in §4.4.4.
4.4.2 The Solidity contract
The ConfidentialVickreyAuction.sol contract has around 450 lines and materialises, in a single file, the entire state machine and the homomorphic operations specified in §4.3. The bid-submission logic, which includes the homomorphic cap and the incremental top-2 update, has already been shown in code in §4.2.3 and §4.2.5 and is not repeated here. This subsection focuses on the trio of functions that drives the final phase, not yet shown: requestSettlement, finalizeSettlement and withdraw.
Before detailing those functions, it is worth making the provenance of the contract explicit, separating what was inherited from what is an original contribution. The life-cycle framework and the two-step settlement pattern, with the requestSettlement and finalizeSettlement functions, come from the official Fhenix proof-of-concept (FHENIX, 2026c), which served as the engineering starting point. That two-step pattern is not an aesthetic choice, but the idiomatic form of client-orchestrated decryption in CoFHE, in which the contract merely marks the handles as publicly decryptable and the plaintext returns verified in a second transaction (§4.3.4); the coincidence of function names with the PoC follows from their reflecting the natural vocabulary of an auction (create, bid, request settlement, finalise, cancel), not from a transposition of design. The homomorphic masking technique for obtaining the second-highest value over encrypted bids, in turn, comes from VeilBid (FARUUKKU, 2026), as credited in §4.2.2. The original contribution of this work is concentrated in the Vickrey logic and in what surrounds it: the computation of the second price by an incremental top-2 pattern of constant cost, materialised in the secondHighestBid field updated at every bid and absent from the PoC, which is first-price; the homomorphic cap of the bid at the collateral (§4.2.3); the uniform collateral model in native ETH, in place of the NFT and confidential-token model adopted by the PoC; and the deterministic handling of ties by order of arrival, obtained for free from the strict inequality of FHE.gt, without the accumulated exclusion flag on which VeilBid depends (§4.2.4). To that core are added the other two pillars of the work, foreign to both sources: the empirical evaluation of cost, in Chapter 5, and the formal leakage analysis under an explicit adversary model, in §4.1 and in Chapter 5.
The requestSettlement function closes the active phase and prepares the revelation. After validating the state, deadline and minimum quorum guards, it marks the two handles of interest as publicly decryptable and extracts their ciphertext hashes so that the client knows what to submit to the TSN (Listing 4):
function requestSettlement(uint256 auctionId) external {
Auction storage auction = auctions[auctionId];
if (auction.status != Status.Active) revert AuctionNotActive();
if (block.timestamp < auction.endTime) revert AuctionNotEnded();
if (auction.totalBids < MIN_BIDDERS) revert InsufficientBidders();
auction.status = Status.SettlementRequested;
FHE.allowPublic(auction.highestBidder);
FHE.allowPublic(auction.secondHighestBid);
bytes32 winnerCt = eaddress.unwrap(auction.highestBidder);
bytes32 secondPriceCt = euint64.unwrap(auction.secondHighestBid);
auction.winnerCtHash = winnerCt;
auction.secondPriceCtHash = secondPriceCt;
emit SettlementRequested(auctionId, winnerCt, secondPriceCt);
}Listing 4: The requestSettlement function: marking the handles as publicly decryptable.
The finalizeSettlement function is the point at which trust in the off-chain client is neutralised, materialising FR-09 and NFR-03. It receives the plaintext values (winner and second price) and the proofs issued by the TSN, and only accepts them if FHE.verifyDecryptResult confirms, on the blockchain itself, that each value corresponds to the original ciphertext. A malicious client that tried to inject a forged winner or price would be blocked, because it would not have a valid TSN proof for the forged value (Listing 5):
function finalizeSettlement(
uint256 auctionId,
address winner,
uint64 secondPrice,
bytes calldata winnerProof,
bytes calldata secondPriceProof
) external {
Auction storage auction = auctions[auctionId];
if (auction.status != Status.SettlementRequested)
revert SettlementNotRequested();
if (!FHE.verifyDecryptResult(auction.highestBidder, winner, winnerProof))
revert InvalidDecryptionProof();
if (!FHE.verifyDecryptResult(auction.secondHighestBid, secondPrice, secondPriceProof))
revert InvalidDecryptionProof();
if (uint256(secondPrice) > collateral[auctionId][winner])
revert WinnerCannotPay();
auction.decryptedWinner = winner;
auction.decryptedSecondPrice = secondPrice;
auction.status = Status.Settled;
emit AuctionSettled(auctionId, winner, secondPrice);
}Listing 5: The finalizeSettlement function: verification of the TSN proofs and recording of the result.
The WinnerCannotPay check is the safeguard complementary to the homomorphic cap discussed in §4.2.5: it guarantees that the second price to be charged to the winner does not exceed the collateral they deposited, aborting the finalisation in the pathological case in which the public cap has somehow been violated.
The withdraw function implements the pull model (FR-10). Instead of pushing payments during finalisation, the contract lets each participant actively withdraw the amount they are entitled to, computed according to their role in the auction (Listing 6):
if (auction.status == Status.Cancelled) {
if (!hasBid[auctionId][msg.sender]) revert NotEligible();
amount = collateral[auctionId][msg.sender];
} else {
// Settled
if (msg.sender == auction.seller) {
amount = auction.decryptedSecondPrice;
} else if (msg.sender == auction.decryptedWinner) {
amount = collateral[auctionId][msg.sender] - auction.decryptedSecondPrice;
} else if (hasBid[auctionId][msg.sender]) {
amount = collateral[auctionId][msg.sender];
} else {
revert NotEligible();
}
}Listing 6: The withdraw function: withdrawal by role, under the pull model.
The three branches of the Settled case translate the Vickrey rule directly: the seller receives the second price, the winner recovers the difference between the collateral deposited and the second price they effectively pay, and each loser recovers the collateral in full. The hasWithdrawn flag (omitted from the excerpt) prevents double withdrawal, and the transfer follows the checks-effects-interactions pattern (check the conditions, apply the effects to the state and only then interact with external accounts, an order that avoids reentrancy), with the state marking done before the external call.
4.4.3 Client-side encryption and frontend
The frontend is a Next.js application whose central cryptographic responsibility is to encrypt the bid in the browser, so that the plaintext value never leaves the user's machine. That encryption is performed by a WebAssembly (WASM) module, a choice motivated by the computational cost of TFHE's encryption and proof-generation operations, prohibitive in pure JavaScript. Integration with the coprocessor is done by the @cofhe/sdk library, instantiated once as a singleton client. The configuration declares the supported networks and creates the client (Listing 7):
const config = createCofheConfig({
supportedChains: [chains.arbSepolia, chains.baseSepolia, chains.sepolia],
});
export const cofheClient = createCofheClient(config);Listing 7: Configuration and creation of the CoFHE client in the graphical interface.
Initialisation follows a three-step flow (configuration, client creation and connection), in which the connection step associates the client with the wallet and with the network provider via cofheClient.connect(publicClient, walletClient). That design replaces the old single cofhejs.initializeWithViem call of earlier SDK versions, and is encapsulated in the useCofhe hook, which reacts to the wallet connection.
Bid submission happens in the useVickreyAuction hook. The encryption is a chained call that accepts a progress callback (used to feed the interface's step bar) and returns the encrypted input, which is then passed directly as an argument to the contract's bid function (Listing 8):
const [encryptedBid] = await cofheClient
.encryptInputs([Encryptable.uint64(amount)])
.onStep((step) => opts?.onEncryptStep?.(step))
.execute();
await walletClient.writeContract({
address: contract(),
abi: vickreyAuctionAbi,
functionName: "bid",
args: [auctionId, encryptedBid],
value: collateral,
});Listing 8: Client-side encryption of the bid via encryptInputs.
The revelation of the result is orchestrated by the client, following Fhenix's two-step pattern described in §2.3.5 and diagrammed in Figure 5. After requestSettlement, the client fetches the ciphertext hashes on-chain, requests the decryption of each one from the TSN, and submits the plaintext values back to the contract. The signatures returned by the TSN (winnerRes.signature and secondRes.signature) are exactly the proofs that finalizeSettlement verifies via FHE.verifyDecryptResult (Listing 9):
const cts = await getSettlementCtHashes(auctionId);
const winnerRes = await cofheClient.decryptForTx(cts.winnerCt).withoutPermit().execute();
const secondRes = await cofheClient.decryptForTx(cts.secondPriceCt).withoutPermit().execute();
await walletClient.writeContract({
functionName: "finalizeSettlement",
args: [auctionId, winnerAddr, secondRes.decryptedValue,
winnerRes.signature, secondRes.signature],
});Listing 9: Querying the TSN and obtaining the decryption proofs for the settlement.
One modelling detail appears here: since the winner is an eaddress, the TSN returns it as a 160-bit integer, which the client converts back into hexadecimal address format (prefixed with 0x) before submitting. The authorisation to decrypt private values to their own owner (for example, a bidder inspecting their own bid) is handled by permits, encapsulated in the usePermit hook; the revelation of the winner and of the second price, by contrast, dispenses with a permit (withoutPermit), since those two handles were made public by the contract.
The interface is composed of a set of React components organised around three main screens. The list screen (AuctionList, with AuctionCard) presents the existing auctions and their states; the creation screen (CreateAuctionForm) allows the seller to instantiate a new auction; and the detail screen (AuctionDetail) concentrates bid submission, the settlement request and its finalisation. Wallet connection is done via RainbowKit, and the interface state is kept in Zustand stores. Figures 7 to 10 illustrate these screens.
Figure 7. Auction list screen.
Figure 8. Auction creation form.
The item identifier field is the point at which the RWA use case, discussed throughout the work, materialises in the interface: in a real instance, it would reference the token of the real-world asset being auctioned (for example, a fraction of tokenised receivables); in the prototype, it is a mock identifier, consistent with the assumed scope in which RWA tokens are simulated. The name field reinforces that reading, suggesting labels such as that of a batch of receivables.
Figure 9. Bid submission screen.
Two components deserve emphasis for making the system's privacy property visible to the user. The StepTimeline displays the phases of encryption (on bid submission) and of decryption via the TSN (on finalisation), making explicit to the user what is happening off-chain at each moment. The PrivacyPanel displays the ciphertext hashes of the auction's handles and makes clear what is encrypted and what is public in each state, functioning as a visual materialisation of the leakage-by-state table presented in §4.1.5.
Figure 10. Result panel (PrivacyPanel) in the Settled state.
4.4.4 Deployment and end-to-end demonstration
Deployment is automated by a hardhat-deploy script. The contract takes no constructor arguments, since each auction is an entry in the internal mapping, created at run time by createAuction (Listing 10):
const vickrey = await deploy("ConfidentialVickreyAuction", {
from: deployer,
args: [],
log: true,
autoMine: true,
});Listing 10: Contract deployment script via hardhat-deploy.
The contract was deployed on the public testnet Arbitrum Sepolia (chainId 421614), at the address 0x1eEa76147cBCD878D1cb5B8fdCb6bd0Ed836D811, at a deployment cost of approximately 1.90 million gas units. On top of that deployment, end-to-end validation occurred at two levels. Through the graphical interface, real demonstration auctions were run covering the whole cycle (creation, submission of encrypted bids, deadline expiry, requestSettlement, the client's query to the TSN, finalizeSettlement with on-chain verification of the proofs and withdrawal by each participant via withdraw) with one seller and three bidders, exactly the minimum required by the contract (MIN_BIDDERS = 3), and also with four bidders in a later run. In the demonstration with three bidders, the sum of funds at the end was checked and balanced: the total withdrawn (second price for the seller, change for the winner, full collateral for the losers) equalled the total deposited. Through the benchmark script described in Chapter 5, the same complete cycle was exercised in the populations defined in the scope, with 3, 5 and 10 bidders and five repetitions per configuration.
It was during the smoke tests (quick tests that check whether the system's essential features operate, ahead of more complete test batteries) on that testnet that two of the defensive design decisions described in §4.2.5 emerged: the prohibition on the seller taking part in their own auction and the homomorphic cap of the bid at the collateral. Both resolved failures observed only under real network conditions. The detailed analysis of the incident that motivated the first is taken up in Chapter 5, as evidence of the value of testing on a public testnet and not only on the mock.
4.4.5 Test structure
The test suite runs on the CoFHE mock and contains twenty tests, organised into five blocks that cover the contract end to end. Table 10 maps each test to the critical scenario it verifies, demonstrating coverage of the cases required by the scope of the work (valid and invalid bid, closing before the deadline, double bidding, correct computation of the winner and the second price, tie handling and recovery by the losers). In terms of code coverage, measured with the solidity-coverage tool, the suite exercises around 85% of the lines and statements of the ConfidentialVickreyAuction.sol contract (70% of its functions and 59% of its branches); the uncovered parts are concentrated in defensive exception branches and in auxiliary query functions.
| Block | Test | Scenario verified |
|---|---|---|
createAuction | creates an auction in Active with the given parameters | valid creation |
createAuction | reverts with an empty name | name validation |
createAuction | reverts with zero collateral | collateral validation |
createAuction | accepts a startTime in the past | immediate-start auction |
createAuction | reverts with an endTime already in the past | deadline validation |
bid | accepts a bid with the exact collateral | valid bid |
bid | rejects a bid with the wrong collateral | incorrect collateral |
bid | rejects a second bid from the same address | double bidding |
bid | rejects a bid after the deadline | closing by deadline |
bid | rejects a bid from the seller | anti-shill |
requestSettlement | rejects a request before the deadline | deadline guard |
requestSettlement | rejects with fewer than MIN_BIDDERS | minimum quorum |
| Vickrey flow | 3 distinct bidders: pays the second price | correct computation (N=3) |
| Vickrey flow | 5 distinct bidders: pays the second price | correct computation (N=5) |
| Vickrey flow | tie at the top: the first wins and pays the tied value | tie at the top |
| Vickrey flow | triple tie at the top | triple tie |
| Vickrey flow | truncates a bid above the collateral | homomorphic cap |
| Vickrey flow | tie at the second place: pays the second value | tie at the second place |
withdraw | loser withdraws collateral, winner withdraws change, seller withdraws the second price | recovery by role |
withdraw | rejects double withdrawal | double withdrawal |
Table 10: Mapping of the twenty tests to the scenarios verified.
The tests rest on a helper module (vickreySetup) that encapsulates the repetitive operations (deployment, auction creation, submission of an encrypted bid via Encryptable.uint64, advancing the clock past the deadline, and the settlement cycle), which keeps each test readable and focused on the property it verifies. The three-participant flow test illustrates the pattern: three bids of 100, 300 and 200 produce the winner with a bid of 300 paying the second price of 200 (Listing 11).
await placeBid(env, auctionId, env.bidders[0], 100n);
await placeBid(env, auctionId, env.bidders[1], 300n);
await placeBid(env, auctionId, env.bidders[2], 200n);
await advancePastDeadline(env, auctionId);
await requestSettlement(env, auctionId);
const { winner, secondPrice } = await finalizeSettlement(env, auctionId);
expect(winner.toLowerCase()).to.equal(env.bidders[1].address.toLowerCase());
expect(secondPrice).to.equal(200n);Listing 11: Test of the three-bid flow with the winner paying the second price.
The two tie cases cited in §4.2.4, triple tie at the top and tie at the second place, are exactly the ones that empirically verify the deterministic behaviour of the incremental top-2 algorithm under ties, confirming that the strict inequality of FHE.gt resolves the tie-break by order of arrival without additional structure. The ability to run all those scenarios on the mock, without cost and without a network, is what made it possible to iterate quickly over the homomorphic logic before each promotion to the testnet.
4.5 Guarantees obtained by the architecture
To close the chapter, Table 11 consolidates the guarantees the architecture offers and the mechanism that sustains each one, with a pointer to the section in which each mechanism was described. The table serves as a bridge to Chapter 5: the qualitative guarantees summarised here are exactly the object of the empirical evaluation that follows.
| Guarantee | How it is satisfied |
|---|---|
| Permanent secrecy of the bids | Bids submitted already encrypted in the client and never stored individually: the contract keeps only the aggregates of the top-2 pattern (§4.2.3), and no losing bid is ever marked as decryptable (§4.1.5). |
| Truthfulness | The second-price rule (§2.5.5) operating over bids effectively sealed throughout the contest; conditional on the assumption v ≤ C imposed by the homomorphic cap (§4.2.5, §5.6). |
| Verifiability of the result | The contract only accepts the result accompanied by the TSN's collective proofs, validated on-chain by FHE.verifyDecryptResult (§4.2.6); the client that publishes cannot forge values. |
| Resistance to content frontrunning | The bid value never travels in the clear: encryption occurs in the client before submission, and the mempool exposes only the ciphertext and the proof (§4.4.3); observing or copying the transaction does not reveal the value. Censorship and reordering remain possible (§5.5). |
| Integrity of funds and states | State machine with explicit guards (§4.2.6); the homomorphic cap guarantees a solvent winner (§4.2.5); pull-pattern payments with checks-effects-interactions against reentrancy (§4.2.5); sum of funds checked in the demonstration (§4.4.4). |
| Settlement without depending on the winner | requestSettlement and finalizeSettlement are permissionless: any participant may close and finalise the auction after the deadline (§4.3.4). |
Table 11: Guarantees offered by the architecture and the mechanisms that sustain them.
With the implementation described and verified, the following chapter evaluates the system along the three axes defined for the work: computational cost, latency and privacy, and discusses the trade-offs and limitations observed.
Notes
-
An attack in which the recipient of a transfer, upon receiving control, calls the contract back before the state has been updated, and may withdraw repeatedly. ↩
-
https://www.npmjs.com/package/@fhenixprotocol/cofhe-contracts ↩