Pages

Minting compliments on the blockchain : Storing NFT metadata onchain






Minting compliments on the Blockchain

shloot.sol is a smart contract which mints unique Shakespearean compliment SVGs as NFT. The images are randomly generated from a list of popular words found in Shakespeare's work.

The contract is deployed on Polygon mainnet



How to use

Simply call the claim function with an id that's greater than 69. (the first 69 compliments are reserved for the contract deployer).

The NFTs are free to mint, the user only has to pay gas fees.


How it works

In this post we will see how to store NFT SVGs and metadata entirely onchain. 

Most of the NFTs minted today store their Metadata on IPFS. This is easy and convenient but comes with some 3rd party risk. 

IPFS is a peer-to-peer (p2p) storage network. Content is accessible through peers located anywhere in the world, that might relay information, store it, or do both. IPFS knows how to find what you ask for using its content address rather than its location.

IPFS allows users to pin files to a server and as long as the files are pinned by atleast one IPFS node the files are available online. As you can see this requires the users to trust in the IPFS pinning network's cryptoeconomics. Blockchain is all about getting things done in a trustless manner, so we will explore an alternative.


Any NFT marketplace like Opensea have a smart contract requirement for listing and displaying NFTs. Here's the complete list https://docs.opensea.io/docs/1-structuring-your-smart-contract But the main requirement for these NFT’s is that your smart contract contains a tokenURI() method which returns the url containing your metadata. Usually NFT smart contract return a ipfs hash containing the NFT metadata here, for example


{
"name": "Herbie Starbelly",
"description": "Friendly OpenSea Creature that enjoys long swims in the ocean.",
"image": "ipfs://bafybeict2kq6gt4ikgulypt7h7nwj4hmfi2kevrqvnx2osibfulyy5x3hu/no-time-to-explain.jpeg"
"attributes": [...]
}


The metadata itself is hosted on ipfs as well as the image.

There are a few key features that define an NFT, regardless of platform.

First, each token has a unique id that distinguishes it from all other tokens. This is in contrast to a fungible token like Ether ETH, which exists as a quantity attached to an account or wallet. There is no way to tell one ETH from another. Because each NFT is unique, they're owned and traded individually, with the smart contract keeping track of who owns what.

Another key feature of an NFT is the ability to link to data that is stored outside of a smart contract. Storing or processing data outside of a smart-contract is known as being off-chain. Because data that's stored on-chain needs to be processed, verified, and replicated across the entire blockchain network, it can be very expensive to store large amounts of data. 

But if the image is simple enough and not huge in size there are ways to return the json object containing the SVG from the tokenURI function.

shloot.sol 's tokenURI function returns the NFT data as follows

