ChainFaces
ChainFaces are on-chain generative ASCII text faces. The smart contract generator produced faces upon minting by using randomness in previous block hashes. All 10000 faces were minted within the first week and are now only available through the secondary market. The project was launched on January 19, 2020.
Even if you missed all the fun/drama, you can relive it by listening to the podcast we did with the NonFungerbils.
Join our discord
ChainFaces are a generative art experiment, designed to be fully retrievable forever from their smart contract which runs on Ethereum. The ASCII text face, as well as the 3 core stats (golf score, face symmetry, percent bear) are all mapped to their respective NFT's token ID. The text color and background of the ChainFaces scale relative with the Golf Score and the Percent Bear stats and are stored as statistics on the contract. ChainFaces was one of the first projects to achieve fully on-chain status for NFTs.
ChainFaces was heavily inspired by some predecessors, CryptoPunks and Autoglyphs, as well as a project that was still in development, Avastars. While CryptoPunks brought collectible, non-fungible tokens to Ethereum, Autoglyphs pioneered the idea of an NFT that was immutable and lived, at least in a sense, fully on-chain. Avastars used this concept and began working towards an NFT that lived on-chain in its full form, as an SVG.
Now that the ChainFaces contract lives on Ethereum, it is important to mention that the generation code can not be changed, and ownership lives solely in their respective owners' hands. These collectibles are 100% immutable and can only be exchanged if their owner chooses to part with them.
Technical Details
ChainFaces conform to the ERC-721 token standard, which was originally defined by another project we are a huge fan of: CryptoKitties. While CryptoKitties were the first exposure to NFTs for most, a surprisingly high % of collectors did not realize that the art for CKs do not actually live on-chain. This is partially due to limitations and technical considerations when designing on Ethereum, but it is one of the reason why we have been pushing the technical boundaries in order to create NFTs that are everlasting. ChainFaces will live on as long as Ethereum.
If you are curious how this works, you can go to here and examine some of the read functions. Below is an example of metadata stats being read from the contract for the lowest authentic golf score face minted: ChainFace ID 4205.
Get Your Own ChainFace
Each ChainFace was created for it's purchaser upon minting by the algorithm embedded in the Ethereum blockchain. The smart contract had a hard cap of 10000 ChainFaces.
0.006 - 0.014Ξ
10000
0
All 10000 ChainFaces have now been created, so to acquire one you will need to visit the secondary market.
View secondary market on Opensea View smart contractFace Generation
Below is the generation function which was used to piece together a minter's ChainFace. Each facial characteristic had it's own rarity scheme. Traits on the left hand side of the character array were the rarest and most difficult to mint, and rarity decreased as the traits moved out to the right hand side. Code has been slightly modified for easier readability.
function createFace(uint256 seed) public payable {
require(totalSupply() <= tokenLimit, "ChainFaces sale has completed. They are now only available on the secondary market. ");
string[8] memory leftFaceCharacters = [ "ʕ" , "✿" ,"꒰" , ":" , "{" , "|" , "[" , "(" ];
string[13] memory eyeCharacters = ["◕" , "👁" , "ಥ" , "♥" , "ʘ̚", "X", "⊙" , "˘" , "ಠ" , "◉" , "⚆" , "¬" , "^" ];
string[11] memory mouthCharacters = [ "ᴥ" , "益" , "෴" , "ʖ" , "ᆺ" , "." , "o", "◡" , "_" , "╭╮" , "‿" ];
string[8] memory rightFaceCharacters = [ "ʔ" , "✿" ,"꒱" , ":" , "}" , "|" , "]" , ")" ];
uint256 leftFacePartID = getLeftFace(seed + totalSupply());
uint256 leftEyePartID = getLeftEye(seed + totalSupply());
uint256 mouthPartID = getMouth(seed + totalSupply());
uint256 rightEyePartID = getRightEye(seed + totalSupply());
uint256 rightFacePartID = getRightFace(seed + totalSupply());
string memory faceAssembly = string(abi.encodePacked(leftFaceCharacters[leftFacePartID], eyeCharacters[leftEyePartID], mouthCharacters[mouthPartID],
eyeCharacters[rightEyePartID], rightFaceCharacters[rightFacePartID]));
uint256 percentBear;
uint256 faceSymmetry;
if (totalSupply() == 77) {
faceAssembly = "ฅ^•ﻌ•^ฅ";
}
if (totalSupply() == 80) {
faceAssembly = "( ● Y ● )";
}
bytes32[] memory pool = Random.initLatest(3, seed);
percentBear = uint256(Random.uniform(pool, 0, 20));
if (leftFacePartID == 0) {
percentBear = percentBear + 16;
}
if (leftEyePartID == 0) {
percentBear = percentBear + 16;
}
if (mouthPartID == 0) {
percentBear = percentBear + 16;
}
if (rightEyePartID == 0) {
percentBear = percentBear + 16;
}
if (rightFacePartID == 0) {
percentBear = percentBear + 16;
}
if (leftFacePartID == rightFacePartID) {
faceSymmetry = faceSymmetry + 50;
}
if (leftEyePartID == rightEyePartID) {
faceSymmetry = faceSymmetry + 50;
}
uint256 faceGolfScore = leftFacePartID + leftEyePartID + mouthPartID + rightEyePartID + rightFacePartID;
uint256 textColor = (8*(30-faceGolfScore))+(256*(8*(30-faceGolfScore)));
uint256 backgroundColor = (2*(100-percentBear))+(256*(2*(100-percentBear)+20))+(65536*(2*(100-percentBear)+56));
if (totalSupply() == 77) {
backgroundColor = 0;
}
if (totalSupply() == 80) {
backgroundColor = 0;
}
_registerToken(faceAssembly, faceGolfScore, percentBear, faceSymmetry, textColor, backgroundColor);
}
Character Selection
A random number between 1 and 70 was selected and used to determine the array positioning of the left face part. The odds of pulling the "bear" piece for the left face were 3 in 70.
function getLeftFace(uint256 seed) internal view returns (uint256 leftFacePartID){
bytes32[] memory pool = Random.initLatest(4, seed);
uint256 leftFaceRNG = uint256(Random.uniform(pool, 1, 70));
if (leftFaceRNG <= 1) {
leftFacePartID = 0;
} else if (leftFaceRNG <= 3) {
leftFacePartID = 1;
} else if (leftFaceRNG <= 6) {
leftFacePartID = 2;
} else if (leftFaceRNG <= 13) {
leftFacePartID = 3;
} else if (leftFaceRNG <= 25) {
leftFacePartID = 4;
} else if (leftFaceRNG <= 40) {
leftFacePartID = 5;
} else if (leftFaceRNG <= 55) {
leftFacePartID = 6;
} else {
leftFacePartID = 7;
}
}