# Ryan 第四週作業 Foundry ## Windows系統從零建立Foundry環境 參考資料 : https://learnblockchain.cn/docs/foundry/i18n/zh/getting-started/installation.html ### 1. 下載並安裝Rust https://www.rust-lang.org/tools/install #### ==== 如果遇到錯誤訊息 Start ==== 假如錯誤訊息如下圖所示,則需要安裝Visual Studio ![](https://i.imgur.com/fxYpmEJ.png) https://visualstudio.microsoft.com/zh-hant/downloads/ 不必安裝全部的東西,所以下載Build Tools即可 進去後往下拉至Visual Studio工具,展開並找到Build Tools for Visual Studio 2022,並點擊旁邊的下載按鈕 ![](https://i.imgur.com/EE9ax1h.png) 只需要安裝 使用C++的桌面開發、.NET桌面建置工具、通用Windows平台建置工具 ![](https://i.imgur.com/gydCket.png) 因預設路徑是放在C槽,建議可以存放於D槽 我在D槽建一個Visual Studio 2022的目錄,但Visual Studio IDE存放的目錄不能有其他檔案,所以又建立一個BuildTools的目錄給他放。 ![](https://i.imgur.com/5QJIglw.png) #### = = = = 如果遇到錯誤訊息 End = = = = 安裝選第一個預設即可 ![](https://i.imgur.com/NnUBIU3.png) ### 2. 安裝Foundry套件(cli、anvil、chisel) 參考資料 : https://github.com/foundry-rs/foundry 因檔案也是滿大的,所以也在D槽建立一個Foundry資料夾,並使用git clone進Foundry資料夾 下方為終端機(cmd)操作指令 ``` D: cd Foundry # git clone到Foundry目錄 git clone https://github.com/foundry-rs/foundry # clone完 會產生一個foundry目錄 cd foundry # 安裝install cast + forge cargo install --path ./cli --profile local --bins --locked --force # 安裝install anvil cargo install --path ./anvil --profile local --locked --force # 安裝install chisel cargo install --path ./chisel --profile local --locked --force ``` 安裝好可以使用下方指令查看版本,是否成功安裝完成 ``` forge --version 成功會跳出類似下方訊息 forge 0.2.0 (f7a535d 2023-04-03T03:06:14.931199000Z) ``` 到這邊環境全部都建置完成,可以開始建立專案了 ## 建立Foundry專案 github : https://github.com/ryan19910912/FoundryDemo ``` ## 建立一個名為FoundryDemo的foundry專案 forge init FoundryDemo ## 添加lib forge install transmissions11/solmate Openzeppelin/openzeppelin-contracts ``` ### 1. 建立一個NFTDemo.sol ```solidity= // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "solmate/tokens/ERC721.sol"; import "openzeppelin-contracts/contracts/utils/Strings.sol"; import "openzeppelin-contracts/contracts/access/Ownable.sol"; contract NFTDemo is ERC721, Ownable { using Strings for uint256; uint256 public currentTokenId; uint256 public constant TOTAL_SUPPLY = 10000; //最大數量 uint256 public constant MINT_PRICE = 10000 wei; //鑄造費用 bool private isBoxOpened = false; //盲盒控制打開與否 string private baseUri; //圖片uri string private unrevealedURI; //盲盒圖片uri constructor( string memory _name, string memory _symbol, string memory _baseUri, string memory _unrevealedURI ) ERC721(_name, _symbol) { baseUri = _baseUri; unrevealedURI = _unrevealedURI; } //打開盲盒 function openBox() external onlyOwner { isBoxOpened = true; } //關閉盲盒 function closeBox() external onlyOwner { isBoxOpened = false; } //鑄造 function mintTo(address recipient) public payable returns (uint256) { if (msg.value != MINT_PRICE) { //如果給的錢不等於鑄造費用 revert("Mint Price Not Paid"); } uint256 newTokenId = ++currentTokenId; if (newTokenId > TOTAL_SUPPLY) { //如果鑄造數量已達最大 revert("Max Supply"); } _safeMint(recipient, newTokenId); return newTokenId; } //取得base Uri function _baseURI() internal view returns (string memory) { return isBoxOpened ? baseUri : unrevealedURI; } //取得token Uri function tokenURI( uint256 _tokenId ) public view override returns (string memory) { require(_tokenId <= TOTAL_SUPPLY, "Token ID not exist!!"); string memory baseURI = _baseURI(); return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI,Strings.toString(_tokenId),".json")): ""; } //提款 function withdrawPayments(address payable payee) external onlyOwner { uint256 balance = address(this).balance; (bool transferTx, ) = payee.call{value: balance}(""); if (!transferTx) { revert("Withdraw Transfer Fail"); } } } ``` ### 2. 建立一個NFTDemoTest.sol用來測試 ```solidity= // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "../src/NFTDemo.sol"; contract NFTDemoTest is Test { using stdStorage for StdStorage; uint256 cost = 10000 wei; NFTDemo private nft; function setUp() public { //初始化建立一個NFTDemo物件 nft = new NFTDemo(vm.envString("TEST_NFT_NAME"), vm.envString("TEST_NFT_SYMBOL"), vm.envString("TEST_NFT_BASE_URI"), vm.envString("TEST_NFT_UNREVEALED_URI")); } //測試mint沒傳遞value function testFailNoMintPricePaid() public { nft.mintTo(address(1)); } //測試mint有傳遞value function testMintPricePaid() public { nft.mintTo{value: cost}(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: cost}(address(1)); } //測試mint到零地址 function testFailMintToZeroAddress() public { nft.mintTo{value: cost}(address(0)); } //測試mint給owner function testNewMintOwnerRegistered() public { nft.mintTo{value: cost}(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: cost}(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: cost}(address(1)); uint256 balanceSecondMint = uint256( vm.load(address(nft), bytes32(slotBalance)) ); assertEq(balanceSecondMint, 2); } //測試合約安全接收 function testSafeContractReceiver() public { Receiver receiver = new Receiver(); nft.mintTo{value: cost}(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: cost}(address(1)); } //測試owner提款功能 function testWithdrawalWorksAsOwner() public { // Mint an NFT, sending eth to the contract Receiver receiver = new Receiver(); address payable payee = payable(address(0x1337)); uint256 priorPayeeBalance = payee.balance; nft.mintTo{value: nft.MINT_PRICE()}(address(receiver)); // Check that the balance of the contract is correct assertEq(address(nft).balance, nft.MINT_PRICE()); uint256 nftBalance = address(nft).balance; // Withdraw the balance and assert it was transferred nft.withdrawPayments(payee); assertEq(payee.balance, priorPayeeBalance + nftBalance); } //測試非owner提款功能 function testWithdrawalFailsAsNotOwner() public { // Mint an NFT, sending eth to the contract Receiver receiver = new Receiver(); nft.mintTo{value: nft.MINT_PRICE()}(address(receiver)); // Check that the balance of the contract is correct assertEq(address(nft).balance, nft.MINT_PRICE()); // Confirm that a non-owner cannot withdraw 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; } } ``` ### 2. 使用指令進行測試 ``` forge test --gas-report ``` ![](https://i.imgur.com/5ccr8uT.png) ## 作業基本題 ### 1. 連接本地節點 ``` //連接本地節點指令 anvil ``` ![](https://i.imgur.com/y64tDST.png) ### 2. 在終端機建立環境變數 ``` //節點 $Env:RPC_URL="http://127.0.0.1:8545" //私鑰 $Env:PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" //NFT名稱 $Env:TEST_NFT_NAME="RyanNFTDemo" //NFT縮寫 $Env:TEST_NFT_SYMBOL="RND" //NFT盲盒開啟網址 $Env:TEST_NFT_BASE_URI="https://gateway.pinata.cloud/ipfs/QmRLxwSkzJskXcjpq7t4Ve6mmvdkWPVZfWaCp6Nhs14KrU/" //NFT盲盒關閉網址 $Env:TEST_NFT_UNREVEALED_URI="https://gateway.pinata.cloud/ipfs/QmbCfG8EFsAisCMZGr9unhUktUPyUNS52TcHcMc5b8bwbh/" ``` ![](https://i.imgur.com/P8I0O3X.png) 建立一個目錄deploy,用來存放deploy用的bat檔 在deploy目錄內建立一個deploy_NFT_0.bat ``` @echo off forge create NFTDemo --rpc-url=%RPC_URL% --private-key=%PRIVATE_KEY% --constructor-args %TEST_NFT_NAME% %TEST_NFT_SYMBOL% %TEST_NFT_BASE_URI% %TEST_NFT_UNREVEALED_URI%; exit ``` ![](https://i.imgur.com/58Cdjo6.png) 執行deploy_NFT_0.bat ``` .\deploy\deploy_NFT_0.bat ``` ![](https://i.imgur.com/ilOsXSD.png) ![](https://i.imgur.com/5ZXPrGx.png) ### 3. 在本地節點鑄造NFT 指令參考 : https://book.getfoundry.sh/reference/cast/cast-send 建立一個deploy_NFT_mint.bat ``` @echo off cast send --rpc-url=%RPC_URL% %TEST_NFT_CONTRACT_ADDRESS% "mintTo(address)" %TEST_NFT_SEND_ADDRESS% --value %TEST_NFT_SEND_VALUE% --private-key=%PRIVATE_KEY% exit ``` 把本地節點的NFT合約地址建立在環境變數 ``` //合約地址 $Env:TEST_NFT_CONTRACT_ADDRESS="0x5fbdb2315678afecb367f032d93f642f64180aa3" //鑄造對象地址 $Env:TEST_NFT_SEND_ADDRESS="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" //鑄造的金額 $Env:TEST_NFT_SEND_VALUE="10000wei" ``` 執行deploy_NFT_mint.bat ![](https://i.imgur.com/uEq3Zwx.png) ![](https://i.imgur.com/Cj4I7um.png) ### 4. 查看NFT tokenId對應的擁有者地址 建立一個deploy_NFT_select.bat ``` @echo off cast call --rpc-url=%RPC_URL% %TEST_NFT_CONTRACT_ADDRESS% "ownerOf(uint256)" %TEST_NFT_TOKEN_ID% --private-key=%PRIVATE_KEY% exit ``` 把要查找的Token ID建立在環境變數 ``` //查找的Token ID $Env:TEST_NFT_TOKEN_ID="3" ``` 執行deploy_NFT_select.bat ![](https://i.imgur.com/f0dGU0b.png) ## 作業進階題 ### 1. 建立測試鏈的環境變數 ``` //goerli測試鏈 rpc url $Env:RPC_URL="https://rpc.ankr.com/eth_goerli" //小狐狸錢包的private key $Env:PRIVATE_KEY="" //NFT名稱 $Env:TEST_NFT_NAME="RyanNFTDemo" //NFT縮寫 $Env:TEST_NFT_SYMBOL="RND" //NFT盲盒開啟時的URL $Env:TEST_NFT_BASE_URI="https://gateway.pinata.cloud/ipfs/QmRLxwSkzJskXcjpq7t4Ve6mmvdkWPVZfWaCp6Nhs14KrU/" //NFT盲盒關閉時的URL $Env:TEST_NFT_UNREVEALED_URI="https://gateway.pinata.cloud/ipfs/QmbCfG8EFsAisCMZGr9unhUktUPyUNS52TcHcMc5b8bwbh/" ``` ==== 取得小狐狸錢包的Private Key步驟 ==== ![](https://i.imgur.com/wJ70h59.png) ![](https://i.imgur.com/SLdPM5d.png) ### 2. 在測試鏈發佈合約 執行deploy_NFT_0.bay ![](https://i.imgur.com/NQWLQaa.png) ![](https://i.imgur.com/9NSXWKy.png) ![](https://i.imgur.com/8Qa6Xtb.png) 把合約地址建立在變數環境 ``` //合約地址 $Env:TEST_NFT_CONTRACT_ADDRESS="0x28fC7124652602ee91afb57c83f175Fde0b2F50F" //鑄造對象地址 $Env:TEST_NFT_SEND_ADDRESS="0x7A4D6c296B28460cda81Fb584234A15Fc105e182" //鑄造金額 $Env:TEST_NFT_SEND_VALUE="10000wei" ``` ### 3. 鑄造NFT 執行deploy_NFT_mint.bat ![](https://i.imgur.com/oILkics.png) ![](https://i.imgur.com/116fxxn.png) ### 4. 查看OpenSea測試網 https://testnets.opensea.io/zh-TW/assets/goerli/0x28fc7124652602ee91afb57c83f175fde0b2f50f/1 ![](https://i.imgur.com/6pSX1Qk.png) ### 5. 發現圖片沒出來,進行Debug 先查詢Token Uri是否正確 把要查詢的Token ID放入環境變數 ``` //查詢的Token ID $Env:TEST_NFT_TOKEN_ID="1" ``` 執行deploy_NFT_token_uri.bat ![](https://i.imgur.com/mlhgyCt.png) token uri 不正確,多個 ; 符號https://gateway.pinata.cloud/ipfs/QmbCfG8EFsAisCMZGr9unhUktUPyUNS52TcHcMc5b8bwbh/;1.json ### 6. 開啟盲盒 建立deploy_NFT_openBox.bat ``` @echo off cast send --rpc-url=%RPC_URL% %TEST_NFT_CONTRACT_ADDRESS% "openBox()" --private-key=%PRIVATE_KEY% exit ``` 執行deploy_NFT_openBox.bat ![](https://i.imgur.com/XwSPHVQ.png) 查看token uri是否改變 執行deploy_NFT_token_uri.bat ![](https://i.imgur.com/7CxEQI1.png) toke uri已成功改變,並正確 https://gateway.pinata.cloud/ipfs/QmRLxwSkzJskXcjpq7t4Ve6mmvdkWPVZfWaCp6Nhs14KrU/1.json ### 7. 查看OpenSea測試網盲盒是否開啟 盲盒成功開啟 ![](https://i.imgur.com/IObwY6C.png) ###### tags: `Solidity 工程師實戰營第 5 期`