LoginSignup
0
1

Foundry チュートリアルをやってみる (NFT 作成)

Posted at

こちらのfoundry チュートリアル(NFT 作成)をやってみた結果と気づきのメモです。
https://book.getfoundry.sh/tutorials/solmate-nft

基本的な NFT contract 作成

  • 手順どおりfoundry でnftディレクトリを作成
forge init nft
cd nft
  • openzeppelin, solmateをいれる。
forge install transmissions11/solmate Openzeppelin/openzeppelin-contracts
  • openzeppelin のライブラリのパスを remappings.txt を作ってマッピングする

  • src/NFT.sol ファイルを写経。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {ERC721} from "solmate/tokens/ERC721.sol";
import {Strings} from "openzeppelin-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);
	}
}
forge build

デプロイ

  • ローカルテストノード起動
    ( address, keyをメモする)
anvil
  • デプロイ
    (環境変数$SEC1にプライベートキー、$PRChttp://127.0.0.1:8545 をいれている)

root@DESKTOP-N2O3OSL:~/nft# forge create NFT --rpc-url=$RPC --private-key=$SEC1 --constructor-args "the first NFT" "first NFT symbol"
[⠰] Compiling...
No files changed, compilation skipped
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Transaction hash: 0xf47c06552b02efd2cfcc4b39b4c5010a6979e90ac0c417ac9ee8892ad76bf015
  • ここでDeployed toのアドレスがContact-addressになるのでメモもしくは環境変数にいれる。
  • name, symbolconstructorで指定してしているだけなので、これはあとで変更できないけど、なんか意味あるのかよくわからない。
  • ERC721 contractを継承しているので、そちらをみてみる

検証

  • 環境変数に以下を入れる
SEC1=<Account 1 privte-key>
SEC2=<Account 2 privat-key>
ADR1=<Account 1 address>
ADR2=<Account 1 address>
ミント

(以下$Conにコンタクトアドレスをいれてます)

root@DESKTOP-N2O3OSL:~/nft# cast send --rpc-url=$RPC $CON "mintTo(address)" $ADR2 --private-key $SEC2

blockHash               0xc92abfb4d0658d88938a20845b427cd1954e6ffa2c32471048da6238f8c7a3a5
blockNumber             2
contractAddress
cumulativeGasUsed       90805
effectiveGasPrice       3883515875
gasUsed                 90805
logs                    [{"address":"0x5fbdb2315678afecb367f032d93f642f64180aa3","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8","0x0000000000000000000000000000000000000000000000000000000000000001"],"data":"0x","blockHash":"0xc92abfb4d0658d88938a20845b427cd1954e6ffa2c32471048da6238f8c7a3a5","blockNumber":"0x2","transactionHash":"0x824d03d888b04f99ad1a1367e581aea54c9b20b3d16e4451c4197ac454a8c681","transactionIndex":"0x0","logIndex":"0x0","transactionLogIndex":"0x0","removed":false}]
logsBloom               0x00000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000008000000000000000000040000000000000000000000000840020000000000000000000800000000000000000000000010000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000060000000000000000000000000000000000001000000000000000000000000000000
root
status                  1
transactionHash         0x824d03d888b04f99ad1a1367e581aea54c9b20b3d16e4451c4197ac454a8c681
transactionIndex        0
type                    2
  • mintoTo()の戻り値はnewItemIDなのにどこにも戻り値がない。だからといって cast tx <transaction-id> でトランザクションをみても記載はない。
  • eventemitを使用してトランザクションログにIDを書き込めるみたい。トランザクションが増えるのでコストがかかる。
  • トランザクションを伴う関数は戻り値がない -> eventで確認
  • トランザクションを伴わない関数は戻り値を取得できる。この場合はownerOf()など
所有証明 引数にitemIDが必要
cast call --rpc-url=$RPC  $CON  "ownerOf(uint256)" 1
0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8
  • 戻り値が$AD2のアドレスと同じ(期待どおり)
何個所有しているか

lib/solmate/src/tokens/ERC721.solをみてみるとbalanceOf関数がある。

cast call --rpc-url=$RPC  $CON  "balanceOf(address)" $SRC2
0x0000000000000000000000000000000000000000000000000000000000000001

一個ミントしてこのアドレスに送付したので現在は1個。複数ミントして送付するとその数だけ増えたことを確認した。他の関数もlib/solmate/src/tokens/ERC721.solで見てNFTを完全に理解したい。

ここまでの気づき
  • NFTってアドレスとIDしか記録しないのか!

NTFコントラクトの拡張とテスト

チュートリアルのとおりNTF.solを以下のとおり写経する (少し修正している。後述)

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {ERC721} from "solmate/tokens/ERC721.sol";
import {Strings} from "openzeppelin-contracts/utils/Strings.sol";
import {Ownable} from "openzeppelin-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) Ownable(msg.sender) {
		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();
		}
	}
}

  • using Srtings for uint256; ... uint256の引数に対してStringsライブラリの関数を Strings.function(uint256)でなくて function(unit256)で呼び出せる。
  • uint256 public constant TOTAL_SUPPLY = 10_000;  ... 可読性のため数字にアンダースコアをいれてよい。10_000 は 10000と同じ。
  • uint256 public constant MINT_PRICE = 0.08 ether; ... ether = 1e18
  • チュートリアルのままではコンパイルできない。以下のエラーが発生。Ownableの仕様が変わってconstructor にアドレスが必要になった。そのためmsg.senderを引数に指定した。
