[SCH] Smart Contract Hacking 4편 - NFT1

0xDave·2023년 3월 21일
0

Ethereum

목록 보기
96/112

Foundry repo


3agle이라는 분이 foundry로 수정된 예제들을 올려줬다. foundry에 목말라 있었는데 너무 잘 됐다는 생각에 감사를 표했다. 가보자고!


Task1


// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract MyToken is ERC721 {
    constructor() ERC721("EthRock", "ROCK") {}

    uint256 public constant MAX_SUPPLY = 10000;
    uint256 public constant MINT_PRICE = 0.1 ether;
    uint256 public counter;
  
    function mint(address to, uint256 tokenId) external payable {
        require(counter < MAX_SUPPLY, "Can't exceed MAX_SUPPLY");
        require(msg.value >= MINT_PRICE, "Lack of fund");
        _mint(to, tokenId);
        counter++;
    }
}

요런 식으로 작성했다. 다 작성하고 보니 누구나 민팅할 수 있지만 address를 파라미터로 넘겨야 하는 불편함이 있었다. tokenId를 굳이 입력할 필요도 없었다.


수정본은 다음과 같다.

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract MyToken is ERC721 {
    constructor() ERC721("EthRock", "ROCK") {}

    uint256 public constant MAX_SUPPLY = 10000;
    uint256 public constant MINT_PRICE = 0.1 ether;
    uint256 public currentTokenId;
  
    function mint() external payable {
        require(currentTokenId < MAX_SUPPLY, "Can't exceed MAX_SUPPLY");
        require(msg.value >= MINT_PRICE, "Lack of fund");
        _mint(msg.sender, currentTokenId);
        currentTokenId++;
    }
}

1차 피드백


편의상 Re-entrancy guard를 안 했지만 그래도 신경써야 한다. mint() 함수 내부에서 counting 하는 부분이 _mint() 보다 앞에 있는 것이 더 안전하다. 따라서 다음과 같이 바꾸면 좋을 것 같다.

    function mint() external payable {
        require(currentTokenId < MAX_SUPPLY, "Can't exceed MAX_SUPPLY");
        require(msg.value >= MINT_PRICE, "Lack of fund");
        currentTokenId++;
        _mint(msg.sender, currentTokenId);
    }

Task2


제공되는 기본 틀은 다음과 같다. 빈칸을 채워보자!

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

import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../../src/erc721-1/MyNFT.sol";

/**
@dev run "forge test --match-contract ERC7211" 
*/

contract TestERC7211 is Test {
    MyNFT public myNft;

    address deployer;
    address user1;
    address user2;

    function setUp() public {
        deployer = address(1);
        user1 = address(2);
        user2 = address(3);

        vm.deal(deployer, 1 ether);
        vm.deal(user1, 1 ether);

        myNft = new MyNFT();
    }

    function test() public {
        // TODO: Deployer mints

        // TODO: Transfering tokenId 6 from user1 to user2

        // TODO: Checking that user2 owns tokenId 6

        // TODO: Deployer approves User1 to spend tokenId 3

        // TODO: Test that User1 has approval to spend TokenId3

        // TODO: Use approval and transfer tokenId 3 from deployer to User1

        // TODO: Checking that user1 owns tokenId 3

        // TODO: Checking balances after transfer

    }
}

foundry는 매우 직관적이라서 비교적 간단하게 작성할 수 있었다. 물론 틀린 부분을 체크해봐야겠지만..

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

import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../../src/erc721-1/MyNFT.sol";

/**
@dev run "forge test --match-contract ERC7211" 
*/

contract TestERC7211 is Test {
    MyNFT public myNft;

    address deployer;
    address user1;
    address user2;

    function setUp() public {
        deployer = address(1);
        user1 = address(2);
        user2 = address(3);

        vm.deal(deployer, 1 ether);
        vm.deal(user1, 1 ether);

        myNft = new MyNFT();
    }

    function test() public {
        // TODO: Deployer mints
        vm.prank(deployer);
        for (uint256 i = 0; i < 5; i++) {
            myNft.mint();
        }

        vm.prank(user1);
        for (uint256 i = 0; i < 3; i++) {
            myNft.mint();
        }

        // TODO: Transfering tokenId 6 from user1 to user2
        vm.prank(user1);
        myNft.transferFrom(user1, user2, 6);

        // TODO: Checking that user2 owns tokenId 6
        assertEq(user2, myNft.ownerOf(6));

        // TODO: Deployer approves User1 to spend tokenId 3
        vm.prank(deployer);
        myNft.approve(user1, 3);

        // TODO: Test that User1 has approval to spend TokenId3
        assertEq(user1, myNft.getApproved(3));

        // TODO: Use approval and transfer tokenId 3 from deployer to User1
        vm.prank(user1);
        myNft.safeTransferFrom(deployer, user1, 3);

        // TODO: Checking that user1 owns tokenId 3
        assertEq(user1, myNft.ownerOf(3));

        // TODO: Checking balances after transfer
        assertEq(myNft.balanceOf(deployer), 4);
        assertEq(myNft.balanceOf(user1), 3);
        assertEq(myNft.balanceOf(user2), 1);
    }
}

아래 명령어로 테스트를 돌려보자.

forge test --match-contract ERC7211 -vvvv    

는 실패 😇

민팅할 때 단순히 mint() 함수를 호출만 해서 그런 것 같다. 아래와 같이 value를 넣어서 함수를 호출할 수 있다.

        vm.prank(deployer);
        for (uint256 i = 0; i < 5; i++) {
            myNft.mint{value: 0.1 ether}();
        }

        vm.prank(user1);
        for (uint256 i = 0; i < 3; i++) {
            myNft.mint{value: 0.1 ether}();
        }

그런데 또 fail이 났다. 자세히 보니 민팅한 NFT들이 user한테 가는게 아니라 컨트랙트한테 가는 걸 알 수 있다(?!)

알고보니 for문 안에 vm.prank()도 같이 넣어줘야 했었다..

이후 테스트 성공!

역시 foundry가 보기도 편하고 깔끔하다.

profile
Just BUIDL :)

0개의 댓글