From 826c989292e4d02b425b5cd45371d3412b3f20f9 Mon Sep 17 00:00:00 2001 From: a55678891 <122245636+a55678891@users.noreply.github.com> Date: Wed, 27 Mar 2024 07:28:38 +0800 Subject: [PATCH] Initial commit --- .github/workflows/main.yml | 39 +++ .gitmodules | 3 + README.md | 1 + hw1/.gitignore | 14 + hw1/README.md | 66 +++++ hw1/foundry.toml | 6 + hw1/lib/forge-std | 1 + hw1/script/Counter.s.sol | 12 + hw1/src/Classroom/Classroom.sol | 29 +++ hw1/src/Delegation/Delegation.sol | 25 ++ hw1/src/LiaoToken/LiaoToken.sol | 76 ++++++ hw1/src/NFinTech/NFinTech.sol | 104 ++++++++ hw1/test/Classroom/Classroom.t.sol | 110 ++++++++ hw1/test/Delegation/Delegation.t.sol | 57 +++++ hw1/test/LiaoToken/LiaoToken.t.sol | 182 +++++++++++++ hw1/test/NFinTech/NFinTech.t.sol | 368 +++++++++++++++++++++++++++ 16 files changed, 1093 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 .gitmodules create mode 100644 README.md create mode 100644 hw1/.gitignore create mode 100644 hw1/README.md create mode 100644 hw1/foundry.toml create mode 160000 hw1/lib/forge-std create mode 100644 hw1/script/Counter.s.sol create mode 100644 hw1/src/Classroom/Classroom.sol create mode 100644 hw1/src/Delegation/Delegation.sol create mode 100644 hw1/src/LiaoToken/LiaoToken.sol create mode 100644 hw1/src/NFinTech/NFinTech.sol create mode 100644 hw1/test/Classroom/Classroom.t.sol create mode 100644 hw1/test/Delegation/Delegation.t.sol create mode 100644 hw1/test/LiaoToken/LiaoToken.t.sol create mode 100644 hw1/test/NFinTech/NFinTech.t.sol diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..5a639d5 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,39 @@ +name: Foundry Testing +run-name: Homework 1 Running by ${{ github.actor }} + +on: + push: + branches: + - "**" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +env: + FOUNDRY_PROFILE: ci + WORKFLOW_NAME: Foundry Test on ${{ github.ref_name }} + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry Testing + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge tests + run: | + cd hw1 + forge test --mt test_check -vvv + id: test \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7bf6150 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "hw1/lib/forge-std"] + path = hw1/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/README.md b/README.md new file mode 100644 index 0000000..902a5e0 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# 2024-Spring-HW1 \ No newline at end of file diff --git a/hw1/.gitignore b/hw1/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/hw1/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/hw1/README.md b/hw1/README.md new file mode 100644 index 0000000..9265b45 --- /dev/null +++ b/hw1/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/hw1/foundry.toml b/hw1/foundry.toml new file mode 100644 index 0000000..25b918f --- /dev/null +++ b/hw1/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/hw1/lib/forge-std b/hw1/lib/forge-std new file mode 160000 index 0000000..ae570fe --- /dev/null +++ b/hw1/lib/forge-std @@ -0,0 +1 @@ +Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce diff --git a/hw1/script/Counter.s.sol b/hw1/script/Counter.s.sol new file mode 100644 index 0000000..df9ee8b --- /dev/null +++ b/hw1/script/Counter.s.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; + +contract CounterScript is Script { + function setUp() public {} + + function run() public { + vm.broadcast(); + } +} diff --git a/hw1/src/Classroom/Classroom.sol b/hw1/src/Classroom/Classroom.sol new file mode 100644 index 0000000..e37b973 --- /dev/null +++ b/hw1/src/Classroom/Classroom.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/* Problem 1 Interface & Contract */ +contract StudentV1 { + // Note: You can declare some state variable + + function register() external returns (uint256) { + // TODO: please add your implementaiton here + } +} + +/* Problem 2 Interface & Contract */ +interface IClassroomV2 { + function isEnrolled() external view returns (bool); +} + +contract StudentV2 { + function register() external view returns (uint256) { + // TODO: please add your implementaiton here + } +} + +/* Problem 3 Interface & Contract */ +contract StudentV3 { + function register() external view returns (uint256) { + // TODO: please add your implementaiton here + } +} diff --git a/hw1/src/Delegation/Delegation.sol b/hw1/src/Delegation/Delegation.sol new file mode 100644 index 0000000..01b76a6 --- /dev/null +++ b/hw1/src/Delegation/Delegation.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ID31eg4t3 { + function proxyCall(bytes calldata data) external returns (address); + function changeResult() external; +} + +contract Attack { + address internal immutable victim; + // TODO: Declare some variable here + // Note: Checkout the storage layout in victim contract + + constructor(address addr) payable { + victim = addr; + } + + // NOTE: You might need some malicious function here + + function exploit() external { + // TODO: Add your implementation here + // Note: Make sure you know how delegatecall works + // bytes memory data = ... + } +} diff --git a/hw1/src/LiaoToken/LiaoToken.sol b/hw1/src/LiaoToken/LiaoToken.sol new file mode 100644 index 0000000..bd1484e --- /dev/null +++ b/hw1/src/LiaoToken/LiaoToken.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); + function approve(address spender, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); +} + +contract LiaoToken is IERC20 { + // TODO: you might need to declare several state variable here + mapping(address account => uint256) private _balances; + mapping(address account => bool) isClaim; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + event Claim(address indexed user, uint256 indexed amount); + + constructor(string memory name_, string memory symbol_) payable { + _name = name_; + _symbol = symbol_; + } + + function decimals() public pure returns (uint8) { + return 18; + } + + function name() public view returns (string memory) { + return _name; + } + + function symbol() public view returns (string memory) { + return _symbol; + } + + function totalSupply() external view returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) external view returns (uint256) { + return _balances[account]; + } + + function claim() external returns (bool) { + if (isClaim[msg.sender]) revert(); + _balances[msg.sender] += 1 ether; + _totalSupply += 1 ether; + emit Claim(msg.sender, 1 ether); + return true; + } + + function transfer(address to, uint256 amount) external returns (bool) { + // TODO: please add your implementaiton here + } + + function transferFrom(address from, address to, uint256 value) external returns (bool) { + // TODO: please add your implementaiton here + } + + function approve(address spender, uint256 amount) external returns (bool) { + // TODO: please add your implementaiton here + } + + function allowance(address owner, address spender) public view returns (uint256) { + // TODO: please add your implementaiton here + } +} diff --git a/hw1/src/NFinTech/NFinTech.sol b/hw1/src/NFinTech/NFinTech.sol new file mode 100644 index 0000000..8f56b17 --- /dev/null +++ b/hw1/src/NFinTech/NFinTech.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC721 { + function balanceOf(address owner) external view returns (uint256 balance); + function ownerOf(uint256 tokenId) external view returns (address owner); + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; + function safeTransferFrom(address from, address to, uint256 tokenId) external; + function transferFrom(address from, address to, uint256 tokenId) external; + function approve(address to, uint256 tokenId) external; + function setApprovalForAll(address operator, bool approved) external; + function getApproved(uint256 tokenId) external view returns (address operator); + function isApprovedForAll(address owner, address operator) external view returns (bool); + + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); +} + +interface IERC721TokenReceiver { + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) + external + returns (bytes4); +} + +contract NFinTech is IERC721 { + // Note: I have declared all variables you need to complete this challenge + string private _name; + string private _symbol; + + uint256 private _tokenId; + + mapping(uint256 => address) private _owner; + mapping(address => uint256) private _balances; + mapping(uint256 => address) private _tokenApproval; + mapping(address => bool) private isClaim; + mapping(address => mapping(address => bool)) _operatorApproval; + + error ZeroAddress(); + + constructor(string memory name_, string memory symbol_) payable { + _name = name_; + _symbol = symbol_; + } + + function claim() public { + if (isClaim[msg.sender] == false) { + uint256 id = _tokenId; + _owner[id] = msg.sender; + + _balances[msg.sender] += 1; + isClaim[msg.sender] = true; + + _tokenId += 1; + } + } + + function name() public view returns (string memory) { + return _name; + } + + function symbol() public view returns (string memory) { + return _symbol; + } + + function balanceOf(address owner) public view returns (uint256) { + if (owner == address(0)) revert ZeroAddress(); + return _balances[owner]; + } + + function ownerOf(uint256 tokenId) public view returns (address) { + address owner = _owner[tokenId]; + if (owner == address(0)) revert ZeroAddress(); + return owner; + } + + function setApprovalForAll(address operator, bool approved) external { + // TODO: please add your implementaiton here + } + + function isApprovedForAll(address owner, address operator) public view returns (bool) { + // TODO: please add your implementaiton here + } + + function approve(address to, uint256 tokenId) external { + // TODO: please add your implementaiton here + } + + function getApproved(uint256 tokenId) public view returns (address operator) { + // TODO: please add your implementaiton here + } + + function transferFrom(address from, address to, uint256 tokenId) public { + // TODO: please add your implementaiton here + } + + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) public { + // TODO: please add your implementaiton here + } + + function safeTransferFrom(address from, address to, uint256 tokenId) public { + // TODO: please add your implementaiton here + } +} diff --git a/hw1/test/Classroom/Classroom.t.sol b/hw1/test/Classroom/Classroom.t.sol new file mode 100644 index 0000000..a333b4a --- /dev/null +++ b/hw1/test/Classroom/Classroom.t.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {Test, console} from "forge-std/Test.sol"; +import {StudentV1, StudentV2, StudentV3} from "../../src/Classroom/Classroom.sol"; + +/* Problem 1 Interface & Contract */ + +interface IStudentV1 { + function register() external returns (uint256); +} + +contract ClassroomV1 { + uint256 public code = 1000; + bool public isEnrolled; + + function enroll(address student) public { + if (IStudentV1(student).register() >= code && !isEnrolled) { + isEnrolled = true; + code = IStudentV1(student).register(); + } + } +} + +/* Problem 2 Interface & Contract */ + +interface IStudentV2 { + function register() external view returns (uint256); +} + +contract ClassroomV2 { + uint256 public code = 1000; + bool public isEnrolled; + + function enroll(address student) public { + if (IStudentV2(student).register() >= code && !isEnrolled) { + isEnrolled = true; + code = IStudentV2(student).register(); + } + } +} + +/* Problem 3 Interface & Contract */ + +interface IStudentV3 { + function register() external view returns (uint256); +} + +contract ClassroomV3 { + uint256 public code = 1000; + bool public isEnrolled; + + function enroll(address student) public { + if (IStudentV3(student).register() >= code) { + code = IStudentV3(student).register(); + } + } +} + +/* The testing contract starts here */ + +contract ClassroomTest is Test { + ClassroomV1 internal class1; + ClassroomV2 internal class2; + ClassroomV3 internal class3; + + address internal user; + + function setUp() public { + class1 = new ClassroomV1(); + class2 = new ClassroomV2(); + class3 = new ClassroomV3(); + + user = makeAddr("user"); + vm.deal(user, 1 ether); + } + + /* Problem 1 Test */ + function test_check_student_v1() public { + vm.startPrank(user); + StudentV1 student = new StudentV1(); + class1.enroll(address(student)); + vm.stopPrank(); + + assertEq(class1.code(), 123); + console.log("Get 10 points"); + } + + /* Problem 2 Test */ + function test_check_student_v2() public { + vm.startPrank(user); + StudentV2 student = new StudentV2(); + class2.enroll(address(student)); + vm.stopPrank(); + + assertEq(class2.code(), 123); + console.log("Get 10 points"); + } + + /* Problem 3 Test */ + function test_check_student_v3() public { + vm.startPrank(user); + StudentV3 student = new StudentV3(); + class3.enroll{gas: 10000 wei}(address(student)); + vm.stopPrank(); + + assertEq(class3.code(), 123); + console.log("Get 10 points"); + } +} diff --git a/hw1/test/Delegation/Delegation.t.sol b/hw1/test/Delegation/Delegation.t.sol new file mode 100644 index 0000000..16dea3e --- /dev/null +++ b/hw1/test/Delegation/Delegation.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test, console2} from "forge-std/Test.sol"; + +import {Attack} from "../../src/Delegation/Delegation.sol"; + +contract D31eg4t3 { + uint256 var0 = 12345; + uint8 var1 = 32; + string private var2; + address private var3; + uint8 private var4; + address public owner; + mapping(address => bool) public result; + + modifier onlyOwner() { + require(msg.sender == owner, "Not a Owner"); + _; + } + + constructor() { + owner = msg.sender; + } + + function proxyCall(bytes calldata data) public returns (address) { + (bool success,) = address(msg.sender).delegatecall(data); + require(success, "Delegate Failed"); + + return owner; + } +} + +contract D31eg4t3Test is Test { + D31eg4t3 internal delegate; + Attack internal attack; + address internal hacker; + + function setUp() public { + delegate = new D31eg4t3(); + attack = new Attack(address(delegate)); + hacker = makeAddr("hacker"); + } + + function test_check_exploit() public { + vm.prank(hacker, hacker); + attack.exploit(); + + bool result = delegate.result(hacker); + assertTrue(result); + + address owner = delegate.owner(); + assertEq(owner, hacker); + + console2.log("Get 10 points"); + } +} diff --git a/hw1/test/LiaoToken/LiaoToken.t.sol b/hw1/test/LiaoToken/LiaoToken.t.sol new file mode 100644 index 0000000..f431d83 --- /dev/null +++ b/hw1/test/LiaoToken/LiaoToken.t.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {LiaoToken} from "../../src/LiaoToken/LiaoToken.sol"; +import {Test, console2} from "forge-std/Test.sol"; + +/// @title Liao Token Test +/// @author Louis Tsai +/// @notice Do NOT modify this contract or you might get 0 points for the assingment. + +contract LiaoTokenTest is Test { + LiaoToken internal token; + address internal Bob; + address internal Alice; + address internal user; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + event Claim(address indexed user, uint256 indexed amount); + + function setUp() public { + token = new LiaoToken("LiaoToken", "Liao"); + Bob = makeAddr("Bob"); + Alice = makeAddr("Alice"); + user = makeAddr("user"); + + vm.prank(Bob); + vm.expectEmit(true, true, false, false); + emit Claim(Bob, 1 ether); + token.claim(); + + vm.prank(Alice); + vm.expectEmit(true, true, false, false); + emit Claim(Alice, 1 ether); + token.claim(); + } + + /* Default Tests */ + function test_decimal() public { + uint8 decimals = token.decimals(); + assertEq(decimals, 18); + } + + function test_name() public { + string memory name = token.name(); + assertEq(name, "LiaoToken"); + } + + function test_symbol() public { + string memory symbol = token.symbol(); + assertEq(symbol, "Liao"); + } + + function test_totalSupply() public { + uint256 totalSupply = token.totalSupply(); + assertEq(totalSupply, 2 ether); + } + + function testBalanceOf() public { + uint256 balance; + + balance = token.balanceOf(Bob); + assertEq(balance, 1 ether); + + balance = token.balanceOf(Alice); + assertEq(balance, 1 ether); + } + + /* PART 1: Complete transfer function -> 10 points */ + function test_transfer() public returns (bool) { + vm.prank(Bob); + vm.expectEmit(true, true, false, false); + emit Transfer(Bob, Alice, 0.5 ether); + bool success = token.transfer(Alice, 0.5 ether); + assertTrue(success); + + uint256 balance; + balance = token.balanceOf(Bob); + assertEq(balance, 0.5 ether); + + balance = token.balanceOf(Alice); + assertEq(balance, 1.5 ether); + + return true; + } + + function test_transfer_balance_not_enough() public returns (bool) { + vm.prank(Bob); + vm.expectRevert(); + token.transfer(Alice, 1.5 ether); + + return true; + } + + /* PART 2: Complete approve and allowance function -> 10 points */ + function test_approve_function() public returns (bool) { + vm.prank(Bob); + vm.expectEmit(true, true, false, false); + emit Approval(Bob, Alice, 1 ether); + bool success = token.approve(Alice, 1 ether); + assertTrue(success); + + uint256 allowance = token.allowance(Bob, Alice); + assertEq(allowance, 1 ether); + + return true; + } + + /* PART 3: Complete transferFrom function -> 10 points */ + function test_transferFrom() public returns (bool) { + bool success; + uint256 balance; + + vm.prank(Bob); + vm.expectEmit(true, true, false, false); + emit Approval(Bob, Alice, 1 ether); + success = token.approve(Alice, 1 ether); + assertTrue(success); + + vm.prank(Alice); + vm.expectEmit(true, true, false, false); + emit Transfer(Bob, user, 1 ether); + success = token.transferFrom(Bob, user, 1 ether); + assertTrue(success); + + balance = token.balanceOf(Bob); + assertEq(balance, 0); + + balance = token.balanceOf(user); + assertEq(balance, 1 ether); + + return true; + } + + function test_transferFrom_allowance_not_enough() public returns (bool) { + bool success; + + vm.prank(Bob); + vm.expectEmit(true, true, false, false); + emit Approval(Bob, Alice, 1 ether); + success = token.approve(Alice, 0.5 ether); + assertTrue(success); + + vm.prank(Alice); + vm.expectRevert(); + token.transferFrom(Alice, user, 1 ether); + + return true; + } + + function test_transferFrom_balance_not_enough() public returns (bool) { + bool success; + vm.prank(Bob); + vm.expectEmit(true, true, false, false); + emit Approval(Bob, Alice, 1 ether); + success = token.approve(Alice, 0.5 ether); + assertTrue(success); + + vm.prank(Alice); + vm.expectRevert(); + token.transferFrom(Alice, user, 2 ether); + + return true; + } + + /* We use the following parts to calculate your score */ + function test_check_transfer_points() public { + bool success = test_transfer() && test_transfer_balance_not_enough(); + if (success) console2.log("Get 10 points"); + } + + function test_check_approve_points() public { + bool success = test_approve_function(); + if (success) console2.log("Get 10 points"); + } + + function test_check_transferFrom_points() public { + bool success = + test_transferFrom() && test_transferFrom_allowance_not_enough() && test_transferFrom_balance_not_enough(); + if (success) console2.log("Get 10 points"); + } +} diff --git a/hw1/test/NFinTech/NFinTech.t.sol b/hw1/test/NFinTech/NFinTech.t.sol new file mode 100644 index 0000000..2b1c03c --- /dev/null +++ b/hw1/test/NFinTech/NFinTech.t.sol @@ -0,0 +1,368 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test, console2} from "forge-std/Test.sol"; +import "../../src/NFinTech/NFinTech.sol"; + +/// @title NFinTech Test +/// @author Louis Tsai +/// @notice Do NOT modify this contract or you might get 0 points for the assingment. + +contract NFinTechTest is Test { + NFinTech internal nft; + address internal Bob; + address internal Alice; + address internal user; + + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + function setUp() public { + nft = new NFinTech("NFinTech", "NFT"); + + user = makeAddr("user"); + + Bob = makeAddr("Bob"); + vm.prank(Bob); + nft.claim(); + + Alice = makeAddr("Alice"); + vm.prank(Alice); + nft.claim(); + } + + /* Default Tests */ + function test_name() public { + string memory name = nft.name(); + assertEq(name, "NFinTech"); + } + + function test_symbol() public { + string memory symbol = nft.symbol(); + assertEq(symbol, "NFT"); + } + + function test_balanceOf() public { + uint256 balance; + + vm.prank(Bob); + balance = nft.balanceOf(Bob); + assertEq(balance, 1); + + vm.prank(Alice); + balance = nft.balanceOf(Alice); + assertEq(balance, 1); + + vm.expectRevert(); + nft.balanceOf(address(0)); + } + + function test_ownerOf() public { + address owner; + + owner = nft.ownerOf(0); + assertEq(owner, Bob); + + owner = nft.ownerOf(1); + assertEq(owner, Alice); + + vm.expectRevert(); + nft.ownerOf(3); + } + + /* PART 1: Complete approve related function -> 10 points */ + + function test_approve() public returns (bool) { + vm.prank(Bob); + vm.expectEmit(true, true, true, false); + emit Approval(Bob, Alice, 0); + nft.approve(Alice, 0); + + address operator = nft.getApproved(0); + assertEq(operator, Alice); + + return true; + } + + function test_setApprovalForAll() public returns (bool) { + bool approved; + vm.prank(Bob); + vm.expectEmit(true, true, true, false); + emit ApprovalForAll(Bob, Alice, true); + nft.setApprovalForAll(Alice, true); + + approved = nft.isApprovedForAll(Bob, Alice); + assertEq(approved, true); + + vm.prank(Bob); + vm.expectEmit(true, true, true, false); + emit ApprovalForAll(Bob, Alice, false); + nft.setApprovalForAll(Alice, false); + + approved = nft.isApprovedForAll(Bob, Alice); + assertEq(approved, false); + + vm.prank(Bob); + vm.expectRevert(); + nft.setApprovalForAll(address(0), true); + + return true; + } + + function test_approve_not_token_owner() public returns (bool) { + vm.prank(Bob); + vm.expectRevert(); + nft.approve(Alice, 1); + return true; + } + + function test_approve_then_setApprovalForAll() public returns (bool) { + vm.prank(Bob); + vm.expectEmit(true, true, true, false); + emit ApprovalForAll(Bob, Alice, true); + nft.setApprovalForAll(Alice, true); + + vm.prank(Alice); + vm.expectEmit(true, true, true, false); + emit Approval(Bob, user, 0); + nft.approve(user, 0); + + address operator = nft.getApproved(0); + assertEq(operator, user); + + return true; + } + + /* PART 2: Complete transferFrom function -> 10 points */ + function test_transferFrom() public returns (bool) { + uint256 balance; + address owner; + + vm.prank(Bob); + nft.approve(Alice, 0); + + vm.prank(Alice); + vm.expectEmit(true, true, true, false); + emit Transfer(Bob, Alice, 0); + nft.transferFrom(Bob, Alice, 0); + + balance = nft.balanceOf(Bob); + assertEq(balance, 0); + balance = nft.balanceOf(Alice); + assertEq(balance, 2); + + owner = nft.ownerOf(0); + assertEq(owner, Alice); + owner = nft.ownerOf(1); + assertEq(owner, Alice); + + return true; + } + + function test_transferFrom_zero_address() public returns (bool) { + vm.prank(Bob); + nft.approve(Alice, 0); + + vm.prank(Alice); + vm.expectRevert(); + nft.transferFrom(Bob, address(0), 0); + + return true; + } + + function test_transferFrom_not_owner() public returns (bool) { + vm.prank(Bob); + nft.approve(Alice, 0); + + vm.prank(Alice); + vm.expectRevert(); + nft.transferFrom(Bob, address(0), 1); + + return true; + } + + /* PART 3: Complete safeTransferFrom function -> 10 points */ + function test_safeTransferFrom_eoa() public returns (bool) { + uint256 balance; + address owner; + + vm.prank(Bob); + nft.approve(Alice, 0); + + vm.prank(Alice); + vm.expectEmit(true, true, true, false); + emit Transfer(Bob, Alice, 0); + nft.transferFrom(Bob, Alice, 0); + + balance = nft.balanceOf(Bob); + assertEq(balance, 0); + balance = nft.balanceOf(Alice); + assertEq(balance, 2); + + owner = nft.ownerOf(0); + assertEq(owner, Alice); + owner = nft.ownerOf(1); + assertEq(owner, Alice); + + return true; + } + + function test_safeTransferFrom_ca_success() public returns (bool) { + uint256 balance; + address owner; + + MockSuccessReceiver receiver = new MockSuccessReceiver(); + + vm.prank(Bob); + nft.approve(Alice, 0); + + vm.prank(Alice); + vm.expectEmit(true, true, true, false); + emit Transfer(Bob, address(receiver), 0); + nft.safeTransferFrom(Bob, address(receiver), 0); + + balance = nft.balanceOf(Bob); + assertEq(balance, 0); + balance = nft.balanceOf(address(receiver)); + assertEq(balance, 1); + + owner = nft.ownerOf(0); + assertEq(owner, address(receiver)); + owner = nft.ownerOf(1); + assertEq(owner, Alice); + + return true; + } + + function test_safeTransferFrom_ca_failure() public returns (bool) { + MockBadReceiver receiver = new MockBadReceiver(); + + vm.prank(Bob); + nft.approve(Alice, 0); + + vm.prank(Alice); + vm.expectRevert(); + nft.safeTransferFrom(Bob, address(receiver), 0); + + return true; + } + + /* PART 4: Mixed operation test */ + function test_approve_then_transferFrom() public { + uint256 balance; + address owner; + + vm.prank(Bob); + nft.approve(Alice, 0); + + vm.prank(Alice); + vm.expectEmit(true, true, true, false); + emit Transfer(Bob, Alice, 0); + nft.transferFrom(Bob, Alice, 0); + + balance = nft.balanceOf(Bob); + assertEq(balance, 0); + balance = nft.balanceOf(Alice); + assertEq(balance, 2); + + owner = nft.ownerOf(0); + assertEq(owner, Alice); + owner = nft.ownerOf(1); + assertEq(owner, Alice); + } + + function test_approve_user_then_transferFrom() public { + uint256 balance; + address owner; + + vm.prank(Bob); + nft.approve(Alice, 0); + + vm.prank(Alice); + vm.expectEmit(true, true, true, false); + emit Transfer(Bob, user, 0); + nft.transferFrom(Bob, user, 0); + + balance = nft.balanceOf(Bob); + assertEq(balance, 0); + balance = nft.balanceOf(user); + assertEq(balance, 1); + + owner = nft.ownerOf(0); + assertEq(owner, user); + owner = nft.ownerOf(1); + assertEq(owner, Alice); + } + + function test_setApprovalForAll_then_transferFrom() public { + uint256 balance; + address owner; + + vm.prank(Bob); + nft.setApprovalForAll(Alice, true); + + vm.prank(Alice); + vm.expectEmit(true, true, true, false); + emit Transfer(Bob, user, 0); + nft.transferFrom(Bob, user, 0); + + balance = nft.balanceOf(Bob); + assertEq(balance, 0); + balance = nft.balanceOf(user); + assertEq(balance, 1); + + owner = nft.ownerOf(0); + assertEq(owner, user); + owner = nft.ownerOf(1); + assertEq(owner, Alice); + } + /* We use the following parts to calculate your score */ + + function test_check_approve_related_points() public { + test_approve(); + test_setApprovalForAll(); + test_approve_not_token_owner(); + test_approve_then_setApprovalForAll(); + console2.log("Get 10 points"); + } + + function test_check_transferFrom_points() public { + test_transferFrom(); + setUp(); + test_transferFrom_zero_address(); + setUp(); + test_transferFrom_not_owner(); + console2.log("Get 10 points"); + } + + function test_check_safeTransferFrom_points() public { + test_safeTransferFrom_eoa(); + setUp(); + test_safeTransferFrom_ca_failure(); + setUp(); + test_safeTransferFrom_ca_success(); + console2.log("Get 10 points"); + } + + function test_check_mix_operation() public { + test_approve_then_transferFrom(); + setUp(); + test_approve_user_then_transferFrom(); + setUp(); + test_setApprovalForAll_then_transferFrom(); + console2.log("Get 10 points"); + } +} + +contract MockSuccessReceiver is IERC721TokenReceiver { + function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) { + return IERC721TokenReceiver.onERC721Received.selector; + } +} + +contract MockBadReceiver is IERC721TokenReceiver { + function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) { + return bytes4(keccak256("approve(address,uint256)")); + } +}