root@DESKTOP-N2O3OSL:~/nft# forge build
[⠆] Compiling...
[⠰] Compiling 3 files with 0.8.22
[⠔] Solc 0.8.22 finished in 6.41ms
Error:
Compiler run failed:
Error (3415): No arguments passed to the base constructor. Specify the arguments or mark "NFT" as abstract.
  --> src/NFT.sol:13:1:
   |
13 | contract NFT is ERC721, Ownable {
   | ^ (Relevant source part starts here and spans across multiple lines).
Note: Base constructor parameters:
  --> lib/openzeppelin-contracts/contracts/access/Ownable.sol:38:16:
   |
38 |     constructor(address initialOwner) {
   |                ^^^^^^^^^^^^^^^^^^^^^^

root@DESKTOP-N2O3OSL:~/nft#

次にテストtest/nft.t.solを写経する。いくつか修正(後述)

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;

import "forge-std/Test.sol";
import "../src/NFT.sol";
import {ERC721TokenReceiver} from "solmate/tokens/ERC721.sol";


contract NFTTest is Test {
    using stdStorage for StdStorage;

    NFT private nft;

    function setUp() public {
        nft = new NFT("NFT_tutorial", "TUT", "http://mynft/");
    }

    function test_RevertMintWithoutValue() public {
        vm.expectRevert(MintPriceNotPaid.selector);
        nft.mintTo(address(1));
    }

    function test_MintPricePaid() public {
        nft.mintTo{value: 0.08 ether}(address(1));
    }

    function test_RevertMintMaxSupplyReached() 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);
        vm.expectRevert(MaxSupply.selector);
        nft.mintTo{value: 0.08 ether}(address(1));
    }

    function testRevertMintToZeroAddress() public {
        vm.expectRevert("INVALID_RECIPIENT");
        nft.mintTo{value: 0.08 ether}(address(0));
    }
     
     function test_BalanceIncremented() 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 balance1 = uint256(vm.load(address(nft), bytes32(slotBalance)));
        assertEq(balance1, 1);
        nft.mintTo{value: 0.08 ether}(address(1));
        uint256 balance2 = uint256(vm.load(address(nft), bytes32(slotBalance)));
        assertEq(balance2, 2);
        }

    function test_SafeContractReceiver() 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 test_RevertUnSafeContractReceiver() public {
        vm.etch(address(11), bytes("mock code"));
        vm.expectRevert(bytes(""));
        nft.mintTo{value: 0.08 ether}(address(11));
    }

    function testWithdrawWorksAsOwner() public {
        Receiver receiver = new Receiver();
        address payable payee = payable(address(0x1337));
        uint256 priorPayeeBalance = payee.balance;
        nft.mintTo{value: nft.MINT_PRICE()}(address(receiver));
        uint256 nftBalance = address(nft).balance;
        nft.withdrawPayments(payee);
        assertEq(payee.balance, priorPayeeBalance + nftBalance);
    }

    function test_WithdrawalFailsAsNotOwner() public {
        Receiver receiver = new Receiver();
        nft.mintTo{value: nft.MINT_PRICE()}(address(receiver));
        assertEq(address(nft).balance, nft.MINT_PRICE());
        vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector,address(0xd3ad)));
        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;
    }
    */
 }
  • setUp() のUは大文字であることに気を付ける。(写経してて間違えた)
  • 最近のOwnableOwnableUnauthorizedAccount(address)を送信者アドレスと一緒に返すのでvm.expectRevertの引数を変更
  • stdstoreで内部の値を操作する。 https://book.getfoundry.sh/reference/forge-std/std-storage
  • vmはhttps://book.getfoundry.sh/cheatcodes/ 後でチートコードをよんでみる 
  • test_RevertUnSafeContractReceiver()がよくわかってない。 

テスト実行

root@DESKTOP-N2O3OSL:~/nft# forge test
[⠆] Compiling...
No files changed, compilation skipped

Running 9 tests for test/nft.t.sol:NFTTest
[PASS] testRevertMintToZeroAddress() (gas: 37611)
[PASS] testWithdrawWorksAsOwner() (gas: 222342)
[PASS] test_BalanceIncremented() (gas: 217272)
[PASS] test_MintPricePaid() (gas: 81302)
[PASS] test_RevertMintMaxSupplyReached() (gas: 103651)
[PASS] test_RevertMintWithoutValue() (gas: 8431)
[PASS] test_RevertUnSafeContractReceiver() (gas: 85619)
[PASS] test_SafeContractReceiver() (gas: 272598)
[PASS] test_WithdrawalFailsAsNotOwner() (gas: 192528)
Test result: ok. 9 passed; 0 failed; 0 skipped; finished in 4.62ms

Ran 1 test suites: 9 tests passed, 0 failed, 0 skipped (9 total tests)

拡張後のcastコマンド

デプロイ

 forge create --rpc-url $RPC --constructor-args "ForgeNFT" "FNFT" "https://aaa.org" --private-key $SEC1 NFT

ミント (etherを送信するひつようあり)

 cast send  $CON "mintTo(address)" $ADR2 --private-key $SEC1 --value "0.08 ether"
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1