commit b9308fd5c317e557dd3bd2f2f4e1f45aed29237f Author: Daniel Perez Date: Thu Feb 2 20:14:18 2023 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/.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/.gitmodules b/.gitmodules new file mode 100644 index 0000000..888d42d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = 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..c97b355 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Principle of Distributed Ledgers, Tutorial 2 + +The goal of this tutorial is to implement an ERC-20 token and an ERC-721 token that can be bought using the ERC-20 token. + +Please refer to the full tutorial for more details. diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..dcfcb1d --- /dev/null +++ b/foundry.toml @@ -0,0 +1,5 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['lib'] +solc = "0.8.17" diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..4a79aca --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 4a79aca83f8075f8b1b4fe9153945fef08375630 diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..845bd0a --- /dev/null +++ b/remappings.txt @@ -0,0 +1,2 @@ +ds-test/=lib/forge-std/lib/ds-test/src/ +forge-std/=lib/forge-std/src/ diff --git a/src/ERC20.sol b/src/ERC20.sol new file mode 100644 index 0000000..473ffab --- /dev/null +++ b/src/ERC20.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "./interfaces/IERC20.sol"; + +contract ERC20 is IERC20Metadata { + constructor (string memory name, string memory symbol) {} + + function name() external view returns (string memory) {} + + function symbol() external view returns (string memory) {} + + function decimals() external view returns (uint8) {} + + function totalSupply() external view returns (uint256) {} + + function balanceOf(address account) external view returns (uint256) {} + + function transfer(address to, uint256 amount) external returns (bool) {} + + function allowance(address owner, address spender) + external + view + returns (uint256) + {} + + function approve(address spender, uint256 amount) external returns (bool) {} + + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool) {} + + function mint(address to, uint256 amount) external {} +} diff --git a/src/ERC721.sol b/src/ERC721.sol new file mode 100644 index 0000000..a92845d --- /dev/null +++ b/src/ERC721.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "./interfaces/IERC20.sol"; +import "./interfaces/IERC721.sol"; + +contract ERC721 is IERC721Metadata { + constructor( + string memory name, + string memory symbol, + string memory baseUri, + IERC20 paymentToken, + uint256 initialTokenPrice + ) {} + + function mint(address to) external {} + + function balanceOf(address _owner) external view returns (uint256) {} + + function ownerOf(uint256 _tokenId) external view returns (address) {} + + function transferFrom( + address _from, + address _to, + uint256 _tokenId + ) external {} + + function approve(address _approved, uint256 _tokenId) external {} + + function getApproved(uint256 _tokenId) external view returns (address) {} + + function name() external view returns (string memory _name) {} + + function symbol() external view returns (string memory _symbol) {} + + function tokenURI(uint256 _tokenId) external view returns (string memory) {} + + // Bonus functions + + function supportsInterface(bytes4 interfaceID) + external + view + returns (bool) + {} + + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId, + bytes memory data + ) external {} + + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId + ) external {} + + function setApprovalForAll(address _operator, bool _approved) external {} + + function isApprovedForAll(address _owner, address _operator) + external + view + returns (bool) + {} +} diff --git a/src/interfaces/IERC165.sol b/src/interfaces/IERC165.sol new file mode 100644 index 0000000..aba8a50 --- /dev/null +++ b/src/interfaces/IERC165.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +interface IERC165 { + /// @notice Query if a contract implements an interface + /// @param interfaceID The interface identifier, as specified in ERC-165 + /// @dev Interface identification is specified in ERC-165. This function + /// uses less than 30,000 gas. + /// @return `true` if the contract implements `interfaceID` and + /// `interfaceID` is not 0xffffffff, `false` otherwise + function supportsInterface(bytes4 interfaceID) external view returns (bool); +} diff --git a/src/interfaces/IERC20.sol b/src/interfaces/IERC20.sol new file mode 100644 index 0000000..914f71c --- /dev/null +++ b/src/interfaces/IERC20.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) + external + view + returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} + +/** + * @dev Interface for the optional metadata functions from the ERC20 standard. + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} diff --git a/src/interfaces/IERC721.sol b/src/interfaces/IERC721.sol new file mode 100644 index 0000000..8d7e5e5 --- /dev/null +++ b/src/interfaces/IERC721.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "./IERC165.sol"; + +/// @title ERC-721 Non-Fungible Token Standard +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +interface IERC721 is IERC165 { + /// @dev This emits when ownership of any NFT changes by any mechanism. + /// This event emits when NFTs are created (`from` == 0) and destroyed + /// (`to` == 0). Exception: during contract creation, any number of NFTs + /// may be created and assigned without emitting Transfer. At the time of + /// any transfer, the approved address for that NFT (if any) is reset to none. + event Transfer( + address indexed _from, + address indexed _to, + uint256 indexed _tokenId + ); + + /// @dev This emits when the approved address for an NFT is changed or + /// reaffirmed. The zero address indicates there is no approved address. + /// When a Transfer event emits, this also indicates that the approved + /// address for that NFT (if any) is reset to none. + event Approval( + address indexed _owner, + address indexed _approved, + uint256 indexed _tokenId + ); + + /// @dev This emits when an operator is enabled or disabled for an owner. + /// The operator can manage all NFTs of the owner. + event ApprovalForAll( + address indexed _owner, + address indexed _operator, + bool _approved + ); + + /// @notice Count all NFTs assigned to an owner + /// @dev NFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param _owner An address for whom to query the balance + /// @return The number of NFTs owned by `_owner`, possibly zero + function balanceOf(address _owner) external view returns (uint256); + + /// @notice Find the owner of an NFT + /// @dev NFTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// @param _tokenId The identifier for an NFT + /// @return The address of the owner of the NFT + function ownerOf(uint256 _tokenId) external view returns (address); + + /// @notice Transfers the ownership of an NFT from one address to another address + /// @dev Throws unless `msg.sender` is the current owner, an authorized + /// operator, or the approved address for this NFT. Throws if `_from` is + /// not the current owner. Throws if `_to` is the zero address. Throws if + /// `_tokenId` is not a valid NFT. When transfer is complete, this function + /// checks if `_to` is a smart contract (code size > 0). If so, it calls + /// `onERC721Received` on `_to` and throws if the return value is not + /// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + /// @param data Additional data with no specified format, sent in call to `_to` + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId, + bytes memory data + ) external; + + /// @notice Transfers the ownership of an NFT from one address to another address + /// @dev This works identically to the other function with an extra data parameter, + /// except this function just sets data to "". + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId + ) external; + + /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + /// TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE + /// THEY MAY BE PERMANENTLY LOST + /// @dev Throws unless `msg.sender` is the current owner, an authorized + /// operator, or the approved address for this NFT. Throws if `_from` is + /// not the current owner. Throws if `_to` is the zero address. Throws if + /// `_tokenId` is not a valid NFT. + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function transferFrom( + address _from, + address _to, + uint256 _tokenId + ) external; + + /// @notice Change or reaffirm the approved address for an NFT + /// @dev The zero address indicates there is no approved address. + /// Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param _approved The new approved NFT controller + /// @param _tokenId The NFT to approve + function approve(address _approved, uint256 _tokenId) external; + + /// @notice Enable or disable approval for a third party ("operator") to manage + /// all of `msg.sender`'s assets + /// @dev Emits the ApprovalForAll event. The contract MUST allow + /// multiple operators per owner. + /// @param _operator Address to add to the set of authorized operators + /// @param _approved True if the operator is approved, false to revoke approval + function setApprovalForAll(address _operator, bool _approved) external; + + /// @notice Get the approved address for a single NFT + /// @dev Throws if `_tokenId` is not a valid NFT. + /// @param _tokenId The NFT to find the approved address for + /// @return The approved address for this NFT, or the zero address if there is none + function getApproved(uint256 _tokenId) external view returns (address); + + /// @notice Query if an address is an authorized operator for another address + /// @param _owner The address that owns the NFTs + /// @param _operator The address that acts on behalf of the owner + /// @return True if `_operator` is an approved operator for `_owner`, false otherwise + function isApprovedForAll(address _owner, address _operator) + external + view + returns (bool); +} + +/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// Note: the ERC-165 identifier for this interface is 0x5b5e139f. +interface IERC721Metadata is IERC721 { + /// @notice A descriptive name for a collection of NFTs in this contract + function name() external view returns (string memory _name); + + /// @notice An abbreviated name for NFTs in this contract + function symbol() external view returns (string memory _symbol); + + /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. + /// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC + /// 3986. The URI may point to a JSON file that conforms to the "ERC721 + /// Metadata JSON Schema". + function tokenURI(uint256 _tokenId) external view returns (string memory); +} diff --git a/src/interfaces/IERC721TokenReceiver.sol b/src/interfaces/IERC721TokenReceiver.sol new file mode 100644 index 0000000..b0eb0a5 --- /dev/null +++ b/src/interfaces/IERC721TokenReceiver.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +/// @dev Note: the ERC-165 identifier for this interface is 0x150b7a02. +interface IERC721TokenReceiver { + /// @notice Handle the receipt of an NFT + /// @dev The ERC721 smart contract calls this function on the recipient + /// after a `transfer`. This function MAY throw to revert and reject the + /// transfer. Return of other than the magic value MUST result in the + /// transaction being reverted. + /// Note: the contract address is always the message sender. + /// @param _operator The address which called `safeTransferFrom` function + /// @param _from The address which previously owned the token + /// @param _tokenId The NFT identifier which is being transferred + /// @param _data Additional data with no specified format + /// @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` + /// unless throwing + function onERC721Received( + address _operator, + address _from, + uint256 _tokenId, + bytes calldata _data + ) external returns (bytes4); +} diff --git a/src/libraries/StringUtils.sol b/src/libraries/StringUtils.sol new file mode 100644 index 0000000..30dd8ce --- /dev/null +++ b/src/libraries/StringUtils.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +library StringUtils { + function toString(uint256 n) internal pure returns (string memory) { + if (n == 0) { + return "0"; + } + uint256 len; + for (uint256 j = n; j != 0; j /= 10) { + len++; + } + bytes memory res = new bytes(len); + for (uint256 k = len; n != 0; (n /= 10, k--)) { + res[k - 1] = bytes1(uint8(48 + (n % 10))); // '0' = 48 + } + return string(res); + } +} diff --git a/test/ERC20.t.sol b/test/ERC20.t.sol new file mode 100644 index 0000000..4dbd89f --- /dev/null +++ b/test/ERC20.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import "../src/ERC20.sol"; + +contract ERC20Test is Test { + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event Approval( + address indexed _owner, + address indexed _spender, + uint256 _value + ); + + ERC20 public token; + + address public alice = makeAddr("alice"); + address public bob = makeAddr("bob"); + address public charlie = makeAddr("charlie"); + + function setUp() public { + token = new ERC20("Test token", "TST"); + } + + function testName() public { + assertEq(token.name(), "Test token"); + } + + function testSymbol() public { + assertEq(token.symbol(), "TST"); + } + + function testDecimals() public { + assertEq(token.decimals(), 18); + } + + function testMintAsMinter() public { + assertEq(token.balanceOf(alice), 0); + assertEq(token.totalSupply(), 0); + token.mint(alice, 1e19); + assertEq(token.balanceOf(alice), 1e19); + assertEq(token.totalSupply(), 1e19); + } + + function testMintAsNonMinter() public { + vm.prank(alice); + vm.expectRevert("only minter can mint"); + token.mint(alice, 1e19); + } + + function testTransfer() public { + token.mint(alice, 1e19); + vm.prank(alice); + vm.expectEmit(true, true, false, true); + emit Transfer(alice, bob, 1e18); + token.transfer(bob, 1e18); + assertEq(token.balanceOf(alice), 9e18); + assertEq(token.balanceOf(bob), 1e18); + } + + function testTransferInsufficientFunds() public { + token.mint(alice, 1e19); + vm.prank(alice); + vm.expectRevert("insufficient balance"); + token.transfer(bob, 2e19); + } + + function testApprove() public { + vm.prank(alice); + vm.expectEmit(true, true, false, true); + emit Approval(alice, bob, 1e18); + token.approve(bob, 1e18); + assertEq(token.allowance(alice, bob), 1e18); + } + + function testTransferFrom() public { + token.mint(alice, 1e19); + vm.prank(alice); + token.approve(bob, 3e18); + vm.prank(bob); + vm.expectEmit(true, true, false, true); + emit Transfer(alice, charlie, 1e18); + token.transferFrom(alice, charlie, 1e18); + assertEq(token.balanceOf(alice), 9e18); + assertEq(token.allowance(alice, bob), 2e18); + assertEq(token.balanceOf(bob), 0); + assertEq(token.balanceOf(charlie), 1e18); + } + + function testTransferFromInsufficientAllowance() public { + token.mint(alice, 1e19); + vm.prank(alice); + token.approve(bob, 3e18); + vm.prank(bob); + vm.expectRevert("insufficient allowance"); + token.transferFrom(alice, charlie, 4e18); + } +} diff --git a/test/ERC721.t.sol b/test/ERC721.t.sol new file mode 100644 index 0000000..c8d1a85 --- /dev/null +++ b/test/ERC721.t.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import "../src/ERC20.sol"; +import "../src/ERC721.sol"; +import "../src/interfaces/IERC721TokenReceiver.sol"; + +contract CompliantReceiver is IERC721TokenReceiver { + bytes4 internal constant _MAGIC_VALUE = + bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); + + function onERC721Received( + address, + address, + uint256, + bytes memory + ) public pure returns (bytes4) { + return _MAGIC_VALUE; + } +} + +contract ReceiverWithoutFunction {} + +contract ReceiverWithWrongReturnValue is IERC721TokenReceiver { + function onERC721Received( + address, + address, + uint256, + bytes memory + ) public pure returns (bytes4) { + return bytes4(uint32(1)); + } +} + +contract BaseERC721Test is Test { + 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 + ); + + ERC20 public paymentToken; + ERC721 public nft; + + uint256 public nftPrice = 1e19; + + address public alice = makeAddr("alice"); + address public bob = makeAddr("bob"); + address public charlie = makeAddr("charlie"); + + function setUp() public { + paymentToken = new ERC20("Test token", "TST"); + nft = new ERC721( + "Test NFT", + "TSN", + "http://example.com/nft/", + paymentToken, + nftPrice + ); + } + + function _mintNFT(address owner) internal { + vm.prank(address(this)); + paymentToken.mint(owner, nftPrice); + vm.prank(owner); + paymentToken.approve(address(nft), nftPrice); + vm.prank(owner); + nft.mint(owner); + } +} + +contract ERC721Test is BaseERC721Test { + function testName() public { + assertEq(nft.name(), "Test NFT"); + } + + function testSymbol() public { + assertEq(nft.symbol(), "TSN"); + } + + function testTokenURI() public { + assertEq(nft.tokenURI(0), "http://example.com/nft/0"); + assertEq(nft.tokenURI(1), "http://example.com/nft/1"); + assertEq(nft.tokenURI(9), "http://example.com/nft/9"); + assertEq(nft.tokenURI(10), "http://example.com/nft/10"); + assertEq(nft.tokenURI(11), "http://example.com/nft/11"); + assertEq(nft.tokenURI(23), "http://example.com/nft/23"); + assertEq(nft.tokenURI(99), "http://example.com/nft/99"); + assertEq(nft.tokenURI(100), "http://example.com/nft/100"); + assertEq(nft.tokenURI(101), "http://example.com/nft/101"); + assertEq(nft.tokenURI(158), "http://example.com/nft/158"); + assertEq(nft.tokenURI(3874), "http://example.com/nft/3874"); + assertEq(nft.tokenURI(9999), "http://example.com/nft/9999"); + assertEq(nft.tokenURI(10000), "http://example.com/nft/10000"); + } + + function testSuccessfulMint() public { + paymentToken.mint(alice, nftPrice); + vm.startPrank(alice); + paymentToken.approve(address(nft), nftPrice); + vm.expectEmit(true, true, true, false); + emit Transfer(address(0), alice, 1); + nft.mint(alice); + assertEq(nft.ownerOf(1), alice); + assertEq(nft.balanceOf(alice), 1); + assertEq(paymentToken.balanceOf(alice), 0); + } + + function testMintWithInsufficientBalance() public { + vm.startPrank(alice); + paymentToken.approve(address(nft), nftPrice); + vm.expectRevert("insufficient balance"); + nft.mint(alice); + } + + function testMintWithInsufficientAllowance() public { + paymentToken.mint(alice, nftPrice); + vm.startPrank(alice); + vm.expectRevert("insufficient allowance"); + nft.mint(alice); + } + + function testSuccessfulMints() public { + paymentToken.mint(alice, 1e20); + vm.startPrank(alice); + paymentToken.approve(address(nft), nftPrice * 10); + nft.mint(alice); + vm.expectEmit(true, true, true, false); + emit Transfer(address(0), alice, 2); + nft.mint(alice); + assertEq(nft.ownerOf(2), alice); + assertEq(nft.balanceOf(alice), 2); + assertEq( + paymentToken.balanceOf(alice), + 1e20 - nftPrice - (nftPrice * 11) / 10 + ); + } + + function testApprove() public { + _mintNFT(alice); + vm.startPrank(alice); + vm.expectEmit(true, true, true, false); + emit Approval(alice, charlie, 1); + nft.approve(charlie, 1); + assertEq(nft.getApproved(1), charlie); + nft.approve(address(0), 1); + assertEq(nft.getApproved(1), address(0)); + } + + function testApproveUnauthorized() public { + vm.startPrank(bob); + vm.expectRevert("not authorized"); + nft.approve(charlie, 1); + } + + function testTransferFromOwner() public { + _mintNFT(alice); + vm.prank(alice); + vm.expectEmit(true, true, true, false); + emit Transfer(alice, bob, 1); + nft.transferFrom(alice, bob, 1); + assertEq(nft.ownerOf(1), bob); + } + + function testTransferFromApproved() public { + _mintNFT(alice); + vm.prank(alice); + nft.approve(charlie, 1); + vm.prank(charlie); + vm.expectEmit(true, true, true, false); + emit Transfer(alice, bob, 1); + nft.transferFrom(alice, bob, 1); + assertEq(nft.ownerOf(1), bob); + } + + function testTransferFromManager() public { + _mintNFT(alice); + vm.prank(alice); + nft.setApprovalForAll(charlie, true); + vm.prank(charlie); + vm.expectEmit(true, true, true, false); + emit Transfer(alice, bob, 1); + nft.transferFrom(alice, bob, 1); + assertEq(nft.ownerOf(1), bob); + } +} + +contract BonusERC721Test is BaseERC721Test { + function testSafeTransferToEOA() public { + _mintNFT(alice); + vm.prank(alice); + nft.setApprovalForAll(charlie, true); + vm.prank(charlie); + vm.expectEmit(true, true, true, false); + emit Transfer(alice, bob, 1); + nft.safeTransferFrom(alice, bob, 1); + assertEq(nft.ownerOf(1), bob); + } + + function testSafeTransferToCompliantContract() public { + _mintNFT(alice); + address receiver = address(new CompliantReceiver()); + vm.prank(alice); + nft.safeTransferFrom(alice, receiver, 1); + assertEq(nft.ownerOf(1), receiver); + } + + function testSafeTransferToContractMissingFunction() public { + _mintNFT(alice); + address receiver = address(new ReceiverWithoutFunction()); + vm.prank(alice); + vm.expectRevert(); + nft.safeTransferFrom(alice, receiver, 1); + } + + function testSafeTransferToContractWrongReturnValue() public { + _mintNFT(alice); + address receiver = address(new ReceiverWithWrongReturnValue()); + vm.prank(alice); + vm.expectRevert("magic value not returned"); + nft.safeTransferFrom(alice, receiver, 1); + } + + function testSetApproveAll() public { + vm.prank(alice); + vm.expectEmit(true, true, false, true); + emit ApprovalForAll(alice, charlie, true); + nft.setApprovalForAll(charlie, true); + assertTrue(nft.isApprovedForAll(alice, charlie)); + } + + function testSetApproveAllTwice() public { + vm.startPrank(alice); + vm.expectEmit(true, true, false, true); + emit ApprovalForAll(alice, charlie, true); + nft.setApprovalForAll(charlie, true); + assertTrue(nft.isApprovedForAll(alice, charlie)); + nft.setApprovalForAll(charlie, true); + assertTrue(nft.isApprovedForAll(alice, charlie)); + } + + function testSetApproveAllFalseAfterSingleAdd() public { + vm.startPrank(alice); + nft.setApprovalForAll(charlie, true); + assertTrue(nft.isApprovedForAll(alice, charlie)); + nft.setApprovalForAll(charlie, false); + assertFalse(nft.isApprovedForAll(alice, charlie)); + } + + function testSetApproveAllFalseAfterTwoAdd() public { + vm.startPrank(alice); + nft.setApprovalForAll(charlie, true); + assertTrue(nft.isApprovedForAll(alice, charlie)); + nft.setApprovalForAll(charlie, true); + assertTrue(nft.isApprovedForAll(alice, charlie)); + nft.setApprovalForAll(charlie, false); + assertFalse(nft.isApprovedForAll(alice, charlie)); + } + + function testSupportsInterface() public { + // ERC721 should have interface id 0x80ac58cd + assertTrue(nft.supportsInterface(0x80ac58cd)); + assertFalse(nft.supportsInterface(0x60ac58cd)); + } +}