Vitalik's 2015 ad-auction sandbox. Spawned 8 child auctions across 4 open-bid and 4 sealed-bid mechanisms.
Historical Significance
Vitalik Buterin's personal experiment in on-chain ad auctions, deployed 2015-10-20 during the Frontier era. Among the earliest factory-pattern contracts on Ethereum mainnet: the constructor spawns 8 child auction contracts via CREATE, one per ad slot, splitting them across 4 OnePhaseAuction (open-bid) and 4 TwoPhaseAuction (commit-reveal sealed-bid) instances. The eight slots cover all four classical auction designs in both open and sealed forms: non-cumulative winner-pays, cumulative winner-pays, non-cumulative all-pay, cumulative all-pay (open variants), and first-price, second-price, all-pay, all-pay-second-price (sealed variants). It is one of the first multi-contract factory dApps on mainnet and likely the first live commit-reveal sealed-bid auction system on Ethereum. The system worked end to end: bids were committed, revealed, and finalized through the ping-based clean-up loop. The only two addresses that ever interacted with it were both Vitalik's, bidding against himself with photographs of himself and Heiko Hees as test ad payloads.
Context
Deployed at block 411,110 on 2015-10-20 06:58:40 UTC by Vitalik Buterin from 0x1db3439a222c519ab44bb1144fc28167b4fa6ee6. Five minutes after the deployment, a throwaway wallet at 0xce7fb4c38949d7c09bd95197c3981ec8bb0638e5 sent eight initialize transactions in blocks 411,117 through 411,121, spawning the eight child auction contracts. That throwaway wallet's private key is hard-coded in dapp-bin's initialize.py and was never used again, it is now empty. Auction parameters were the short test profile from the source comment: 240-second hash submission, 240-second reveal, 240-second base duration, 120-second bump, 50 millis minimum increment, 10 millis subsidy. The eight children were each handed one slot index. Slot 0 (one-phase, non-cumulative, winner pays): 0x3d9e942f183a7e564dd27479081f745a4f081f28. Slot 1 (one-phase, cumulative, winner pays): 0x31e684ad5c33b741b4043bb28ece3b0c1bf9e4d4. Slot 2 (one-phase, non-cumulative, all-pay): 0xb8aa37f4f99018fddccc85ba0c95e6cfe7f3f8b6. Slot 3 (one-phase, cumulative, all-pay): 0x859f5bb20aa1e4e44efddf1d45ecb40490374156. Slot 4 (two-phase, first price): 0x90786bf95df3f62c774695b166e6adfc8a0d18a5. Slot 5 (two-phase, second price): 0xb1247665ae0cbba65343cb7382d29b077ec47927. Slot 6 (two-phase, all-pay): 0x848c87d50533d4b9e5169519335e862a1c917d57. Slot 7 (two-phase, all-pay second price): 0xff54217f20ab88d6a982a89b8d2a0410af5ef4a7. Between block 422,172 (Oct 22) and 442,864 (Oct 26), Vitalik ran roughly 250 test transactions across the slots, bidding against himself from his old wallet 0x1db3439a and his current vitalik.eth address 0xd8da6bf26964af9d7eed9e03e53415d37aa96045. The advertised URLs were http://vitalik.ca/files/me.jpg (a self-portrait) and http://vitalik.ca/files/heiko.jpg (Heiko Hees of Brainbot). No other Ethereum address ever bid. Source code was committed to ethereum/dapp-bin in commit cab4374 on 2015-10-24, alongside a React/web3 frontend with paged status displays for each slot. The next commit (40b8c07, 2015-10-25) carries the message 'Looks like the ether_ad dapp (mostly) works!'. The work appears to have been prep for DevCon 1, which ran 2015-11-09 through 11-13 in London, three weeks after the deployment. Ethereum mainnet was less than three months old, Frontier was still tagged alpha, and ETH traded near 1 USD. Compiled with solc v0.1.1+commit.6ff4cd6 (the very first tagged solc release, August 2015), optimizer on. Exact byte-for-byte match against the on-chain creation bytecode at block 411,110.
Key Facts
Description
An early Ethereum ad auction system deployed by Vitalik Buterin in October 2015, part of the ether_ad dapp from ethereum/dapp-bin. The adStorer contract manages 8 auction slots (mix of OnePhaseAuction and TwoPhaseAuction contracts). Winners of each auction can store a URL and address. The adStorer was initialized by deploying and linking the individual auction contracts.
Source: ethereum/dapp-bin ether_ad/adStorer.sol (commits 46b9554 through 40b8c07b, Oct-Nov 2015). The exact intermediate version deployed was not committed to the public repo. All 6 function selectors verified against the dapp-bin source.
Source Verified
Source: ethereum/dapp-bin@cab4374:ether_ad/ (one_phase_auction.sol + two_phase_auction.sol + adStorer.sol). Compiled with soljson-v0.1.1+commit.6ff4cd6 optimizer ON. The output is a byte-for-byte match against the on-chain creation bytecode (8752 bytes, tx input of 0x2c09efb1) and against the runtime returned by eth_getCode. v0.1.1 was the first tagged solc release (August 2015), still in production use when Vitalik deployed this in October 2015. Every later compiler from v0.1.4 through v0.2.0 produces a different output for the same source. Sourcify cannot verify this contract: the oldest compiler version it supports is v0.1.7. Submission to /server/v2/verify with compilerVersion=0.1.1+commit.6ff4cd6f was rejected with customCode unsupported_compiler_version (verificationId 969ec1a0-e6f6-455a-8feb-ad9237721d5e on 2026-05-13). Submitting the same standard-json with compilerVersion=0.1.7+commit.b4e666cc was accepted but returned no_match because v0.1.7 emits different bytecode. Independent reproduction proof lives at https://github.com/cartoonitunes/adstorer-verification: running verify.js compiles the bundled source with soljson-v0.1.1+commit.6ff4cd6.js and asserts the 8752-byte exact match.
Heuristic Analysis
The following characteristics were detected through bytecode analysis and may not be accurate.
Frontier Era
The initial release of Ethereum. A bare-bones implementation for technical users.
Bytecode Overview
Verified Source Available
Source verified through compiler archaeology and exact bytecode matching.
View Verification ProofShow source code (Solidity)
// Submitted by EthereumHistory (ethereumhistory.com)
//
// Source: ethereum/dapp-bin@cab4374:ether_ad/ (one_phase_auction.sol + two_phase_auction.sol + adStorer.sol)
// Compiled with solc v0.1.1+commit.6ff4cd6, optimizer enabled.
// Exact bytecode match against on-chain creation code at block 411110.
contract OnePhaseAuction {
adStorer target;
address owner;
uint256 phase;
uint256 auctionEnd;
uint256 durationBumpTo;
uint256 minIncrementMillis;
uint256 mostRecentAuctionStart;
struct Bid {
uint256 bidValue;
string metadata;
address bidder;
}
Bid[999999999999999999999] bids;
uint256 bestBidIndex;
uint256 bestBidValue;
uint256 secondBestBidValue;
uint256 nextBidIndex;
uint256 totalRevenue;
// Bitmask: +1 if bids cumulative +0 if independent, +2 if all-pay +0 if lead pays
uint256 auctionType;
event BidSubmitted(uint256 index, uint256 bidValue, string metadata, address indexed bidder);
event BidIncreased(uint256 index, uint256 bidValue, uint256 cumValue, string metadata, address indexed bidder);
event AuctionWinner(uint256 index, uint256 bidValue, string metadata, address indexed bidder);
event AuctionFinalized(uint256 revenue);
event AuctionInitialized();
function OnePhaseAuction() {
owner = msg.sender;
}
// Initialize the auction
function initialize(address _t, uint256 _baseDuration, uint256 _durationBumpTo, uint256 _minIncrementMillis, uint256 _tp) returns (bool) {
if (msg.sender != owner) return false;
if (phase == 1 || phase == 2) return false;
phase = 1;
target = adStorer(_t);
auctionEnd = block.timestamp + _baseDuration;
durationBumpTo = _durationBumpTo;
minIncrementMillis = _minIncrementMillis;
nextBidIndex = 0;
bestBidValue = 0;
bestBidIndex = 0;
auctionType = _tp;
mostRecentAuctionStart = block.number;
AuctionInitialized();
return true;
}
// Place one's bid
function bid(string metadata) returns (int256) {
if (phase != 1) {
msg.sender.send(msg.value);
return (-1);
}
if (phase == 1 && block.timestamp >= auctionEnd) {
phase = 2;
msg.sender.send(msg.value);
return (-1);
}
if (msg.value * 1000 < bestBidValue * (1000 + minIncrementMillis)) {
msg.sender.send(msg.value);
return (-1);
}
if (msg.value > bestBidValue) {
bestBidValue = msg.value;
bestBidIndex = nextBidIndex;
}
bids[nextBidIndex].bidValue = msg.value;
bids[nextBidIndex].metadata = metadata;
bids[nextBidIndex].bidder = msg.sender;
BidSubmitted(nextBidIndex, msg.value, metadata, msg.sender);
nextBidIndex = nextBidIndex + 1;
if (auctionEnd - block.timestamp < durationBumpTo)
auctionEnd = block.timestamp + durationBumpTo;
if ((auctionType & 2) == 2)
totalRevenue += msg.value;
return int256(nextBidIndex) - 1;
}
// Increase one's bid
function increaseBid(uint256 index) returns (bool) {
if ((auctionType & 1) == 0) {
msg.sender.send(msg.value);
return (false);
}
if (phase != 1) {
msg.sender.send(msg.value);
return (false);
}
if (phase == 1 && block.timestamp >= auctionEnd) {
msg.sender.send(msg.value);
phase = 2;
return (false);
}
if ((bids[index].bidValue + msg.value) * 1000 < bestBidValue * (1000 + minIncrementMillis)) {
msg.sender.send(msg.value);
return(false);
}
if (index >= nextBidIndex) {
msg.sender.send(msg.value);
return false;
}
bids[index].bidValue += msg.value;
if (bids[index].bidValue > bestBidValue) {
bestBidValue = bids[index].bidValue;
bestBidIndex = index;
}
if ((auctionType & 2) == 2)
totalRevenue += msg.value;
BidIncreased(index, msg.value, bids[index].bidValue, bids[index].metadata, bids[index].bidder);
if (auctionEnd - block.timestamp < durationBumpTo)
auctionEnd = block.timestamp + durationBumpTo;
return(true);
}
// Clean up during phase 3
function ping() returns(bool) {
if (phase == 1 && block.timestamp >= auctionEnd)
phase = 2;
if (phase != 2) return(false);
uint _nbi = nextBidIndex;
while (msg.gas > 100000 && _nbi > 0) {
_nbi -= 1;
if (_nbi == bestBidIndex) {
}
else {
if ((auctionType & 2) == 0)
bids[_nbi].bidder.send(bids[_nbi].bidValue);
bids[_nbi].bidValue = 0;
}
}
nextBidIndex = _nbi;
if (_nbi == 0) {
phase = 0;
bool success;
if (bestBidValue > 0) {
AuctionWinner(bestBidIndex, bestBidValue, bids[bestBidIndex].metadata, bids[bestBidIndex].bidder);
success = target.acceptAuctionResult(bids[bestBidIndex].bidder, bestBidValue, bids[bestBidIndex].metadata);
}
else {
success = target.acceptAuctionResult(0, 0, "");
}
if ((auctionType & 2) == 2)
AuctionFinalized(totalRevenue);
else
AuctionFinalized(bestBidValue);
if (!success) { while (1 == 1) { _nbi = _nbi; } }
return(true);
}
return(false);
}
function setOwner(address newOwner) {
if (owner == msg.sender) owner = newOwner;
}
function withdraw() {
if (msg.sender == owner) msg.sender.send(this.balance);
}
function getPhase() constant returns (uint256) {
return phase;
}
function getMostRecentAuctionStart() constant returns (uint256) {
return mostRecentAuctionStart;
}
function getPhaseExpiry() constant returns (uint256) {
return auctionEnd;
}
}
contract AuctionResultAcceptor {
function acceptAuctionResult(address winner, uint256 value, string metadata) { }
}
contract TwoPhaseAuction {
adStorer target;
address owner;
uint256 phase;
uint256 hashSubmissionEnd;
uint256 hashRevealEnd;
uint256 mostRecentAuctionStart;
uint256 valueSubmissionSubsidyMillis;
struct Bid {
bytes32 bidValueHash;
uint256 bidValue;
uint256 valueSubmitted;
string metadata;
address bidder;
}
Bid[999999999999999999999] bids;
uint256 bestBidIndex;
uint256 bestBidValue;
uint256 secondBestBidValue;
uint256 nextBidIndex;
uint256 totalValueSubmitted;
uint256 auctionRevenue;
// 1 = first price, 2 == second price, 3 == all pay, 4 == all pay + second price
uint256 auctionType;
event BidCommitted(uint256 index, bytes32 bidValueHash, string metadata, address indexed bidder);
event BidRevealed(uint256 index, uint256 bidValue, string metadata, address indexed bidder);
event AuctionWinner(uint256 index, uint256 bidValue, string metadata, address indexed bidder);
event AuctionFinalized(uint256 revenue);
event AuctionInitialized();
function TwoPhaseAuction() {
owner = msg.sender;
}
// Initialize the auction
function initialize(address _t, uint256 _hsp, uint256 _hrp, uint256 _vssm, uint256 _tp) returns (bool) {
if (msg.sender != owner) return false;
if (phase == 1 || phase == 2) return false;
phase = 1;
target = adStorer(_t);
hashSubmissionEnd = block.timestamp + _hsp;
hashRevealEnd = block.timestamp + _hsp + _hrp;
valueSubmissionSubsidyMillis = _vssm;
nextBidIndex = 0;
bestBidValue = 0;
secondBestBidValue = 0;
totalValueSubmitted = 0;
auctionRevenue = 0;
auctionType = _tp;
mostRecentAuctionStart = block.number;
AuctionInitialized();
return true;
}
// Commit one's bid. This also entails sending an amount of ether at least
// equal to, but potentially more than, one's bid; if you send a greater
// amount than the difference between the submission and your actual bid
// will be refunded to you (even in all-pay auctions). This protects bid
// privacy.
function commitBid(bytes32 bidValueHash, string metadata) returns (int256) {
if (phase != 1) {
msg.sender.send(msg.value);
return (-1);
}
if (phase == 1 && block.timestamp >= hashSubmissionEnd) {
msg.sender.send(msg.value);
return (-1);
}
bids[nextBidIndex].bidValueHash = bidValueHash;
bids[nextBidIndex].valueSubmitted = msg.value;
bids[nextBidIndex].metadata = metadata;
bids[nextBidIndex].bidder = msg.sender;
BidCommitted(nextBidIndex, bidValueHash, metadata, msg.sender);
nextBidIndex = nextBidIndex + 1;
totalValueSubmitted += msg.value;
return int256(nextBidIndex) - 1;
}
// Reveal one's bid
function revealBid(uint256 index, uint256 bidValue, bytes32 nonce) returns (bool) {
if (phase == 1 && block.timestamp >= hashRevealEnd) {
phase = 2;
}
if (phase != 1) {
return (false);
}
if (phase == 1 && block.timestamp < hashSubmissionEnd) {
return (false);
}
if (index >= nextBidIndex)
return false;
if (bidValue > bids[index].valueSubmitted)
return false;
if (sha3(bidValue, nonce) != bids[index].bidValueHash)
return false;
if (bidValue > bestBidValue) {
secondBestBidValue = bestBidValue;
bestBidValue = bidValue;
bestBidIndex = index;
}
else if (bidValue > secondBestBidValue) {
secondBestBidValue = bidValue;
}
// Only need to keep track of bid values for all-pay auctions
if (auctionType == 3 || auctionType == 4) {
bids[index].bidValue = bidValue;
auctionRevenue += bidValue;
}
BidRevealed(index, bidValue, bids[index].metadata, bids[index].bidder);
return true;
}
// Clean up during phase 2
function ping() returns(bool) {
if (phase == 1 && block.timestamp >= hashRevealEnd)
phase = 2;
if (phase != 2) return(false);
uint _nbi = nextBidIndex;
uint _ar;
if (auctionType == 1) _ar = bestBidValue;
else if (auctionType == 2) _ar = secondBestBidValue;
else if (auctionType == 3) _ar = auctionRevenue;
else if (auctionType == 4) _ar = auctionRevenue + secondBestBidValue - bestBidValue;
while (msg.gas > 500000 && _nbi > 0) {
_nbi -= 1;
uint256 subsidy = bids[_nbi].valueSubmitted * _ar * valueSubmissionSubsidyMillis / totalValueSubmitted / 1000;
if (_nbi == bestBidIndex) {
// First price auction or all-pay auction: take winner's bid at its own value
if (auctionType == 1 || auctionType == 3)
bids[_nbi].bidder.send(bids[_nbi].valueSubmitted - bestBidValue + subsidy);
// Second price auction: take winner's bid at second highest value
else if (auctionType == 2 || auctionType == 4)
bids[_nbi].bidder.send(bids[_nbi].valueSubmitted - secondBestBidValue + subsidy);
}
else {
// First price or second price auction: refund everyone else's bids
if (auctionType == 1 || auctionType == 2)
bids[_nbi].bidder.send(bids[_nbi].valueSubmitted + subsidy);
// All-pay auction: don't refund everyone else's bids
else
bids[_nbi].bidder.send(bids[_nbi].valueSubmitted - bids[_nbi].bidValue + subsidy);
bids[_nbi].bidValueHash = 0;
}
}
nextBidIndex = _nbi;
if (_nbi == 0) {
phase = 0;
bool success;
if (bestBidValue > 0) {
AuctionWinner(bestBidIndex, bestBidValue, bids[bestBidIndex].metadata, bids[bestBidIndex].bidder);
success = target.acceptAuctionResult(bids[bestBidIndex].bidder, bestBidValue, bids[bestBidIndex].metadata);
}
else {
success = target.acceptAuctionResult(0, 0, "");
}
AuctionFinalized(_ar);
if (!success) { while (1 == 1) { _nbi = _nbi; } }
return(true);
}
return(false);
}
function setOwner(address newOwner) {
if (owner == msg.sender) owner = newOwner;
}
function withdraw() {
if (msg.sender == owner) msg.sender.send(this.balance);
}
function getPhase() constant returns (uint256) {
return phase;
}
function getMostRecentAuctionStart() constant returns (uint256) {
return mostRecentAuctionStart;
}
function getHashSubmissionEnd() constant returns (uint256) {
return hashSubmissionEnd;
}
function getHashRevealEnd() constant returns (uint256) {
return hashRevealEnd;
}
}
contract adStorer {
string[8] urls;
address[8] winners;
address owner;
address[8] auctions;
uint256 hashSubmissionPeriod;
uint256 hashRevealPeriod;
uint256 baseDuration;
uint256 durationBumpTo;
uint256 minIncrementMillis;
uint256 initializedTo;
uint256 valueSubmissionSubsidyMillis;
event GasRemaining(uint256 g, uint256 i);
// Recommend: 86400 86400 86400 3600 50 10 live
// Recommend: 240 240 240 120 50 10 test
function initialize(uint256 _hsp, uint256 _hrp, uint256 _bdur, uint256 _dbt, uint256 _mim, uint256 _vssm) returns (bool) {
if (initializedTo < 8) {
if (owner == 0) owner = msg.sender;
hashSubmissionPeriod = _hsp;
hashRevealPeriod = _hrp;
baseDuration = _bdur;
durationBumpTo = _dbt;
minIncrementMillis = _mim;
valueSubmissionSubsidyMillis = _vssm;
for (uint256 i = initializedTo; i < 8 && msg.gas > 1100000; i++) {
GasRemaining(msg.gas, i);
if (i < 4) {
auctions[i] = new OnePhaseAuction();
OnePhaseAuction(auctions[i]).initialize(this, baseDuration, durationBumpTo, minIncrementMillis, i);
}
else {
auctions[i] = new TwoPhaseAuction();
TwoPhaseAuction(auctions[i]).initialize(this, hashSubmissionPeriod, hashRevealPeriod, valueSubmissionSubsidyMillis, i - 3);
}
}
initializedTo = i;
if (initializedTo == 8) return true;
else return false;
}
}
function acceptAuctionResult(address winner, uint256 value, string metadata) returns (bool) {
for (uint256 i = 0; i < 8; i++) {
if (msg.sender == auctions[i]) {
if (winner != 0) {
urls[i] = metadata;
winners[i] = winner;
}
if (i < 4) OnePhaseAuction(msg.sender).initialize(this, baseDuration, durationBumpTo, minIncrementMillis, i);
else TwoPhaseAuction(msg.sender).initialize(this, hashSubmissionPeriod, hashRevealPeriod, valueSubmissionSubsidyMillis, i - 3);
return true;
}
}
return false;
}
function getAuctionAddress(uint256 id) constant returns (address) {
return auctions[id];
}
function getWinnerUrl(uint256 id) constant returns (string) {
return urls[id];
}
function getWinnerAddress(uint256 id) constant returns (address) {
return winners[id];
}
}