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

Imports

First, import all necessary modules from the SDK:

import {
Contract,
External,
U256,
U256Factory,
Address,
Mapping,
MappingNested,
View,
ErrorFactory,
Str,
StrFactory,
msg,
AddressFactory,
EventFactory,
} from "@wakeuplabs/as-stylus";

Custom Errors

Specific errors for different validations:

import { ErrorFactory } from "@wakeuplabs/as-stylus";

const ERC721InvalidOwner = ErrorFactory.create<[owner: Address]>();
const ERC721NonexistentToken = ErrorFactory.create<[tokenId: U256]>();
const ERC721IncorrectOwner =
ErrorFactory.create<[sender: Address, tokenId: U256, owner: Address]>();
const ERC721InsufficientApproval = ErrorFactory.create<[sender: Address, tokenId: U256]>();
const ERC721InvalidApprover = ErrorFactory.create<[approver: Address]>();
const ERC721InvalidOperator = ErrorFactory.create<[operator: Address]>();
const ERC721InvalidReceiver = ErrorFactory.create<[receiver: Address]>();
const ERC721InvalidSender = ErrorFactory.create<[sender: Address]>();

Events

Events to track transfers and approvals:

import { EventFactory } from "@wakeuplabs/as-stylus";

const Transfer = EventFactory.create<[Address, Address, U256]>({
indexed: [true, true, true],
});

const Approval = EventFactory.create<[Address, Address, U256]>({
indexed: [true, true, true],
});

const ApprovalForAll = EventFactory.create<[Address, Address, boolean]>({
indexed: [true, true, false],
});

Storage State

Mappings to manage ownership and approvals:

@Contract
export class ERC721 {
owners: Mapping<U256, Address> = new Mapping<U256, Address>();
balances: Mapping<Address, U256> = new Mapping<Address, U256>();
tokenApprovals: Mapping<U256, Address> = new Mapping<U256, Address>();
operatorApprovals: MappingNested<Address, Address, boolean> = new MappingNested<Address, Address, boolean>();
nameValue: Str;
symbolValue: Str;

Constructor

Initialize the NFT collection:

  constructor(_name: string, _symbol: string) {
const nameStr = StrFactory.fromString(_name);
const symbolStr = StrFactory.fromString(_symbol);
this.nameValue = nameStr;
this.symbolValue = symbolStr;
}

Approval Functions

Permission system for transferring NFTs:

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

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

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

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

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

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

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

Transfer Function

Transfer NFTs between accounts with full validations:

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

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

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

const isOwnerZero = owner.isZero();
const approvedAddress = this.tokenApprovals.get(tokenId);
const isApprovedForAll = this.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);
}

this.tokenApprovals.set(tokenId, zeroAddress);
const fromBalance = this.balances.get(owner);
this.balances.set(owner, fromBalance.sub(U256Factory.fromString("1")));
const toBalance = this.balances.get(to);
this.balances.set(to, toBalance.add(U256Factory.fromString("1")));

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

Mint & Burn

Functions to create and destroy NFTs:

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

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

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

const toBalance = this.balances.get(to);
this.balances.set(to, toBalance.add(U256Factory.fromString("1")));
this.owners.set(tokenId, to);

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

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

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

this.tokenApprovals.set(tokenId, zeroAddress);
const fromBalance = this.balances.get(from);
this.balances.set(from, fromBalance.sub(U256Factory.fromString("1")));
this.owners.set(tokenId, zeroAddress);

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

View Functions

Query methods for NFT information:

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

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

@View
name(): Str {
return this.nameValue;
}

@View
symbol(): Str {
return this.symbolValue;
}

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

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