Letβs understand this with an example: We want to build an emoji π NFTs.
- People should be able to mint it at the start for a fixed price and trade it.
- People should be able to breed them to burn existing NFTs and mint new ones.
What are the main Components and Interactions?
- There should be an NFT contract. What makes a contract NFT?
- It should have basic operations like transfer, allow, balance, etc
- Users should be able to mint it at a fixed price.
- Users should be able to breed the NFT to generate new NFT. Maybe the minting logic should be built on an external contract that has minting rights? How do we handle different kinds of access?
Should it have to follow standards like ERC20, ERC721, and ERC1155? π
- Ethereum community has defined standard interfaces for contracts that want to perform a certain set of functionalities: https://eips.ethereum.org/erc
- Most commonly used standards:
- This allows for high composability between DAPPs and the reuse of standard libraries.
- Our NFT contract should follow the ERC721 interface to obey the NFT standard set by the Ethereum community
interface ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
function balanceOf(address _owner) external view returns (uint256);
function ownerOf(uint256 _tokenId) external view returns (address);
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
function setApprovalForAll(address _operator, bool _approved) external;
function getApproved(uint256 _tokenId) external view returns (address);
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}
Quick Recap of External Calls
- There are two main ways of calling one contract from another (Ignoring
CALLCODE
as it's not advised to use). The best way to understand them is with an example: Alice calls contract A which calls contract B:
- External Calls (
CALL
):
- Simply calls external contract from the existing contract
- Storage of the callee contract (B) is used
msg.sender
is caller contract (A), msg.value
whatever specified by the caller (A)
callee.call{value: <value>}(<calldata>)
- Delegate Calls (
DELEGATECALL
)
- Calls external contract in the context of the current contract
- Storage of the callee contract(A) is used
msg.sender
****is original called of caller contract (Alice), msg.value
is also what was specified by the called of caller contract (Alice)
- The best way to think about delegateCall is as a call where only the logic of the external contract is used all the context and storage of the original call are unchanged.
callee.delegatecall(<calldata>)
- It is essential that the callee contract (B) obeys the storage layout of the caller contract (A). If the storage layout is not obeyed correctly this can lead to memory corruption.
Is the contract Upgradable? If so which Upgradable Patterns?