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

こちらの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);
	}
}
```
- Best Practice(https://book.getfoundry.sh/tutorials/best-practices)には importするときは全部ファイルをインポートするなと記載があったので、その部分は変更した。
- ビルドする。
```
forge build
```
### デプロイ 
- ローカルテストノード起動
( address, keyをメモする)
```
anvil
```
- デプロイ 
(環境変数`$SEC1`にプライベートキー、`$PRC`に `http://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`, `symbol`は`constructor`で指定してしているだけなので、これはあとで変更できないけど、なんか意味あるのかよくわからない。
- `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>` でトランザクションをみても記載はない。
- `event`と`emit`を使用してトランザクションログに`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
```
一個ミントしてこのアドレスに送付したので現在は１個。複数ミントして送付するとその数だけ増えたことを確認した。他の関数も`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は大文字であることに気を付ける。(写経してて間違えた）
- 最近の`Ownable`は`OwnableUnauthorizedAccount(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"
```