The first ERC721 Contract Fully in Yul (Solidity Assembly) with Dynamic Purchase
In the ever-evolving landscape of blockchain technology, Non-Fungible Tokens (NFTs) have emerged as a prominent force, offering unique digital assets that defy traditional fungibility. Unlike the simpler ERC20 token contracts, NFT contracts boast a myriad of functionalities and external factors, sparking debates on whether existing Web2 technologies outshine them. Nevertheless, a wave of innovative projects has already gained traction, carving out their niche communities.
Diving into the fray of experimentation and pushing the boundaries of smart contract development, this article explores the creation of an ERC721 contract entirely in Yul language. By leveraging Yul, a low-level intermediate language for the Ethereum Virtual Machine (EVM), this contract offers direct access to storage slots, enhancing its versatility and functionality.
I encourage you to review my previous article, Lightest ERC20 Token Contract in Full Yul (Solidity Assembly) Language with Suspension Function , for a detailed explanation of functions covered therein. To avoid redundancy, functions discussed previously will not be re-explained in this context.
The entire code of my ERC721.sol
// SPDX-License-Identifier: None
pragma solidity 0.8.0;
import {Check} from "https://github.com/aloycwl/Util.sol/blob/main/Security/Check.sol";
import {Top5} from "https://github.com/aloycwl/DAO.sol/blob/main/Node/Top5.sol";
import {Mergeable} from "https://github.com/aloycwl/Token.sol/blob/main/ERC721/Mergeable.sol";
import {Upgradeable} from "https://github.com/aloycwl/Token.sol/blob/main/ERC721/Upgradeable.sol";
contract ERC721 is Check, Top5, Mergeable, Upgradeable {
event Transfer(address indexed, address indexed, uint indexed);
event Approval(address indexed, address indexed, uint indexed);
event ApprovalForAll(address indexed, address indexed, bool);
function supportsInterface (bytes4 a) external pure returns (bool bol) {
assembly { bol := or(eq(a, INF), eq(a, IN2)) }
}
function name () external pure returns (string memory) {
assembly {
mstore(0x80, 0x20)
mstore(0xa0, 0x08)
mstore(0xc0, "Game NFT")
return(0x80, 0x60)
}
}
function symbol () external pure returns (string memory) {
assembly {
mstore(0x80, 0x20)
mstore(0xa0, 0x02)
mstore(0xc0, "GN")
return(0x80, 0x60)
}
}
function count () external view returns (uint amt) {
assembly { amt := sload(INF) }
}
function balanceOf (address adr) external view returns (uint amt) {
assembly {
mstore(0x00, adr)
amt := sload(keccak256(0x00, 0x20))
}
}
function ownerOf (uint tid) external view returns (address adr) {
assembly {
mstore(0x00, tid)
adr := sload(keccak256(0x00, 0x20))
}
}
function tokenURI (uint tid) external view returns (string memory) {
assembly {
mstore(0xc0, sload(ER4))
mstore(0xe0, sload(add(ER4, 0x01)))
let len := 0x00
for { let i := tid } gt(i, 0x00) { } {
len := add(len, 0x01)
i := div(i, 0x0a)
}
for { let ptr := add(0xf6, len) } gt(tid, 0x00) { tid := div(tid, 0x0a) } {
ptr := sub(ptr, 0x01)
mstore8(ptr, add(0x30, mod(tid, 0xa)))
}
if eq(len, 0x00) {
len := 0x01
mstore8(0xf6, 0x30)
}
mstore(0x80, 0x20)
mstore(0xa0, add(len, 0x36))
return(0x80, 0x80)
}
}
function getApproved (uint tid) external view returns (address adr) {
assembly {
mstore(0x00, tid)
adr := sload(add(keccak256(0x00, 0x20), 0x03))
}
}
function isApprovedForAll (address frm, address toa) external view returns(bool bol) {
assembly {
mstore(0x00, frm)
mstore(0x20, toa)
bol := sload(keccak256(0x00, 0x40))
}
}
function approve (address toa, uint tid) external {
assembly {
mstore(0x00, tid)
let tmp := keccak256(0x00, 0x20)
let oid := sload(tmp)
mstore(0x00, oid)
mstore(0x20, caller())
mstore(0x00, sload(keccak256(0x00, 0x40)))
if and(iszero(eq(caller(), oid)), iszero(mload(0x00))) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER1)
revert(0x80, 0x64)
}
sstore(add(tmp, 0x03), toa)
log4(0x00, 0x00, EAP, oid, toa, tid)
}
}
function setApprovalForAll (address toa, bool bol) external {
assembly {
mstore(0x00, caller())
mstore(0x20, toa)
sstore(keccak256(0x00, 0x40), bol)
mstore(0x00, bol)
log3(0x00, 0x20, EAA, caller(), toa)
}
}
function _transfer (address toa, uint tid) private {
address frm;
assembly {
mstore(0x00, tid)
let ptr := keccak256(0x00, 0x20)
frm := sload(ptr)
mstore(0x00, frm)
mstore(0x20, caller())
let tmp:= sload(keccak256(0x00, 0x40))
mstore(0x00, sload(add(ptr, 0x03)))
if and(and(iszero(eq(mload(0x00), toa)), iszero(eq(frm, caller()))), eq(tmp, 0x00)) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER2)
revert(0x80, 0x64)
}
sstore(ptr, toa)
sstore(add(ptr, 0x03), 0x00)
mstore(0x00, frm)
tmp := keccak256(0x00, 0x20)
sstore(tmp, sub(sload(tmp), 0x01))
mstore(0x00, toa)
tmp := keccak256(0x00, 0x20)
sstore(tmp, add(sload(tmp), 0x01))
log4(0x00, 0x00, ETF, frm, toa, tid)
}
isSuspended(frm, toa);
_setTop5(toa);
}
function transferFrom (address, address toa, uint tid) external {
_transfer(toa, tid);
}
function safeTransferFrom (address, address toa, uint tid) external {
_transfer(toa, tid);
}
function safeTransferFrom (address, address toa, uint tid, bytes memory) external {
_transfer(toa, tid);
}
}
Below are function-by-function explanation
function supportsInterface (bytes4 a) external pure returns (bool bol) {
assembly { bol := or(eq(a, INF), eq(a, IN2)) }
}
This function checks if the interface belongs to the support type and tally the selector. So instead of identifying the type explicitly, I let them compare with the header which is more direct.
function ownerOf (uint tid) external view returns (address adr) {
assembly {
mstore(0x00, tid)
adr := sload(keccak256(0x00, 0x20))
}
}
Since owner of is the first function that requires a uint256, no additional salt is required and a hash is sufficient to set it’s storage position without any conflict.
function tokenURI (uint tid) external view returns (string memory) {
assembly {
mstore(0xc0, sload(ER4))
mstore(0xe0, sload(add(ER4, 0x01)))
let len := 0x00
for { let i := tid } gt(i, 0x00) { } {
len := add(len, 0x01)
i := div(i, 0x0a)
}
for { let ptr := add(0xf6, len) } gt(tid, 0x00) { tid := div(tid, 0x0a) } {
ptr := sub(ptr, 0x01)
mstore8(ptr, add(0x30, mod(tid, 0xa)))
}
if eq(len, 0x00) {
len := 0x01
mstore8(0xf6, 0x30)
}
mstore(0x80, 0x20)
mstore(0xa0, add(len, 0x36))
return(0x80, 0x80)
}
}
This is another piece of art where in this short routine, it calls the base URI, converts the token ID into string, concat them and return as an entire string. This is probably the first functional uint256 to string written in Yul.?
First, we need to know the length of the number. A direct function will be using log10 but it does not exist here so we use a for loop to count how many times it can be divided by 10.
The next for loop is similar where it loops through every single digit and appends to the desired position. String in EVM is stored from left to right, unlike address or uint256 where their position starts from the right.
By calling mstore8() I can place the digits exactly at the end of the base URI and in the right sequence. If the length of the uint256 is not added, it will be a reversed digit, e.g. 123 becomes 321.
In a short example, the base URI string is “ipfs://Qazwsx/” and to process the token ID number 123, the first loop returns 3 and the second loop will look for 1 then append to “ipfs://Qazwsx/1”. It will continue the process until it becomes “ipfs://Qazwsx/123”.
How to set the base URI?
bytes32 b01;
bytes32 b02;
assembly {
b01 := "ipfs://QmbpbGsoMJs7x87t7MXhQTuBo"
b02 := "rebQCbP9NHACkbM1dhNqA/"
}
Proxy(payable(address(this))).mem(0x0000000773696720657272000000000000000000000000000000000000000000, b01);
Proxy(payable(address(this))).mem(0x0000000773696720657272000000000000000000000000000000000000000001, b02);
The base URI uses 2 storage slots as the uri consists of the content identifier (CID) which is longer than 32 bytes. The bytes32 uses the ER4 hash and ER4 + 1 which is 0x0000000773696720657272000000000000000000000000000000000000000000 and 0x0000000773696720657272000000000000000000000000000000000000000001.
function isApprovedForAll (address frm, address toa) external view returns(bool bol) {
assembly {
mstore(0x00, frm)
mstore(0x20, toa)
bol := sload(keccak256(0x00, 0x40))
}
}
function setApprovalForAll (address toa, bool bol) external {
assembly {
mstore(0x00, caller())
mstore(0x20, toa)
sstore(keccak256(0x00, 0x40), bol)
mstore(0x00, bol)
log3(0x00, 0x20, EAA, caller(), toa)
}
}
Approval for all is to allow access of an operator to all your tokens. Commonly, the operator is a marketplace contract where it allows buyer and seller to transact without any intermediary.?
Since there are no other mappings consisting of 2 addresses, to store their data I can simply hash both the spender and the operator’s address.
function _transfer (address toa, uint tid) private {
address frm;
assembly {
mstore(0x00, tid)
let ptr := keccak256(0x00, 0x20)
frm := sload(ptr)
mstore(0x00, frm)
mstore(0x20, caller())
let tmp:= sload(keccak256(0x00, 0x40))
mstore(0x00, sload(add(ptr, 0x03)))
if and(and(iszero(eq(mload(0x00), toa)), iszero(eq(frm, caller()))), eq(tmp, 0x00)) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER2)
revert(0x80, 0x64)
}
sstore(ptr, toa)
sstore(add(ptr, 0x03), 0x00)
mstore(0x00, frm)
tmp := keccak256(0x00, 0x20)
sstore(tmp, sub(sload(tmp), 0x01))
mstore(0x00, toa)
tmp := keccak256(0x00, 0x20)
sstore(tmp, add(sload(tmp), 0x01))
log4(0x00, 0x00, ETF, frm, toa, tid)
}
isSuspended(frm, toa);
_setTop5(toa);
}
The first storage load gets the token owner’s address, the second storage load gets the boolean value from approval from all and the last storage load gets the boolean value of a single approved ID. If any of the cases matches, it will change ownership, reset approval and emit an event.
For all the rest of the transfer functions I am using this method even for transfer form. Basically the form is redundant as the form must be checked and tally with the token’s real owner. There is no need for a from address and the transfer should not take place when all 3 criterias are not fulfilled as mentioned earlier.
There is a _setTop5() function but I am not going to discuss it in this post. This is a DAO mechanism and will be discussed in my next article.
function _mint (address adr) internal {
assembly {
let tid := add(sload(INF), 0x01)
sstore(INF, tid)
mstore(0x00, adr)
let tmp := keccak256(0x00, 0x20)
sstore(tmp, add(sload(tmp), 0x01))
mstore(0x00, tid)
tmp := keccak256(0x00, 0x20)
sstore(tmp, adr)
log4(0x00, 0x00, ETF, 0x00, adr, tid)
}
}
function mint (uint lis, uint len, uint8 v, bytes32 r, bytes32 s) external payable {
bytes32 tmp;
unchecked { for(uint i; i < len; ++i) _mint(msg.sender); }
assembly { tmp := add(AFA, lis) }
_pay(tmp, owner(), len);
isVRS(lis, v, r, s);
_setTop5(msg.sender);
}
function mint (address adr) external onlyOwner {
_mint(adr);
_setTop5(msg.sender);
}
There are 2 mint functions here. One is the normal mint where v, r and s is provided to ensure the correct amount is being paid and the other mint is meant for cross chain usage. The normal minting has 2 parameters which are the (uint lis) and (uint len).
领英推荐
“lis” - The payment struct to be used. If this NFT does not need any payment, this can be kept at 0.
“len” - The number of NFTs to be minted.
Below are the explanations of the secondary functions.
isVRS() function
function isVRS(uint amt, uint8 v, bytes32 r, bytes32 s) internal {
bytes32 hsh;
assembly {
mstore(0x00, origin())
let ptr := add(keccak256(0x00, 0x20), 0x01)
let ind := sload(ptr)
sstore(ptr, add(ind, 0x01))
mstore(0x80, origin())
mstore(0xa0, ind)
mstore(0xc0, amt)
hsh := keccak256(0x80, 0x60)
}
address val = ecrecover(hsh, v, r, s);
isSuspended(msg.sender);
assembly {
if iszero(eq(val, sload(APP))) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER4)
revert(0x80, 0x64)
}
}
}
ecrecover() function is to get the signer from a hashed signed message. For our case we are signing a combination of the user address, a running count and the amount or number that could be used for withdrawal or payment. You need a running number (the count) to prevent the same signed message generated for the same function call.
There will be cases where users withdraw the same amount and generate the same signed message. Some recovery messages use timestamps as part of the signed message. This method has to be used carefully as the timestamp is different from every node and one can even modify the timestamp at their own node.
In the above code, first I perform an increment on the index then I put the message sender in the first memory position, the index in the second memory position and the amount in the third memory position. Next, a hashing is performed on them to create a single signed message for comparison.
If the address is not equivalent to the address stored in APP position, it will return an error.
How to set the signer?
bytes32 b32;
assembly {
b32 := 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// Replace this with the signer's address
}
Proxy(payable(address(this))).mem(0x095ea7b300000000000000000000000000000000000000000000000000000000, b32;
How to generate v, r, s?
// Get the index
function getIndex(address adr) external view returns (bytes32 val) {
assembly { val := add(keccak256(0x00, 0x20), 0x01) }
}
bytes[] message;
// Prepare the data and appended 0x00 to 32 bytes each
// Convert the everything to bytes
// First slot is the address
// e.g. 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// Second slot is the index
// e.g. 11
// Third slot is the amount
// e.g. 10000000000000000000
message =
00 00 00 00 00 00 00 00 00 00 00 00 5B 38 Da 6a 70 1c 56 85 45 dC fc B0 3F cB 87 5f 56 be dd C4
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0B
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 8A C7 23 04 89 E8 00 00;
ECDSASignature signature = sign(message, privateKey);
pay() function
function _pay(bytes32 lst, address toa, uint qty) internal {
assembly {
let tka := sload(lst)
let amt := mul(sload(add(lst, 0x01)), qty)
if gt(tka, 0x00) {
let fee := div(mul(amt, sload(TFM)), 0x2710)
if eq(tka, 0x01) {
if gt(amt, callvalue()) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER5)
revert(0x80, 0x64)
}
pop(call(gas(), toa, sub(amt, fee), 0x00, 0x00, 0x00, 0x00))
pop(call(gas(), sload(OWO), selfbalance(), 0x00, 0x00, 0x00, 0x00))
}
if gt(tka, 0x01) {
mstore(0x80, TFM)
mstore(0x84, caller())
mstore(0xa4, address())
mstore(0xc4, amt)
if iszero(call(gas(), tka, 0x00, 0x80, 0x64, 0x00, 0x00)) {
mstore(0x80, ERR)
mstore(0xa0, STR)
mstore(0xc0, ER5)
revert(0x80, 0x64)
}
mstore(0x80, TTF)
mstore(0x84, toa)
mstore(0xa4, sub(amt, fee))
pop(call(gas(), tka, 0x00, 0x80, 0x44, 0x00, 0x00))
if gt(fee, 0x00) {
mstore(0x84, sload(OWO))
mstore(0xa4, fee)
pop(call(gas(), tka, 0x00, 0x80, 0x44, 0x00, 0x00))
}
}
}
}
}
This is a versatile pay function where it has 2 payment parties and 2 payment sources. It can be used to pay the owner of the asset and the owner of the contract. For example, a user sells a NFT and transact using this function, the owner of NFT will receive the price less the admin and the owner of the contract will get the admin fee. Payment form can be in coin or token. The coin is the primary currency of the chain you are using and any token address that is launched in the same chain can be used.
The first part of the code checks for the payment type, if the storage “tka returns 0x00 means this item does not need to pay and will skip the entire code.
If “tka” returns 0x01, the payment mode is coin, it will check the send value is sufficient. If the value is correct, the recipient will receive the coin less the fee. Then all the balance of this contract will be sent to the contract owner.
If “tka” returns an address, the payment mode is token, an approval should be done from the sender to this contract on the amount to be deducted. This contract will call the transferFrom() function and pay both parties accordingly.
How to set the contract owner?
bytes32 b32;
assembly {
b32 := 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// Replace this with the owner's address
}
Proxy(payable(address(this))).mem(0x6352211e00000000000000000000000000000000000000000000000000000000, b32;
How to set the fee?
bytes32 b32;
assembly {
b32 := 50
// This means 0.5%, the number will divide by 10000
// For example if you want the fee to be 15%, the value
// will be 1500
}
Proxy(payable(address(this))).mem(0x23b872dd00000000000000000000000000000000000000000000000000000000, b32;
How to set “lis”?
bytes32 01;
bytes32 02;
bytes32 03;
bytes32 04;
assembly {
l01 := 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
l02 := 500000000000000000000
l03 := 1
l04 := 30000000000000000
}
Proxy(payable(address(this))).mem(0xe985e9c500000000000000000000000000000000000000000000000000000001, l01;
Proxy(payable(address(this))).mem(0xe985e9c500000000000000000000000000000000000000000000000000000002, l02;
Proxy(payable(address(this))).mem(0xe985e9c5000000000000000000000000000000
00000000000000000000000003, l03;
Proxy(payable(address(this))).mem(0xe985e9c500000000000000000000000000000000000000000000000000000004, l04;
The first position “l01” is the token address which means payment will be done in ERC20, the next position “l02” is the amount which is 500.
The third position “l03” is payment through coins and the next position “l04” is the amount which is 0.03.
The full repo can be found here: Aloycwl ERC721 Github