A primary-source reconstruction, 2015 to 2016
How the token standard was written
Between June 2015 and January 2016 the interface now carried by every Ethereum token was put together member by member, across a wiki page, a GitHub issue, a handful of gists, two sets of official documentation and one implementation repository. No single document contains its history. This page reconstructs it from the artifacts that survive.
The two members that define what ERC-20 is, allowance and Approval, arrived about thirty hours after the issue was filed and five months after the wiki page was started. The specification stopped moving on 6 January 2016. The first contract on mainnet carrying all six methods appeared four days later.
Carrying the six methods and behaving the way the standard requires are separate claims with separate dates. The first contract to deploy the interface appeared on 10 January 2016, the first to pair it with a real supply on 14 January, and the first to satisfy every requirement of EIP-20 as finalised on 20 March 2016. That last one was established by running the contracts, not by reading them.
The interface
The final six, plus two events
function totalSupply() constant returns (uint256 supply)
function balanceOf(address _owner) constant returns (uint256 balance)
function transfer(address _to, uint256 _value) returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) returns (bool success)
function approve(address _spender, uint256 _value) returns (bool success)
function allowance(address _owner, address _spender) constant returns (uint256 remaining)
event Transfer(address indexed _from, address indexed _to, uint256 _value)
event Approval(address indexed _owner, address indexed _spender, uint256 _value)
This is the interface as issue #20 finally stated it. Reading the trail requires holding two things apart: the name of a member and its signature. Several members reached their final name months before they reached their final parameter list, and one, transferFrom, was born as a typo and corrected twenty-four seconds later.
Three further members appear in almost every real token and in none of the required set: name, symbol and decimals. They were proposed as wallet rendering hints, marked optional at birth, and are still optional today.
Marks used in every table below
- ✔ present
- Present under its final name and with its final parameter signature.
- ~ partial
- Name present, parameter signature differs from the final one.
- – absent
- Not present.
What the reconstruction establishes
Findings
Each row is a question with a single dated answer and a single artifact. The dates are not approximate.
Table scrolls sideways →
| # | Question | Answer (UTC) | Artifact |
|---|---|---|---|
| 1 | All six methods and both events first co-exist anywhere | 2015-11-20T15:53:42Z | ethereum/EIPs issue #20 body, revision 7. Prose spec, still carrying decimals and unapprove. |
| 2 | First compilable Solidity containing all six and both events | 2015-11-30T21:06:17Z | ConsenSys/Tokens Token.sol at 4ba2396. Still declares unapprove. |
| 3 | First file containing exactly the final interface, nothing extra | 2015-12-21T15:55:57Z | ConsenSys/Tokens Token.sol at c3a3426, “Derp. Approve is not a noun.” |
| 4 | First time the specification itself states exactly the final interface | between 2016-01-06T10:12:13Z and 2016-01-06T10:28:48Z | Issue #20 body revision 15. The bound is the gap between two comments. |
| 5 | Wiki mirrors the final interface | 2016-01-06T10:48:26Z | ethereum/wiki 02c64c1 by caktux, twenty minutes after the issue. |
| 6 | Contracts deployed 2015-11-03 to 2016-01-06 implementing all six | None. The maximum reached is three of six. | 3,062 contracts, 2,941 with runtime bytecode. |
| 7 | Deployed instances of approve(address,uint256) or allowance(address,address) in that window | Zero, by two independent detectors. | Opcode walk and substring match, in exact agreement. |
| 8 | First contract on mainnet that deploys the ERC-20 interface, meaning all six selectors are present in its dispatcher | 2016-01-10T00:21:08Z block 824,235 | 0x99146Bab2bB34D9Ca49EC4f0c82De3E5789ae22e, four days after the spec froze. It carries the interface and is not a token: supply reads zero at every block checked, and it has never emitted a Transfer. Selector presence is a bytecode fact and says nothing about behaviour. |
| 9 | First contract with the interface and a real supply, that is, the first that is a token rather than a bare interface | 2016-01-14T15:22:33Z block 847,527 | 0x55b9a11c2e8351b4Ffc7b11561148bfaC9977855, Digix Gold 1.0. Supply 1,400,331,016,000 at block 847,528, the block after deployment, and unchanged at every block sampled since. It is not fully compliant with EIP-20. Its Transfer event indexes _value, so it emits four topics and an empty data field where the standard requires three topics and the value in data, and transferFrom reports the spender as _from rather than the owner. |
| 10 | First ERC-20 Transfer event on mainnet | 2016-01-27T16:54:44Z block 913,198 | 0xa04bf47F0E9D1745D254b9B89f304c7d7ad121Aa, elcoin. Deployer to another account, 1,000,000 units. Not a mint from the zero address. The contract is not compliant. Its own transfer, approve and transferFrom return false, move nothing and emit nothing. Every transfer it recorded was driven through a controller, not through the ERC-20 entry points. |
| 11 | First contract whose dispatcher is exactly the six and nothing else | 2016-01-28T14:01:48Z block 917,622 | 0x37Dca38b1CBB2Cd043910eC46fe82Ddb9e38F00d, 754 bytes. Under execution it satisfies every requirement of EIP-20 except one: a guard of && _value > 0 makes a zero-value transfer return false and emit nothing. That requirement is in EIP-20 as finalised and is absent from the 2015 text, so by the standard as it stood when this was deployed, it passes. |
| 12 | First contract fully compliant with EIP-20 as finalised, tested by execution against all ten requirements | 2016-03-20T11:09:16Z block 1,184,107 | 0xacFD9D15fA769EaBb68410c4c675Ff2030f26416, 2,356 bytes. An ether wrapper: deposit() mints, withdraw(uint256) burns, totalSupply() returns the contract's own ether balance. It was never used, holding one transaction in its life, its creation. Its byte-identical twin 0xd654bDD32FC99471455e86C2E7f7D7b6437e9179, 81 blocks later, is the one that saw traffic. |
| 13 | The source in the Foundation's 2015-12-03 tutorial | Byte-identical to MistCoin's source | Gist 21935dc… equals gist revision 7bcfaef3. Both compile to MistCoin's exact bytecode. |
| 14 | What MistCoin implements | 2 of 6 required methods, 1 of 2 required events, 3 of 3 optional | Decoded from deployed runtime bytecode. |
approve that took no amount, no allowance, no Approval event, and five members that do not exist in the standard today: decimals, unapprove, isApprovedFor, approveOnce and isApprovedOnceFor.
Every dated event, all sources interleaved
The timeline
Fifty-eight entries, from the first official token tutorial to the standard's formal adoption. Open any entry for the underlying evidence: the source as it stood, the commit diff, the issue body, the decoded bytecode. Bold markers are the milestones from the findings above.
The wiki page and the guides
Before there was an issue there was a page. Standardized_Contract_APIs described currencies, exchanges and registries together, and its currency section is the direct ancestor of ERC-20. Over its first 35 revisions its vocabulary is replaced word by word, mostly by people renaming each other's functions. Running alongside it, and never quite agreeing with it, is the official documentation: the Frontier Guide for the first live network, and the ethereum.org token page. Both taught a vocabulary of their own.
-
2015-02-2312:59:40Z
2015-02-23 · 12:59:40Z A coin contract with no standard API Gav Wood commits coin.sol to ethereum/dapp-bin. It is a currency contract written before anyone proposed that currency contracts should agree on anything.
Included as the floor of the trail. Nothing in this file survives into the standard, and that is the point: the problem the wiki page was created to solve is visible here as an absence.
Sources
-
2015-05-0422:09:49Z
22:11:37Z2015-05-04 · 22:09:49Z and 22:11:37Z The Frontier Guide adds a token chapter Taylor Gerring creates contract_coin.md in the Ethereum Frontier Guide, then creates the page it transcludes. The contract uses sendToken and getBalance. Six weeks before Standardized_Contract_APIs exists.
The Frontier Guide is the official Ethereum user documentation for the first live network. It is a GitBook, and it holds almost no prose of its own: chapters are one-line transclusions of pages in the go-ethereum wiki. contract_coin.md in full, as created:
# Coin contractOne hundred and eighteen seconds later Gerring created the wiki page it would point at, and rewrote the chapter to pull it in:
frontier-guide contract_coin.md at 8c91460, 2015-05-04T22:11:47Z {% include "git+https://github.com/ethereum/go-ethereum.wiki.git/Coin-Contract-Tutorial.md" %}That transclusion is why the guide's own repository contains no token vocabulary at any point in its history: 216 commits between 2015-04-30 and 2015-07-28, and a search of every revision for sendCoin, balanceOf, coinBalance or the word “token” returns nothing. The content lives in the wiki.
The contract the official guide shipped
go-ethereum.wiki Coin-Contract-Tutorial.md at cc23634, verbatim contract token { mapping (address => uint) balances; // Initializes contract with 10 000 tokens to the creator of the contract function token() { balances[msg.sender] = 10000; } // Very simple trade function function sendToken(address receiver, uint amount) returns(bool sufficient) { if (balances[msg.sender] < amount) return false; balances[msg.sender] -= amount; balances[receiver] += amount; return true; } // Check balances of any account function getBalance(address account) returns(uint balance){ return balances[account]; } }This is a third vocabulary, and it belongs to neither of the others. Not the wiki's
sendCoinandcoinBalanceOf, which do not exist yet. Not the standard'stransferandbalanceOf, which are five months away. The official guide for the first live Ethereum network taughtsendTokenandgetBalance, and it did so before there was any standardization effort to disagree with.There is no event of any kind. Nothing a client could watch.
The selectors, and where they can be seen
sendToken(address,uint256) 0x412664ae getBalance(address) 0xf8b2cb4fBoth appear in the compiled bytecode the tutorial pastes underneath the source, in its dispatcher: …90048063412664ae1461003a578063f8b2cb4f1461005257005b…. The published bytecode and the published source agree.
Sources
- Guide chapterfrontier-guide a465ac5 · Create contract_coin.md
- Transclusionfrontier-guide 8c91460 · Update contract_coin.md
- Tutorial pagego-ethereum wiki cc23634 · Created Coin Contract Tutorial
- Wiki clonegit clone https://github.com/ethereum/go-ethereum.wiki.git · cc236342996894a61a884303a3e9869bed2fb7a7
- Repo created2015-04-30T21:04:22Z, Taylor Gerring
-
2015-06-1716:06:56Z
2015-06-17 · 16:06:56Z Vitalik Buterin creates Standardized_Contract_APIs The page that becomes ERC-20 begins as a three-part document covering currencies, exchanges and registries. Its currency API has balance, send, and an
approvethat takes a boolean. Zero of the final six.The framing sentence of the page states the whole design intent, and it is worth reading before anything else in this trail:
Although Ethereum allows developers to create absolutely any kind of application without restriction to specific feature types, and prides itself on its "lack of features", there is nevertheless a need to standardize certain very common use cases in order to allow users and applications to more easily interact with each other.Standardized_Contract_APIs, revision 1, verbatim
The currency API as created
balance(address addr) returns (uint256 bal) send(address to, uint256 value) returns (bool success) send(address to, uint256 value, address from) returns (bool success) approve(address addr, bool status) approved(address addr) returns (bool status) approve_once(address addr, bool status, uint256 maxval)Two structures already present here survive all the way to the finished standard. The first is the pull model: a third-party spender is authorised in advance, then moves funds on the owner's behalf. The second is the paragraph justifying it, which travels almost unedited into the issue five months later:
The third command is used for a "direct debit" workflow, allowing contracts to charge fees in sub-currencies; the second
sendcommand should fail unless thefromaccount has deliberately authorized the sender of the message via some mechanism; we propose these standardized APIs for approvalStandardized_Contract_APIs, revision 1, verbatimWhat is not here: no events at all, no
totalSupply, no read of another account's balance, and an approval that is a boolean switch rather than an amount.Sources
- Wiki revision748c9b0a7f459ed5754420b6368bdd536bc4bdc4
- MessageCreated Standardized_Contract_APIs (markdown)
- Raw artifactraw/wiki-revisions/01-2015-06-17_18-06-56-748c9b0.md
-
2015-06-1803:37:28Z
2015-06-18 · 03:37:28Z Rewritten as the sendCoin API Eleven hours after creating it, Buterin replaces the whole currency section. balance becomes coinBalanceOf, send becomes sendCoin, and the approval members take the names they will keep for the next five months.
This revision installs the vocabulary that the rest of 2015 runs on:
sendCoin,coinBalanceOf,isApprovedFor,approveOnce,isApprovedOnceFor. Every one of these names is eventually replaced, and every replacement is dated in this timeline.Sources
- Wiki revisionb65a40cbaee585674f17cbe9d471ac7e6c40227a
-
2015-06-2307:46:09Z
2015-06-23 · 07:46:09Z The first event enters the specification CoinSent(address,uint256,address). The ancestor of Transfer, with the value in the middle and the recipient last.
Two renames separate this from the final event. It becomes
CoinTransferon 5 July 2015 andTransferon 4 October 2015. The parameter order also changes: the deployedTransfertopic is (from, to, value), not (from, value, to).The
CoinSentform was not merely a draft. It reached mainnet: two contracts deployed in the 2015-11-03 to 2016-01-06 window still carry its topic hash.Sources
- Wiki revision6fa7692d225a8562f65c9c1d44712ac4d73e7c95
- Rename to CoinTransferee6f5a0, 2015-07-05T10:48:34Z
-
2015-07-0221:51:06Z
2015-07-02 · 21:51:06Z coinBalanceOf enters the Frontier Guide tutorial “Updated contracts with latest google docs.” The tutorial gains coinBalanceOf and one mention of sendCoin, while still carrying sendToken and getBalance elsewhere. The page is briefly bilingual.
This is the first appearance of the wiki standardization vocabulary inside the official user guide, and it is partial. Counting occurrences across the page at this revision: sendToken 12, coinBalanceOf 5, getBalance 2, sendCoin 1. The token contract itself has been rewritten around a public coinBalanceOf mapping but has not yet regained a transfer function or an event.
go-ethereum.wiki Contract-Tutorial.md at 3efbb93, contract opening contract token { mapping (address => uint) public coinBalanceOf; /* Initializes contract with 10 000 tokens to the creator of the contract */ function token() { coinBalanceOf[msg.sender] = 10000; }Declaring the mapping
publicis what makescoinBalanceOf(address)callable: the getter is generated, not written. Its selector, 0xbbd39ac0, is the same one the wiki specification asks for. That coincidence is what makes the onchain census below readable at all, and also what makes it ambiguous.The commit message is the useful part. “latest google docs” places the drafting somewhere outside version control, which is consistent with the rest of this trail: almost nothing here was designed in the repository it ended up in.
Sources
- Tutorial pagego-ethereum wiki 3efbb93 · Updated contracts with latest google docs.
- Author date2015-07-02 18:51:06 -0300
- Wiki clone3efbb93212cea5d90c1d9239f8f6b5cd9c260292
-
2015-07-1508:52:32Z
10:23:14Z2015-07-15 · 08:52:32Z and 10:23:14Z ConsenSys/Tokens restarts, and implements the wiki Simon de la Rouviere restarts the repository with “Let there be tokens.”, then ninety minutes later commits the first Solidity implementation of the wiki API. This repository tracks the specification revision by revision for the next year.
ConsenSys/Tokens is the implementation lineage of ERC-20, and it starts four months before issue #20 exists. Its abstract Token.sol is the file that eventually holds the exact final interface, on 21 December 2015.
Sources
- Restarte9817a3 · Refactor & Restart. Let there be tokens.
- Implementation0463a18 · Standardised Token
-
2015-07-1515:38:24Z
2015-07-15 · 15:38:24Z ethereum.org publishes a token page Van de Sande adds token.md to the ethereum.org source. It carries coinBalanceOf, sendCoin and event CoinTransfer, and links the wiki as the “Meta coin standard”. Nine days before the same contract reaches the go-ethereum wiki.
This is the page reached at ethereum.org/token, and it is the third piece of official documentation in this trail, alongside the Frontier Guide and the wallet's own ABI. It is the one Van de Sande points the issue thread to eight months later, when he writes that “the latest proposed standard is kept updated at ethereum.org/token”.
ethereum-org views/content/token.md at 33b1217, contract as published contract token { mapping (address => uint) public coinBalanceOf; event CoinTransfer(address sender, address receiver, uint amount); /* Initializes contract with initial supply tokens to the creator of the contract */ function token(uint supply) { coinBalanceOf[msg.sender] = (supply || 10000); } /* Very simple trade function */ function sendCoin(address receiver, uint amount) returns(bool sufficient) { if (coinBalanceOf[msg.sender] < amount) return false; coinBalanceOf[msg.sender] -= amount; coinBalanceOf[receiver] += amount; CoinTransfer(msg.sender, receiver, amount); return true; } }This is the contract the Frontier Guide would carry, and ethereum.org has it first. On this date the go-ethereum wiki tutorial that the guide transcludes still hascoinBalanceOfwith no event and nosendCoinin the contract; it does not reach this form until Van de Sande's rewrite on 2015-07-24, nine days later. The same author wrote both.What the page said the standard was
[Meta coin standard](https://github.com/ethereum/wiki/wiki/Standardized_Contract_APIs) is a proposed standardization of function names for coin and token contracts, to allow them to be automatically added to other ethereum contract that utilizes trading, like exchanges or escrow.ethereum.org/token, Learn More section, verbatim including its markdown link syntax
A pointer to the wiki, under the name “Meta coin standard”. It stays on the page for five months and is removed on 23 December 2015.
Sources
- Commitethereum-org 33b1217 · Updating the release to the latest frontier page
- Author date2015-07-15 12:38:24 -0300
- Fileviews/content/token.md, first of 117 revisions
- Referenced byissue #20, comment 196816918, 2016-03-15
-
2015-07-2414:51:33Z
16:24:55Z2015-07-24 · 14:51:33Z and 16:24:55Z The guide's token contract reaches its final form Van de Sande's rewrite drops sendToken entirely and adds event CoinTransfer. Ninety-three minutes later zelig updates the guide's section anchors “for Alex tutorial updates”. Six days before Frontier launches.
After this edit the page contains coinBalanceOf 35 times, sendCoin 14 times, and sendToken zero. The token contract is now this, and the source does not change again until a one-operator compile fix four weeks later:
go-ethereum.wiki Contract-Tutorial.md, section “The Coin”, verbatim including its irregular indentation contract token { mapping (address => uint) public coinBalanceOf; event CoinTransfer(address sender, address receiver, uint amount); /* Initializes contract with initial supply tokens to the creator of the contract */ function token(uint supply) { coinBalanceOf[msg.sender] = (supply || 10000); } /* Very simple trade function */ function sendCoin(address receiver, uint amount) returns(bool sufficient) { if (coinBalanceOf[msg.sender] < amount) return false; coinBalanceOf[msg.sender] -= amount; coinBalanceOf[receiver] += amount; CoinTransfer(msg.sender, receiver, amount); return true; } }The guide and the specification agree on two names and disagree on the signature of one of them. The wiki specifies sendCoin(uint _value, address _to). The guide writes sendCoin(address receiver, uint amount). Same name, reversed parameters, different ABI selector, no interoperability.guide sendCoin(address,uint256) 0x90b98a11 wiki sendCoin(uint256,address) 0xc86a90fe both coinBalanceOf(address) 0xbbd39ac0 guide CoinTransfer(address,address,uint256) 0x16cdf170…6146The
coinBalanceOfgetter matches. The transfer function does not. A contract written from the guide and a contract written from the specification could read each other's balances and could not move each other's tokens.Note also what the event is not. The wiki had specified indexed parameters since June; this one indexes nothing, so a client cannot filter its logs by sender or recipient. And it is named
CoinTransfer, which the wiki had already renamed fromCoinSenton 5 July and would rename toTransferon 4 October. The guide never follows.The guide follows, ninety-three minutes later
frontier-guide contract_coin.md at 7366342, complete {% sections "the-coin", "" %} {% endsections %} {% include "git+https://github.com/ethereum/go-ethereum.wiki.git/Contract-Tutorial.md" %}The commit message is “fix section anchors for Alex tutorial updates”. The guide had been pulling the section anchored "coin"; the rewrite renamed the heading, so the anchor became "the-coin". That one-word edit is the entire mechanism by which the official Frontier documentation adopted this contract.
Sources
- Tutorial rewritego-ethereum wiki 8eaddd0 · updated tutorial
- Guide anchor fixfrontier-guide 7366342 · fix section anchors for Alex tutorial updates
- Author dates2015-07-24 11:51:33 -0300 · 2015-07-24 17:24:55 +0100
- Last guide commit2015-07-28T09:05:36Z, Fabian Vogelsteller, “Update SUMMARY.md”. The repository stops here, two days before Frontier.
-
2015-08-0720:42:23Z
2015-08-07 · 20:42:23Z · block 49,853 The guide's contract on mainnet Eight days after Frontier launch. 264 of the 628 token-vocabulary contracts deployed on mainnet in 2015 carry coinBalanceOf together with the CoinTransfer topic, the guide's exact pairing. Seven carry the specification's sendCoin.
Measured across every contract created on mainnet from Frontier launch on 2015-07-30 to 2015-12-31: 6,187 create traces, 5,724 with runtime bytecode, 628 carrying token vocabulary.
In 2015 Contracts coinBalanceOf(address) 274 CoinTransfer(address,address,uint256) 287 Both together, the guide's pairing 264 …of those, also carrying sendCoin(uint256,address) 7 The first six
UTC Block Address Runtime Family 2015-08-07T20:42:23Z 49,853 0x8374f5CC22eDA52e960D9558fb48DD4b7946609a 495 21c82a42 2015-08-07T20:45:00Z 49,864 0x3B4446ACD9547D0183811F0E7c31b63706295f52 495 21c82a42 2015-08-07T20:50:03Z 49,888 0xD958b51bC95338D152D55BEEd17a156e8aeC4c9f 607 b9184117 2015-08-08T11:01:11Z 53,051 0x3c401B518252aBE3BBBf898A44939699E7dA1634 495 21c82a42 2015-08-08T11:01:38Z 53,054 0x33e98638ea7F2C2fd83731528fb53802Af395D13 495 21c82a42 2015-08-08T17:41:09Z 54,537 0xE9712E9d4635f4c6937E9982A1596C64F0968A4c 495 21c82a42 None of the 264 has
name,symbolordecimals, and none has any of the final six. They cluster tightly by bytecode: 91 in one family, 54 in a second, 46 in a third, and runtime sizes of 495 bytes (99 contracts), 278 (66) and 238 (46). By month: 65 in August, 51 in September, 102 in October, 12 in November, 34 in December.This exposes a gap in the original scan. Its superseded-vocabulary list was built from the wiki, dapp-bin and the DAO, so it tested sendCoin(uint256,address) and never tested the guide's sendCoin(address,uint256). That is why the drafting-window figures elsewhere on this page report 56 contracts with coinBalanceOf and only 2 with sendCoin: 53 of those 56 pair coinBalanceOf with CoinTransfer, and none of the 53 carries the specification's sendCoin at all. Their transfer function is under a signature the scan did not look for.What is established is the co-occurrence and its size. The attribution to the guide is an inference, though a tight one: the pairing of a public
coinBalanceOfmapping with an unindexedCoinTransferevent, no optional metadata, and no specificationsendCoin, is exactly the shape of the contract printed in the official tutorial and of nothing else in the vocabulary. It has not been re-verified by re-scanning, because the archived corpus stores decoded member lists rather than bytecode. Re-running the walk with 0x90b98a11 added would settle it.Sources
- Corpusraw/onchain/y2015-token-vocabulary-hits.json (628 records), window-token-vocabulary-hits.json (320 records)
- Scan vocabularyraw/onchain/scan-selectors.py, LEGACY map
- Missing selectorsendCoin(address,uint256) = 0x90b98a11
-
2015-08-2219:23:23Z
2015-08-22 · 19:23:23Z A compile fix, and the last change to the contract Eerik Puska: “Solc gave error ‘Type error: Operator || not compatible with types uint256 and int_const’, fixed code to make it compile”. After this edit the guide's token contract is byte-identical through 2019.
The fix is coinBalanceOf[msg.sender] = (supply || 10000); becoming coinBalanceOf[msg.sender] = supply;. The default-supply idea is dropped rather than rewritten. The reported error means the contract as published at Frontier launch did not compile under the solc of the day, and stood that way for twenty-three days after launch, and twenty-nine days after it was written.
The token contract source across every revision of the page
UTC Commit Author Source state 2015-05-09T20:50:35Z 49baa0a Viktor Trón sendToken / getBalance, no event 2015-07-02T21:51:06Z 3efbb93 Alexandre Van de Sande public coinBalanceOf mapping 2015-07-24T14:51:33Z 8eaddd0 Alexandre Van de Sande sendCoin(address,uint) + CoinTransfer 2015-08-22T19:23:23Z 4c31226 Eerik Puska compile fix. Unchanged from here 2016-03-31T16:56:51Z 8bab868 Péter Szilágyi a missing + in a console.log, outside the contract 2016-10-31T18:29:02Z f49ff0e Felix Lange no change to the section at all 2017-12-20T11:07:43Z 620afae Felix Lange “Delete legacy documentation” Between 2015-08-22 and 2016-10-31 the whole “The Coin” section changed by exactly one character, and the contract source not at all. Verified by extracting the section from each of the 29 revisions of the page and hashing it.
The official Ethereum client documentation never adopted ERC-20. The wiki renamedcoinBalanceOftobalanceOfandCoinTransfertoTransferon 4 October 2015. The specification froze on 6 January 2016. The guide's tutorial went on printingcoinBalanceOf,sendCoinandCoinTransferunchanged for another twenty-two months, until the page was marked legacy in December 2017.Placed beside the onchain census, this is the plainest available explanation for a number that otherwise looks strange: 53 contracts emitting
CoinTransferduring the drafting window, months after the standard had renamed it. They were following the documentation, and the documentation had not moved.Sources
- Compile fixgo-ethereum wiki 4c31226
- Legacy marker620afae, 2017-12-20T11:07:43Z, “Delete legacy documentation”
- MethodSection extracted from all 29 revisions of Contract-Tutorial.md in a fresh clone and hashed; the contract body isolated by regex and hashed separately.
-
2015-08-2414:35:48Z
2015-08-24 · 14:35:48Z The approval system reaches its largest form caktux adds disapprove, isApprovedOnce, isApprovedOnceFor, and the events AddressApproval and AddressApprovalOnce. The approval system now has six members. It ends with two.
This is the high-water mark of the approval design. Both events added here reached mainnet: one contract in the drafting window carries the
AddressApprovaltopic and one carriesAddressApprovalOnce.On 2 September 2015 Simon de la Rouviere renames
disapprovetounapprove, the form that survives into issue #20 and is not removed until 6 January 2016.Sources
-
2015-09-0612:04:52Z
2015-09-06 · 12:04:52Z The wiki API as runnable Solidity standardized_contract_apis/currency.sol lands in ethereum/dapp-bin. Zero of the final six, by signature.
The interface at that commit
function currency() function sendCoin(uint _value, address _to) function sendCoinFrom(address _from, uint _value, address _to) function coinBalance() function coinBalanceOf(address _addr) function approve(address _addr) function isApproved(address _proxy) function approveOnce(address _addr, uint256 _maxValue) function isApprovedOnceFor(address _target, address _proxy) function disapprove(address _addr) event CoinSent(address indexed from, uint256 value, address indexed to)Not one member here carries a name that ERC-20 requires. This file is worth keeping in view when reading the onchain section: fifty-six contracts deployed during the drafting window implement
coinBalanceOf, months after the wiki had renamed it away.Sources
-
2015-10-0120:38:26Z
2015-10-01 · 20:38:26Z Alex Van de Sande proposes the optional three A new section, “Variables”, adds coinSymbol, coinName and coinBaseUnit to a page that until then described only methods and events. He marks them optional in the same sentence he introduces them.
These three members are invisible in the finished EIP-20 text precisely because they are the three the standard marks OPTIONAL. They are also, in practice, the three that most token contracts of the era actually implemented.
The section as added, from the diff against revision 26
ethereum/wiki 1f0f0a5, abridged where marked ### Variables These variables contain information about the coin. They are optional but adding them would increase the experience of the user that the GUI Client can use or not. #### coinSymbol (string) Contains a short sequence of letters that are used to represent the unit of the coin. [...] Examples: `USDX`, `BOB$`, `Ƀ`, `% of shares`. #### coinName (string) Contains a longer sequence of the coin name. [...] Examples: `e-Dollar`, `BobCoin`, `Bitcoin-Eth`. #### coinBaseUnit (integer) Although most coins are displayed to the final user as containing decimal points, coin values are unsigned integers, as the recommended method is to to calculations in the smallest possible unit. The client should always display the total units divided by coinBaseUnit. [...] Example: Bob has a balance of 100000 BobCoins, whose base unit is 100. His balance will be displayed on the client as **BOB$100.00**Three things in this text survive into the standard unchanged. The members themselves. Their optionality, written 49 days before issue #20 was filed. And their justification: these were never protocol features, they were wallet rendering hints.
How the third member got its final name
UTC Source Form 2015-10-01T20:38:26Z wiki 1f0f0a5, Van de Sande coinSymbol, coinName, coinBaseUnit 2015-10-04T15:07:06Z wiki 607b6ac, Gav Wood name, symbol, baseUnit 2015-10-30T12:51:31Z meteor-dapp-wallet 860b85f tokenName, tokenSymbol, tokenDecimals 2015-10-30T18:04:44Z meteor-dapp-wallet ce24214 name, symbol, decimals decimalsnever appears on the wiki page at all. The wiki carriedbaseUnitunchanged from revision 27 through revision 40. The worddecimalsenters the record from the wallet side and travels from there into issue #20.Sources
- Wiki revision1f0f0a5 · Standard Suggestion: Coin Variables
- Author date2015-10-01 17:38:26 -0300
- Raw artifactraw/wiki-revisions/27-2015-10-01_17-38-26-1f0f0a5.md
-
2015-10-0415:07:06Z
2015-10-04 · 15:07:06Z Gav Wood renames coinBalanceOf to balanceOf, and CoinTransfer to Transfer Two of the eight final names arrive in a single wiki edit whose commit message is “Updated Standardized_Contract_APIs (markdown)”. Both keep their final signature from this moment onward.
This is the earliest public appearance of
balanceOfand of theTransferevent anywhere in the sources examined. The same edit shortens Van de Sande'scoinBaseUnittobaseUnitand drops thecoinprefix from the other two optional members.The full interface after this edit
sendCoin(uint _value, address _to) sendCoinFrom(address _from, uint _value, address _to) balanceOf(address _addr) approve(address _addr) unapprove(address _addr) isApprovedFor(address _target, address _proxy) approveOnce(address _addr, uint256 _maxValue) isApprovedOnceFor(address _target, address _proxy) event Transfer(address indexed from, address indexed to, uint256 value) event AddressApproval(address indexed address, address indexed proxy, bool result) event AddressApprovalOnce(address indexed address, address indexed proxy, uint256 value)Note what has and has not happened.
balanceOfandTransferare final. The two transfer methods are stillsendCoinandsendCoinFrom, and still take value before recipient. There is nototalSupply, noallowance, andapprovetakes no amount.A second, cosmetic revision follows nineteen seconds later at 15:07:25Z.
Sources
- Wiki revision607b6ac9c090c45c10e05a00c172793811d02621
- Follow-up352eade · 2015-10-04T15:07:25Z
- Raw artifactraw/wiki-revisions/28-2015-10-04_17-07-06-607b6ac.md
-
2015-10-0612:57:06Z
12:57:30Z2015-10-06 · 12:57:06Z and 12:57:30Z Simon de la Rouviere adds transfer and transferFrom, twenty-four seconds apart The commit message is “Function names were still ‘coin’ related.” sendCoin becomes transfer. sendCoinFrom becomes trasnferFrom, misspelled. The typo is fixed in the next revision, 24 seconds later.
The function is named in a wiki edit that also misspells its sibling. The correction commit message is “Typo fix”.
After the typo fix, revision 31
transfer(uint _value, address _to) transferFrom(address _from, uint _value, address _to) balanceOf(address _addr) approve(address _addr) unapprove(address _addr) isApprovedFor(address _target, address _proxy) approveOnce(address _address, uint256 _maxValue) isApprovedOnceFor(address _target, address _proxy) event Transfer(address indexed from, address indexed to, uint256 value) event AddressApproval(address indexed address, address indexed proxy, bool result) event AddressApprovalOnce(address indexed address, address indexed proxy, uint256 value)The names are now final but the signatures are not.
transferstill takes value first and recipient second, which is the reverse of the standard. That is corrected on 28 October 2015 by Fabian Vogelsteller, and only then do these two members reach the form the ABI selectors are computed from.Sources
- Renamebfc39cb · Function names were still "coin" related.
- Typo fix9721a6b · Typo fix
- Raw artifactsraw/wiki-revisions/30-…-bfc39cb.md, 31-…-9721a6b.md
-
2015-10-0621:37:39Z
2015-10-06 · 21:37:39Z The wallet's token interface Alex Van de Sande adds tokenABI.js to ethereum/meteor-dapp-wallet. It expects three members: sendCoin, coinBalanceOf, and the CoinTransfer event. Nine hours after the wiki renamed all three away.
This file is the interface the Ethereum Wallet, shipped inside Mist, actually required of a token. It is the operative definition of “token” for the entire period covered here, and it never required the six. Its full history:
UTC Commit Members in the ABI 2015-10-06T21:37:39Z 43a5115 sendCoin, coinBalanceOf, CoinTransfer (event) 2015-10-23T13:44:44Z cc1be08 balanceOf, transfer, Transfer (event) 2015-10-27T14:40:33Z 518db2d balances, balanceof, transfer, Transfer (event) 2015-10-30T12:51:31Z 860b85f balanceOf, transfer, tokenDecimals, tokenName, tokenSymbol, Transfer (event) 2015-10-30T18:04:44Z ce24214 balanceOf, transfer, decimals, name, symbol, Transfer (event) 2015-11-03T12:06:23Z 0022e37 name, decimals, balanceOf, symbol, transfer, Transfer (event) 518db2d contains the typo balanceof alongside balances, preserved here as found. After 2 December 2015 the file moved to tokenInterface.js with the same five members plus the event.
The wallet wantedname,symbolanddecimals, none of which ERC-20 requires. It did not wanttotalSupply,transferFrom,approveorallowance, four of the six that it does. The wallet's requirement and the specification's requirement were never the same document, and were never reconciled during this period.Sources
- Raw artifactsraw/code-snapshots/mist-wallet-43a5115-tokenABI.js, mist-wallet-cc1be08-tokenABI.js, mist-wallet-0022e37-tokenABI.js
-
2015-10-2309:41:56Z
2015-10-23 · 09:41:56Z · block 426,661 First contract on mainnet with balanceOf, transfer and Transfer together 0x3C655ccb35666579511489af88153517fc58b017, 508 bytes. The first of nine contracts from a two-address prototyping run over the following week. None of the nine has a name, a symbol or a decimal place.
Established against every contract created on mainnet from Frontier launch on 2015-07-30 to 2015-12-31: 6,187 create traces, 5,724 with runtime bytecode, 628 carrying token vocabulary.
The prototyping run
UTC Block Address Runtime Family Deployer 2015-10-23T09:41:56Z 426,661 0x3C655ccb35666579511489af88153517fc58b017 508 6ec841c7 0xB1a2B43A7433dd150BB82227eD519Cd6b142d382 2015-10-23T12:18:34Z 427,198 0xe6512959d9cAA531c260E97C731BA0C821EC0C13 508 6ec841c7 0xB1a2B43A7433dd150BB82227eD519Cd6b142d382 2015-10-23T12:30:46Z 427,248 0xCAe62D22E8480b230d7aD93039167a0FfA7A2B8B 625 0316932e 0xB1a2B43A7433dd150BB82227eD519Cd6b142d382 2015-10-23T12:34:58Z 427,265 0x11485C5f164d6A67A72eEE9093b2581D1c304094 625 0316932e 0xB1a2B43A7433dd150BB82227eD519Cd6b142d382 2015-10-26T16:09:34Z 443,423 0xE274d18EF7b194A1EDEbB04cfE297CFe1489ef65 625 0316932e 0x9b22a80D5c7B3374a05b446081f97d0A34079e7F 2015-10-26T16:11:46Z 443,429 0x00576287D3263ba831C8cf0886f06537e0515A2C 625 0316932e 0xB1a2B43A7433dd150BB82227eD519Cd6b142d382 2015-10-27T11:21:38Z 447,432 0x27Cb40ce7EB4d078196923d608Eb903A17E0C0ED 625 c56ecb93 0xB1a2B43A7433dd150BB82227eD519Cd6b142d382 2015-10-27T17:47:26Z 448,848 0x22D9d0D4c30f7ad097E46669F3D624923179e949 611 ede40200 0xB1a2B43A7433dd150BB82227eD519Cd6b142d382 2015-10-30T11:05:29Z 462,663 0xe6EE69495B571e1042f760d7f34009164AFF87a2 211 15c53184 0xB1a2B43A7433dd150BB82227eD519Cd6b142d382 The second of those deployers, 0x9b22a80D5c7B3374a05b446081f97d0A34079e7F, is MistCoin's own, eight days later.
These are not nine independent tokens. Eight come from one address, which deployed twelve token-vocabulary contracts between 23 October and 20 November 2015 across six distinct bytecode families: one developer iterating. The ninth comes from MistCoin's own deployer and is byte-identical to a contract 0xB1a2B43A7433dd150BB82227eD519Cd6b142d382 deployed two minutes and twelve seconds later from the same family. Two addresses put the same build on mainnet two minutes apart.
The wallet's tokenABI.js at cc1be08, committed on the same day, opens with a commented-out address: //"0x11485C5f164d6A67A72eEE9093b2581D1c304094". That is row four of the table above. The client's token interface was being written against these prototypes as they were deployed.Sources
- CorpusBigQuery crypto_ethereum.traces export, cross-checked against a local index of 12,023,046 contracts
- Raw artifactsraw/onchain/y2015-token-vocabulary-hits.json, raw/code-snapshots/mist-wallet-cc1be08-tokenABI.js
-
2015-10-2813:44:47Z
2015-10-28 · 13:44:47Z Fabian Vogelsteller fixes the parameter order “changed order to parameters in transfer and transferFrom”. transfer(address _to, uint256 _value) and transferFrom(_from, _to, _value) reach their final signatures, three weeks after reaching their final names.
This edit is what fixes the two ABI selectors that matter most: 0xa9059cbb for
transferand 0x23b872dd fortransferFrom. Before it, the same names hash to different selectors and would not interoperate with anything deployed after.transfer(address _to, uint256 _value) transferFrom(address _from, address _to, uint256 _value) balanceOf(address _address) approve(address _address) unapprove(address _address) isApprovedFor(address _target, address _proxy) approveOnce(address _address, uint256 _maxValue) isApprovedOnceFor(address _target, address _proxy) event Transfer(address indexed from, address indexed to, uint256 value) event AddressApproval(address indexed address, address indexed proxy, bool result) event AddressApprovalOnce(address indexed address, address indexed proxy, uint256 value)Four of the six now stand in final form:
balanceOf,transfer,transferFrom, and theTransferevent. Missing:totalSupply,allowance, anapprovethat takes an amount, and theApprovalevent.Sources
-
2015-10-3018:01:51Z
2015-10-30 · 18:01:51Z An anonymous gist, and a rename in the wallet Gist 909d02feff3a2e59f714, “myToken”, one revision. Three minutes later the wallet ABI renames tokenName, tokenSymbol and tokenDecimals to name, symbol and decimals.
This gist is the one the Ethereum Wallet 0.3.5 release notes link as their example token, four days later. It declares contract myToken, its
transfercarries returns(bool success), and it has no overflow guard.Both differences matter, because the contract deployed as MistCoin on 3 November has the opposite of each: no return value, and an overflow check. The wallet release shipped pointing at a source that is not the one its own author deployed.
Sources
- Gist909d02feff3a2e59f714
- Revision6eedf7f4863be61c40137433aa44e1957304cb12
- Wallet renamece24214 · 2015-10-30T18:04:44Z
- Raw artifactraw/gists/mytoken-anon-909d02feff3a2e59f714-20151030.sol
-
2015-11-0311:26:01Z
→ 13:43:51Z2015-11-03 · 11:26:01Z to 13:43:51Z MistCoin is deployed Fabian Vogelsteller creates the MyToken gist at 11:26:01Z. Ethereum Wallet 0.3.5 is drafted twelve minutes later. MistCoin is deployed at 12:03:29Z. The gist revision it was compiled from is saved seventeen seconds after the block that contains the deployment. The wallet ships at 13:43:51Z.
Sixteen days before issue #20 was filed, and two months before the specification stopped moving, the token that is most often called the first ERC-20 was put on mainnet. This is the day, minute by minute. Gist times come from the GitHub gist history API, release times from the GitHub releases API, and the deployment time from the block timestamp of its create trace.
UTC Event 11:26:01Z frozeman creates gist 20c8b5658349b003b08d, “MyToken solidity contract”. Revision bce55cca. 11:28:43Z Revision 251c7324. 11:28:56Z Revision 9f826f1a. 11:37:56Z Ethereum Wallet release 0.3.5 (Beta 3) created as a draft by frozeman. Eleven minutes and fifty-five seconds after the gist. 11:59:12Z Revision 759cddeb removes returns (bool success) from transfer. Constructor is still (_supply, _name, _decimals, _symbol).12:03:29Z MistCoin deployed. Block 483,325, tx 0x74349ce6…54a7, deployer 0x9b22a80D5c7B3374a05b446081f97d0A34079e7F. 12:03:46Z Revision 7bcfaef3 swaps the constructor to (_supply, _name, _symbol, _decimals). Final revision; the gist has not changed since. 12:06:23Z Wallet ABI adds name,decimals,symbol(0022e37).13:43:51Z Ethereum Wallet 0.3.5 (Beta 3) published. 18:27:06Z Second contract with the full wallet-renderable token shape, 0x853737186cb24D4152f979B9152F652b67F7e9b7. Whitcoin is third at 18:46:18Z. MistCoin was deployed seventeen seconds before the gist revision whose source it was compiled from, and one hour forty minutes before the wallet release that made the feature public.The seventeen-second ordering is not a paradox. Recompilation shows the deployed bytecode carries revision 5's constructor argument order, not revision 4's: the source was edited locally, compiled, deployed, and saved back to the gist afterwards.
The source, gist revision 7bcfaef3, contract body
License header omitted. Verbatim otherwise, including the two typographic errors in the comments. contract MyToken { /* Public variables of the token */ string public name; string public symbol; uint8 public decimals; /* This creates an array with all balances */ mapping (address => uint256) public balanceOf; /* This generates a public event on the blockchain that will notify clients */ event Transfer(address indexed from, address indexed to, uint256 value); /* Initializes contract with initial supply tokens to the creator of the contract */ function MyToken(uint256 _supply, string _name, string _symbol, uint8 _decimals) { /* if supply not given then generate 1 million of the smallest unit of the token */ if (_supply == 0) _supply = 1000000; /* Unless you add other functions these variables will never change */ balanceOf[msg.sender] = _supply; name = _name; symbol = _symbol; /* If you want a divisible token then add the amount of decimals the base unit has */ decimals = _decimals; } /* Send coins */ function transfer(address _to, uint256 _value) { /* if the sender doenst have enough balance then stop */ if (balanceOf[msg.sender] < _value) throw; if (balanceOf[_to] + _value < balanceOf[_to]) throw; /* Add and subtract new balances */ balanceOf[msg.sender] -= _value; balanceOf[_to] += _value; /* Notifiy anyone listening that this transfer took place */ Transfer(msg.sender, _to, _value); } }That is the entire contract. One method, one event, three public variables. The
transferhere declares no return value at all, which is why the deployed function returns nothing where EIP-20 specifies returns (bool success).Issue #20 did not exist on this day, and would not for another sixteen. See the MistCoin section for what the deployed bytecode contains and where the contract sits in the record.
What the release notes said
This release fixes a lot of bugs and adds a new custom Token system, as well as a simple way to deploy contracts right from the wallet!Ethereum Wallet 0.3.5 release notes, verbatim
The notes never use the words “standard”, “ERC” or “EIP”. They describe tokens purely as a wallet feature, and they are explicit that names and symbols are not trustworthy identifiers:
Tokens can have any names or symbols (including "US dollar" or "BTC" and other token names) but each one will have a single unique icon, this is your guarantee of sending and receiving the token you want.Ethereum Wallet 0.3.5 release notes, verbatim
The release links its example token to the anonymous gist of 30 October, not to frozeman's own gist of that morning.
Sources
- Gistfrozeman/20c8b5658349b003b08d
- First revisionbce55cca788d90df01447f74eb227e3ed953ec4c
- Releasegithub.com/ethereum/mist/releases/tag/0.3.5
- Announcementr/ethereum, the wallet release thread
- Fabian on MistCoinr/ethereum, his own account of the deployment
- Wallet ABI0022e37
- Raw artifactsraw/gists/mytoken-rev5-7bcfaef3…sol, raw/onchain/mist-release-0.3.5.json, raw/onchain/mistcoin-0xf4eced2f-runtime.hex
The proposal
Two weeks in which the standard acquires its number, loses five members, and gains the two that define it. Everything here happens on a gist, an issue thread, a poll page and one repository, and most of it happens within seventy-two hours.
-
2015-11-1710:24:27Z
2015-11-17 · 10:24:27Z The draft that becomes issue #20 Fabian Vogelsteller creates gist 090ae32041bcfe120824, “Token proposal”. Eight methods and three events, five of the eight methods being approval machinery. Two days later this text is posted as an EIP.
The gist is written in the same shape the issue will use: a method list with a prose paragraph for each. It is the document ethers cites two days later when updating the wiki, and it is the direct source of the issue body.
Revision 1, complete method list
transfer(address _to, uint256 _value) returns (bool success) transferFrom(address _from, address _to, uint256 _value) returns (bool success) balanceOf(address _address) constant returns (uint256 balance) approve(address _address) returns (bool _success) unapprove(address _address) returns (bool _success) isApprovedFor(address _target, address _proxy) constant returns (bool _r) approveOnce(address _address, uint256 _maxValue) returns (bool _success) isApprovedOnceFor(address _target, address _proxy) returns (uint256 _maxValue) Transfer(address indexed from, address indexed to, uint256 value) AddressApproval(address indexed address, address indexed proxy, bool result) AddressApprovalOnce(address indexed address, address indexed proxy, uint256 value)Five of the eight methods are the approval system: a full-custody
approve, itsunapprove, a query, a one-time variant and that variant's query. Of the five, exactly one survives, and only after being redefined. There is nototalSupplyand noallowance.The paragraph introducing the approval members is the one Buterin wrote on the wiki seventeen months earlier, carried across almost unedited:
The
transferFrommethod is used for a "direct debit" workflow, allowing contracts to send coins on your behalf, for example to "deposit" to a contract address and/or to charge fees in sub-currencies; the command should fail unless the_fromaccount has deliberately authorized the sender of the message via some mechanism; we propose these standardized APIs for approval:Token proposal gist, revision 1, verbatimAll six revisions
Rev UTC Bytes totalSupply balanceOf transfer transferFrom approve allowance Transfer Approval v1 2015-11-17T10:24:27Z 2604 – ✔ ✔ ✔ ~ – – – v2 2015-11-17T15:25:23Z 2619 – ✔ ✔ ✔ ~ – – – v3 2015-11-17T18:48:06Z 2709 – ✔ ✔ ✔ ~ – ✔ – v4 2015-11-18T08:59:46Z 2817 ✔ ✔ ✔ ✔ ~ – ✔ – v5 2016-12-13T09:30:08Z 2874 ✔ ✔ ✔ ✔ ~ – ✔ – v6 2016-12-13T09:30:35Z 2904 ✔ ✔ ✔ ✔ ~ – ✔ – Revisions 5 and 6, thirteen months later, add only a banner: “This is outdated: The ERC-20 is here”. The gist never gains
allowanceorApproval. Those two members are born on the issue, not here.Sources
- Gistfrozeman/090ae32041bcfe120824
- Revision 1d3c1475afd436dbf1b04158bebf552851ffb6cb8
- Raw artifactsraw/gists/tokenproposal-*.txt
-
2015-11-1808:59:46Z
2015-11-18 · 08:59:46Z totalSupply appears for the first time anywhere Gist revision 4. Unlike every other member of ERC-20, totalSupply is not a rename of something older. It is newly invented, 16 hours 29 minutes before the wiki has it and 24 hours 53 minutes before issue #20 is opened.
Every other member of the final eight can be traced to an ancestor with a different name.
balanceOfwascoinBalanceOf,transferwassendCoin,allowancewasisApprovedFor,TransferwasCoinTransferand before thatCoinSent.totalSupplyhas no ancestor.It reaches implementation code the same day, 4 hours 7 minutes later, in ConsenSys/Tokens.
Sources
- Gist revisione7abcdde39e2ba576a170b07e06c5f84017dc267
- Into codeConsenSys/Tokens 45c5488 · 2015-11-18T13:07:05Z
- Into the wikie5e56c0 · 2015-11-19T01:29:04Z
-
2015-11-1901:29:04Z
01:31:08Z2015-11-19 · 01:29:04Z and 01:31:08Z The wiki is updated from the gist, and issue #19 is opened ethers copies the gist into the wiki with the message “updating from https://gist.github.com/frozeman/090ae32041bcfe120824”, then two minutes later opens issue #19, “APIs for Transferable Fungibles”. It is closed in favour of #20 nine hours later.
For about eight hours there are two competing token EIPs. Issue #19 is opened at 01:31:08Z, issue #20 at 09:52:56Z, and #19 is closed at 10:36:26Z, less than an hour after #20 appears. The number that the standard carries for the rest of its life is decided in that window.
Sources
-
2015-11-1909:52:56Z
2015-11-19 · 09:52:56Z Issue #20 is opened: “ERC: Token standard” Four of the six methods, in final form. An
approvethat takes no amount. Noallowance, noApprovalevent, and five members that are not in the standard today. This is the body exactly as posted, recovered from the IssuesEvent payload.GitHub did not record issue-body edit history until late 2016. The API returns diff: null for this issue and serves only the current text. The document that defines ERC-20 has no visible history at the place it lives. Everything below is recovered from GH Archive event payloads, which embed the entire parent issue object including its body as it stood at that instant. See Method.Members as posted
function decimals() function totalSupply() function balanceOf(address _address) function transfer(address _to, uint256 _value) function transferFrom(address _from, address _to, uint256 _value) function approve(address _address) function unapprove(address _address) function isApprovedFor(address _target, address _proxy) function approveOnce(address _address, uint256 _maxValue) function isApprovedOnceFor(address _target, address _proxy) event Transfer(address indexed _from, address indexed _to, uint256 _value) event AddressApproval(address indexed _address, address indexed _proxy, bool _result) event AddressApprovalOnce(address indexed _address, address indexed _proxy, uint256 _value)Ten methods, of which four match the final standard by name and signature. Three events, of which one survives. Five members here do not exist in ERC-20 today:
decimals,unapprove,isApprovedFor,approveOnce,isApprovedOnceFor.The body as posted, verbatim
3,334 bytes. Recovered from the IssuesEvent/opened payload in data.gharchive.org/2015-11-19-9.json.gz. Reproduced complete, including the unclosed code fence in the original. The following describes standard functions a token contract can implement. Those will allow dapps and wallets to handle tokens across multiple interfaces/dapps. The most important here are, `transfer`, `balanceOf`, `decimals` and the `Transfer` event. ```js ## Token ### Methods #### decimals ```js function decimals() constant returns (uint256 decimals) ``` Returns the number of decimal points this token requires, e.g. `2` #### totalSupply ```js function totalSupply() constant returns (uint256 supply) ``` Get the total coin supply #### balanceOf ```js function balanceOf(address _address) constant returns (uint256 balance) ``` Get the account balance of another account with address `_address` #### transfer ```js function transfer(address _to, uint256 _value) returns (bool _success) ``` Send `_value` amount of coins to address `_to` #### transferFrom ```js function transferFrom(address _from, address _to, uint256 _value) returns (bool success) ``` Send `_value` amount of coins from address `_from` to address `_to` The `transferFrom` method is used for a "direct debit" workflow, allowing contracts to send coins on your behalf, for example to "deposit" to a contract address and/or to charge fees in sub-currencies; the command should fail unless the `_from` account has deliberately authorized the sender of the message via some mechanism; we propose these standardized APIs for approval: #### approve ```js function approve(address _address) returns (bool success) ``` Allow `_address ` to direct debit from your account with full custody. Only implement if absolutely required and use carefully. See `approveOnce` below for a more limited method. #### unapprove ```js function unapprove(address _address) returns (bool success) ``` Unapprove address `_address ` to direct debit from your account if it was previously approved. Must reset both one-time and full custody approvals. #### isApprovedFor ```js function isApprovedFor(address _target, address _proxy) constant returns (bool success) ``` Returns 1 if `_proxy` is allowed to direct debit from `_target` #### approveOnce ```js function approveOnce(address _address, uint256 _maxValue) returns (bool success) ``` Makes a one-time approval for `_address ` to send a maximum amount of currency equal to `_maxValue` #### isApprovedOnceFor ```js function isApprovedOnceFor(address _target, address _proxy) returns (uint256 maxValue) ``` Returns `_maxValue` if `_proxy` is allowed to direct debit the returned `maxValue` from address `_target` only once. The approval must be reset on any transfer by `_proxy` of `_maxValue` or less. ### Events #### Transfer ```js event Transfer(address indexed _from, address indexed _to, uint256 _value) ``` Triggered when tokens are transferred. #### AddressApproval ```js event AddressApproval(address indexed _address, address indexed _proxy, bool _result) ``` Triggered when an `_address` approves `_proxy` to direct debit from their account. #### AddressApprovalOnce ```js event AddressApprovalOnce(address indexed _address, address indexed _proxy, uint256 _value) ``` Triggered when an `_address` approves `_proxy` to direct debit from their account only once for a maximum of `_value` ```Two sentences in that text are worth marking. The first line of the Motivation is the standard's actual purpose, and it does not mention exchanges, allowances or contract-to-contract accounting at all. The second is the ranking: “The most important here are,
transfer,balanceOf,decimalsand theTransferevent.” That sentence namesdecimals, which is removed from the standard seven days later, and it survives into the final text unedited, still namingdecimals, long after the member itself is gone.Sources
- Issueethereum/EIPs issue #20
- GH Archive filedata.gharchive.org/2015-11-19-9.json.gz
- OriginIssuesEvent / opened
- Raw artifactraw/issue20-bodies/r01-20151119T095256Z.md
-
2015-11-1915:33:05Z
2015-11-19 · 15:33:05Z The issue gets a number Revision 4 adds an EIP header block to the top of the body. It is the first time the document identifies itself as ERC 20. The interface below it is unchanged: still four of six.
The header, as it still reads in the final text
ERC: 20 Title: Token standard Status: Draft Type: Informational Created: 19-11.2015 Resolution: https://github.com/ethereum/wiki/wiki/Standardized_Contract_APIsThe malformed date, 19-11.2015, is in the original and is never corrected. The Resolution line points back at the wiki page, which at this moment does not carry the same interface as the issue and will not for another seven weeks.
Sources
- Snapshot carried bycomment 158091127, niran
- GH Archive filedata.gharchive.org/2015-11-19-15.json.gz
- Edit windowafter 2015-11-19T12:24:52Z, at or before 15:33:05Z
- Raw artifactraw/issue20-bodies/r04-20151119T153305Z.md
-
2015-11-1916:31:57Z
2015-11-19 · 16:31:57Z “Pave the cowpaths” Six hours and thirty-nine minutes after the issue is filed, Alex Van de Sande posts the design doctrine that keeps the standard small: implement what everyone already agrees on, and let real use decide the rest.
@ethers decimals, name and symbol are important for displaying to the end user. […] Regarding the approve/cheque discussion, I feel that we should always use focus on paving cow paths: implement what everyone is on absolute consensus as the basic "standard" and then allow real world usage dictate how to better define more advanced use cases.Verbatim, with the source's own ellipsis. Link text inlined.
This is the argument that prevailed. The approval system was cut from five members to two, and the three optional members were kept. It is also the argument that produces the gap this whole reconstruction measures: the standard describes an allowance model that almost nobody implements, while the wallet ships a definition of “token” that almost everybody implements.
Sources
- Commentissuecomment-158110210
-
2015-11-1919:07:35Z
2015-11-19 · 19:07:35Z approve gains an amount Revision 5. approve(address _for, uint256 _value). approveOnce and isApprovedOnceFor are deleted. Five of the six methods now stand in final form, on the day the issue opened.
The one-time approval is gone and the full-custody approval has been replaced by a capped one. The two ideas merged: instead of “approve with no limit” plus “approve once up to a maximum”, there is one
approvethat takes an amount. This is the single most consequential edit in the standard's history, and it happens nine hours and fifteen minutes after the issue was posted.function decimals() function totalSupply() function balanceOf(address _address) function transfer(address _to, uint256 _value) function transferFrom(address _from, address _to, uint256 _value) function approve(address _for, uint256 _value) function unapprove(address _address) function isApprovedFor(address _allowed, address _for) event Transfer(address indexed _from, address indexed _to, uint256 _value) event AddressApproval(address indexed _address, address indexed _proxy, bool _result) event AddressApprovalOnce(address indexed _address, address indexed _proxy, uint256 _value)The parameter names are not yet
_spender. The query is stillisApprovedFor. The two events are still the superseded ones. All of that changes the next afternoon.Sources
- GH Archive filedata.gharchive.org/2015-11-19-19.json.gz
- Edit windowafter 2015-11-19T19:00:41Z, at or before 19:07:35Z
- Raw artifactraw/issue20-bodies/r05-20151119T190735Z.md
-
2015-11-2015:53:42Z
2015-11-20 · 15:53:42Z allowance and Approval appear, and all eight members co-exist Revision 7. isApprovedFor becomes allowance. AddressApproval becomes Approval. Thirty hours after the issue was filed, the six methods and both events exist together in one document for the first time anywhere.
The two members that make ERC-20 what it is, and that exchanges and later DeFi protocols are built on, are the last two to arrive. Both arrive as renames, in an edit between two comments three minutes and forty-four seconds apart.
function decimals() function totalSupply() function balanceOf(address _address) function transfer(address _to, uint256 _value) function transferFrom(address _from, address _to, uint256 _value) function approve(address _spender, uint256 _value) function unapprove(address _spender) function allowance(address _address, address _spender) event Transfer(address indexed _from, address indexed _to, uint256 _value) event Approval(address indexed _address, address indexed _spender, uin256 _value)The uin256 in the
Approvaldeclaration is a typo in the original. It persists through revisions 7 to 13 and is not corrected until revision 14 on 2 December 2015.Two members that are not in the final standard are still here:
decimalsandunapprove. The parameter names are still_addressrather than_owner. Revision 8, two minutes and twenty-four seconds later, is a thirteen-byte change.This is finding 1. Every earlier document is missing at least one member. Every later one is a refinement of this shape. If a single date is wanted for the interface, this one has a better claim than the day the issue was opened.Sources
- Snapshot carried bycomment 158439737, frozeman
- GH Archive filedata.gharchive.org/2015-11-20-15.json.gz
- Edit windowafter 2015-11-20T15:49:58Z, at or before 15:53:42Z
- Raw artifactraw/issue20-bodies/r07-20151120T155342Z.md
-
2015-11-2113:31:28Z
2015-11-21 · 13:31:28Z The poll on which members to keep ethers creates a wiki page where participants list which members they want IN and which OUT. Twelve people vote over five days. decimals is the member most often listed OUT, and it is removed from the issue body five days later.
The page, verbatim
ethereum/wiki, Poll for token proposal EIP 20, final state Poll for https://github.com/ethereum/EIPs/issues/20 github username | IN | OUT | NextEIP*? | Comments ----------------|----------------|---------------|-----------------------------------|--------- example | Set1, identifier | decimals, approve, unapprove, allowance | YES | christianlundkvist| Set1 | decimals | YES | nmushegian | Set1 | decimals | YES | joeykrug | Set1, identifier | ? | ? | koeppelmann | Set1, identifier | ? | ? | Georgi87 | Set1, identifier | decimals | Yes | niran | Set1 | decimals | YES?| ethers | Set1, identifier | decimals | YES | simondlr | Set1 | decimals | YES | frozeman | Set1, decimals | | NO | alexvandesande | Set1, decimals | | | caktux | Set1, decimals | | | firecar96 | Set1 | decimals, identifier | YES | * Set1 = balanceOf, transfer, transferFrom, totalSupply, approve, unapprove, allowance * "identifier" = https://github.com/ethereum/EIPs/issues/20#issuecomment-158436720 and the discussion * NextEIP means should the OUT items be in separate EIP/s or be in EIP20 itself but marked Optional. YES means separate EIP/s, NO means keep in EIP20 and mark it Optional.Set1 is the final six plus
unapprove. Nobody voted againstunapproveexcept the example row, and it survived this poll. It was not dropped untilapprovewas redefined as absolute, six weeks later.The three people who voted to keep
decimalsare frozeman, alexvandesande and caktux: the author of the proposal, the author of the optional three, and the person who maintained the wiki copy. They lost.Page revisions
UTC Author Message SHA 2015-11-21T13:31:28Z ethers Created Poll for token proposal EIP 20 (markdown) 0604d8f1 2015-11-25T19:15:21Z ethers Updated Poll for token proposal EIP 20 (markdown) cad8db58 2015-11-25T19:26:05Z ethers Updated Poll for token proposal EIP 20 (markdown) 574050aa 2015-11-25T19:29:14Z ethers add alexvandesande 3af24c1d 2015-11-25T19:31:25Z ethers Updated Poll for token proposal EIP 20 (markdown) f3713a7f 2015-11-25T19:37:36Z ethers better format with identifier 61466494 2015-11-25T19:38:33Z Niran Babalola Updated Poll for token proposal EIP 20 (markdown) 41d4f528 2015-11-25T22:45:22Z caktux decimals 5733dc2d 2015-11-26T09:58:56Z Fabian Vogelsteller Updated Poll for token proposal EIP 20 (markdown) e55064c5 2015-11-26T09:59:13Z Fabian Vogelsteller Updated Poll for token proposal EIP 20 (markdown) 7074a665 Sources
- PageOutdated: Poll for token proposal EIP 20
- Raw artifactraw/wiki-revisions/_poll-page-final.md
-
2015-11-2610:34:22Z
2015-11-26 · 10:34:22Z decimals is removed from the specification Revision 11, 156 bytes smaller than revision 10. The member the wallet actually used, and the one the opening sentence names as most important, is voted out of the standard seven days after it was proposed.
Revision 10, sixty-nine minutes earlier, had already done the other cleanup: _address becomes _owner throughout, giving
balanceOf(address _owner)andallowance(address _owner, address _spender)their final parameter names.Revision 11, members. Only unapprove is now non-standard. function totalSupply() function balanceOf(address _owner) function transfer(address _to, uint256 _value) function transferFrom(address _from, address _to, uint256 _value) function approve(address _spender, uint256 _value) function unapprove(address _spender) function allowance(address _owner, address _spender) event Transfer(address indexed _from, address indexed _to, uint256 _value) event Approval(address indexed _owner, address indexed _spender, uin256 _value)One member stands between this and the finished standard:
unapprove. It takes another six weeks to remove.decimalsis deleted from the specification but not from the sentence recommending it. “The most important here are,transfer,balanceOf,decimalsand theTransferevent” is still in the body at revision 15, the first exact statement of the final interface, six weeks after the member itself was removed. The standard recommends a member it does not define.Sources
- Snapshot carried bycomment 159872082, simondlr
- GH Archive filedata.gharchive.org/2015-11-26-10.json.gz
- Edit windowafter 2015-11-26T09:25:09Z, at or before 10:34:22Z
- Raw artifactraw/issue20-bodies/r11-20151126T103422Z.md
-
2015-11-3021:06:17Z
2015-11-30 · 21:06:17Z First compilable Solidity with all six and both events ConsenSys/Tokens Token.sol at 4ba2396, “Refactored to current standards.” Ten days after the interface first existed as prose, it exists as code. unapprove is still declared.
function totalSupply() function balanceOf(address _owner) function transfer(address _to, uint256 _value) function transferFrom(address _from, address _to, uint256 _value) function approve(address _spender, uint256 _value) function unapprove(address _spender) function allowance(address _owner, address _spender) event Transfer(address indexed _from, address indexed _to, uint256 _value) event Approval(address indexed _owner, address indexed _spender, uint256 _value)This is finding 2. Note that the typo in the specification, uin256, does not appear here: the implementation writes uint256, because it has to compile. The specification is corrected two days later.
Sources
- Commit4ba2396 · Refactored to current standards.
- Raw artifactraw/code-snapshots/ConsenSysTokens-4ba2396-Token.sol
Settling the interface
The specification goes backwards before it goes forwards. Implementation reaches the final shape three weeks before the specification does, and mainnet reaches it four days after. This is also the period in which the Ethereum Foundation publishes a token tutorial that neither names the standard nor implements it.
-
2015-12-0209:28:55Z
2015-12-02 · 09:28:55Z Regression: Approval is renamed Approved Revision 13 moves away from the final form. Approval becomes Approved, and a second event Unapproved is added. The standard is further from its final state on 2 December than it was on 20 November.
The rename is consistent with the design at that moment: if
unapproveis a real method, it needs its own event, and a pair calledApprovedandUnapprovedis more symmetrical thanApprovalandUnapproved. The whole branch is abandoned five weeks later whenapproveis redefined as absolute andunapprovestops being needed.function totalSupply() function balanceOf(address _owner) function transfer(address _to, uint256 _value) function transferFrom(address _from, address _to, uint256 _value) function approve(address _spender, uint256 _value) function unapprove(address _spender) function allowance(address _owner, address _spender) event Transfer(address indexed _from, address indexed _to, uint256 _value) event Approved(address indexed _owner, address indexed _spender, uin256 _value) event Unapproved(address indexed _owner, address indexed _spender)Revision 14, fifty-three minutes later, is a one-byte change: the uin256 typo introduced on 20 November is finally corrected to uint256.
The regression propagates. ConsenSys/Tokens follows it on the same day at 13:24:54Z with “Added unapprove”, and the wiki follows it on 16 December with “update to latest Token standard draft”. For five weeks the specification, the reference implementation and the wiki all agree on an interface that no longer exists.
Sources
- Snapshot carried bycomment 161234094, frozeman
- GH Archive filedata.gharchive.org/2015-12-02-9.json.gz
- Into codeConsenSys/Tokens f3c0fc1 · Added unapprove
- Into the wiki551fce2 · 2015-12-16T02:00:37Z
- Raw artifactsraw/issue20-bodies/r13-…md, r14-…md
-
2015-12-03
2015-12-03 The Foundation's token tutorial Alex Van de Sande's “Ethereum in practice part 1” is the Foundation's canonical token tutorial. It links a gist that is character-for-character identical to the source MistCoin was compiled from, and it never uses the words ERC-20, ERC20, EIP-20 or “token standard”.
Full title: “Ethereum in practice part 1: how to build your own cryptocurrency without touching a line of code”. Published two weeks after issue #20 was filed.
The post contains no inline Solidity. It instructs the reader to load the contract into the browser-based compiler from a gist: chriseth.github.io/browser-solidity/?gist=21935dc37c5bfbe92e5a. Its only description of what the token implements is:
Since all tokens implement some basic features in a standard way, this also means that your token will be instantly compatible with the ethereum wallet and any other client or contract that uses the same standards.Verbatim
It also concedes the wallet's discovery limitation directly: “the wallet only tracks tokens it knows about, and you have to add these manually.”
What the linked gist contains
contract MyToken { event Transfer(address indexed from, address indexed to, uint256 value); function MyToken(uint256 _supply, string _name, string _symbol, uint8 _decimals) function transfer(address _to, uint256 _value) }One method and one event. As of December 2015 the Ethereum Foundation's own public tutorial for creating a token neither named the standard nor implemented it.
Gist 21935dc37c5bfbe92e5a, created 2015-12-01T18:37:25Z, is character-for-character identical to frozeman's gist revision 7bcfaef3, the revision MistCoin was compiled from. Verified by diff, and independently by compilation: built with solc 0.1.6+commit.d41f8b7c with the optimizer on, the blog post's gist produces MistCoin's exact 716-byte runtime and its exact 1,150-byte creation prefix. The token contract the Foundation published to the world on 3 December 2015 is, byte for byte, the contract deployed as MistCoin on 3 November.The post left a measurable trace onchain
The tutorial tells the reader to enter “10,000 as the supply, any name you want,
"%"for a symbol and 2 decimal places.” That instruction is visible in the constructor arguments of contracts deployed afterwards.Deployed n Constructor symbol is "%" 2015-11-03 to 2015-12-02 21 1 (5%) 2015-12-03 to 2016-01-06 97 42 (43%) Weekly MyToken-shaped deployments
Contracts with the exact MyToken shape, by week week of 2015-11-02: 17 (Wallet 0.3.5 ships) week of 2015-11-09: 10 week of 2015-11-16: 2 week of 2015-11-23: 2 week of 2015-11-30: 36 (blog post, 2015-12-03) week of 2015-12-07: 33 week of 2015-12-14: 17 week of 2015-12-21: 10 week of 2015-12-28: 8 week of 2016-01-04: 5Two spikes, both attributable: eight deployments on 3 November, the day Wallet 0.3.5 shipped, and fifteen each on 4 and 5 December, the two days after the blog post. From the week of 23 November to the week of 30 November is an eighteenfold jump.
Caveat on the "%" signal. The Wallet 0.3.5 release notes of 3 November also suggested"%"as a symbol, with three decimals rather than two. The rise in its frequency therefore measures readership, not novelty. The correlation between the post and the deployment spike is strong and the mechanism is plausible, but this is correlational evidence from timing, not proof of causation.Two circulating argument layouts
Layout Constructor Published in rev≤4 (uint256 _supply, string _name, uint8 _decimals, string _symbol) anonymous gist 909d02… of 2015-10-30, linked by Wallet 0.3.5; frozeman gist revisions 1 to 4 rev5 (uint256 _supply, string _name, string _symbol, uint8 _decimals) frozeman gist revision 7bcfaef3 of 2015-11-03; blog gist 21935dc… of 2015-12-01 Of the 118 window deployments whose constructor arguments decode cleanly, 94 carry the rev5 layout and 24 the rev≤4 layout. The two published sources are distinguishable in the deployed bytes, in roughly a four-to-one ratio favouring the one the blog post carried.
Sources
- Postblog.ethereum.org/2015/12/03/how-to-build-your-own-cryptocurrency
- Gist21935dc37c5bfbe92e5a
- Gist revisione055b2f271def0a188cc2da13c75a190cc770dfa, single revision, created 2015-12-01T18:37:25Z
- Raw artifactsraw/gists/mytoken-blogpost-21935dc37c5bfbe92e5a-20151201.sol, raw/onchain/window-token-census.tsv, raw/onchain/decode-constructor-args.py
-
2015-12-1408:55:58Z
2015-12-14 · 08:55:58Z · block 689,715 The only contract in the window with totalSupply SubEthaNomic, symbol SEN, at 0xCc0eE510BC4b5CD4D31Da49f672AB5aa6806F70a. Across 2,941 contracts with runtime bytecode in the drafting window, exactly one implements totalSupply().
It carries
balanceOf,totalSupply,transfer,name,symboland theTransferevent. No allowance machinery at all. It is one of only ten contracts in the window that reach three of the six, and the only one of the ten that is not part of the 9-contract prototype cluster from early November.Sources
- Raw artifactraw/onchain/window-token-vocabulary-hits.json
-
2015-12-2114:28:42Z
15:55:57Z2015-12-21 · 14:28:42Z and 15:55:57Z “Derp. Approve is not a noun.” Two commits ninety minutes apart. The first deletes unapprove and renames the event Approve. The second fixes the name to Approval. The result is the first publicly available file containing exactly the ERC-20 interface and nothing else, sixteen days before the specification says the same thing.
UTC SHA Change 14:28:42Z 9b89829 “Absolute approval”. unapprove() deleted, event renamed Approve. 15:55:57Z c3a3426 “Derp. Approve is not a noun.” Approve becomes Approval. “Absolute approval” is the decision that removes
unapprove: onceapproveoverwrites the allowance rather than adding to it, setting it to zero is unapproving, and a separate method is redundant. That is the design argument the specification adopts on 6 January. The code makes it first.Token.sol at c3a3426, complete
ConsenSys/Tokens Token_Contracts/contracts/Token.sol, verbatim, whole file contract Token { function totalSupply() constant returns (uint256 supply) {} function balanceOf(address _owner) constant returns (uint256 balance) {} function transfer(address _to, uint256 _value) returns (bool success) {} function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {} function approve(address _spender, uint256 _value) returns (bool success) {} function allowance(address _owner, address _spender) constant returns (uint256 remaining) {} event Transfer(address indexed _from, address indexed _to, uint256 _value); event Approval(address indexed _owner, address indexed _spender, uint256 _value); }This is finding 3. Every declaration matches the final standard, and there is nothing else in the file. The word ERC does not appear in it, in the commit message, or anywhere in the repository at this date.
Sources
- Commitc3a3426bb1c8ac8aac627d246cc4e011273dd6c4
- BlobToken_Contracts/contracts/Token.sol
- Raw artifactraw/code-snapshots/ConsenSysTokens-c3a3426-Token.sol
-
2015-12-2319:36:12Z
2015-12-23 · 19:36:12Z ethereum.org drops sendCoin and adopts transfer Commit message “add the DAO page”. The token contract is replaced with the MyToken shape: balanceOf, transfer, event Transfer, plus name, symbol and decimals. The link to the wiki standard is removed and not replaced.
Two days after ConsenSys reached the exact interface, and fourteen days before the specification froze, the Foundation's own token page moves off the Frontier vocabulary. It does not move to the specification. It moves to the contract from the December blog post, which is the contract deployed as MistCoin.
ethereum-org views/content/token.md at b0abb37, contract opening contract MyToken { /* Public variables of the token */ string public name; string public symbol; uint8 public decimals; /* This creates an array with all balances */ mapping (address => uint256) public balanceOf; /* This generates a public event on the blockchain that will notify clients */ event Transfer(address indexed from, address indexed to, uint256 value); /* Initializes contract with initial supply tokens to the creator of the contract */ function myToken(uint256 initialSupply, string tokenName, uint8 decimalUnits, string tokenSymbol) {Two of the six required methods, one of the two required events, three of three optional members: the same score as MistCoin, and the same shape the wallet rendered. The
Transferevent is indexed here, which the Frontier Guide'sCoinTransfernever was.The same commit removes the “Meta coin standard” link to Standardized_Contract_APIs that had been on the page since July. From this date ethereum.org's token page points at no standard at all.
Sources
- Commitethereum-org b0abb37 · add the DAO page
- Author date2015-12-23 17:36:12 -0200
- Link removal“Meta coin standard” added 33b1217 (2015-07-15), removed b0abb37 (2015-12-23). No other commit touches the string.
-
2015-12-2910:55:42Z
2015-12-29 · 10:55:42Z The DAO is still running the superseded interface Eight days after ConsenSys shipped the exact final interface, the largest token contract of the era carries one of the six, plus six members that had already been removed from the specification.
slockit/DAO Token.sol at 95d85c6 function transfer(uint _value, address _to) function transferFrom(address _from, uint _value, address _to) function balanceOf(address _addr) function approve(address _addr) function unapprove(address _addr) function isApprovedFor(address _target, address _proxy) function approveOnce(address _addr, uint256 _maxValue) function isApprovedOnceFor(address _target, address _proxy) event Transfer(address indexed from, address indexed to, uint256 value) event AddressApproval(address indexed addr, address indexed proxy, bool result) event AddressApprovalOnce(address indexed addr, address indexed proxy, uint256 value)This is the October wiki, not the December issue.
transferstill takes value before recipient, a form the wiki had already corrected two months earlier, on 28 October. The DAO adopts the exact final interface on 16 January 2016, and two days after that ConsenSys imports the DAO contract's NatSpec back into its own.Sources
- Commit95d85c6 · update DAO
- Raw artifactraw/code-snapshots/slockitDAO-95d85c6-Token.sol
-
2016-01-0108:56:36Z
2016-01-01 · 08:56:36Z A README calls issue #20 “de facto finalised” The first README in any code repository to point at the issue. It appears five days before the issue body actually reaches its final form.
It follows the cutting edge standards (which is de facto finalised by the community here: https://github.com/ethereum/EIPs/issues/20)ConsenSys/Tokens README at bbbff96, verbatim
The claim is true of the code and premature about the document. ConsenSys/Tokens had held the exact interface since 21 December. The issue body still said
unapprove,ApprovedandUnapproved, and would for another five days.Sources
-
2016-01-0610:28:48Z
2016-01-06 · 10:28:48Z The specification reaches its final form Revision 15. approve becomes absolute, unapprove and Unapproved are deleted, Approved is renamed back to Approval. Six methods, two events, nothing extra. Forty-eight days after the issue was filed.
This is finding 4, and it is a bound rather than a timestamp. The issue thread was silent between 2015-12-02T10:22:08Z and 2016-01-06T10:12:13Z, so no snapshot exists inside that window. What the evidence supports exactly: the final interface was not present at 10:12:13Z and was present at 10:28:48Z, sixteen minutes and thirty-five seconds later.
The body, verbatim, complete
2,351 bytes. Carried by the comment at 2016-01-06T10:28:48Z. ``` ERC: 20 Title: Token standard Status: Draft Type: Informational Created: 19-11.2015 Resolution: https://github.com/ethereum/wiki/wiki/Standardized_Contract_APIs ``` # Abstract The following describes standard functions a token contract can implement. # Motivation Those will allow dapps and wallets to handle tokens across multiple interfaces/dapps. The most important here are, `transfer`, `balanceOf`, `decimals` and the `Transfer` event. # Specification ## Token ### Methods #### totalSupply ```js function totalSupply() constant returns (uint256 supply) ``` Get the total token supply #### balanceOf ```js function balanceOf(address _owner) constant returns (uint256 balance) ``` Get the account balance of another account with address `_owner` #### transfer ```js function transfer(address _to, uint256 _value) returns (bool success) ``` Send `_value` amount of tokens to address `_to` #### transferFrom ```js function transferFrom(address _from, address _to, uint256 _value) returns (bool success) ``` Send `_value` amount of tokens from address `_from` to address `_to` The `transferFrom` method is used for a withdraw workflow, allowing contracts to send tokens on your behalf, for example to "deposit" to a contract address and/or to charge fees in sub-currencies; the command should fail unless the `_from` account has deliberately authorized the sender of the message via some mechanism; we propose these standardized APIs for approval: #### approve ```js function approve(address _spender, uint256 _value) returns (bool success) ``` Allow _spender to withdraw from your account, multiple times, up to the _value amount. If this function is called again it overwrites the current allowance with _value. #### allowance ```js function allowance(address _owner, address _spender) constant returns (uint256 remaining) ``` Returns the amount which `_spender ` is still allowed to withdraw from `_owner` ### Events #### Transfer ```js event Transfer(address indexed _from, address indexed _to, uint256 _value) ``` Triggered when tokens are transferred. #### Approval ```js event Approval(address indexed _owner, address indexed _spender, uint256 _value) ``` Triggered whenever `approve(address _spender, uint256 _value)` is called.Three details are worth noting in that text. The Motivation still recommends
decimals, which was removed six weeks earlier and is not defined anywhere below. The word “coins” from the original has been replaced by “tokens” throughout. And the sentence definingapproveas overwriting rather than accumulating, “If this function is called again it overwrites the current allowance with _value”, is the sentence that makesunapproveunnecessary and is the direct descendant of ConsenSys's “Absolute approval” commit sixteen days earlier.What happened next, the same morning
- 10:48:26Z, twenty minutes later, caktux mirrors it on the wiki: “update to latest draft - approve() now absolute, remove unapprove() and Unapproved(), Approved() renamed to Approval()”.
- 20:55:55Z, revision 16, a twelve-byte change. The body then does not change again across 111 consecutive comment snapshots, until 28 October 2016. That is genuine stability, not a gap in coverage.
Sources
- Snapshot carried bycomment 169288676, frozeman
- GH Archive filedata.gharchive.org/2016-01-06-10.json.gz
- Wiki mirror02c64c1 · 2016-01-06T10:48:26Z
- Raw artifactraw/issue20-bodies/r15-20160106T102848Z.md
-
2016-01-1000:21:08Z
2016-01-10 · 00:21:08Z · block 824,235 First contract on mainnet carrying all six selectors 0x99146Bab2bB34D9Ca49EC4f0c82De3E5789ae22e, 6,701 bytes. Four days after the specification stopped moving. It carries the interface and is not a token: its supply reads zero at every block checked and it has never emitted a Transfer.
- Address
- 0x99146Bab2bB34D9Ca49EC4f0c82De3E5789ae22e
- Deployed
- 2016-01-10T00:21:08Z, block 824,235
- Deployer
- 0x4f53269e422711d4725F7381444c7F66F7D05788
- Runtime size
- 6,701 bytes
- Contents
- All six methods. Both the Transfer and Approval topics. Plus owner() and 45 further unrecognised selectors.
- Supply
- totalSupply() returns zero at the block after deployment, five thousand blocks later, forty thousand blocks later, and at block 913,198. It has never emitted a Transfer.
Carrying the interface and being a token are different claims, and this contract separates them. It is the Digix gold ledger, and this deployment and the one that follows it eleven hours later, 0xB2dd0dc22c7D103928650AbD260935Ef9EF40CFc, are staging runs: the same 6,701 bytes, no supply, no transfer, ever. The ledger that went into service is the third, on 14 January.
The gap this closes is the central empirical result of the reconstruction. Between 3 November 2015 and 6 January 2016, across 2,941 contracts with runtime bytecode, nothing implements more than three of the six, and nothing at all implements
approveorallowance. The allowance model, the half of ERC-20 that makes exchanges possible and the half that consumed almost all of the argument on the issue, has zero deployed instances during the period in which it was being specified.See the onchain section for the full census and the adoption curve.
Sources
- Raw artifactraw/onchain/y2016-final-six-4plus.json
- DetectionOpcode walk over the runtime blob, cross-checked by substring match. See Method.
-
2016-01-1415:22:33Z
2016-01-14 · 15:22:33Z · block 847,527 The first contract with the interface and a real supply 0x55b9a11c2e8351b4Ffc7b11561148bfaC9977855, Digix Gold 1.0, 6,770 bytes. The first contract that is a token and not only an interface: a supply exists from the block after it was deployed. It is not fully compliant with EIP-20, and the defects are in the event shape.
- Address
- 0x55b9a11c2e8351b4Ffc7b11561148bfaC9977855
- Deployed
- 2016-01-14T15:22:33Z, block 847,527
- Deployer
- 0x4f53269e422711d4725F7381444c7F66F7D05788
- Runtime size
- 6,770 bytes
- Supply
- 1,400,331,016,000
- First Transfer
- block 937,821
Four tests separate a token from a contract that merely exposes the interface, and this is the first deployment to pass all four. It carries the six selectors, among 38 other functions. It has a real supply: totalSupply() returns 1,400,331,016,000 at block 847,528, the block after deployment, and the same value at every block sampled between there and block 937,821, so it was never an empty ledger waiting to be filled. It was meant as a token, being the address Digix names in its own gold-tokens-interface repository and the one MyEtherWallet's token list carries as DGX 1.0. And it emits Transfer when tokens move, from block 937,821 onward.
The two Digix ledgers deployed four days earlier hold the identical interface and fail every test but the first. Their supply reads zero at deployment, five thousand blocks later, forty thousand blocks later, and at block 913,198, and neither has emitted a Transfer in its life. They are staging deployments of the contract this one became.
What it is not is fully compliant with EIP-20. Run on a mainnet fork it breaks two mandatory requirements, both visible in what it emits:
- Event shapeTransfer is declared with _value indexed. Every one it has emitted carries four topics and an empty data field. EIP-20 requires three topics with the value in data, so a reader following the standard finds no amount at all.
- Wrong sendertransferFrom names the spender as _from, not the owner whose balance moved. One call emits three Transfer events, one of them a fee leg, and the movement leg attributes the debit to the wrong account.
Both are properties of the deployed runtime, unchanged since 2016 and reproducible at any block. The first contract that satisfies every EIP-20 requirement arrives on 20 March 2016, sixty-six days later.
The first ERC-20 token cannot be verified on Etherscan. It belongs to a Digix system compiled by solc v0.1.7 commit c806b9bc, a build that exists in the Solidity history and was never published to solc-bin, so no released compiler reproduces its bytecode.
Sources
- InterfaceDigixGlobal/gold-tokens-interface
- DetectionSelector walk over the runtime, then totalSupply() at historical blocks and a log scan on the Transfer topic.
-
2016-01-1614:34:37Z
2016-01-16 · 14:34:37Z The DAO adopts the exact interface “update Token.sol to new standard”. Six of six, no extras. Two days later ConsenSys imports this contract's NatSpec into its own.
function totalSupply() function balanceOf(address _owner) function transfer(address _to, uint256 _value) function transferFrom(address _from, address _to, uint256 _value) function approve(address _spender, uint256 _value) function allowance(address _owner, address _spender) event Transfer(address indexed _from, address indexed _to, uint256 _value) event Approval(address indexed _owner, address indexed _spender, uint256 _value)Documentation flows in the opposite direction to the interface. The interface went from the issue to ConsenSys to the DAO. The NatSpec goes from the DAO back to ConsenSys, on 18 January, with the commit message “Added NATSPEC from Christoph Jentzsch's Slock.it Token Contract.”
Sources
- Commitbf27cf7 · update Token.sol to new standard
- NatSpec importConsenSys/Tokens cebd132 · 2016-01-18T09:27:41Z
- Raw artifactraw/code-snapshots/slockitDAO-bf27cf7-Token.sol
-
2016-01-2500:39:07Z
2016-01-25 · 00:39:07Z “implement ERC 20”, the first commit message to name the standard A single-line change to a source comment. It is the moment the reference implementation stopped citing the wiki page and started citing the issue. The code does not change.
ConsenSys/Tokens 85610c3, complete diff diff --git a/Token_Contracts/contracts/Standard_Token.sol b/Token_Contracts/contracts/Standard_Token.sol index ab28fea..28f1ceb 100644 --- a/Token_Contracts/contracts/Standard_Token.sol +++ b/Token_Contracts/contracts/Standard_Token.sol @@ -1,7 +1,7 @@ /*Most, basic default, standardised Token contract. Allows the creation of a token with a finite issued amount to the creator. -Based on standardised APIs: https://github.com/ethereum/wiki/wiki/Standardized_Contract_APIs +Implements ERC 20 Token standard: https://github.com/ethereum/EIPs/issues/20 .*/ import "Token";Two months after the issue was filed and five weeks after the code was already correct, someone writes the standard's name down in a repository for the first time. The name lags the thing by a wide margin at every stage of this trail.
The naming sequence
UTC What Where Text 2016-01-01T08:56:36Z First README pointing at issue #20 ConsenSys/Tokens bbbff96 “de facto finalised by the community here” 2016-01-25T00:39:07Z First commit message naming ERC 20 ConsenSys/Tokens 85610c3 implement ERC 20 2016-02-08T20:06:34Z First closed-up form “ERC20” ConsenSys/Tokens aba060b “Commented out TransferFrom to reflect current uncertainty in ERC20.” 2016-04-16T19:14:35Z Wiki commit naming ERC20 ethereum/wiki f3f6a74 “further highlight ERC20” 2016-04-24T19:55:53Z First repo named erc20; first Solidity contract ERC20 dapphub/erc20 e970781 erc20 type definition “First” here means first across the sources enumerated in Method. Commit-message evidence outside those repositories was sampled through GH Archive rather than exhaustively searched.
Sources
- Commit85610c3ed1a614453fe32834a2694dd9ec000de9
- Authorethers <ethereum@outlook.com>
- Author date2016-01-24 16:39:07 -0800
-
2016-01-2716:54:44Z
2016-01-27 · 16:54:44Z · block 913,198 The first ERC-20 transfer elcoin, 0xa04bf47F0E9D1745D254b9B89f304c7d7ad121Aa. One account pays another 1,000,000 units, two hours after the contract was deployed. The first time a token moves under this interface. The contract itself is not compliant: its ERC-20 entry points return false and do nothing.
- Address
- 0xa04bf47F0E9D1745D254b9B89f304c7d7ad121Aa
- Deployed
- 2016-01-27T14:47:57Z, block 912,760
- First Transfer
- 2016-01-27T16:54:44Z, block 913,198
- From
- 0x48175Da4c20313bcb6B62d74937d3fF985885701
- To
- 0x96CB25A6445648A56352677D6C80600F769F2642
- Value
- 1,000,000
Not a mint. The sender is the deployer's own account, not the zero address, so this is one holder paying another rather than a supply being created. The contract answers name() with a bytes32 that decodes to elcoin, and answers neither symbol() nor decimals(), both of which were optional and arrived later.
Thirteen days separate the first token from the first transfer, and in that window no contract carrying the interface moved a single token. Digix Gold, deployed first, did not emit its own first Transfer until block 937,821.
The event is real, the interface behind it is not. elcoin holds all six selectors, but its own ERC-20 entry points do nothing. Called on a fork, transfer, approve and transferFrom each return false, move no balance and emit no event, from a funded holder as readily as from an empty account. The contract asks a security registry at 0xa95b9127e7102dCFa3869c47ee12a0Ec85C261C5 whether the caller is permitted, receives zero, and returns. The behaviour is identical at block 950,000 and at the head of the chain, so this is not a later configuration change.
Every transfer elcoin recorded, all 1,450 of them, was driven through a platform controller rather than through transfer. The chain also records the other half of that: every direct call to transfer(address,uint256) in elcoin's history failed. It is an asset on a permissioned platform that exposes the ERC-20 shape, not a contract that behaves as ERC-20 requires.
Sources
- DetectionLog scan on topic ddf252ad… from each candidate's deployment block, earliest first.
-
2016-01-2814:01:48Z
2016-01-28 · 14:01:48Z · block 917,622 The first minimal ERC-20 on mainnet 0x37Dca38b1CBB2Cd043910eC46fe82Ddb9e38F00d, 754 bytes. A dispatcher containing the six selectors and nothing else. A real token, with a supply of 1,000,000 that has never moved. It meets nine of the ten requirements of EIP-20 as finalised, and the tenth was written after it was deployed.
- Address
- 0x37Dca38b1CBB2Cd043910eC46fe82Ddb9e38F00d
- Deployed
- 2016-01-28T14:01:48Z, block 917,622
- Deployer
- 0x16893e10b99A59afd2C60331E0B49241d4d4d7cC
- Runtime size
- 754 bytes
- Contents
- Exactly totalSupply, balanceOf, transfer, transferFrom, approve, allowance. Both event topics. Zero unrecognised selectors. No name, symbol or decimals.
- Supply
- 1,000,000, all of it the deployer's
- Transfer events
- None, ever
It is a token, not a bare interface. totalSupply() returns 1,000,000 and balanceOf of the deployer returns the same, so the whole supply sits where it was created and has never moved: the contract has not emitted a single Transfer. The source Etherscan serves for it has no constructor, which is what makes the supply look impossible. Etherscan verifies against the deployed runtime, and constructor code never reaches the runtime, so a verified source can be missing the constructor that set the contract's opening state. The chain is the record, not the source.
This is the deployed counterpart of ConsenSys's c3a3426: the interface and nothing else, on mainnet, thirty-eight days after the same shape appeared in a file. Only 28 contracts across January and February 2016 combined carry all six.
It is one requirement short of full compliance, and the requirement did not exist yet. Executed against all ten obligations of EIP-20 as finalised, it satisfies nine. Both transfer and transferFrom are guarded by && _value > 0, so a transfer of zero returns false and emits nothing, where the finished standard requires it to be treated as an ordinary transfer and to fire Transfer.
That clause is worth dating. It appears in the earliest copy of eip-20-token-standard.md in the EIPs repository, on 13 July 2017. It is not in the text of issue #20 as filed on 19 November 2015, and it is not in the issue today. Judged against EIP-20 as it was finally written, this contract fails. Judged against the specification as it stood on the day it was deployed, it passes, and it is the first contract that does.
Everything else holds: all three mutators return a 32-byte boolean, approve overwrites rather than accumulates, transferFrom decrements the allowance, and both events carry three topics with the value in data. It is also the only one of the early candidates with verified source on Etherscan, so the guard can be read rather than inferred.
Sources
- Raw artifactraw/onchain/y2016-final-six-4plus.json
Adopting the name
The interface has stopped changing. What happens through 2016 is that the phrase “ERC20” detaches from the issue number and becomes the name of a thing, while the wiki page that started it stops specifying anything and defers.
-
2016-02-0820:06:34Z
2016-02-08 · 20:06:34Z The closed-up form: “ERC20” “Commented out TransferFrom to reflect current uncertainty in ERC20.” The first use of the name without a space, two weeks after the first use with one.
The commit message is also evidence that the standard was not settled in practice even after the text froze. A TransferFrom event had been added on 30 January and is commented out nine days later, explicitly because of “current uncertainty”.
Sources
- Commitaba060b
- Event added8fb1f31 · Added in TransferFrom event, 2016-01-30T13:10:17Z
-
2016-02-1219:22:49Z
2016-02-12 · 19:22:49Z approve and transferFrom reach ethereum.org The allowance model appears on the Foundation's token page for the first time, five weeks after the specification froze and three months after it was written into issue #20.
Commit message: “Added improve this button. Removed old images”. The page roughly doubles in size, from 14.8 KB to 36.1 KB, and gains an advanced token section carrying
approveandtransferFrom.The ordering across the Foundation's own properties is the point.
approveandallowanceentered the specification on 19 and 20 November 2015. They reach ethereum.org on 12 February 2016. The go-ethereum wiki tutorial behind the Frontier Guide never gains them at all.Even here the page does not name the standard. The word ERC does not appear.
Sources
- Commitethereum-org 88c217e · Added improve this button. Removed old images
- Author date2016-02-12 17:22:49 -0200
-
2016-02-1318:38:56Z
2016-02-13 · 18:38:56Z Van de Sande's own implementation Gist alexvandesande/0d1a998d949e26942212, “Token Standard”. Five of six by signature, missing totalSupply, and no Approval event. Its
approvecalls back into the spender.contract tokenRecipient { function sendApproval(address _from, uint256 _value, address _token); } contract MyToken { string public name; string public symbol; uint8 public decimals; mapping (address => uint256) public balanceOf; mapping (address => mapping (address => uint)) public allowance; mapping (address => mapping (address => uint)) public spentAllowance; event Transfer(address indexed from, address indexed to, uint256 value); function transfer(address _to, uint256 _value) { … } function approve(address _spender, uint256 _value) returns (bool success) { allowance[msg.sender][_spender] = _value; tokenRecipient spender = tokenRecipient(_spender); spender.sendApproval(msg.sender, _value, this); } function transferFrom(address _from, address _to, uint256 _value) returns (bool success) { … } function () { throw; } }allowanceis present as a public-mapping getter rather than a declared function, which produces the same selector. ThespentAllowancemapping meansapprovehere sets a cumulative cap rather than the remaining allowance, which is not the final semantics.The
tokenRecipient.sendApprovalcallback, renamedreceiveApprovalin his next comment three days later, is the direct ancestor of theapproveAndCallandreceiveApprovalpattern that shipped in the ethereum.org token tutorial and propagated into thousands of 2017 token contracts. It is not part of ERC-20.Sources
- Gistalexvandesande/0d1a998d949e26942212
- Revisiona7f33cd9e905b4b0819c38c265a7ca2903f43ab7
- Posted to the issueissuecomment-183720260
- Raw artifactraw/gists/avsa-token-standard-a7f33cd9-20160213.sol
-
2016-03-1513:28:32Z
2016-03-15 · 13:28:32Z “All your token need is balanceOf and transfer” Four months after MistCoin and two months after the specification froze, the Foundation's UX lead is still telling token authors on the standard's own issue thread that two methods and one event are sufficient.
In order to work on the Ethereum Wallet all your token need is to implement correctly
balanceOfandtransferand their corresponding events. We are waiting for all others but the latest proposed standard is kept updated at ethereum.org/tokenVerbatimThis is the single most useful sentence in the whole issue for dating what “compliance” meant. It is stated by the person who built the client that defined the requirement, on the standard's own thread, and it corroborates the wallet ABI evidence from the other direction. See Compliance.
His other interventions on the thread
UTC Comment Substance 2015-11-19T16:31:57Z 158110210 Defends decimals, name, symbol; states the “pave the cowpaths” doctrine 2015-11-30T19:13:48Z 160729345 Proposes int256 over uint256 so tokens can represent debt. Rejected. 2015-12-02T16:00:32Z 161344667 Continues the signed-balance argument 2016-02-13T18:42:35Z 183720260 Posts an implementation with approve plus a recipient callback 2016-02-16T12:18:18Z
2016-02-16T12:24:17Z184661092, 184662479 Argues approve should always notify the spender; proposes bytes metadata 2016-02-17T12:39:09Z 185181532 Had originally wanted hooks on transfer too; was talked out of it 2016-03-01T18:17:32Z
2016-03-01T22:23:11Z190839646, 190933561 Refers to the member as decimalPlaces() and to baseUnit as prior art 2016-03-15T13:28:32Z 196816918 The operative statement of what compliance meant 2016-04-12
2016-04-15
2016-04-19208944198, 210397271, 211868211 transfer and balanceOf are settled, approve is not; asks for a standard version variable Sources
- Raw artifactraw/gharchive-events/issue20-events.jsonl
-
2016-03-2011:09:16Z
2016-03-20 · 11:09:16Z · block 1,184,107 The first fully ERC-20 compliant contract 0xacFD9D15fA769EaBb68410c4c675Ff2030f26416, 2,356 bytes. An ether wrapper. The first contract on mainnet that satisfies all ten requirements of EIP-20 as finalised when its own code is executed. It was never used.
- Address
- 0xacFD9D15fA769EaBb68410c4c675Ff2030f26416
- Deployed
- 2016-03-20T11:09:16Z, block 1,184,107
- Deployer
- 0x3a77633f26ddD2cf58D1fea2C889098565cA5C8C
- Runtime size
- 2,356 bytes
- Contents
- The six required methods, both event topics, plus deposit() and withdraw(uint256). No name, symbol or decimals.
- Use
- One transaction in its life, its own creation. Zero logs.
It is an ether wrapper, the pattern later made familiar by WETH. deposit() credits the caller with the ether sent, withdraw(uint256) returns it, and totalSupply() reports the contract's own ether balance rather than a stored figure, so supply and backing cannot drift apart. It emits the same Deposit and Withdrawal topics that canonical WETH9 would later use.
Compliance here is a claim about behaviour, so it was tested by behaviour. The contract was run on a mainnet fork, funded through its own deposit() rather than by writing storage, and every obligation checked against real transactions: receipts read for the events actually emitted, eth_call used to measure the width of each return value. It passes all ten, and passes them twice, once at the head of the chain and once on a fork pinned 93 blocks after its deployment under the hardfork rules of the day.
Reaching this point took a single opcode. The contract deployed on 25 February, 0xb345180D0a2c791d4943a239f8eBb50eFA01C81a, is the same code for its first 2,342 bytes and differs only in the tail bounds helper, which compares with ADD GT where this one compares with ADD LT ISZERO. Strict against inclusive. Under the strict form an amount of zero fails the check and the call throws, which is why the February contract rejects zero-value transfers and this one accepts them. That one comparison is the whole twenty-three day gap.
Two things this claim does not say. It does not say this contract has name, symbol or decimals, because it has none. Those three are optional in EIP-20 and their absence does not affect compliance, but anyone who reads “fully compliant” as including them will want a later contract. Unknown selectors on this contract fall through to deposit(), so name() appears to answer; that is the fallback, not a metadata function. And it does not say this contract was important. It was never used. The address that carried this code into service is its byte-identical twin 0xd654bDD32FC99471455e86C2E7f7D7b6437e9179, deployed 81 blocks later the same morning by the same account, which went on to record 806 logs and 363 transfers from June 2016.Everything deployed before it fails. All 34 contracts carrying the six selectors that predate this block were executed the same way and every one breaks at least one requirement: thirteen return no boolean from transfer, four return no data at all from totalSupply() and balanceOf(), three index _value in Transfer, one has ERC-20 entry points that do nothing, and the remaining thirteen reject transfers of zero, some of them emitting the wrong events besides.
Sources
- CorpusEvery create trace of 2015 and 2016, 6,187 and 230,818 respectively, internal creates included. 2015 yields no contract with all six selectors; 2016 yields 1,014.
- DetectionPUSH-aware disassembly for selector presence, then execution on an anvil mainnet fork for behaviour. Balances obtained through each contract's own mint path where one exists, otherwise by impersonating a live holder.
-
2016-04-2419:55:53Z
2016-04-24 · 19:55:53Z The first repository named erc20 Nikolai Mushegian commits “erc20 type definition”. It is the first repository whose entire purpose is the interface, and the first Solidity file named erc20.sol declaring contract ERC20.
Cloned and read from the initial commit. README.md in full: “dapple package for ERC20 token type interface”. dappfile declares name: erc20.
contracts/erc20.sol at that commit, complete
contract ERC20 { function totalSupply() constant returns (uint); function balanceOf(address who) constant returns (uint); function allowance(address owner, address spender) constant returns (uint); function transfer(address to, uint value) returns (bool ok); function transferFrom(address from, address to, uint value) returns (bool ok); function approve(address spender, uint value) returns (bool ok); event Transfer(address indexed from, address indexed to, uint value); event Approval(address indexed owner, address indexed spender, uint value); }Six methods, two events, nothing else. Scored by literal declaration text against the final signatures this reads as three of six, because uint and uint256 are different strings. They are the same ABI type. Scored by canonical selector, which is what interoperability actually depends on, it is six of six and both events match:
totalSupply() 0x18160ddd balanceOf(address) 0x70a08231 allowance(address,address) 0xdd62ed3e transfer(address,uint256) 0xa9059cbb transferFrom(address,address,uint256) 0x23b872dd approve(address,uint256) 0x095ea7b3 Transfer(address,address,uint256) 0xddf252ad…b3ef Approval(address,address,uint256) 0x8c5be1e5…b925Two qualifications on “first repo named erc20”. The repository was originally under the nexusdev organisation, not dapphub, visible in the merge commit 7e8cb92 of 2016-08-08: “Merge branch 'master' of github.com:nexusdev/erc20”. And “first” remains scoped to a gh search repos sweep over 2015 and 2016 by name and description. It is not a claim about every repository that has ever existed.The base implementation arrives sixteen minutes later in c0b1dfc, “copy base implementation and tests from dappsys”. The repository keeps its purpose: fc53f48 of 2017-02-02 reduces it back to the interface, and the file survives through solc 0.5.0 and 0.6.6.
No repository created in 2015 has ERC20 in its name or description
Created Repository Description 2016-02-22 daifoundation/maker-otc The OasisDEX protocol, simple onchain market for ERC20 tokens 2016-04-24 dapphub/erc20 erc20 interface definition container package 2016-05-11 nexusdev/token-freezer Multi-tenant ERC-20 token locker 2016-08-04 BangkitSedar/ERC20-Token-Standard ERC20 Token Standard 2016-08-10 dapphub/ds-eth-token ERC20 ETH token wrapper 2016-08-15 dapphub/ds-token A simple and sufficient ERC20 implementation 2016-11-08 Giveth/minime MiniMe Token. ERC20 compatible clonable token 2016-12-02 danfinlay/human-standard-token-abi A JSON ABI for the Ethereum ERC 20 Token Standard A search for “token standard” restricted to Solidity over the same two years returns zero results.
Sources
- Commite9707811d80ab36fecc67deb3a75a41adda3a954
- Blobcontracts/erc20.sol
- Author date2016-04-24 15:55:53 -0400
- Raw artifactraw/code-snapshots/dapphub-erc20-e970781-erc20-INITIAL.sol
-
2016-05-2020:38:26Z
2016-05-20 · 20:38:26Z The wiki stops specifying the interface “Transferable Fungibles is ERC 20”. Revision 46 replaces the interface section with a pointer to the issue. The page that started the standard eleven months earlier stops describing it.
Revisions 46 onward no longer specify anything. From this date the issue is the only document that defines ERC-20, which is also the reason its edit history had to be reconstructed from event payloads.
ConsenSys makes the same separation a week earlier, on 13 May: StandardToken.sol is made ERC20-only, and HumanStandardToken.sol is created to carry
name,symbolanddecimals. The optional three finally get their own file.Sources
- Wiki revisioneca915a · Transferable Fungibles is ERC 20
- ConsenSys split5252384 · 2016-05-13T19:16:16Z
-
2016-11-0209:47:51Z
2016-11-292016-11-02 · 09:47:51Z, and 2016-11-29 · 09:49:56Z totalSupply is briefly dropped, then restored Revision 18 removes totalSupply from the body. Revision 19, twenty-seven days later, restores it as returns (uint256 totalSupply). That is the text still live on the issue.
Between revision 16 on 6 January 2016 and revision 17 on 1 November 2016 the body did not change across 111 consecutive comment snapshots. The three late revisions are the only movement in the text after the standard froze.
The last of the nineteen recovered revisions, on 2016-11-29, is the body as it stands. Two weeks after that, on 13 December 2016, frozeman marks his original proposal gist outdated with a banner pointing at the issue.
Sources
- Revision 18comment 257817926
- Gist bannergist revision e7d0cf0, 2016-12-13T09:30:35Z
- Raw artifactsraw/issue20-bodies/r18-…md, r19-…md
Formal adoption
The events that turn a GitHub issue into a numbered, merged, Final standard. The first three fall outside the corpus this page is built from and are marked as such, with dashed markers, so that nothing here is mistaken for evidence gathered in the same way as everything above. The fourth is from the ethereum.org clone and carries its commit.
-
2016-12-07
2016-12-07 An “ERC” category is created in EIP-1 Hudson Jameson adds ERC as a category to the EIP process document, giving the label the standard had been using informally for thirteen months an official definition. Not evidenced by this corpus.
No artifact for this event is held in the collection behind this page. The reconstruction covers ethereum/EIPs issue #20, the wiki, the gists, the implementation repositories and the onchain corpus, through late 2016. It contains no EIP-1 revision history and no pull request data. The date and description above are supplied context, not a finding, and carry no citation because there is no primary source here to cite.Note the ordering it implies, which the rest of this page does support: the term “ERC” was in use in a commit message from 25 January 2016, in a repository name from 24 April 2016, and in the issue's own header block from 19 November 2015. The category is defined last.
-
2017-04-24
2017-04-24 Fabian Vogelsteller submits the standard as a pull request Pull request #610 against ethereum/EIPs, moving the text out of the issue and into a file in the repository. Seventeen months after the issue was opened. Not evidenced by this corpus.
No artifact for this event is held in the collection behind this page. The corpus holds 285 GH Archive events for issue #20 and 479 for the repository as a whole, all from 2015 and 2016, plus the comment index. It holds no pull request records. The date and number above are supplied context, not a finding.What this page does establish about the same interval is that nothing in the text changed after 29 November 2016, so whatever was submitted in April 2017 was a document that had been stable for months.
-
2017-09-11
2017-09-11 Merged as Final EIP-20 reaches Final status, one year and ten months after issue #20 was opened, and one year and eight months after the interface it describes stopped changing. Not evidenced by this corpus.
No artifact for this event is held in the collection behind this page. The date above is supplied context, not a finding.Placed against the dated findings on this page, the interval is the point. The specification's text froze on 6 January 2016. The first mainnet contract carrying the six selectors appeared on 10 January 2016, and the first that was actually a token on 14 January 2016. A thousand and fourteen contracts carried all six methods before the end of 2016. The formal status arrived after all of that.
-
2017-09-2921:22:55Z
2017-09-29 · 21:22:55Z The word ERC20 reaches ethereum.org, as a filename Commit “Separate solidity files” moves the inline contracts into solidity/, one of them named token-erc20.sol. It is the first and only occurrence of the string in the repository, eighteen days after EIP-20 became Final.
Searching the whole repository across all 1,210 commits from 2015-03-07 to 2019-04-17:
String First commit that introduces it ERC20 298cb09, 2017-09-29, in the filename solidity/token-erc20.sol ERC-20 never ERC 20 never EIPs/issues/20 never And in views/content/token.md itself, the page a reader actually saw, the substring ERC never appears in any of its 117 revisions, through the last commit in the repository.
The Ethereum Foundation's own token page never named the standard in prose. It linked the wiki as the “Meta coin standard” from July to December 2015, then linked nothing. The only place ERC20 is written anywhere on the site is a Solidity filename, added eighteen days after EIP-20 reached Final status on 11 September 2017.That closes the loop with the Frontier Guide. Neither of the Foundation's two documentation properties adopted the name while the standard was being written, and only one of them ever adopted the interface.
Sources
- Commitethereum-org 298cb09 · Separate solidity files (#693)
- Author date2017-09-29 18:22:55 -0300
- Methodgit log --all -S"<string>" over the full clone; and a scan of all 117 revisions of views/content/token.md for the substring ERC
Method by method
When each member appeared
Seven of the eight members are renames of something older. One, totalSupply, is newly invented. The two that define the standard are the last to arrive.
Table scrolls sideways →
| Member | First public appearance (UTC) | Author | Artifact | Replaced |
|---|---|---|---|---|
| balanceOf | 2015-10-04T15:07:06Z | Gav Wood | ethereum/wiki 607b6ac | coinBalanceOf |
| Transfer (event) | 2015-10-04T15:07:06Z | Gav Wood | Same commit | CoinTransfer |
| transfer | 2015-10-06T12:57:06Z | Simon de la Rouviere | ethereum/wiki bfc39cb | sendCoin |
| transferFrom | 2015-10-06T12:57:30Z | Simon de la Rouviere | ethereum/wiki 9721a6b | sendCoinFrom, via the typo trasnferFrom 24 seconds earlier |
| final transfer / transferFrom signatures | 2015-10-28T13:44:47Z | Fabian Vogelsteller | ethereum/wiki 0627f24 | Parameter order reversed |
| totalSupply | 2015-11-18T08:59:46Z | frozeman | gist e7abcdd | Newly invented |
| approve(address,uint256) | 2015-11-19T19:07:35Z | frozeman | Issue #20 revision 5 | approve(address) |
| allowance | 2015-11-20T15:53:42Z | frozeman | Issue #20 revision 7 | isApprovedFor |
| Approval (event) | 2015-11-20T15:53:42Z | frozeman | Issue #20 revision 7 | AddressApproval |
Members that existed and were removed
| Member | Present from | Removed | Where |
|---|---|---|---|
| decimals() | 2015-11-19T09:52:56Z, issue #20 revision 1 | 2015-11-26T10:34:22Z, revision 11 | Voted out on the wiki poll |
| unapprove() | Wiki 2015-09-02, as disapprove from 2015-08-24 | 2016-01-06 in the issue, 2015-12-21 in code | Removed when approve became absolute |
| approveOnce / isApprovedOnceFor | Wiki 2015-06-18 and 2015-08-24 | 2015-11-19T19:07:35Z, revision 5 | Merged into a capped approve |
| isApproved | Wiki 2015-06-18 | Wiki 2015-09-06 | |
| AddressApproval, AddressApprovalOnce | Wiki 2015-08-24 | Issue #20 revision 7 | Replaced by Approval |
| Approved, Unapproved | Issue #20 revision 13, 2015-12-02 | 2016-01-06 | Short-lived regression |
balanceOf, 4 October 2015) to the last (allowance and Approval, 20 November 2015) is 47 days. The elapsed time from the creation of the wiki page to the last is 5 months and 3 days. Almost all of the interface's design happened in a seven-week span, and the decisive part of it in about thirty hours.
Side by side
What each source implemented
Every document and contract in the trail, scored against the finished standard. Read down a column to watch a member arrive. Read across a row to see how far any one artifact got.
Table scrolls sideways →
| UTC | Artifact | totalSupply | balanceOf | transfer | transferFrom | approve | allowance | Transfer | Approval | Score |
|---|---|---|---|---|---|---|---|---|---|---|
| 2015-06-17 | Wiki 748c9b0, as created | – | – | – | – | ~ | – | – | – | 0/6 |
| 2015-05-04 | Frontier Guide tutorial, as created | – | – | – | – | – | – | – | – | 0/6 |
| 2015-07-24 | Frontier Guide tutorial, final form | – | – | – | – | – | – | – | – | 0/6 |
| 2015-09-06 | dapp-bin currency.sol | – | – | – | – | ~ | – | – | – | 0/6 |
| 2015-10-04 | Wiki 607b6ac, Gav Wood | – | ✔ | – | – | ~ | – | ✔ | – | 1/6 |
| 2015-10-06 | Wiki 9721a6b, after the typo fix | – | ✔ | ~ | ~ | ~ | – | ✔ | – | 1/6 |
| 2015-10-28 | Wiki 0627f24, parameter order fixed | – | ✔ | ✔ | ✔ | ~ | – | ✔ | – | 3/6 |
| 2015-11-03 | MistCoin, deployed bytecode | – | ✔ | ~ | – | – | – | ✔ | – | 2/6 |
| 2015-11-17 | Token proposal gist v1 | – | ✔ | ✔ | ✔ | ~ | – | – | – | 3/6 |
| 2015-11-18 | Token proposal gist v4 | ✔ | ✔ | ✔ | ✔ | ~ | – | ✔ | – | 4/6 |
| 2015-11-19 | Issue #20 revision 1, as posted | ✔ | ✔ | ✔ | ✔ | ~ | – | ✔ | – | 4/6 |
| 2015-11-19 | Issue #20 revision 5 | ✔ | ✔ | ✔ | ✔ | ✔ | – | ✔ | – | 5/6 |
| 2015-11-20 | Issue #20 revision 7, all eight co-exist | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | 6/6 |
| 2015-11-30 | ConsenSys/Tokens 4ba2396, first compilable | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | 6/6 |
| 2015-12-01 | Blog post gist 21935dc… | – | ✔ | ~ | – | – | – | ✔ | – | 2/6 |
| 2015-12-02 | Issue #20 revision 13, the regression | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | – | 6/6 |
| 2015-12-21 | ConsenSys/Tokens c3a3426, exactly ERC-20 | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | 6/6 |
| 2015-12-29 | slockit/DAO 95d85c6 | – | ~ | ~ | ~ | ~ | – | ✔ | – | 1/6 |
| 2016-01-06 | Issue #20 revision 15, final form | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | 6/6 |
| 2016-01-16 | slockit/DAO bf27cf7 | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | 6/6 |
| 2016-02-13 | Van de Sande gist 0d1a998… | – | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | – | 5/6 |
| 2016-04-24 | dapphub/erc20 e970781, by selector | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | 6/6 |
The two Frontier Guide rows score zero across the board and are included for exactly that reason: the official documentation for the first live Ethereum network shares no member, under any signature, with the standard that was being written beside it. Its coinBalanceOf matches the wiki's superseded getter, not balanceOf.
Two notes on scoring. The Score column counts only the six required methods at their final signatures, which is why revision 13 scores 6/6 despite having renamed the Approval event away. ~ against approve before 19 November 2015 means the member exists but takes no amount. MistCoin's transfer is marked ~ because it shares the final selector but returns nothing, where the standard specifies returns (bool success). Cosmetic differences in return variable names are treated as equivalent, since they do not change the ABI.
Deployed 3 November 2015
MistCoin
Deployed 3 November 2015 at 12:03:29Z, sixteen days before issue #20 was filed. What the deployed bytecode contains, where the source came from, and where the contract sits in the record.
- Contract
- 0xf4eCEd2f682CE333f96f2D8966C613DeD8fC95DD
- Deployed
- 2015-11-03T12:03:29Z, block 483,325
- Transaction
- 0x74349ce6…54a7
- Deployer
- 0x9b22a80D5c7B3374a05b446081f97d0A34079e7F
- Name, symbol
- MistCoin, MC
- Supply
- 100,000,000 raw units at 2 decimals, so 1,000,000 MC
- Runtime
- 716 bytes
- Creation payload
- 1,406 bytes = 1,150 code + 256 bytes of ABI-encoded constructor arguments
What the deployed bytecode contains
Decoded by walking the dispatcher. The complete set of PUSH4 operands in the 716-byte runtime is five, and the complete set of PUSH32 operands is one. Not a sample. All of them.
0x06fdde03 name()
0x313ce567 decimals()
0x70a08231 balanceOf(address)
0x95d89b41 symbol()
0xa9059cbb transfer(address,uint256)
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
Transfer(address,address,uint256)
Against the finished standard
| EIP-20 member | Required? | In MistCoin |
|---|---|---|
| totalSupply() | required | – |
| balanceOf(address) | required | ✔ |
| transfer(address,uint256) | required | ✔ * |
| transferFrom(address,address,uint256) | required | – |
| approve(address,uint256) | required | – |
| allowance(address,address) | required | – |
| Transfer(address,address,uint256) | required | ✔ |
| Approval(address,address,uint256) | required | – |
| name() | optional | ✔ |
| symbol() | optional | ✔ |
| decimals() | optional | ✔ |
* Same selector, but the deployed transfer returns nothing. EIP-20 specifies returns (bool success). The dispatcher pushes return address 0x0045, which is a JUMPDEST immediately followed by STOP.
Bytecode-verified provenance
The source is not inferred from resemblance. It was recompiled with a period-correct compiler and matched byte for byte.
- Source
- Gist frozeman/20c8b5658349b003b08d revision 7bcfaef3be689c81f8ee20ddb190031efe8fc110, committed 2015-11-03T12:03:46Z
- Compiler
- solc 0.1.6-d41f8b7c/.-Emscripten/clang/int (soljson-v0.1.6+commit.d41f8b7.js)
- Settings
- Optimizer enabled
- Runtime
- 716 bytes, exact match to the deployed runtime
- Creation
- 1,150 bytes, exact prefix match to the deployed creation payload, with exactly 256 bytes of constructor arguments appended
The previous gist revision, 759cddeb of 11:59:12Z, produces the same runtime but a different creation prefix: it diverges at byte 41, in the constructor's argument handling. Decoding MistCoin's 256-byte argument tail settles which one was deployed.
[0] 0x…05f5e100 uint256 _supply = 100,000,000
[1] 0x…00000080 offset -> "MistCoin"
[2] 0x…000000c0 offset -> "MC"
[3] 0x…00000002 uint8 _decimals = 2
The order is (_supply, _name, _symbol, _decimals). Revision 759cddeb declares (_supply, _name, _decimals, _symbol). MistCoin was compiled from revision 7bcfaef3, the revision saved seventeen seconds after the block that contains the deployment.
Where it sits in the record
Earlier contracts with the core three
| First occurrence | UTC | Contract |
|---|---|---|
| balanceOf(address) | 2015-08-08T16:05:56Z block 54,180 | 0x5fC8AeFc86884f0792995c015ff12647fafA0d83 |
| transfer(address,uint256) | 2015-08-19T09:36:20Z block 110,635 | 0x65c4E65113DB14f8c15702883791cCA9E66C5Ed2 |
| Transfer event | 2015-10-23T09:41:56Z block 426,661 | 0x3C655ccb35666579511489af88153517fc58b017 |
| All three together | 2015-10-23T09:41:56Z block 426,661 | 0x3C655ccb35666579511489af88153517fc58b017 |
Measured across every contract created on mainnet from Frontier launch to 2015-12-31: 6,187 create traces, 5,724 with runtime bytecode, 628 carrying token vocabulary. By that measure MistCoin is the tenth contract to carry balanceOf, transfer and the Transfer event together, eleven days after the first.
The nine earlier ones are not nine independent tokens. They are a prototyping run by two addresses over eight days, detailed in the timeline entry for 23 October 2015. Not one of the nine has name, symbol or decimals. They had a balance and a transfer. None had anything a wallet could label.
What it is first at
MistCoin is the first contract on Ethereum mainnet carrying the complete shape the Ethereum Wallet rendered: balanceOf, transfer and Transfer, and name, symbol and decimals. It is number one of 130 such contracts in 2015. The second arrives 6 hours 24 minutes later.
transfer, and not an implementation of a standard that did not yet exist. The first that the wallet could render with a name, a symbol and a decimal point, on the day the wallet gained the ability to render one.It is also, per an index of 12,023,046 contracts, the earliest of 173 contracts across all of Ethereum history sharing its exact runtime bytecode. It is the genesis member of the MyToken family, which is the largest single artifact of the 2015 tutorial.
Who made it
MistCoin was deployed by Fabian Vogelsteller, from the address 0x9b22a80D5c7B3374a05b446081f97d0A34079e7F. He wrote the MyToken gist it was compiled from that morning, and he authored and published Ethereum Wallet 0.3.5, the release that gave the wallet its token feature, later the same day. He has described the deployment publicly since.
Its first transfer sent half the supply to Alex Van de Sande. Both were at the Ethereum Foundation: Vogelsteller built the wallet and Mist, Van de Sande led its user experience. A month later Van de Sande wrote the Foundation's tutorial on creating a token, “Ethereum in practice part 1”, which shipped this contract's source to a general audience.
Sources
- Fabian on MistCoinreddit.com/r/ethereum
- Wallet announcementreddit.com/r/ethereum
- Release 0.3.5github.com/ethereum/mist/releases/tag/0.3.5
- Source gistfrozeman/20c8b5658349b003b08d
What the chain shows that week
The deployment was the end of a short run of rehearsals, and the traces show the shape of the work rather than a single moment.
- In all of 2015 the MistCoin address deployed exactly two contracts carrying token vocabulary: MistCoin itself, and 0xE274d18EF7b194A1EDEbB04cfE297CFe1489ef65 on 26 October, the same pattern rehearsed eight days earlier.
- That rehearsal is byte-identical to a contract deployed two minutes and twelve seconds later by 0xB1a2B43A7433dd150BB82227eD519Cd6b142d382, which was running a twelve-contract prototyping campaign across six bytecode families over the same weeks. Two addresses putting the same build on mainnet two minutes apart.
- The final deployment sits seventeen seconds before the gist revision it was compiled from, and one hour forty minutes before the wallet release that shipped the feature.
Read together with the gist and release history, this is a client feature and its first example token being finished on the same morning, hours before the release went out.
What the standard was that day
On 3 November 2015 the standard was the wiki page, and issue #20 did not exist. The wiki had held the names balanceOf and transfer for thirty and twenty-eight days respectively, and their final parameter order for six days, so MistCoin's two methods do match the page as it stood.
What the page also specified, and MistCoin does not implement, is the entire rest of it: transferFrom, and five approval members, approve, unapprove, isApprovedFor, approveOnce and isApprovedOnceFor, plus the events AddressApproval and AddressApprovalOnce. There was no totalSupply on the page yet, no allowance and no Approval. Those three arrive on 18, 20 and 20 November. MistCoin implements the two members of the October wiki that a wallet needed, and none of the eight it did not.
This is a fact about the calendar rather than about the contract. A token deployed sixteen days before the proposal was written cannot implement the proposal. What it did do was establish the shape the wallet rendered, which a few hundred contracts copied over the following two months.
3 November 2015 to 6 January 2016
What was deployed
This is the empirical test the documentary trail cannot supply. While the standard was being written, what went on mainnet? Every create trace in the window was collected and every runtime blob decoded.
Coverage of the final six
| Members present | Contracts |
|---|---|
| 0 | 82 |
| 1 | 83 |
| 2 | 145 |
| 3 | 10 |
| 4 | 0 |
| 5 | 0 |
| 6 | 0 |
No contract deployed to Ethereum mainnet between 3 November 2015 and 6 January 2016 implements more than three of the six ERC-20 methods.
Frequency of each member
| Member | Contracts | Share of the 320 |
|---|---|---|
| balanceOf(address) | 207 | |
| transfer(address,uint256) | 185 | |
| transferFrom(…) | 10 | |
| totalSupply() | 1 | |
| approve(address,uint256) | 0 | |
| allowance(address,address) | 0 |
Approval. Nothing in the window implements approve(address,uint256) or allowance(address,address). The allowance model, the half of ERC-20 that makes exchanges and later DeFi possible, and the half that consumed almost all of the argument on issue #20, has zero deployed instances during the period in which it was being specified.
The optional three, and the events
| Member or topic | Contracts |
|---|---|
| name() | 172 |
| symbol() | 142 |
| decimals() | 140 |
| Transfer(address,address,uint256) | 154 |
| CoinTransfer(address,address,uint256) | 53 |
| CoinSent(address,uint256,address) | 2 |
| AddressApproval(address,address,bool) | 1 |
| AddressApprovalOnce(address,address,uint256) | 1 |
| Approval(address,address,uint256) | 0 |
The three members the standard marks optional are implemented more often than four of the six it requires.
The superseded vocabulary
| Superseded member | Contracts |
|---|---|
| coinBalanceOf(address) | 56 |
| approve(address) | 12 |
| approveOnce(address,uint256) | 12 |
| isApprovedOnceFor(address,address) | 12 |
| isApprovedFor(address,address) | 10 |
| unapprove(address) | 10 |
| currency() | 8 |
| balances(address) | 6 |
| coinBalance(), disapprove, isApproved, sendCoin, sendCoinFrom | 2 each |
| transfer(uint256,address), transferFrom(address,uint256,address) | 1 each |
coinBalanceOf, which Gav Wood renamed away on the wiki on 4 October 2015, was still being deployed 56 times through December. 53 of those 56 pair it with the CoinTransfer event and carry no sendCoin under the specification's signature: that is the shape of the contract in the official Frontier Guide tutorial, which never changed. The count of 2 for sendCoin reflects the scan's vocabulary, not the deployments; see the note below.
sendCoin row understates its subject. The scan's superseded-vocabulary list was built from the wiki, dapp-bin and the DAO, so it tested sendCoin(uint256,address), selector 0xc86a90fe. The Ethereum Frontier Guide taught sendCoin(address receiver, uint amount), selector 0x90b98a11, which was never tested. Across all of 2015, 264 of the 628 token-vocabulary contracts carry coinBalanceOf together with the CoinTransfer topic, the guide's exact pairing, and only 7 of those also carry the specification's sendCoin. The dominant token contract on mainnet in 2015 was not written against the standard being drafted. It was written against the documentation, and the documentation had its own vocabulary.
A sample of what was on mainnet
Of the 320, 140 have the exact MyToken shape: balanceOf, transfer, name, symbol, decimals and Transfer, and nothing else. 118 yield a clean constructor decode. The first row of that census, sorted by block, is MistCoin.
| Deployed (UTC) | Address | Name | Symbol |
|---|---|---|---|
| 2015-11-03T12:03:29Z | 0xf4eCEd2f682CE333f96f2D8966C613DeD8fC95DD | MistCoin | MC |
| 2015-11-03T18:46:18Z | 0x796Ed7f47E100984e7aA7ca51D55e9B68eCb1C19 | Whitcoin | WHIT |
| 2015-11-03T22:17:12Z | 0xDafE447177aEfb05Dd7eDAe2F5781c98A858d320 | UniCoin | UNC |
| 2015-11-03T23:23:36Z | 0xaB3652FD492FbB0d6b63acB742f3eD12AfBAEf52 | Bizilicas | BiZ |
| 2015-11-04T08:54:38Z | 0xEFB1775952642353c0386410212D7638c9fB2426 | EtherMusic | ETM |
| 2015-11-08T23:29:00Z | 0xA1162CBb7F6cc8F8476c5f4783761302a9aBaf69 | Ethereum Unit | ETH |
| 2015-11-12T16:41:26Z | 0x896BA935dfBe3c5dDFBc1b637bE60964e5244465 | CannabisToken | CANN |
| 2015-11-16T16:46:40Z | 0x3B683F1ba138A094042e368415d8B9fEF86731A4 | CoinAwesome | AWE |
| 2015-12-04T17:38:17Z | 0xE671b8Acf6aCD77Ec885EfA7e3C93bE05E887407 | My DAO Shares | % |
| 2015-12-14T08:55:58Z | 0xCc0eE510BC4b5CD4D31Da49f672AB5aa6806F70a | SubEthaNomic | SEN |
| 2016-01-06T16:13:52Z | 0x5A2EbC3AC433fd6c9Ad2B1a56033Dc2D45945315 | pieshopdollar | $ |
Not one of these is an ERC-20 token by the finished standard. All of them were, at the time, tokens, because the wallet said so.
When the six appear
The same scan run over all of 2016: 230,818 create traces, 213,112 with runtime bytecode.
| Contracts in 2016 | Count |
|---|---|
| Four or more of the final six | 2,295 |
| All six | 1,014 |
| All six plus both final event topics | 554 |
Contracts with all six, by month
2016-01: 11 2016-07: 101
2016-02: 17 2016-08: 126
2016-03: 19 2016-09: 162
2016-04: 35 2016-10: 115
2016-05: 34 2016-11: 154
2016-06: 91 2016-12: 149
The answer to “does any token from 2015 or 2016 implement all six?” is: none in 2015, and 1,014 in 2016, the first on 10 January 2016, four days after the text stopped moving.
Adoption of the specification as written is a 2016 phenomenon that only reaches triple digits per month in June 2016, seven months after issue #20 was filed. Only 28 contracts across January and February 2016 combined carry all six.
Approval topic but missed Transfer, which is unambiguously present. Every headline count was therefore re-run with plain substring matching over the runtime hex, which cannot desynchronise.Two detectors, in exact agreement
| Member | Opcode walk | Substring |
|---|---|---|
| totalSupply() | 1 | 1 |
| balanceOf(address) | 207 | 207 |
| transfer(address,uint256) | 185 | 185 |
| transferFrom(address,address,uint256) | 10 | 10 |
| approve(address,uint256) | 0 | 0 |
| allowance(address,address) | 0 | 0 |
| Transfer topic | 154 | 154 |
| Approval topic | 0 | 0 |
Zero contracts where the substring test found a member the walk missed. The four-byte substring test is the looser of the two: it produces false positives, not false negatives. A zero from both methods is as strong as this corpus can make it. The claim that nothing in the window implements approve or allowance does not depend on the choice of detector.
Four definitions
What compliance meant
Four readings are in circulation, and they date differently. The corpus lets each be dated precisely. The first three are answerable from documents and bytecode. The fourth is answerable only by running the code.
1. As the wallet defined it
balanceOf, transfer, Transfer. This is the operative definition for the whole period. It is what the Ethereum Wallet's ABI required from 23 October 2015, what Alex Van de Sande told the issue thread as late as 15 March 2016, and what 154 of the 320 token-vocabulary contracts in the window implement.
By this definition MistCoin complies, along with nine earlier contracts and about 150 later ones. It has nothing to do with EIP-20's required set.
2. As the specification defines it
All six methods, both events. First stated in prose on 2015-11-20T15:53:42Z. First expressed exactly, with nothing extra, on 2015-12-21T15:55:57Z in ConsenSys/Tokens. First stated exactly by the specification itself on 2016-01-06.
Zero contracts in the window meet it. Zero implement even four of the six. approve and allowance have no deployed instances at all in the window, and Approval is emitted by nothing.
3. As applied retroactively
The reading under which a 2015 contract “is” ERC-20 because ERC-20 later described something like it. This is not a property of the contract. It is a property of the reader.
It is also unfalsifiable, which is why it is worth naming rather than arguing with.
4. As EIP-20 finally required it
Not the selectors but the behaviour: the return values, the event shape, the allowance arithmetic, and transfers of zero. Definition 2 can be answered from bytecode. This one cannot, because a function that returns a boolean and one that returns nothing share a selector, and indexing _value does not change an event's topic hash.
First met on 2016-03-20T11:09:16Z, block 1,184,107, by 0xacFD9D15fA769EaBb68410c4c675Ff2030f26416. Nothing in 2015 meets it, and neither does any of the 34 qualifying contracts deployed before that block.
The gap between the first two definitions is the whole finding. The standard was written by people who were, at the same time, shipping a client that did not require it. The wallet's tokenInterface.js wanted name, symbol and decimals, none of which ERC-20 requires, and did not want totalSupply, transferFrom, approve or allowance, four of the six that it does.
The accurate statement about any 2015 token, MistCoin included, is that it meets the first definition and cannot meet the second, because the second did not exist yet. Nothing deployed in the window complies with it. The first contracts that do are a 2016 phenomenon.
One note on how compliance is scored on this page. Text-identity and ABI-identity are different tests, and only the second determines whether two contracts interoperate. dapphub/erc20 reads as three of six by literal declaration text, because uint and uint256 are different strings, and six of six by canonical selector. The selector count is the one that matters.
The selector count settles definition 2 and stops there. It cannot settle definition 4, so for that question every candidate was executed.
Definition 4, tested by execution
Every contract deployed in 2015 or 2016 whose dispatcher carries all six selectors was collected, 1,014 of them, all in 2016. All 34 deployed before the first one to pass were then run on an anvil mainnet fork and checked against all ten obligations of EIP-20 as finalised: real transactions, receipts read for the logs actually emitted, eth_call used to measure return width. The milestones of that pass, in order:
Table scrolls sideways →
| Deployed (UTC) | Contract | Deploys the interface | Interface and a real supply | Fully compliant | What breaks |
|---|---|---|---|---|---|
| 2016-01-10 824,235 |
0x99146Bab2bB34D9Ca49EC4f0c82De3E5789ae22e Digix gold ledger |
✔ | – | – | Supply is zero at every block checked, so it is an interface and not a token. Transfer indexes _value; transferFrom reports the spender as _from. |
| 2016-01-14 847,527 |
0x55b9a11c2e8351b4Ffc7b11561148bfaC9977855 Digix Gold 1.0 |
✔ | ✔ | – | Event shape. Transfer indexes _value, so it emits four topics and an empty data field. transferFrom names the spender as _from rather than the owner. |
| 2016-01-27 912,760 |
0xa04bf47F0E9D1745D254b9B89f304c7d7ad121Aa elcoin |
✔ | ✔ | – | The entry points do nothing. transfer, approve and transferFrom return false, move no balance and emit no event. Its transfers were driven through a controller. |
| 2016-01-28 917,622 |
0x37Dca38b1CBB2Cd043910eC46fe82Ddb9e38F00d | ✔ | ✔ | ~ | Zero-value transfers only. Nine of ten. A guard of && _value > 0 makes a transfer of zero return false and emit nothing. The earliest copy of that requirement is dated July 2017 and it is absent from issue #20, so it post-dates this contract. |
| 2016-02-25 1,059,698 |
0xb345180D0a2c791d4943a239f8eBb50eFA01C81a | ✔ | ✔ | ~ | The same code as the row below except one comparison, ADD GT against ADD LT ISZERO. Under the strict form a transfer of zero throws. |
| 2016-03-20 1,184,107 |
0xacFD9D15fA769EaBb68410c4c675Ff2030f26416 ether wrapper |
✔ | ✔ | ✔ | Nothing. All ten. Verified at the head of the chain and on a fork pinned 93 blocks after deployment. Never used: one transaction, its creation. Its twin 0xd654bDD32FC99471455e86C2E7f7D7b6437e9179, 81 blocks later, is the one that saw traffic. |
The middle two columns are bytecode and state questions and were answered as such. The third is a behavioural question and was answered by execution. A contract can hold every selector, hold a real supply, and still not behave the way the standard requires, which is what the first three rows are.
Transfer event” is in EIP-20 as finalised and in the earliest copy of eip-20-token-standard.md in the EIPs repository, dated 13 July 2017. It is not in issue #20 as filed on 19 November 2015, and it is not in the issue today. It is also the only requirement separating 0xacFD9D15fA769EaBb68410c4c675Ff2030f26416 from candidates as much as seven weeks earlier. Applying it to a contract from early 2016 is applying a rule written after the fact, which is worth saying plainly rather than burying: by EIP-20 as finalised the answer is 20 March 2016; by the specification as it stood when these contracts were deployed the answer is 0x37Dca38b1CBB2Cd043910eC46fe82Ddb9e38F00d, 28 January 2016. Both dates are stated here, and neither is presented as the only one.
How the dating was done, and where it stops
Method, sources and limits
The problem with issue #20
GitHub did not record issue-body edit history until late 2016. GET /repos/ethereum/EIPs/issues/20 returns diff: null and serves only the current body. The document that defines ERC-20 has no visible history at the place it lives.
The recovery
Every IssueCommentEvent in the GitHub public event stream embeds the entire parent issue object, including issue.body as it stood at that instant. Issue #20 accumulated 284 comments between 2015-11-19 and 2016-12-22. Each is a dated snapshot of the body.
- Fetch all comments on the issue. 362 comments; 284 fall inside 2015-11 to 2016-12.
- Map each created_at to its GH Archive hour. 198 distinct hourly files.
- Download each. Zero missing or corrupt files, verified with gzip -t.
- Filter for the repository and issue number. 285 events: one IssuesEvent:opened and 284 IssueCommentEvent.
- De-duplicate payload.issue.body in timestamp order. 19 distinct revisions.
Eight of these were previously known. Eleven are recovered here for the first time.
# 1. every comment on issue 20
gh api --paginate repos/ethereum/EIPs/issues/20/comments \
-q '.[]|[.created_at,(.id|tostring),.user.login,.html_url]|@tsv' > comments.tsv
# 2. the distinct GH Archive hours those comments fall in
awk -F'\t' '{gsub("T","-",$1); split($1,a,"-");
printf "%s-%s-%s-%d\n",a[1],a[2],a[3],a[4]+0}' comments.tsv | sort -u > hours.txt
# 3. fetch them
xargs -P 8 -I{} curl -s -o {}.json.gz http://data.gharchive.org/{}.json.gz < hours.txt
# 4. pull the embedded issue body out of every event
# (write per-file, never to a shared stdout: parallel writes interleave
# and corrupt the JSON)
for f in *.json.gz; do gzcat "$f" | grep -a 'ethereum/EIPs' > "parts/${f%.json.gz}.jsonl"; done
cat parts/*.jsonl | jq -c 'select((.payload.issue.number//0)==20)' \
| jq -r '[.created_at,((.payload.issue.body//"")|@base64)]|@tsv' | sort | uniq -f0
2015-era GH Archive rows store payload as an object in these files, but BigQuery's githubarchive.day.2015* tables store it as a JSON string. JSON_EXTRACT_SCALAR is required there, not dot access.
Sources examined
| Source | Access | Coverage |
|---|---|---|
| ethereum/EIPs issue #20 | GH Archive + GitHub API | 19 body revisions, 2015-11-19 to 2016-11-29 |
| ethereum/wiki Standardized_Contract_APIs | git clone | All 54 revisions, 2015-06-17 to 2018-08-22 |
| ethereum/wiki poll page | git clone | 9 revisions, 2015-11-21 to 2015-11-26 |
| gist frozeman/090ae32… | gist history API | 6 revisions |
| gist frozeman/20c8b56… | gist history API | 5 revisions |
| ConsenSys/Tokens | full clone | 2015-07-15 to present |
| slockit/DAO | full clone | Token.sol from 2015-12-29 |
| ethereum/meteor-dapp-wallet | full clone | tokenABI.js 2015-10-06 to 2015-12-02 |
| ethereum/mist | full clone, releases API | Release 0.3.5 metadata and body |
| ethereum/dapp-bin | full clone | standardized_contract_apis/currency.sol |
| ethereum/frontier-guide | full clone | All 216 commits, 2015-04-30 to 2015-07-28 |
| go-ethereum.wiki | full clone | Contract-Tutorial.md all 29 revisions, plus Coin-Contract-Tutorial.md |
| ethereum/ethereum-org | full clone | All 1,210 commits, 2015-03-07 to 2019-04-17. views/content/token.md all 117 revisions |
| dapphub/erc20 | full clone | From 2016-04-24 |
| blog.ethereum.org | fetched | “Ethereum in practice part 1” by Alex Van de Sande, 2015-12-03 |
| r/ethereum | linked | The Wallet 0.3.5 announcement thread, and Fabian Vogelsteller's account of MistCoin |
| GitHub repo search | gh search repos | All repos created 2015-01-01 to 2016-12-31 |
| gist alexvandesande/0d1a998… | gist history API | 2 revisions, from 2016-02-13 |
| gist 909d02…, anonymous | gist history API | 1 revision, 2015-10-30 |
| Mainnet create traces | local BigQuery export | 2015-07-30 to 2016-12-31. 6,187 (2015) and 230,818 (2016) traces with runtime bytecode |
| Local contract index | SQLite, 12,023,046 contracts | Independent cross-check of the create-trace counts and bytecode families |
| solc 0.1.x archive | local soljson builds | Period-correct recompilation |
Onchain detection
For each runtime blob the EVM opcode stream is walked and every PUSH4 and PUSH32 operand collected. Walking the opcode stream rather than substring-matching avoids false positives from data bytes that happen to spell a selector. PUSH4 operands are matched against the keccak-256 selectors of the final six, the optional three and 20 superseded members; PUSH32 operands against 8 candidate event topic hashes.
def push_operands(code_hex):
b = bytes.fromhex(code_hex[2:]); p4=set(); p32=set(); i=0
while i < len(b):
op=b[i]; i+=1
if 0x60 <= op <= 0x7f: # PUSH1..PUSH32
ln=op-0x5f; data=b[i:i+ln]; i+=ln
if ln==4: p4.add(data.hex())
if ln==32: p32.add(data.hex())
return p4, p32
Limits of this evidence
- Issue-body snapshots exist only where a comment landed. The thread was silent between 2015-12-02T10:22:08Z and 2016-01-06T10:12:13Z, so an intermediate edit inside that window is invisible. The correct statement of finding 4 is a bound: the exact interface was present at 10:28:48Z and was not present at 10:12:13Z.
- A long stable period is genuine, not a gap. Between 2016-01-06T20:55:55Z and 2016-10-28T04:21:56Z the body did not change across 111 consecutive comment snapshots.
- “First public appearance” means first in the sources listed above. These are the canonical ones for this standard, but the claim is a lower bound on lateness, not proof of universal novelty. An exhaustive search would require the full 2015 and 2016 GH Archive corpus, roughly 8,760 files per year.
- Commit-message evidence for the term “ERC 20” was searched exhaustively within the cloned repositories and sampled through 280 GH Archive hours. An earlier use may exist in a repository not examined.
- Author dates, not committer dates, are used throughout. For these repositories the two agree on the commits cited.
- Onchain detection is bytecode-level, not semantic. A selector in a dispatcher proves a function is callable, not that it behaves as the standard specifies. The onchain section counts interfaces, not correctness.
- Create traces only. The onchain corpus contains contract creations and their bytecode. It holds no transaction logs, so no claim about transfers, balances or holders is testable from it.
- Onchain addresses are not identities. The corpus resolves deployments to addresses, not to people. Where a deployment is attributed to a named person on this page, the basis is the public record outside the corpus: gist and release authorship, and the participants' own accounts.
- Signature matching is mechanical. A
~means the parameter list differs from the final form in order, type or arity. Differences in return variable names are treated as equivalent, since they do not change the ABI. - A dispatcher scan can miss non-standard dispatchers. A contract using a hand-written dispatcher, or a proxy, would be missed. For 2015 Solidity output this is not a realistic concern, but it is a stated assumption rather than a proof.
- 121 of the 3,062 window contracts have no runtime bytecode and were not analysed. They are almost certainly failed constructors, but that is an assumption.
- The scan's vocabulary list is incomplete, and the Frontier Guide shows where. Its superseded members were drawn from the wiki, dapp-bin and the DAO, so it tested sendCoin(uint256,address) and not the guide's sendCoin(address,uint256), selector 0x90b98a11. Every count of sendCoin on this page is therefore a count of the specification's signature only, and understates how many contracts had a working transfer function. The affected contracts are identified by their coinBalanceOf and CoinTransfer pairing instead.
Gaps this pass did not close
MistCoin's first transfer is recorded outside this corpus. The onchain export holds contract-creation traces and bytecode only, with no transaction logs, so the transfer of half the supply to Alex Van de Sande is carried here on Fabian Vogelsteller's public account rather than on the traces. Reading it off the chain would need event-log data for blocks 483,325 and later.
The guide-shaped contracts have not been re-scanned. The 264 contracts identified by the coinBalanceOf plus CoinTransfer pairing are almost certainly compiled from the official tutorial, but the archived corpus stores decoded member lists rather than runtime bytecode, so sendCoin(address,uint256) could not be tested directly. Re-running the opcode walk over the 2015 and window create traces with 0x90b98a11 added to the vocabulary would convert the inference into a count.
The three formalisation events of December 2016 and 2017 have no artifact here. They are shown in the timeline's final era and marked as outside the corpus. Closing them needs the ethereum/EIPs pull request history and the EIP-1 revision history.