function tokenURI(uint256 id) override public view returns (string memory) {
require(_exists(id), "not exist");
string[8] memory parts;
parts[0] = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet"
         viewBox="0 0 350 350"><style>
        .base { fill: black; font-family: serif; font-size: 17px; }</style>
        <rect width="100%" height="100%" fill="white" /><text x="10" y="20" class="base">';

parts[1] = 'Thou </text><text x="10" y="40" class="base">';
parts[2] = adjective[id%40];

parts[3] = '</text><text x="10" y="60" class="base">';

parts[4] = participle[id%39 +1];

parts[5] = '</text><text x="10" y="80" class="base">';

parts[6] = noun[id%38 +2];

parts[7] = '!</text></svg>';

string memory output = string(abi.encodePacked(parts[0], parts[1], parts[2],
        parts[3], parts[4], parts[5], parts[6], parts[7]));
string memory json = Base64.encode(bytes(string(
        abi.encodePacked('{"name": "Compliment #', toString(id), '",
        "description": "Rare compliments generator based on the work of William Shakespeare",
        "image": "data:image/svg+xml;base64,', Base64.encode(bytes(output)), '"}'))));
output = string(abi.encodePacked('data:application/json;base64,', json));
return output;
}

Here the link to the complete code of the smart contract. Link to sourcecode 


Alternatively, you may also try out the project on rinkeby network 

OpenSea : https://testnets.opensea.io/collection/the-shakespearean-compliments-generator

SmartContract: https://rinkeby.etherscan.io/address/0x6571ce37DcF0b78408686412e103432a2c134ED3#writeContract



This project is inspired by the loot project, read more about it here https://blog.coinbase.com/loot-project-the-first-community-owned-nft-gaming-platform-125fa1d5ffa8


Dark version



Solidity in depth series part #1

 

Introduction

Ethereum is a decentralized, open-source blockchain with smart contract functionality. Ether is the native cryptocurrency of the platform. After Bitcoin, it is the largest cryptocurrency by market capitalization.Ethereum has its own programming language, called Solidity. As a blockchain network, Ethereum is a decentralized public ledger for verifying and recording transactions. 


Basics of Solidity and Ethereum


Variables and Math operations

Solidity is a statically typed language, which means that variable's datatype must be specified upon declaration.

There are 3 types of variables in Solidity

  1. State Variables : values permanently stored in contract storage
  2. Local Variables : values stored in memory, impermanently. Declared inside a function
  3. Global Variables : special variables which exists in global namespace


Global Variables in Solidity

blockhash(uint blockNumber) returns (bytes32) : Hash of the given block - only works for 256 most recent, excluding current, blocks

block.coinbase (address payable): Current block miner's address

block.difficulty (uint): Current block difficulty

block.gaslimit (uint): Current block gaslimit

block.number (uint): Current block number

block.timestamp (uint): Current block timestamp as seconds since unix epoch

gasleft() returns (uint256): Remaining gas

msg.data (bytes calldata): Complete calldata

msg.sender (address payable): Sender of the message (current caller)

msg.sig (bytes4): First four bytes of the calldata (function identifier)

msg.value (uint): Number of wei sent with the message

now (uint): Current block timestamp

tx.gasprice (uint): Gas price of the transaction

tx.origin (address payable): Sender of the transaction


Storage vs Memory

In solidity there are two locations to store variables, in storage and in memory. Memory in Solidity is a temporary place to store data whereas Storage holds data between function calls. The Solidity Smart Contract can use any amount of memory during the execution but once the execution stops, the Memory is completely wiped off for the next execution. Whereas Storage on the other hand is persistent, each execution of the Smart contract has access to the data previously stored on the storage area.

It is always better to use Memory for intermediate calculations and store the final result in Storage.

  • State variables and Local Variables of structs, array are always stored in storage by default.
  • Function arguments are in memory.
  • Whenever a new instance of an array is created using the keyword ‘memory’, a new copy of that variable is created. Changing the array value of the new instance does not affect the original array.


Storing time in solidity

Solidity provides some native units for dealing with time.

The variable now will return the current unix timestamp of the latest block (the number of seconds that have passed since January 1st 1970).

Solidity also contains the time units seconds, minutes, hours, days, weeks and years. These will convert to a uint of the number of seconds in that length of time. So 1 minutes is 60, 1 hours is 3600 (60 seconds x 60 minutes), 1 days is 86400 (24 hours x 60 minutes x 60 seconds), etc.

Here's an example of how these time units can be useful:

uint lastUpdated;

// Set `lastUpdated` to `now`
function updateTimestamp() public {
lastUpdated = now;
}

// Will return `true` if 5 minutes have passed since `updateTimestamp` was
// called, `false` if 5 minutes have not passed
function fiveMinutesHavePassed() public view returns (bool) {
return (now >= (lastUpdated + 5 minutes));
}


Variable conversions in solidity

... 


Exercise:

What will be the value in the numbers array after this code has run once?

contract helloGeeks
{
int[] public numbers;
function Numbers() public
{
numbers.push(1);
numbers.push(2);
int[] memory myArray = numbers;

myArray[0] = 0;
}
}

Write a function for generating random number in solidity using global variables


Data Structures


Structs

Array 

dynamic and fixed, public array gets their own getter functions

Array of structs


add to array


uint[] public numbers;

numbers.push(12)


getting length of an array

uint length_of_array = numbers.push(99)


Mappings

key-value store for storing and retrieving data

mapping (address => uint) luckyNumber;



function setMyNumber(uint _myNumber) public {

luckyNumber[msg.sender] = _myNumber;

}



function myNumberIs() public view returns (uint) {

// Retrieve the value stored in the sender's address

// Will be `0` if the sender hasn't called `setMyNumber` yet

return luckyNumber[msg.sender];

}


When to use an array and when to use a mapping ?

Use an array to iterate over objects and use a mapping for quick lookups.


Addresses in Ethereum

The Ethereum blockchain is made up of accounts, which you can think of like bank accounts. An account has a balance of Ether (the currency used on the Ethereum blockchain), and you can send and receive Ether payments to other accounts, just like your bank account can wire transfer money to other bank accounts.


Each account has an address, which you can think of like a bank account number. It's a unique identifier that points to that account, and it looks like this:


retrieving address inside a function using a global variable msg.sender



Functions


public and private


Internal and External

internal is the same as private, except that it's also accessible to contracts that inherit from this contract. 

external is similar to public, except that these functions can ONLY be called outside the contract — they can't be called by other functions inside that contract.


Pure and View

Pass arguments by value vs passing reference


Constructors

Special function that has the same name as the contract. It will get executed only one time, when the contract is first created.


Function Modifiers:

Modifiers are kind of half-functions that are used to modify other functions, usually to check some requirements prior to execution.


<modifier example>



Working with Events

//declare the event

event AdditionEvent(uint a, uint b, uint sum);

//trigger the event

function add(uint _a, uint _b) public returns(uint){
uint sum = _a + _b;
// fire the event
emit AdditionEvent(_a, _b, sum);
return sum;
}

//listen to the event
MyContract.AdditionEvent(function(error, result){
console.log(result);
});


event Transfer(address indexed from, address indexed to, uint256 value);

Q What does the "indexed" keyword do in the below line of code? I'm guessing it just tells the event object that the following input should be logged?

The indexed parameters for logged events will allow you to search for these events using the indexed parameters as filters.


Q Can we use it other places ie outside of events?

The indexed keyword is only relevant to logged events.


Require in solidity

Revert in solidity



Working with web3.js


Exercise : Write a function to compare two strings in solidity.

function sayHiToVitalik(string memory _name) public returns (string memory) {
  // Compares if _name equals "Vitalik". Throws an error and exits if not true.
  // (Side note: Solidity doesn't have native string comparison, so we
  // compare their keccak256 hashes to see if the strings are equal)
  require(keccak256(abi.encodePacked(_name)) == keccak256(abi.encodePacked("Vitalik")));
  // If it's true, proceed with the function:
  return "Hi!";
}


Contracts

Inheritance

contract Groot {
function catchphrase() public returns (string memory) {
return "I am groot";
}
}

contract BabyGroot is Doge {
function anotherCatchphrase() public returns (string memory) {
return "I am smol groot";
}
}


Multifile Inheritance

import "./Groot.sol";

contract BabyGroot is Groot {
    ...
}


Interacting with another contracts by declaring an interface

// sample contract luckynumber.sol
contract LuckyNumber {
mapping(address => uint) numbers;

function setNum(uint _num) public {
numbers[msg.sender] = _num;
}

function getNum(address _myAddress) public view returns (uint) {
return numbers[_myAddress];
}
}

// declaring inheritance in another contract MyContract.sol
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}

// using the above declared interface
contract MyContract {
address NumberInterfaceAddress = 0xab38...
// ^ The address of the FavoriteNumber contract on Ethereum
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// Now `numberContract` is pointing to the other contract

function someFunction() public {
// Now we can call `getNum` from that contract:
uint num = numberContract.getNum(msg.sender);
// ...and do something with `num` here
}
}



Gas optimization tips


Struct Packing to save gas

struct options{
uint amount;
uint price;
uint expiry;
bool exercised;
}
uint defaults to uint256 , and bool is uint8 in solidity
so the above struct occupies 4 storage slots of size 256bits each.
struct options{
uint128 amount;
uint64 price;
uint64 expiry;
bool exercised;
}
After some modifications we are able to pack the amount and price variable as well as the expiry and exercised variable together. So that the options struct now occupies only 2 storage slots.
Further optimization is possible
struct options{
uint64 amount;
uint56 price; // <-----------
uint64 expiry;
bool exercised;
}
By changing the price variable from uint64 to uint56 we are able to pack the whole struct in just one storage space since 64+56+64+8 = 256

uint56 is more than enough for price, as it can fit numbers from 0 to 2**56 - 1.

So now the options struct only costs one storage slot worth of gas.



Security Best Practices

An important security practice is to examine all your public and external functions, and try to think of ways users might abuse them. Remember — unless these functions have a modifier like onlyOwner, any user can call them and pass them any data they want to.

Solidity Gotchas


compile learnings from interview questions etc


underflow and overflow in solidity


Source

https://docs.soliditylang.org/en/v0.5.5/structure-of-a-contract.html#:~:text=State%20variables%20are%20variables%20whose,State%20variable%20%2F%2F%20...%20%7D


https://www.tutorialspoint.com/solidity/solidity_types.htm


https://www.bitdegree.org/learn/solidity-types


https://www.geeksforgeeks.org/solidity-types/








Setting up docker on AWS

Create a EC2 instance, I used  and enter the following commands sequentially

`sudo yum update -y`

`sudo yum install -y docker`

Run `sudo docker ps` to verify if docker was installed successfully