March 2018 hot-potato NFT featuring 25 Gen-1 Pokémon.
Historical Significance
An early Ethereum NFT collectible from the brief CryptoKitties-imitator wave of early 2018, distinguished mostly by its choice of theme (the 25 Gen-1 starter Pokémon) and by how little ever happened on it: exactly one purchase, on Squirtle, in its entire on-chain life. The complete 25-token launch set has been acquired and wrapped for permanent preservation, paired with primary-source documentation (Reddit announcement, The Merkle coverage, archived site) of the project as it appeared in April 2018.
Context
The launch. All 25 Pokémon were minted in a single burst from 0xc695a3281bf84f53116cd5ddc93457dc88f443b7 on the night of March 28–29, 2018 UTC, via 25 sequential createPromoPokemon calls. The site at cryptopokemons.com went live around the same time, featuring what appear to be original illustrations of the 25 characters. The project was announced publicly on r/CryptoCurrency on April 3 (u/skylarbeaudette, post id 89igv6) and covered by The Merkle on April 5 ("What Is CryptoPokemons?"). The Reddit thread received 13 comments and closed with a score of 0 (upvote ratio 0.43).
A single trade. The contract's purchase(uint256) function — the hot-potato mechanic at the heart of the game — was successfully invoked exactly once. On March 29, 2018 at 09:14 UTC, the address 0x6002fdbca9da651a3bdf22b4e36fe9bf26c3c8ac (which had been funded with 0.0104 ETH by the deployer two hours earlier) bought Squirtle for 0.01 ETH. No other Pokémon ever changed hands through purchase(), and that one buyer never bought anything else either.
The deployer. 0xc695a3281bf84f53116cd5ddc93457dc88f443b7 is an EOA with nonce 28, no ENS, no public tags, no other deployed contracts, funded by another unlabeled EOA. Active for exactly 32 days (March 28 → April 29, 2018), then dormant. The Reddit announcer u/skylarbeaudette uses "we" in their replies (consistent with a small team) but no cryptographic link between the Reddit account and the on-chain deployer has been established.
Token Information
Key Facts
Description
CryptoPokemons is a "hot-potato" collectible game on Ethereum, deployed late March 2018. It mints exactly 25 ERC-721-draft tokens, one for each of the original Generation-1 starter Pokémon: Bulbasaur (#0), Ivysaur, Venusaur, Charmander, Charmeleon, Charizard, Squirtle, Wartortle, Blastoise, Caterpie, Butterfree, Weedle, Kakuna, Beedrill, Pidgey, Pidgeotto, Metapod, Rattata, Raticate, Spearow, Fearow, Ekans, Arbok, Pikachu, Raichu (#24). Each name is stored on-chain in the token's struct.
The hot-potato mechanic. Anyone can take a Pokémon from its current owner at any time by calling purchase(uint256) and paying the current sellingPrice. The contract keeps a 4.5% dev fee and forwards 95.5% to the previous owner. After every forced sale the price ratchets up: 3.0× while the price is below 0.1 ETH, 2.0× up to 0.4 ETH, 1.25× up to 1.0 ETH, and 1.15× beyond. The starting price for every Pokémon is 0.01 ETH.
Lineage. A fork of the CelebrityToken (CryptoCelebrities) template, with a fourth pricing stage added, a lower 4.5% dev fee, and a per-token txs field exposed via getPokemon(uint256) and the TokenSold event.
Announcement and coverage. Announced on Reddit on April 3, 2018 by u/skylarbeaudette (Reddit "Developer" flair) in r/CryptoCurrency, with the title "CryptoPokemons - new Ethereum blockchain collectible game (child of CryptoKitties)" and a direct link to cryptopokemons.com. The Merkle covered the launch two days later in an article titled "What Is CryptoPokemons?" (April 5, 2018). The original site featured what appear to be original illustrations of the 25 Pokémon characters.
On-chain activity. Almost none. The deployer (0xc695a3281bf84f53116cd5ddc93457dc88f443b7) minted all 25 tokens via 25 calls to createPromoPokemon on March 28–29, 2018. Exactly one purchase() was ever called in the contract's life: Squirtle (#6) on March 29, 2018 at 0.01 ETH, by an address the deployer had funded 0.0104 ETH a few hours earlier. No other token has ever changed hands via the hot-potato mechanism. The deployer made their last on-chain action on April 29, 2018 — 32 days after deployment — and never returned to this contract or deployed another.
Preservation (May 2026). All 25 Pokémon have been acquired and wrapped into WrappedCryptoPokemons at 0x30982eF551EA4583857578f3B28018aEfECf7F9C by cart00n.eth — an ERC-721 wrapper with on-chain JSON metadata, owner-only unwrap for future migration, and a deliberately one-way wrap that retires the hot-potato purchase() flow. The domain cryptopokemons.com has been acquired for restoration; the original site is preserved on the Wayback Machine at https://web.archive.org/web/20181031225133/cryptopokemons.com/.
Source Verified
Reconstructed from CelebrityToken template (CryptoCelebrities fork). Runtime + creation bytecode byte-identical to on-chain except for source metadata swarm hash (partial match in pre-v2 Sourcify terminology). Verified on Sourcify v2 (full match flag).. Optimizer: OFF
Historian Categories
Heuristic Analysis
The following characteristics were detected through bytecode analysis and may not be accurate.
Byzantium Era
First Metropolis hard fork. Added zk-SNARK precompiles, REVERT opcode, and staticcall.
Bytecode Overview
Verified Source Available
Source verified through compiler archaeology and exact bytecode matching.
View Verification ProofShow source code (Solidity)
// Submitted by EthereumHistory (ethereumhistory.com)
pragma solidity ^0.4.18; // solhint-disable-line
/// @title Interface for contracts conforming to ERC-721: Non-Fungible Tokens
/// @author Dieter Shirley <dete@axiomzen.co> (https://github.com/dete)
contract ERC721 {
// Required methods
function approve(address _to, uint256 _tokenId) public;
function balanceOf(address _owner) public view returns (uint256 balance);
function implementsERC721() public pure returns (bool);
function ownerOf(uint256 _tokenId) public view returns (address addr);
function takeOwnership(uint256 _tokenId) public;
function totalSupply() public view returns (uint256 total);
function transferFrom(address _from, address _to, uint256 _tokenId) public;
function transfer(address _to, uint256 _tokenId) public;
event Transfer(address indexed from, address indexed to, uint256 tokenId);
event Approval(address indexed owner, address indexed approved, uint256 tokenId);
// Optional
// function name() public view returns (string name);
// function symbol() public view returns (string symbol);
// function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256 tokenId);
// function tokenMetadata(uint256 _tokenId) public view returns (string infoUrl);
}
contract PokemonToken is ERC721 {
/*** EVENTS ***/
/// @dev The Birth event is fired whenever a new pokemon comes into existence.
event Birth(uint256 tokenId, string name, address owner);
/// @dev The TokenSold event is fired whenever a token is sold.
event TokenSold(uint256 tokenId, uint256 oldPrice, uint256 newPrice, address prevOwner, address winner, string name, uint256 txs);
/// @dev Transfer event as defined in current draft of ERC721.
/// ownership is assigned, including births.
event Transfer(address from, address to, uint256 tokenId);
/*** CONSTANTS ***/
/// @notice Name and symbol of the non fungible token, as defined in ERC721.
string public constant NAME = "CryptoPokemons"; // solhint-disable-line
string public constant SYMBOL = "PokemonToken"; // solhint-disable-line
uint256 private startingPrice = 0.01 ether;
uint256 private firstStepLimit = 0.1 ether;
uint256 private secondStepLimit = 0.4 ether;
uint256 private thirdStepLimit = 1.0 ether;
uint256 private constant PROMO_CREATION_LIMIT = 6000;
/*** STORAGE ***/
/// @dev A mapping from pokemon IDs to the address that owns them. All pokemons have
/// some valid owner address.
mapping (uint256 => address) public pokemonIndexToOwner;
// @dev A mapping from owner address to count of tokens that address owns.
// Used internally inside balanceOf() to resolve ownership count.
mapping (address => uint256) private ownershipTokenCount;
/// @dev A mapping from PokemonIDs to an address that has been approved to call
/// transferFrom(). Each Pokemon can only have one approved address for transfer
/// at any time. A zero value means no approval is outstanding.
mapping (uint256 => address) public pokemonIndexToApproved;
// @dev A mapping from PokemonIDs to the price of the token.
mapping (uint256 => uint256) private pokemonIndexToPrice;
// The addresses of the accounts (or contracts) that can execute actions within each roles.
address public ceoAddress;
address public cooAddress;
uint256 public promoCreatedCount;
/*** DATATYPES ***/
struct Pokemon {
string name;
uint256 txs;
}
Pokemon[] private pokemons;
/*** ACCESS MODIFIERS ***/
/// @dev Access modifier for CEO-only functionality
modifier onlyCEO() {
require(msg.sender == ceoAddress);
_;
}
/// @dev Access modifier for COO-only functionality
modifier onlyCOO() {
require(msg.sender == cooAddress);
_;
}
/// Access modifier for contract owner only functionality
modifier onlyCLevel() {
require(
msg.sender == ceoAddress ||
msg.sender == cooAddress
);
_;
}
/*** CONSTRUCTOR ***/
function PokemonToken() public {
ceoAddress = msg.sender;
cooAddress = msg.sender;
}
/*** PUBLIC FUNCTIONS ***/
/// @notice Grant another address the right to transfer token via takeOwnership() and transferFrom().
/// @param _to The address to be granted transfer approval. Pass address(0) to
/// clear all approvals.
/// @param _tokenId The ID of the Token that can be transferred if this call succeeds.
/// @dev Required for ERC-721 compliance.
function approve(
address _to,
uint256 _tokenId
) public {
// Caller must own token.
require(_owns(msg.sender, _tokenId));
pokemonIndexToApproved[_tokenId] = _to;
Approval(msg.sender, _to, _tokenId);
}
/// For querying balance of a particular account
/// @param _owner The address for balance query
/// @dev Required for ERC-721 compliance.
function balanceOf(address _owner) public view returns (uint256 balance) {
return ownershipTokenCount[_owner];
}
/// @dev Creates a new promo Pokemon with the given name, with given _price and assignes it to an address.
function createPromoPokemon(address _owner, string _name, uint256 _price) public onlyCOO {
require(promoCreatedCount < PROMO_CREATION_LIMIT);
address pokemonOwner = _owner;
if (pokemonOwner == address(0)) {
pokemonOwner = cooAddress;
}
if (_price <= 0) {
_price = startingPrice;
}
promoCreatedCount++;
_createPokemon(_name, pokemonOwner, _price);
}
/// @dev Creates a new Pokemon with the given name.
function createContractPokemon(string _name) public onlyCOO {
_createPokemon(_name, address(this), startingPrice);
}
/// @notice Returns all the relevant information about a specific pokemon.
/// @param _tokenId The tokenId of the pokemon of interest.
function getPokemon(uint256 _tokenId) public view returns (
string pokemonName,
uint256 txs,
uint256 sellingPrice,
address owner
) {
Pokemon storage pokemon = pokemons[_tokenId];
pokemonName = pokemon.name;
txs = pokemon.txs;
sellingPrice = pokemonIndexToPrice[_tokenId];
owner = pokemonIndexToOwner[_tokenId];
}
function implementsERC721() public pure returns (bool) {
return true;
}
/// @dev Required for ERC-721 compliance.
function name() public pure returns (string) {
return NAME;
}
/// For querying owner of token
/// @param _tokenId The tokenID for owner inquiry
/// @dev Required for ERC-721 compliance.
function ownerOf(uint256 _tokenId)
public
view
returns (address owner)
{
owner = pokemonIndexToOwner[_tokenId];
require(owner != address(0));
}
function payout(address _to) public onlyCLevel {
_payout(_to);
}
// Allows someone to send ether and obtain the token
function purchase(uint256 _tokenId) public payable {
address oldOwner = pokemonIndexToOwner[_tokenId];
address newOwner = msg.sender;
uint256 sellingPrice = pokemonIndexToPrice[_tokenId];
// Making sure token owner is not sending to self
require(oldOwner != newOwner);
// Safety check to prevent against an unexpected 0x0 default.
require(_addressNotNull(newOwner));
// Making sure sent amount is greater than or equal to the sellingPrice
require(msg.value >= sellingPrice);
uint256 payment = uint256(SafeMath.div(SafeMath.mul(sellingPrice, 955), 1000));
uint256 purchaseExcess = SafeMath.sub(msg.value, sellingPrice);
// Update prices
if (sellingPrice < firstStepLimit) {
// first stage
pokemonIndexToPrice[_tokenId] = SafeMath.div(SafeMath.mul(sellingPrice, 300), 100);
} else if (sellingPrice < secondStepLimit) {
// second stage
pokemonIndexToPrice[_tokenId] = SafeMath.div(SafeMath.mul(sellingPrice, 200), 100);
} else if (sellingPrice < thirdStepLimit) {
// third stage
pokemonIndexToPrice[_tokenId] = SafeMath.div(SafeMath.mul(sellingPrice, 125), 100);
} else {
// fourth stage
pokemonIndexToPrice[_tokenId] = SafeMath.div(SafeMath.mul(sellingPrice, 115), 100);
}
_transfer(oldOwner, newOwner, _tokenId);
// Pay previous tokenOwner if owner is not contract
if (oldOwner != address(this)) {
oldOwner.transfer(payment); //(1-0.045)
}
pokemons[_tokenId].txs += 1;
TokenSold(_tokenId, sellingPrice, pokemonIndexToPrice[_tokenId], oldOwner, newOwner, pokemons[_tokenId].name, pokemons[_tokenId].txs);
msg.sender.transfer(purchaseExcess);
}
function priceOf(uint256 _tokenId) public view returns (uint256 price) {
return pokemonIndexToPrice[_tokenId];
}
/// @dev Assigns a new address to act as the CEO. Only available to the current CEO.
/// @param _newCEO The address of the new CEO
function setCEO(address _newCEO) public onlyCEO {
require(_newCEO != address(0));
ceoAddress = _newCEO;
}
/// @dev Assigns a new address to act as the COO. Only available to the current COO.
/// @param _newCOO The address of the new COO
function setCOO(address _newCOO) public onlyCEO {
require(_newCOO != address(0));
cooAddress = _newCOO;
}
/// @dev Required for ERC-721 compliance.
function symbol() public pure returns (string) {
return SYMBOL;
}
/// @notice Allow pre-approved user to take ownership of a token
/// @param _tokenId The ID of the Token that can be transferred if this call succeeds.
/// @dev Required for ERC-721 compliance.
function takeOwnership(uint256 _tokenId) public {
address newOwner = msg.sender;
address oldOwner = pokemonIndexToOwner[_tokenId];
// Safety check to prevent against an unexpected 0x0 default.
require(_addressNotNull(newOwner));
// Making sure transfer is approved
require(_approved(newOwner, _tokenId));
_transfer(oldOwner, newOwner, _tokenId);
}
/// @param _owner The owner whose pokemons we are interested in.
/// @dev This method MUST NEVER be called by smart contract code. First, it's fairly
/// expensive (it walks the entire Pokemons array looking for pokemons belonging to owner),
/// but it also returns a dynamic array, which is only supported for web3 calls, and
/// not contract-to-contract calls.
function tokensOfOwner(address _owner) public view returns(uint256[] ownerTokens) {
uint256 tokenCount = balanceOf(_owner);
if (tokenCount == 0) {
// Return an empty array
return new uint256[](0);
} else {
uint256[] memory result = new uint256[](tokenCount);
uint256 totalPokemons = totalSupply();
uint256 resultIndex = 0;
uint256 pokemonId;
for (pokemonId = 0; pokemonId <= totalPokemons; pokemonId++) {
if (pokemonIndexToOwner[pokemonId] == _owner) {
result[resultIndex] = pokemonId;
resultIndex++;
}
}
return result;
}
}
/// For querying totalSupply of token
/// @dev Required for ERC-721 compliance.
function totalSupply() public view returns (uint256 total) {
return pokemons.length;
}
/// Owner initates the transfer of the token to another account
/// @param _to The address for the token to be transferred to.
/// @param _tokenId The ID of the Token that can be transferred if this call succeeds.
/// @dev Required for ERC-721 compliance.
function transfer(
address _to,
uint256 _tokenId
) public {
require(_owns(msg.sender, _tokenId));
require(_addressNotNull(_to));
_transfer(msg.sender, _to, _tokenId);
}
/// Third-party initiates transfer of token from address _from to address _to
/// @param _from The address for the token to be transferred from.
/// @param _to The address for the token to be transferred to.
/// @param _tokenId The ID of the Token that can be transferred if this call succeeds.
/// @dev Required for ERC-721 compliance.
function transferFrom(
address _from,
address _to,
uint256 _tokenId
) public {
require(_owns(_from, _tokenId));
require(_approved(_to, _tokenId));
require(_addressNotNull(_to));
_transfer(_from, _to, _tokenId);
}
/*** PRIVATE FUNCTIONS ***/
/// Safety check on _to address to prevent against an unexpected 0x0 default.
function _addressNotNull(address _to) private pure returns (bool) {
return _to != address(0);
}
/// For checking approval of transfer for address _to
function _approved(address _to, uint256 _tokenId) private view returns (bool) {
return pokemonIndexToApproved[_tokenId] == _to;
}
/// For creating Pokemon
function _createPokemon(string _name, address _owner, uint256 _price) private {
Pokemon memory _pokemon = Pokemon({
name: _name,
txs: 0
});
uint256 newPokemonId = pokemons.push(_pokemon) - 1;
// It's probably never going to happen, 4 billion tokens are A LOT, but
// let's just be 100% sure we never let this happen.
require(newPokemonId == uint256(uint32(newPokemonId)));
Birth(newPokemonId, _name, _owner);
pokemonIndexToPrice[newPokemonId] = _price;
// This will assign ownership, and also emit the Transfer event as
// per ERC721 draft
_transfer(address(0), _owner, newPokemonId);
}
/// Check for token ownership
function _owns(address claimant, uint256 _tokenId) private view returns (bool) {
return claimant == pokemonIndexToOwner[_tokenId];
}
/// For paying out balance on contract
function _payout(address _to) private {
if (_to == address(0)) {
ceoAddress.transfer(this.balance);
} else {
_to.transfer(this.balance);
}
}
/// @dev Assigns ownership of a specific Pokemon to an address.
function _transfer(address _from, address _to, uint256 _tokenId) private {
// Since the number of pokemons is capped to 2^32 we can't overflow this
ownershipTokenCount[_to]++;
//transfer ownership
pokemonIndexToOwner[_tokenId] = _to;
// When creating new pokemons _from is 0x0, but we can't account that address.
if (_from != address(0)) {
ownershipTokenCount[_from]--;
// clear any previously approved ownership exchange
delete pokemonIndexToApproved[_tokenId];
}
// Emit the transfer event.
Transfer(_from, _to, _tokenId);
}
}
library SafeMath {
/**
* @dev Multiplies two numbers, throws on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
/**
* @dev Integer division of two numbers, truncating the quotient.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Substracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
/**
* @dev Adds two numbers, throws on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}