mirror of
https://github.com/supleed2/COMP70017-PoDL-T2.git
synced 2024-12-22 05:35:50 +00:00
Initial commit
This commit is contained in:
commit
b9308fd5c3
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal 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
3
.gitmodules
vendored
Normal 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
5
README.md
Normal 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
5
foundry.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
[profile.default]
|
||||
src = 'src'
|
||||
out = 'out'
|
||||
libs = ['lib']
|
||||
solc = "0.8.17"
|
1
lib/forge-std
Submodule
1
lib/forge-std
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 4a79aca83f8075f8b1b4fe9153945fef08375630
|
2
remappings.txt
Normal file
2
remappings.txt
Normal 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
36
src/ERC20.sol
Normal 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
66
src/ERC721.sol
Normal 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)
|
||||
{}
|
||||
}
|
12
src/interfaces/IERC165.sol
Normal file
12
src/interfaces/IERC165.sol
Normal 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
109
src/interfaces/IERC20.sol
Normal 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
146
src/interfaces/IERC721.sol
Normal 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);
|
||||
}
|
24
src/interfaces/IERC721TokenReceiver.sol
Normal file
24
src/interfaces/IERC721TokenReceiver.sol
Normal 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);
|
||||
}
|
19
src/libraries/StringUtils.sol
Normal file
19
src/libraries/StringUtils.sol
Normal 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
98
test/ERC20.t.sol
Normal 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
276
test/ERC721.t.sol
Normal 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));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue