Initial commit

This commit is contained in:
Daniel Perez 2023-02-02 20:14:18 +00:00
commit b9308fd5c3
No known key found for this signature in database
GPG key ID: A0B4CCFEBD33E18A
15 changed files with 816 additions and 0 deletions

14
.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
# Compiler files
cache/
out/
# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/
# Docs
docs/
# Dotenv file
.env

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std

5
README.md Normal file
View file

@ -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.

5
foundry.toml Normal file
View file

@ -0,0 +1,5 @@
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
solc = "0.8.17"

1
lib/forge-std Submodule

@ -0,0 +1 @@
Subproject commit 4a79aca83f8075f8b1b4fe9153945fef08375630

2
remappings.txt Normal file
View file

@ -0,0 +1,2 @@
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/

36
src/ERC20.sol Normal file
View file

@ -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 {}
}

66
src/ERC721.sol Normal file
View file

@ -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)
{}
}

View file

@ -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);
}

109
src/interfaces/IERC20.sol Normal file
View file

@ -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);
}

146
src/interfaces/IERC721.sol Normal file
View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}
}

98
test/ERC20.t.sol Normal file
View file

@ -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);
}
}

276
test/ERC721.t.sol Normal file
View file

@ -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));
}
}