Skip to main content

ERC721 NFT

A complete example of an ERC721 (NFT) implementation using the AssemblyScript Stylus SDK.

Complete Contract: You can view the full ERC721 contract implementation here.

Missing Methods

Note: The safeTransferFrom and safeMint methods are not currently implemented in this example. These methods will be added in future updates to provide full ERC721 compliance with receiver callbacks.

Contract Structure

Custom Errors

Specific errors for different validations:

@Error
class ERC721InvalidOwner {
owner: Address;
}

@Error
class ERC721NonexistentToken {
tokenId: U256;
}

@Error
class ERC721IncorrectOwner {
sender: Address;
tokenId: U256;
owner: Address;
}

@Error
class ERC721InsufficientApproval {
sender: Address;
tokenId: U256;
}

Events

Events to track transfers and approvals:

@Event
export class Transfer {
@Indexed from: Address;
@Indexed to: Address;
@Indexed tokenId: U256;
}

@Event
export class Approval {
@Indexed owner: Address;
@Indexed spender: Address;
@Indexed tokenId: U256;
}

@Event
export class ApprovalForAll {
@Indexed owner: Address;
@Indexed operator: Address;
approved: boolean;
}

Storage State

Mappings to manage ownership and approvals:

@Contract
export class ERC721 {
static owners: Mapping<U256, Address> = new Mapping<U256, Address>();
static balances: Mapping<Address, U256> = new Mapping<Address, U256>();
static tokenApprovals: Mapping<U256, Address> = new Mapping<U256, Address>();
static operatorApprovals: Mapping2<Address, Address, boolean> = new Mapping2<Address, Address, boolean>();
static name: Str;
static symbol: Str;

Constructor

Initialize the NFT collection:

  constructor(_name: string, _symbol: string) {
const nameStr = StrFactory.fromString(_name);
const symbolStr = StrFactory.fromString(_symbol);
name = nameStr;
symbol = symbolStr;
}

Approval Functions

Permission system for transferring NFTs:

  @External
static approve(to: Address, tokenId: U256): void {
const authorizer = msg.sender;
const owner = owners.get(tokenId);

if (owner.isZero()) {
ERC721NonexistentToken.revert(tokenId);
}

const isOwnerAuth = owner.equals(authorizer);
const isApprovedForAll = operatorApprovals.get(owner, authorizer);
const isAuthorized = isOwnerAuth || isApprovedForAll;

if (!isAuthorized) {
ERC721InvalidApprover.revert(authorizer);
}

tokenApprovals.set(tokenId, to);
Approval.emit(owner, to, tokenId);
}

@External
static setApprovalForAll(operator: Address, approved: boolean): void {
if (operator.isZero()) {
ERC721InvalidOperator.revert(operator);
}

const owner = msg.sender;
operatorApprovals.set(owner, operator, approved);
ApprovalForAll.emit(owner, operator, approved);
}

Transfer Function

Transfer NFTs between accounts with full validations:

  @External
static transferFrom(from: Address, to: Address, tokenId: U256): void {
const zeroAddress = AddressFactory.fromString("0x0000000000000000000000000000000000000000");
const one = U256Factory.fromString("1");

if (to.isZero()) {
ERC721InvalidReceiver.revert(to);
}

const owner = owners.get(tokenId);
const authorizer = msg.sender;

const isOwnerZero = owner.isZero();
const approvedAddress = tokenApprovals.get(tokenId);
const isApprovedForAll = operatorApprovals.get(owner, authorizer);
const isAuthOwner = authorizer.equals(owner);
const isAuthApproved = authorizer.equals(approvedAddress);
const isAuthorized = isAuthOwner || isAuthApproved || isApprovedForAll;

if (!isAuthorized) {
if (isOwnerZero) {
ERC721NonexistentToken.revert(tokenId);
} else {
ERC721InsufficientApproval.revert(authorizer, tokenId);
}
}

if (!owner.equals(from)) {
ERC721IncorrectOwner.revert(authorizer, tokenId, owner);
}

tokenApprovals.set(tokenId, zeroAddress);
const fromBalance = balances.get(owner);
balances.set(owner, fromBalance.sub(one));
const toBalance = balances.get(to);
balances.set(to, toBalance.add(one));

owners.set(tokenId, to);
Transfer.emit(owner, to, tokenId);
}

Mint & Burn

Functions to create and destroy NFTs:

  @External
static mint(to: Address, tokenId: U256): void {
const zeroAddress = AddressFactory.fromString("0x0000000000000000000000000000000000000000");
const one = U256Factory.fromString("1");

if (to.isZero()) {
ERC721InvalidReceiver.revert(zeroAddress);
}

const from = owners.get(tokenId);
if (!from.isZero()) {
ERC721InvalidSender.revert(zeroAddress);
}

const toBalance = balances.get(to);
balances.set(to, toBalance.add(one));
owners.set(tokenId, to);

Transfer.emit(from, to, tokenId);
}

@External
static burn(tokenId: U256): void {
const zeroAddress = AddressFactory.fromString("0x0000000000000000000000000000000000000000");
const one = U256Factory.fromString("1");

const from = owners.get(tokenId);
if (from.isZero()) {
ERC721NonexistentToken.revert(tokenId);
}

tokenApprovals.set(tokenId, zeroAddress);
const fromBalance = balances.get(from);
balances.set(from, fromBalance.sub(one));
owners.set(tokenId, zeroAddress);

Transfer.emit(from, zeroAddress, tokenId);
}

View Functions

Query methods for NFT information:

  @View
static balanceOf(owner: Address): U256 {
if (owner.isZero()) {
ERC721InvalidOwner.revert(owner);
}
return balances.get(owner);
}

@View
static ownerOf(tokenId: U256): Address {
const owner = owners.get(tokenId);
if (owner.isZero()) {
ERC721NonexistentToken.revert(tokenId);
}
return owner;
}

@View
static name(): string {
return name;
}

@View
static symbol(): string {
return symbol;
}

@View
static getApproved(tokenId: U256): Address {
const owner = owners.get(tokenId);
if (owner.isZero()) {
ERC721NonexistentToken.revert(tokenId);
}
return tokenApprovals.get(tokenId);
}

@View
static isApprovedForAll(owner: Address, operator: Address): boolean {
return operatorApprovals.get(owner, operator);
}
}