Este tutorial irá guiá-lo para a criação de um NFT compatível do OpenSea com o Foundry e o Solmate. Uma implementação completa deste tutorial pode ser encontrada aqui.
Este tutorial é apenas para fins ilustrativos e é fornecido com base no estado em que se encontra. O tutorial não está auditado, nem totalmente testado. Nenhum código deste tutorial deve ser usado em um ambiente de produção.
Crie o projeto e instale as dependências
Comece configurando um projeto do Foundry seguindo os passos destacados na seção “começando” (Getting started). Também instalaremos o Solmate para sua implementação do ERC721, bem como algumas bibliotecas utilitárias do OpenZeppelin. Instale as dependências executando os seguintes comandos da raiz do seu projeto:
forge install transmissions11/solmate Openzeppelin/openzeppelin-contracts
Essas dependências serão adicionadas como submódulos do Git ao seu projeto.
Se você seguiu as instruções corretamente, seu projeto deve estar estruturado desta forma:
Implemente um NFT básico
Vamos agora renomear o contrato boilerplate no src/Contract.sol
para src/NFT.sol
e substituir o código:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.10;
import "solmate/tokens/ERC721.sol";
import "openzeppelin-contracts/contracts/utils/Strings.sol";
contract NFT is ERC721 {
uint256 public currentTokenId;
constructor(
string memory _name,
string memory _symbol
) ERC721(_name, _symbol) {}
function mintTo(address recipient) public payable returns (uint256) {
uint256 newItemId = ++currentTokenId;
_safeMint(recipient, newItemId);
return newItemId;
}
function tokenURI(uint256 id) public view virtual override returns (string memory) {
return Strings.toString(id);
}
}
Vamos dar uma olhada nessa implementação super básica de um NFT. Começamos importando dois contratos dos nossos submódulos do Git. Importamos a implementação otimizada de gás do Solmate do padrão ERC721, do qual nosso contrato de NFT herdará. Nosso construtor pega os argumentos _name
e _symbol
para o nosso NFT e passa eles adiante para o construtor de implementação-pai do ERC721. Por fim, implementamos a função mintTo
, que permite a qualquer um cunhar um NFT. Essa função incrementa o currentTokenId
e faz uso da função _safeMint
do nosso contrato-pai.
Compile e implante com o Forge
Para compilar o contrato de NFT, execute forge build
. Você pode experienciar uma falha na construção devido ao mapeamento incorreto:
Error:
Compiler run failed
error[6275]: ParserError: Source "lib/openzeppelin-contracts/contracts/contracts/utils/Strings.sol" not found: File not found. Searched the following locations: "/PATH/TO/REPO".
--> src/NFT.sol:5:1:
|
5 | import "openzeppelin-contracts/contracts/utils/Strings.sol";
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Isso pode ser corrigido configurando o remapeamento correto. Crie um arquivo remappings.txt
no seu projeto e adicione a linha:
openzeppelin-contracts/=lib/openzeppelin-contracts/
Você pode encontrar mais sobre remapeamentos na documentação de dependências.
Por padrão, a saída do compilador estará no diretório out
. Para implantar nosso contrato compilado como o Forge, temos que definir as variáveis de ambiente para o ponto de extremidade da RPC (Remote Procedure Call - chamada de procedimento remoto) e a chave privada que desejamos usar para implantar.
Defina suas variáveis de ambiente executando:
export RPC_URL=<Your RPC endpoint>
export PRIVATE_KEY=<Your wallets private key>
Uma vez definidas, você pode implantar seu NFT com o Forge executando o comando abaixo enquanto adiciona os argumentos relevantes do construtor para o contrato de NFT:
forge create NFT --rpc-url=$RPC_URL --private-key=$PRIVATE_KEY --constructor-args <name> <symbol>
Se for implantado com sucesso, você verá o endereço da carteira de implantação, o endereço do contrato, bem como o hash da transação impressos no seu terminal.
Cunhando NFTs do seu contrato
A chamada de funções no seu contrato de NFT é simplificada com o Cast, a ferramenta de linha de comando do Foundry para interação com contratos inteligentes, envio de transações e obtenção de dados de cadeia. Vamos dar uma olhada em como podemos usá-lo para cunhar NFTs do nosso contrato de NFT.
Dado que você já definiu seu RPC e as variáveis env de chave privada durante a implantação, cunhe um NFT do seu contrato executando:
cast send --rpc-url=$RPC_URL <contractAddress> "mintTo(address)" <arg> --private-key=$PRIVATE_KEY
Estendendo a funcionalidade do nosso contrato de NFT e testando
Vamos estender nosso NFT adicionando metadados para representar o conteúdo dos nossos NFTS, assim como definir um preço de cunhagem, um fornecimento máximo e a possibilidade de withdraw (retirar) os produtos coletados da cunhagem. Para acompanhar, você pode substituir seu contrato de NFT atual pelo trecho de código abaixo:
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.10;
import "solmate/tokens/ERC721.sol";
import "openzeppelin-contracts/contracts/utils/Strings.sol";
import "openzeppelin-contracts/contracts/access/Ownable.sol";
error MintPriceNotPaid();
error MaxSupply();
error NonExistentTokenURI();
error WithdrawTransfer();
contract NFT is ERC721, Ownable {
using Strings for uint256;
string public baseURI;
uint256 public currentTokenId;
uint256 public constant TOTAL_SUPPLY = 10_000;
uint256 public constant MINT_PRICE = 0.08 ether;
constructor(
string memory _name,
string memory _symbol,
string memory _baseURI
) ERC721(_name, _symbol) {
baseURI = _baseURI;
}
function mintTo(address recipient) public payable returns (uint256) {
if (msg.value != MINT_PRICE) {
revert MintPriceNotPaid();
}
uint256 newTokenId = ++currentTokenId;
if (newTokenId > TOTAL_SUPPLY) {
revert MaxSupply();
}
_safeMint(recipient, newTokenId);
return newTokenId;
}
function tokenURI(uint256 tokenId)
public
view
virtual
override
returns (string memory)
{
if (ownerOf(tokenId) == address(0)) {
revert NonExistentTokenURI();
}
return
bytes(baseURI).length > 0
? string(abi.encodePacked(baseURI, tokenId.toString()))
: "";
}
function withdrawPayments(address payable payee) external onlyOwner {
uint256 balance = address(this).balance;
(bool transferTx, ) = payee.call{value: balance}("");
if (!transferTx) {
revert WithdrawTransfer();
}
}
}
Entre outras coisas, adicionamos metadados que podem ser investigados por qualquer aplicativo de front-end como o OpenSea, chamando o método tokenURI
no nosso contrato de NFT.
Observação: se você quer fornecer um URL real para o construtor na implantação e hospedar os metadados deste contrato de NFT, por favor, siga os passos destacados aqui.
Vamos testar algumas dessas funcionalidades adicionadas para certificarmo-nos de que funcionam como pretendido. O Foundry oferece um framework de teste nativo de EVM (Ethereum Virtual Machine - máquina virtual da Ethereum) extremamente rápido por meio do Forge.
Dentro da sua pasta de teste, renomeie o arquivo de teste atual Contract.t.sol
para NFT.t.sol
. Esse arquivo conterá todos os testes referentes ao método mintTo
do NFT. Em seguida, substitua o código boilerplate existente pelo abaixo:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.10;
import "forge-std/Test.sol";
import "../src/NFT.sol";
contract NFTTest is Test {
using stdStorage for StdStorage;
NFT private nft;
function setUp() public {
// Implantar contrato NFT
nft = new NFT("NFT_tutorial", "TUT", "baseUri");
}
function testFailNoMintPricePaid() public {
nft.mintTo(address(1));
}
function testMintPricePaid() public {
nft.mintTo{value: 0.08 ether}(address(1));
}
function testFailMaxSupplyReached() public {
uint256 slot = stdstore
.target(address(nft))
.sig("currentTokenId()")
.find();
bytes32 loc = bytes32(slot);
bytes32 mockedCurrentTokenId = bytes32(abi.encode(10000));
vm.store(address(nft), loc, mockedCurrentTokenId);
nft.mintTo{value: 0.08 ether}(address(1));
}
function testFailMintToZeroAddress() public {
nft.mintTo{value: 0.08 ether}(address(0));
}
function testNewMintOwnerRegistered() public {
nft.mintTo{value: 0.08 ether}(address(1));
uint256 slotOfNewOwner = stdstore
.target(address(nft))
.sig(nft.ownerOf.selector)
.with_key(1)
.find();
uint160 ownerOfTokenIdOne = uint160(
uint256(
(vm.load(address(nft), bytes32(abi.encode(slotOfNewOwner))))
)
);
assertEq(address(ownerOfTokenIdOne), address(1));
}
function testBalanceIncremented() public {
nft.mintTo{value: 0.08 ether}(address(1));
uint256 slotBalance = stdstore
.target(address(nft))
.sig(nft.balanceOf.selector)
.with_key(address(1))
.find();
uint256 balanceFirstMint = uint256(
vm.load(address(nft), bytes32(slotBalance))
);
assertEq(balanceFirstMint, 1);
nft.mintTo{value: 0.08 ether}(address(1));
uint256 balanceSecondMint = uint256(
vm.load(address(nft), bytes32(slotBalance))
);
assertEq(balanceSecondMint, 2);
}
function testSafeContractReceiver() public {
Receiver receiver = new Receiver();
nft.mintTo{value: 0.08 ether}(address(receiver));
uint256 slotBalance = stdstore
.target(address(nft))
.sig(nft.balanceOf.selector)
.with_key(address(receiver))
.find();
uint256 balance = uint256(vm.load(address(nft), bytes32(slotBalance)));
assertEq(balance, 1);
}
function testFailUnSafeContractReceiver() public {
vm.etch(address(1), bytes("mock code"));
nft.mintTo{value: 0.08 ether}(address(1));
}
function testWithdrawalWorksAsOwner() public {
// Cunhar um NFT, enviando eth para o contrato
Receiver receiver = new Receiver();
address payable payee = payable(address(0x1337));
uint256 priorPayeeBalance = payee.balance;
nft.mintTo{value: nft.MINT_PRICE()}(address(receiver));
// Verificar se o saldo do contrato está correto
assertEq(address(nft).balance, nft.MINT_PRICE());
uint256 nftBalance = address(nft).balance;
// Retirar o saldo e afirmar que foi transferido
nft.withdrawPayments(payee);
assertEq(payee.balance, priorPayeeBalance + nftBalance);
}
function testWithdrawalFailsAsNotOwner() public {
// Cunhar um NFT, enviando eth para o contrato
Receiver receiver = new Receiver();
nft.mintTo{value: nft.MINT_PRICE()}(address(receiver));
// Verificar se o saldo do contrato está correto
assertEq(address(nft).balance, nft.MINT_PRICE());
// Confirmar que um não-proprietário não possa fazer retirada
vm.expectRevert("Ownable: caller is not the owner");
vm.startPrank(address(0xd3ad));
nft.withdrawPayments(payable(address(0xd3ad)));
vm.stopPrank();
}
}
contract Receiver is ERC721TokenReceiver {
function onERC721Received(
address operator,
address from,
uint256 id,
bytes calldata data
) external override returns (bytes4) {
return this.onERC721Received.selector;
}
}
O conjunto de testes está configurado como um contrato com um método setUp
, que executa antes de cada teste individual.
Como você pode ver, o Forge oferece vários cheatcodes (códigos de trapaça) para manipular o estado para acomodar seu cenário de teste.
Por exemplo, nosso teste testFailMaxSupplyReached
verifica se uma tentativa de cunhagem falha quando o fornecimento máximo de NFT é alcançado. Por isso, o currentTokenId
do nosso contrato de NFT precisa ser definido para o fornecimento máximo usando o cheatcode de armazenamento, que permite que você escreva os dados nos slots de armazenamento do seu contratos. Os slots de armazenamento nos quais você deseja escrever podem ser facilmente encontrados usando a biblioteca auxiliar [forge-std](https://github.com/foundry-rs/forge-std/)
. Você pode executar o teste com o comando a seguir:
forge test
Se você quiser colocar suas habilidades do Forge em prática, escreva testes para os métodos restantes do nosso contrato de NFT. Sinta-se à vontade para enviá-los ao tutorial-nft, onde você encontrará a implementação completa deste tutorial.
Relatórios de gás para suas funções de chamadas
O Foundry fornece relatórios de gás abrangentes sobre seus contratos. Para cada função chamada dentro dos seus testes, ele retorna o custo mínimo, a média, o custo mediano e o custo máximo de gás. Para publicar o relatório de gás, simplesmente execute:
forge test --gas-report
Isso é útil ao analisar várias otimizações de gás dentro dos seus contratos.
Vamos dar uma olhada na economia de gás que fizemos substituindo o OpenZeppelin pelo Solmate em nossa implementação do ERC721. Você pode encontrar a implementação do NFT usando ambas as bibliotecas aqui. Abaixo estão os relatórios de gás resultantes ao executar forge test --gas-report
nesse repositório.
Como você pode ver, nossa implementação usando o Solmate economiza em torno de 500 de gás em uma cunhagem bem sucedida (o custo máximo de gás das chamadas da função mintTo
).
É isso. Espero que isso dê a você uma boa base prática em como começar com o Foundry. Pensamos que não existe forma melhor de entender profundamente o Solidity do que escrever seus próprios testes em Solidity. Você também experienciará menos trocas de contexto entre JavaScript e Solidity. Boa codificação!
Observação: siga este tutorial para aprender como implantar o contrato de NFT utilizado aqui com o script do Solidity.
Esse artigo foi retirado do livro Foundry-rs, escrito por uma equipe de contribuidores em uma iniciativa open-source All Contributors (Todos os contribuidores) e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.
Latest comments (0)