The first post-v0.9 release of Etheria, a fully on-chain hexagonal tile world and one of the earliest NFT projects on Ethereum, deployed October 22, 2015.
Key Facts
Description
Etheria v1.0 (contract name: etheria) was deployed at block 420,373 on October 22, 2015, by the same pseudonymous developer who created the earlier v0.9 prototype. It represented the first public release of the Etheria game following the v0.9 beta, introducing the TileChanged event—the first on-chain event emitted by the Etheria contract system. The contract governed a 33×33 hexagonal grid stored entirely in Ethereum state, in which players could purchase unowned tiles for 1 ETH, farm blocks from their tiles, stack and color blocks, and name their territory.
The v1.0 deployment was compiled with Solidity v0.1.6, one of the earliest public compiler releases. The contract structure included a Tile struct holding ownership, name, status, farming data, and an offer system for secondary market trading—an in-contract marketplace predating modern NFT platforms by years. Ownership was tracked per address rather than via a transferable token standard (ERC-721 would not exist until 2018), making tiles non-fungible assets managed directly by contract state.
Etheria v1.0 is historically significant as one of the very first deployments of a blockchain-native game with persistent, player-owned digital land. The developer later updated the etheria.world site in 2021 to note that Etheria had "ended up being the first true NFT project in history (by more than a year)." The v1.0 contract was quickly followed by v1.1 (block 459,708, October 29) and v1.2 (block 470,957, November 1), as the developer rapidly iterated on the design in the weeks after launch.
Source Verified
Heuristic Analysis
The following characteristics were detected through bytecode analysis and may not be accurate.
Frontier Era
The initial release of Ethereum. A bare-bones implementation for technical users.
Bytecode Overview
Verified Source Available
Source verified on Etherscan.
Show source code (Solidity)
//
contract BlockDefStorage
{
function getOccupies(uint8 which) public returns (int8[24])
{}
function getAttachesto(uint8 which) public returns (int8[48])
{}
}
contract MapElevationRetriever
{
function getElevation(uint8 col, uint8 row) public constant returns (uint8)
{}
}
contract Etheria
{
// change from v0.9 - event added
event TileChanged(uint8 col, uint8 row);//, address owner, string name, string status, uint lastfarm, address[] offerers, uint[] offers, int8[5][] blocks);
uint8 mapsize = 33;
Tile[33][33] tiles;
address creator;
struct Tile
{
address owner;
string name;
string status;
address[] offerers;
uint[] offers;
int8[5][] blocks; //0 = which,1 = blockx,2 = blocky,3 = blockz, 4 = color
uint lastfarm;
int8[3][] occupado; // the only one not reported in the //TileChanged event
}
BlockDefStorage bds;
MapElevationRetriever mer;
function Etheria() {
creator = msg.sender;
bds = BlockDefStorage(0x782bdf7015b71b64f6750796dd087fde32fd6fdc);
mer = MapElevationRetriever(0x68549d7dbb7a956f955ec1263f55494f05972a6b);
}
function getOwner(uint8 col, uint8 row) public constant returns(address)
{
return tiles[col][row].owner; // no harm if col,row are invalid
}
/***
* _ _ ___ ___ ___ _____ _____ _____ ___ _____ _ _ _____
* | \ | | / _ \ | \/ || ___| ___ / ___|_ _/ _ \_ _| | | / ___|
* | \| |/ /_\ \| . . || |__ ( _ ) \ `--. | |/ /_\ \| | | | | \ `--.
* | . ` || _ || |\/| || __| / _ \/\ `--. \ | || _ || | | | | |`--. \
* | |\ || | | || | | || |___ | (_> < /\__/ / | || | | || | | |_| /\__/ /
* \_| \_/\_| |_/\_| |_/\____/ \___/\/ \____/ \_/\_| |_/\_/ \___/\____/
*
*
*/
function getName(uint8 col, uint8 row) public constant returns(string)
{
return tiles[col][row].name; // no harm if col,row are invalid
}
// change from v0.9 - event emission added
function setName(uint8 col, uint8 row, string _n) public
{
if(isOOB(col,row)) // row and/or col was not between 0-mapsize
{
whathappened = 50;
return;
}
Tile tile = tiles[col][row];
if(tile.owner != msg.sender)
{
whathappened = 51;
return;
}
tile.name = _n;
TileChanged(col,row);
whathappened = 52;
return;
}
function getStatus(uint8 col, uint8 row) public constant returns(string)
{
return tiles[col][row].status; // no harm if col,row are invalid
}
// change from v0.9 - incoming money sent to creator, event emissions added
function setStatus(uint8 col, uint8 row, string _s) public // setting status costs 1 eth to prevent spam
{
if(msg.value == 0) // the only situation where we don't refund money.
{
whathappened = 40;
return;
}
if(msg.value != 1000000000000000000)
{
msg.sender.send(msg.value); // return their money
whathappened = 41;
return;
}
if(isOOB(col,row)) // row and/or col was not between 0-mapsize
{
msg.sender.send(msg.value); // return their money
whathappened = 42;
return;
}
Tile tile = tiles[col][row];
if(tile.owner != msg.sender)
{
msg.sender.send(msg.value); // return their money
whathappened = 43;
return;
}
tile.status = _s;
creator.send(msg.value);
TileChanged(col,row);
whathappened = 44;
return;
}
/***
* ______ ___ _________ ________ _ _ _____ ___________ _____ _____ _____ _ _ _____
* | ___/ _ \ | ___ \ \/ |_ _| \ | | __ \ ___ | ___| _ \_ _|_ _|_ _| \ | | __ \
* | |_ / /_\ \| |_/ / . . | | | | \| | | \/ ( _ ) | |__ | | | | | | | | | | | \| | | \/
* | _|| _ || /| |\/| | | | | . ` | | __ / _ \/\ | __|| | | | | | | | | | | . ` | | __
* | | | | | || |\ \| | | |_| |_| |\ | |_\ \ | (_> < | |___| |/ / _| |_ | | _| |_| |\ | |_\ \
* \_| \_| |_/\_| \_\_| |_/\___/\_| \_/\____/ \___/\/ \____/|___/ \___/ \_/ \___/\_| \_/\____/
*
*/
// change from v0.9 - getLastFarm added
function getLastFarm(uint8 col, uint8 row) public constant returns (uint)
{
return tiles[col][row].lastfarm;
}
// changes from v0.9
// added ability to pay to farm more often
// added event emission
// first block farmed will always be a column, rest are randomized
function farmTile(uint8 col, uint8 row) public
{
if(isOOB(col,row)) // row and/or col was not between 0-mapsize
{
whathappened = 30;
return;
}
Tile tile = tiles[col][row];
if(tile.owner != msg.sender)
{
whathappened = 31;
return;
}
if((block.number - tile.lastfarm) < 2500) // ~12 hours of blocks
{
if(msg.value == 0)
{
whathappened = 32;
return;
}
else if(msg.value != 1000000000000000000)
{
msg.sender.send(msg.value); // return their money
whathappened = 34;
return;
}
else // they paid 1 ETH
{
creator.send(msg.value);
}
// If they haven't waited long enough, but they've paid 1 eth, let them farm again.
}
else
{
if(msg.value > 0) // they've waited long enough but also sent money. Return it and continue normally.
{
msg.sender.send(msg.value); // return their money
}
}
// by this point, they've either waited 2500 blocks or paid 1 ETH
bytes32 lastblockhash = block.blockhash(block.number - 1);
for(uint8 i = 0; i < 20; i++)
{
tile.blocks.length+=1;
if(tile.blocks.length == 1) // The VERY FIRST block, ever for this tile
tile.blocks[tile.blocks.length - 1][0] = 0; // make it a column for easy testing and tutorial
else
tile.blocks[tile.blocks.length - 1][0] = int8(getUint8FromByte32(lastblockhash,i) % 32); // which, guaranteed 0-31
tile.blocks[tile.blocks.length - 1][1] = 0; // x
tile.blocks[tile.blocks.length - 1][2] = 0; // y
tile.blocks[tile.blocks.length - 1][3] = -1; // z
tile.blocks[tile.blocks.length - 1][4] = 0; // color
}
tile.lastfarm = block.number;
TileChanged(col,row);
whathappened = 33;
return;
}
// change from v0.9 - event emission added
function editBlock(uint8 col, uint8 row, uint index, int8[5] _block)
{
if(isOOB(col,row)) // row and/or col was not between 0-mapsize
{
whathappened = 20;
return;
}
Tile tile = tiles[col][row];
if(tile.owner != msg.sender) // 1. DID THE OWNER SEND THIS MESSAGE?
{
whathappened = 21;
return;
}
if(_block[3] < 0) // 2. IS THE Z LOCATION OF THE BLOCK BELOW ZERO? BLOCKS CANNOT BE HIDDEN
{
whathappened = 22;
return;
}
_block[0] = tile.blocks[index][0]; // can't change the which, so set it to whatever it already was
int8[24] memory didoccupy = bds.getOccupies(uint8(_block[0]));
int8[24] memory wouldoccupy = bds.getOccupies(uint8(_block[0]));
for(uint8 b = 0; b < 24; b+=3) // always 8 hexes, calculate the didoccupy
{
wouldoccupy[b] = wouldoccupy[b]+_block[1];
wouldoccupy[b+1] = wouldoccupy[b+1]+_block[2];
if(wouldoccupy[1] % 2 != 0 && wouldoccupy[b+1] % 2 == 0) // if anchor y is odd and this hex y is even, (SW NE beam goes 11,`2`2,23,`3`4,35,`4`6,47,`5`8 ` = x value incremented by 1. Same applies to SW NE beam from 01,12,13,24,25,36,37,48)
wouldoccupy[b] = wouldoccupy[b]+1; // then offset x by +1
wouldoccupy[b+2] = wouldoccupy[b+2]+_block[3];
didoccupy[b] = didoccupy[b]+tile.blocks[index][1];
didoccupy[b+1] = didoccupy[b+1]+tile.blocks[index][2];
if(didoccupy[1] % 2 != 0 && didoccupy[b+1] % 2 == 0) // if anchor y and this hex y are both odd,
didoccupy[b] = didoccupy[b]+1; // then offset x by +1
didoccupy[b+2] = didoccupy[b+2]+tile.blocks[index][3];
}
if(!isValidLocation(col,row,_block, wouldoccupy))
{
return; // whathappened is already set
}
// EVERYTHING CHECKED OUT, WRITE OR OVERWRITE THE HEXES IN OCCUPADO
if(tile.blocks[index][3] >= 0) // If the previous z was greater than 0 (i.e. not hidden) ...
{
for(uint8 l = 0; l < 24; l+=3) // loop 8 times,find the previous occupado entries and overwrite them
{
for(uint o = 0; o < tile.occupado.length; o++)
{
if(didoccupy[l] == tile.occupado[o][0] && didoccupy[l+1] == tile.occupado[o][1] && didoccupy[l+2] == tile.occupado[o][2]) // x,y,z equal?
{
tile.occupado[o][0] = wouldoccupy[l]; // found it. Overwrite it
tile.occupado[o][1] = wouldoccupy[l+1];
tile.occupado[o][2] = wouldoccupy[l+2];
}
}
}
}
else // previous block was hidden
{
for(uint8 ll = 0; ll < 24; ll+=3) // add the 8 new hexes to occupado
{
tile.occupado.length++;
tile.occupado[tile.occupado.length-1][0] = wouldoccupy[ll];
tile.occupado[tile.occupado.length-1][1] = wouldoccupy[ll+1];
tile.occupado[tile.occupado.length-1][2] = wouldoccupy[ll+2];
}
}
tile.blocks[index] = _block;
TileChanged(col,row);
return;
}
function getBlocks(uint8 col, uint8 row) public constant returns (int8[5][])
{
return tiles[col][row].blocks; // no harm if col,row are invalid
}
/***
* _________________ ___________ _____
* | _ | ___| ___| ___| ___ \/ ___|
* | | | | |_ | |_ | |__ | |_/ /\ `--.
* | | | | _| | _| | __|| / `--. \
* \ \_/ / | | | | |___| |\ \ /\__/ /
* \___/\_| \_| \____/\_| \_|\____/
*
*/
// change from v0.9 - event emission added and .push() used instead of array.length++ notation
function makeOffer(uint8 col, uint8 row)
{
if(msg.value == 0) // checking this first means that we will ALWAYS need to return money on any other failure
{
whathappened = 1;
return;
} // do nothing, just return
if(isOOB(col,row)) // row and/or col was not between 0-mapsize
{
whathappened = 2;
msg.sender.send(msg.value); // return their money
return;
}
Tile tile = tiles[col][row];
if(tile.owner == address(0x0000000000000000000000000000000000000000)) // if UNOWNED
{
if(msg.value != 1000000000000000000 || mer.getElevation(col,row) < 125) // 1 ETH is the starting value. If not return; // Also, if below sea level, return.
{
msg.sender.send(msg.value); // return their money
whathappened = 3;
return;
}
else
{
creator.send(msg.value); // this was a valid offer, send money to contract creator
tile.owner = msg.sender; // set tile owner to the buyer
TileChanged(col,row);
whathappened = 4;
return;
}
}
else // if already OWNED
{
if(tile.owner == msg.sender || msg.value < 10000000000000000 || msg.value > 1000000000000000000000000 || tile.offerers.length >= 10 ) // trying to make an offer on their own tile. or the offer list is full (10 max) or the value is out of range (.01 ETH - 1 mil ETH is range)
{
msg.sender.send(msg.value); // return the money
whathappened = 5;
return;
}
else
{
for(uint8 i = 0; i < tile.offerers.length; i++)
{
if(tile.offerers[i] == msg.sender) // user has already made an offer. Update it and return;
{
msg.sender.send(tile.offers[i]); // return their previous money
tile.offers[i] = msg.value; // set the new offer
TileChanged(col,row);
whathappened = 6;
return;
}
}
// the user has not yet made an offer
tile.offerers.push(msg.sender); // make room for 1 more
tile.offers.push(msg.value); // make room for 1 more
TileChanged(col,row);
whathappened = 7;
return;
}
}
}
// change from v0.9 - deleteOffer created to combine retractOffer, rejectOffer, and removeOffer
function deleteOffer(uint8 col, uint8 row, uint8 i, uint amt) // index 0-10
{
if(isOOB(col,row)) // row and/or col was not between 0-mapsize
{
whathappened = 70;
return;
}
Tile tile = tiles[col][row];
if(i < 0 || i > (tile.offers.length - 1)) // index oob
{
whathappened = 72;
return;
}
if(tile.owner == msg.sender || tile.offerers[i] == msg.sender) // If this is the tile owner OR the offerer of the offer at index i, they can delete the request.
{
if(amt != tile.offers[i]) // to prevent conflicts of offers and deletions by multiple parties, amt must be specified and match
{
whathappened = 74;
return;
}
//removeOffer(col,row,i);
tile.offerers[i].send(tile.offers[i]); // return the money
delete tile.offerers[i]; // zero out user
delete tile.offers[i]; // zero out offer
for(uint8 j = i+1; j < tile.offerers.length; j++) // reshape arrays after deletion
{
tile.offerers[j-1] = tile.offerers[j];
tile.offers[j-1] = tile.offers[j];
}
tile.offerers.length--;
tile.offers.length--;
// end removeOffer
TileChanged(col,row);
whathappened = 73;
return;
}
else // permission to delete denied
{
whathappened = 71;
return;
}
}
// change from v0.9 - added amt check and event emission
function acceptOffer(uint8 col, uint8 row, uint8 i, uint amt) // accepts the offer at index (1-10)
{
if(isOOB(col,row)) // row and/or col was not between 0-mapsize
{
whathappened = 80;
return;
}
Tile tile = tiles[col][row];
if(tile.owner != msg.sender) // only the owner can reject offers
{
whathappened = 81;
return;
}
if(i < 0 || i > (tile.offers.length - 1)) // index oob
{
whathappened = 82;
return;
}
uint offeramount = tile.offers[i];
if(amt != offeramount) // to prevent conflicts of offers and deletions by multiple parties, amt must be specified and match
{
whathappened = 84;
return;
}
uint housecut = offeramount / 10;
creator.send(housecut);
tile.owner.send(offeramount-housecut); // send offer money to oldowner
tile.owner = tile.offerers[i]; // new owner is the offerer
for(uint8 j = 0; j < tile.offerers.length; j++) // return all the other offerers' offer money
{
if(j != i) // don't return money for the purchaser
tile.offerers[j].send(tile.offers[j]);
}
delete tile.offerers; // delete all offerers
delete tile.offers; // delete all offers
TileChanged(col,row);
whathappened = 83;
return;
}
function getOfferers(uint8 col, uint8 row) constant returns (address[])
{
return tiles[col][row].offerers; // no harm if col,row are invalid
}
function getOffers(uint8 col, uint8 row) constant returns (uint[])
{
return tiles[col][row].offers; // no harm if col,row are invalid
}
function isOOB(uint8 col, uint8 row) private constant returns (bool)
{
if(col < 0 || col > (mapsize-1) || row < 0 || row > (mapsize-1))
return true; // is out of bounds
}
/***
* _ _ _____ _____ _ _____ _______ __
* | | | |_ _|_ _| | |_ _|_ _\ \ / /
* | | | | | | | | | | | | | | \ V /
* | | | | | | | | | | | | | | \ /
* | |_| | | | _| |_| |_____| |_ | | | |
* \___/ \_/ \___/\_____/\___/ \_/ \_/
*
*/
// changed from v0.9, but unused, irrelevant
function blockHexCoordsValid(int8 x, int8 y) private constant returns (bool)
{
uint8 absx;
uint8 absy;
if(x < 0)
absx = uint8(x*-1);
else
absx = uint8(x);
if(y < 0)
absy = uint8(y*-1);
else
absy = uint8(y);
if(absy <= 33) // middle rectangle
{
if(y % 2 != 0 ) // odd
{
if(-50 <= x && x <= 49)
return true;
}
else // even
{
if(absx <= 49)
return true;
}
}
else
{
if((y >= 0 && x >= 0) || (y < 0 && x > 0)) // first or 4th quadrants
{
if(y % 2 != 0 ) // odd
{
if (((absx*2) + (absy*3)) <= 198)
return true;
}
else // even
{
if ((((absx+1)*2) + ((absy-1)*3)) <= 198)
return true;
}
}
else
{
if(y % 2 == 0 ) // even
{
if (((absx*2) + (absy*3)) <= 198)
return true;
}
else // odd
{
if ((((absx+1)*2) + ((absy-1)*3)) <= 198)
return true;
}
}
}
return false;
}
// changed from v0.9, but unused, irrelevant
function isValidLocation(uint8 col, uint8 row, int8[5] _block, int8[24] wouldoccupy) private constant returns (bool)
{
bool touches;
Tile tile = tiles[col][row]; // since this is a private method, we don't need to check col,row validity
for(uint8 b = 0; b < 24; b+=3) // always 8 hexes, calculate the wouldoccupy and the didoccupy
{
if(!blockHexCoordsValid(wouldoccupy[b], wouldoccupy[b+1])) // 3. DO ANY OF THE PROPOSED HEXES FALL OUTSIDE OF THE TILE?
{
whathappened = 10;
return false;
}
for(uint o = 0; o < tile.occupado.length; o++) // 4. DO ANY OF THE PROPOSED HEXES CONFLICT WITH ENTRIES IN OCCUPADO?
{
if(wouldoccupy[b] == tile.occupado[o][0] && wouldoccupy[b+1] == tile.occupado[o][1] && wouldoccupy[b+2] == tile.occupado[o][2]) // do the x,y,z entries of each match?
{
whathappened = 11;
return false; // this hex conflicts. The proposed block does not avoid overlap. Return false immediately.
}
}
if(touches == false && wouldoccupy[b+2] == 0) // 5. DO ANY OF THE BLOCKS TOUCH ANOTHER? (GROUND ONLY FOR NOW)
{
touches = true; // once true, always true til the end of this method. We must keep looping to check all the hexes for conflicts and tile boundaries, though, so we can't return true here.
}
}
// now if we're out of the loop and here, there were no conflicts and the block was found to be in the tile boundary.
// touches may be true or false, so we need to check
if(touches == false) // 6. NONE OF THE OCCUPY BLOCKS TOUCHED THE GROUND. BUT MAYBE THEY TOUCH ANOTHER BLOCK?
{
int8[48] memory attachesto = bds.getAttachesto(uint8(_block[0]));
for(uint8 a = 0; a < 48 && !touches; a+=3) // always 8 hexes, calculate the wouldoccupy and the didoccupy
{
if(attachesto[a] == 0 && attachesto[a+1] == 0 && attachesto[a+2] == 0) // there are no more attachestos available, break (0,0,0 signifies end)
break;
//attachesto[a] = attachesto[a]+_block[1];
attachesto[a+1] = attachesto[a+1]+_block[2];
if(attachesto[1] % 2 != 0 && attachesto[a+1] % 2 == 0) // (for attachesto, anchory is the same as for occupies, but the z is different. Nothing to worry about)
attachesto[a] = attachesto[a]+1; // then offset x by +1
//attachesto[a+2] = attachesto[a+2]+_block[3];
for(o = 0; o < tile.occupado.length && !touches; o++)
{
if((attachesto[a]+_block[1]) == tile.occupado[o][0] && attachesto[a+1] == tile.occupado[o][1] && (attachesto[a+2]+_block[3]) == tile.occupado[o][2]) // a valid attachesto found in occupado?
{
whathappened = 12;
return true; // in bounds, didn't conflict and now touches is true. All good. Return.
}
}
}
whathappened = 13;
return false;
}
else // touches was true by virtue of a z = 0 above (touching the ground). Return true;
{
whathappened = 14;
return true;
}
}
// changed from v0.9, but unused, irrelevant
function getUint8FromByte32(bytes32 _b32, uint8 byteindex) public constant returns(uint8) {
uint numdigits = 64;
uint base = 16;
uint digitsperbyte = 2;
uint buint = uint(_b32);
//uint upperpowervar = 16 ** (numdigits - (byteindex*2)); // @i=0 upperpowervar=16**64 (SEE EXCEPTION BELOW), @i=1 upperpowervar=16**62, @i upperpowervar=16**60
uint lowerpowervar = base ** (numdigits - digitsperbyte - (byteindex*digitsperbyte)); // @i=0 upperpowervar=16**62, @i=1 upperpowervar=16**60, @i upperpowervar=16**58
uint postheadchop;
if(byteindex == 0)
postheadchop = buint; //for byteindex 0, buint is just the input number. 16^64 is out of uint range, so this exception has to be made.
else
postheadchop = buint % (base ** (numdigits - (byteindex*digitsperbyte))); // @i=0 _b32=a1b2c3d4... postheadchop=a1b2c3d4, @i=1 postheadchop=b2c3d4, @i=2 postheadchop=c3d4
return uint8((postheadchop - (postheadchop % lowerpowervar)) / lowerpowervar);
}
uint8 whathappened;
function getWhatHappened() public constant returns (uint8)
{
return whathappened;
}
/**********
Standard lock-kill methods // added from v0.9
**********/
bool locked;
function setLocked()
{
locked = true;
}
function getLocked() public constant returns (bool)
{
return locked;
}
function kill()
{
if (!locked && msg.sender == creator)
suicide(creator); // kills this contract and sends remaining funds back to creator
}
}
//