Skip to content
LogoLogo

ERC-721 NFT

ERC-721 with ERC-6093 errors, including safeTransferFrom and its receiver check.

erc721.lithe
(* ERC-721: ownership, approvals, operators, safe and unsafe transfers, mint, and burn. *)
 
import "std/abi";
import "std/address";
import "std/evm";
 
interface ERC721Receiver is {
  function onERC721Received(operator : address; from : address; tokenId : uint; data : bytes) : bytes4;
}
 
error ERC721NonexistentToken of uint;
error ERC721InvalidReceiver of address;
error ERC721InsufficientApproval of record {
  operator : address;
  tokenId : uint;
}
error ERC721InvalidSender of address;
error ERC721InvalidOwner of address;
error AlreadyMinted of uint;
error NotMinter of address;
 
event Transfer of record {
  indexed from : address;
  indexed to : address;
  indexed tokenId : uint;
}
 
event Approval of record {
  indexed owner : address;
  indexed approved : address;
  indexed tokenId : uint;
}
 
event ApprovalForAll of record {
  indexed owner : address;
  indexed operator : address;
  approved : bool;
}
 
storage is record {
  minter : address;
  owners : map(uint, address);
  balances : map(address, uint);
  approvals : map(uint, address);
  operators : map(address, map(address, bool));
}
 
external function init() is {
  storage.minter := Evm.sender();
}
 
view function requireOwner(const id : uint) : address
  raises ERC721NonexistentToken
is {
  const owner : address := storage.owners[id];
  assert(owner <> Address.zero, ERC721NonexistentToken(id));
} with owner
 
external view function ownerOf(const tokenId : uint) : address
  raises ERC721NonexistentToken
is
  requireOwner(tokenId)
 
external view function balanceOf(const owner : address) : uint
  raises ERC721InvalidOwner
is {
  assert(owner <> Address.zero, ERC721InvalidOwner(owner));
} with storage.balances[owner]
 
external view function getApproved(const tokenId : uint) : address
  raises ERC721NonexistentToken
is {
  const owner : address := requireOwner(tokenId);
} with storage.approvals[tokenId]
 
external view function isApprovedForAll(const owner : address; const operator : address) : bool is
  storage.operators[owner][operator]
 
external function approve(const to : address; const tokenId : uint)
  raises ERC721NonexistentToken | ERC721InsufficientApproval
is {
  const owner : address := requireOwner(tokenId);
  assert(
    Evm.sender() = owner or storage.operators[owner][Evm.sender()],
    ERC721InsufficientApproval { operator = Evm.sender(); tokenId = tokenId; }
  );
  storage.approvals[tokenId] := to;
  emit Approval { owner = owner; approved = to; tokenId = tokenId; };
}
 
external function setApprovalForAll(const operator : address; const approved : bool) is {
  storage.operators[Evm.sender()][operator] := approved;
  emit ApprovalForAll { owner = Evm.sender(); operator = operator; approved = approved; };
}
 
function move(const from : address; const to : address; const tokenId : uint)
  raises ERC721NonexistentToken | ERC721InsufficientApproval | ERC721InvalidSender | ERC721InvalidReceiver
is {
  assert(to <> Address.zero, ERC721InvalidReceiver(to));
  const owner : address := requireOwner(tokenId);
  assert(owner = from, ERC721InvalidSender(from));
  const caller : address := Evm.sender();
  assert(
    caller = owner or storage.approvals[tokenId] = caller or storage.operators[owner][caller],
    ERC721InsufficientApproval { operator = caller; tokenId = tokenId; }
  );
  storage.approvals[tokenId] := Address.zero;
  storage.balances[from] := storage.balances[from] - 1;
  storage.balances[to] := storage.balances[to] + 1;
  storage.owners[tokenId] := to;
  emit Transfer { from = from; to = to; tokenId = tokenId; };
}
 
(* A contract receiver must answer with `onERC721Received`'s selector. The call runs
   after the transfer's effects. A receiver that answers wrongly, or reverts without a
   reason, is rejected; one that reverts with a reason has it passed on unchanged.
   Either reverts the whole transfer. *)
function checkReceived(const from : address; const to : address; const tokenId : uint; const data : bytes)
  raises ERC721InvalidReceiver | forwarded
is {
  if Evm.codeSize(to) > 0 then {
    defer ERC721Receiver(to).onERC721Received(Evm.sender(), from, tokenId, data) then {
      | Ok(answer : bytes4) -> {
          assert(answer = Abi.selector(ERC721Receiver.onERC721Received), ERC721InvalidReceiver(to));
        }
      | Error(reason : bytes) -> {
          if reason.length = 0 then {
            fail ERC721InvalidReceiver(to);
          }
          forward reason;
        }
    };
  }
}
 
external function transferFrom(const from : address; const to : address; const tokenId : uint)
  raises ERC721NonexistentToken | ERC721InsufficientApproval | ERC721InvalidSender | ERC721InvalidReceiver
is {
  move(from, to, tokenId);
}
 
external nonreentrant function safeTransferFrom(const from : address; const to : address; const tokenId : uint)
  raises ERC721NonexistentToken | ERC721InsufficientApproval | ERC721InvalidSender | ERC721InvalidReceiver | forwarded
is {
  move(from, to, tokenId);
  checkReceived(from, to, tokenId, "");
}
 
external nonreentrant function safeTransferFrom(
  const from : address;
  const to : address;
  const tokenId : uint;
  const data : bytes
)
  raises ERC721NonexistentToken | ERC721InsufficientApproval | ERC721InvalidSender | ERC721InvalidReceiver | forwarded
is {
  move(from, to, tokenId);
  checkReceived(from, to, tokenId, data);
}
 
external function mint(const to : address; const tokenId : uint)
  raises NotMinter | AlreadyMinted | ERC721InvalidReceiver
is {
  assert(Evm.sender() = storage.minter, NotMinter(Evm.sender()));
  assert(to <> Address.zero, ERC721InvalidReceiver(to));
  assert(storage.owners[tokenId] = Address.zero, AlreadyMinted(tokenId));
  storage.owners[tokenId] := to;
  storage.balances[to] := storage.balances[to] + 1;
  emit Transfer { from = Address.zero; to = to; tokenId = tokenId; };
}
 
external function burn(const tokenId : uint)
  raises ERC721NonexistentToken | ERC721InsufficientApproval
is {
  const owner : address := requireOwner(tokenId);
  assert(Evm.sender() = owner, ERC721InsufficientApproval { operator = Evm.sender(); tokenId = tokenId; });
  storage.approvals[tokenId] := Address.zero;
  storage.balances[owner] := storage.balances[owner] - 1;
  storage.owners[tokenId] := Address.zero;
  emit Transfer { from = owner; to = Address.zero; tokenId = tokenId; };
}
  • safeTransferFrom is overloaded, as the standard requires: safeTransferFrom(address,address,uint256) and safeTransferFrom(address,address,uint256,bytes).
  • ERC721Receiver(to) turns the recipient's address into an interface value, so the receiver hook can be deferred.
  • The hook is a recoverable call. A receiver that reverts without a reason becomes a typed ERC721InvalidReceiver error; one with a reason has it forwarded, so the safe transfers raise forwarded.