├── .nvmrc ├── .gitattributes ├── .solhintignore ├── .mocharc.js ├── .vscode └── settings.json ├── contracts ├── interfaces │ ├── IERC165.sol │ ├── IERC721TokenReceiver.sol │ ├── IMasterContract.sol │ ├── IERC1155TokenReceiver.sol │ ├── IERC1155.sol │ ├── IBoringGenerativeNFT.sol │ ├── IERC20.sol │ └── IERC721.sol ├── mocks │ ├── MockBoringBatchable.sol │ ├── MockERC20.sol │ ├── MockBoringSingleNFT.sol │ ├── MockBoringMultipleNFT.sol │ ├── MockMasterContract.sol │ ├── MockERC721ReceiverWrong.sol │ ├── MockERC721Receiver.sol │ ├── MockBoringERC20.sol │ └── MockBoringRebase.sol ├── libraries │ ├── BoringAddress.sol │ ├── Base64.sol │ ├── BoringRebase.sol │ └── BoringERC20.sol ├── FixedTrait.sol ├── Domain.sol ├── BoringOwnable.sol ├── BoringBatchable.sol ├── BoringFactory.sol ├── BoringSingleNFT.sol ├── BoringCooker.sol ├── ERC1155.sol ├── BoringGenerativeNFT.sol ├── ERC20.sol └── BoringMultipleNFT.sol ├── conf └── MockBoringSingleNFT.SingleNFT.conf ├── .env.example ├── .gitignore ├── .solcover.js ├── .solhint.json ├── .prettierrc.js ├── test ├── utilities │ ├── BoringOwnable.js │ └── index.js ├── BoringERC20.js ├── BoringFactory.js ├── BoringBatchable.js ├── certora │ ├── BoringMath.spec │ ├── SingleNFT.spec │ └── ERC20.spec ├── BoringOwnable.js ├── BoringRebase.js ├── BoringGenerativeNFT.js ├── ERC20.js └── BoringSingleNFT.js ├── README.md ├── package.json ├── scripts └── create_interfaces.js ├── docs └── checks.txt └── hardhat.config.js /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.15.1 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | contracts/external 2 | contracts/libraries 3 | node_modules -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | require: "hardhat/register", 3 | timeout: 20000, 4 | }; 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true, 3 | "solidity.compileUsingRemoteVersion": "v0.8.9+commit.e5eed63a" 4 | } -------------------------------------------------------------------------------- /contracts/interfaces/IERC165.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IERC165 { 5 | function supportsInterface(bytes4 interfaceID) external view returns (bool); 6 | } 7 | -------------------------------------------------------------------------------- /conf/MockBoringSingleNFT.SingleNFT.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "contracts/mocks/MockBoringSingleNFT.sol:MockBoringSingleNFT" 4 | ], 5 | "verify": [ 6 | "MockBoringSingleNFT:test/certora/SingleNFT.spec" 7 | ], 8 | "staging": "master" 9 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ALCHEMY_API_KEY= 2 | ETHERSCAN_API_KEY= 3 | COINMARKETCAP_API_KEY= 4 | TENDERLY_PROJECT= 5 | TENDERLY_USERNAME= 6 | REPORT_GAS= 7 | INFURA_API_KEY= 8 | PRIVATE_KEY= 9 | HARDHAT_NETWORK=hardhat 10 | HARDHAT_MAX_MEMORY=4096 11 | HARDHAT_SHOW_STACK_TRACES=true 12 | HARDHAT_VERBOSE=true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | build/ 3 | cache/ 4 | coverage/ 5 | deployments/ 6 | node_modules/ 7 | package-lock.json 8 | .coverage_artifacts/ 9 | .coverage_contracts/ 10 | coverage/ 11 | coverage.json 12 | scripts/make_abi.js 13 | .env 14 | .certora_config 15 | .last_confs 16 | .certora* 17 | emv-* 18 | resource_errors.json -------------------------------------------------------------------------------- /contracts/mocks/MockBoringBatchable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | import "../BoringBatchable.sol"; 5 | import "./MockERC20.sol"; 6 | 7 | // solhint-disable no-empty-blocks 8 | 9 | contract MockBoringBatchable is MockERC20(10000), BoringBatchable { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC721TokenReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IERC721TokenReceiver { 5 | function onERC721Received( 6 | address _operator, 7 | address _from, 8 | uint256 _tokenId, 9 | bytes calldata _data 10 | ) external returns (bytes4); 11 | } 12 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | norpc: true, 3 | testCommand: "npm test", 4 | compileCommand: "npm run compile", 5 | skipFiles: [ 6 | "mocks/", 7 | "interfaces/" 8 | ], 9 | providerOptions: { 10 | default_balance_ether: "10000000000000000000000000", 11 | }, 12 | mocha: { 13 | fgrep: "[skip-on-coverage]", 14 | invert: true, 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /contracts/mocks/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "../ERC20.sol"; 4 | 5 | contract MockERC20 is ERC20WithSupply { 6 | constructor(uint256 _initialAmount) { 7 | // Give the creator all initial tokens 8 | balanceOf[msg.sender] = _initialAmount; 9 | // Update total supply 10 | totalSupply = _initialAmount; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/IMasterContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IMasterContract { 5 | /// @notice Init function that gets called from `BoringFactory.deploy`. 6 | /// Also kown as the constructor for cloned contracts. 7 | /// Any ETH send to `BoringFactory.deploy` ends up here. 8 | /// @param data Can be abi encoded arguments or anything else. 9 | function init(bytes calldata data) external payable; 10 | } 11 | -------------------------------------------------------------------------------- /contracts/mocks/MockBoringSingleNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "../BoringSingleNFT.sol"; 4 | 5 | contract MockBoringSingleNFT is BoringSingleNFT { 6 | constructor() { 7 | hodler = msg.sender; 8 | } 9 | 10 | string public override name = "Mock"; 11 | string public override symbol = "MOCK"; 12 | 13 | function _tokenURI() internal pure override returns (string memory) { 14 | return ""; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "func-visibility": ["error",{"ignoreConstructors":true}], 6 | "avoid-throw": "error", 7 | "avoid-suicide": "error", 8 | "avoid-sha3": "warn", 9 | "compiler-version": "off", 10 | "max-states-count": ["error", 18], 11 | "max-line-length": ["warn", 145], 12 | "not-rely-on-time": "warn", 13 | "quotes": ["error","double"], 14 | "prettier/prettier": "on" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | overrides: [ 3 | { 4 | files: "*.sol", 5 | options: { 6 | bracketSpacing: false, 7 | printWidth: 145, 8 | tabWidth: 4, 9 | useTabs: false, 10 | singleQuote: false, 11 | explicitTypes: "always", 12 | }, 13 | }, 14 | { 15 | files: "*.js", 16 | options: { 17 | printWidth: 145, 18 | tabWidth: 4, 19 | semi: false, 20 | trailingComma: "es5", 21 | }, 22 | }, 23 | ], 24 | } 25 | -------------------------------------------------------------------------------- /contracts/mocks/MockBoringMultipleNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "../BoringMultipleNFT.sol"; 4 | 5 | contract MockBoringMultipleNFT is BoringMultipleNFT { 6 | constructor() BoringMultipleNFT("Mock", "MK") { 7 | this; // Hide empty code block warning 8 | } 9 | 10 | function _tokenURI(uint256) internal pure override returns (string memory) { 11 | return ""; 12 | } 13 | 14 | function mint(address owner) public { 15 | _mint(owner, TraitsData(0, 0, 0, 0, 0, 0, 0, 0, 0)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC1155TokenReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IERC1155TokenReceiver { 6 | function onERC1155Received( 7 | address _operator, 8 | address _from, 9 | uint256 _id, 10 | uint256 _value, 11 | bytes calldata _data 12 | ) external returns (bytes4); 13 | 14 | function onERC1155BatchReceived( 15 | address _operator, 16 | address _from, 17 | uint256[] calldata _ids, 18 | uint256[] calldata _values, 19 | bytes calldata _data 20 | ) external returns (bytes4); 21 | } 22 | -------------------------------------------------------------------------------- /contracts/libraries/BoringAddress.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | // solhint-disable no-inline-assembly 5 | 6 | library BoringAddress { 7 | function isContract(address account) internal view returns (bool) { 8 | uint256 size; 9 | assembly { 10 | size := extcodesize(account) 11 | } 12 | return size > 0; 13 | } 14 | 15 | function sendNative(address to, uint256 amount) internal { 16 | // solhint-disable-next-line avoid-low-level-calls 17 | (bool success, ) = to.call{value: amount}(""); 18 | require(success, "BoringAddress: transfer failed"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/mocks/MockMasterContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "../interfaces/IMasterContract.sol"; 4 | import "./MockERC20.sol"; 5 | 6 | contract MockMasterContract is MockERC20(10000), IMasterContract { 7 | function init(bytes calldata data) public payable override { 8 | uint256 extraAmount = abi.decode(data, (uint256)); 9 | 10 | // Give the caller some extra tokens 11 | balanceOf[msg.sender] += extraAmount; 12 | // Update total supply 13 | totalSupply += extraAmount; 14 | } 15 | 16 | function getInitData(uint256 extraAmount) public pure returns (bytes memory data) { 17 | return abi.encode(extraAmount); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/mocks/MockERC721ReceiverWrong.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "../BoringMultipleNFT.sol"; 4 | 5 | contract MockERC721ReceiverWrong { 6 | address public sender; 7 | address public operator; 8 | address public from; 9 | uint256 public tokenId; 10 | bytes public data; 11 | 12 | function onERC721Received( 13 | address _operator, 14 | address _from, 15 | uint256 _tokenId, 16 | bytes calldata _data 17 | ) external returns (bytes8) { 18 | sender = msg.sender; 19 | operator = _operator; 20 | from = _from; 21 | tokenId = _tokenId; 22 | data = _data; 23 | 24 | return bytes4(keccak256("")); 25 | } 26 | 27 | function returnToken() external { 28 | BoringMultipleNFT(sender).transferFrom(address(this), from, tokenId); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contracts/mocks/MockERC721Receiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "../interfaces/IERC721TokenReceiver.sol"; 4 | import "../BoringMultipleNFT.sol"; 5 | 6 | contract MockERC721Receiver is IERC721TokenReceiver { 7 | address public sender; 8 | address public operator; 9 | address public from; 10 | uint256 public tokenId; 11 | bytes public data; 12 | 13 | function onERC721Received( 14 | address _operator, 15 | address _from, 16 | uint256 _tokenId, 17 | bytes calldata _data 18 | ) external override returns (bytes4) { 19 | sender = msg.sender; 20 | operator = _operator; 21 | from = _from; 22 | tokenId = _tokenId; 23 | data = _data; 24 | 25 | return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); 26 | } 27 | 28 | function returnToken() external { 29 | BoringMultipleNFT(sender).transferFrom(address(this), from, tokenId); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/mocks/MockBoringERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "../libraries/BoringERC20.sol"; 4 | 5 | contract MockBoringERC20 { 6 | using BoringERC20 for IERC20; 7 | 8 | IERC20 public token; 9 | 10 | constructor(IERC20 token_) { 11 | token = token_; 12 | } 13 | 14 | function safeSymbol() public view returns (string memory) { 15 | return token.safeSymbol(); 16 | } 17 | 18 | function safeName() public view returns (string memory) { 19 | return token.safeName(); 20 | } 21 | 22 | function safeDecimals() public view returns (uint8) { 23 | return token.safeDecimals(); 24 | } 25 | 26 | function safeTransfer(address to, uint256 amount) public { 27 | return token.safeTransfer(to, amount); 28 | } 29 | 30 | function safeTransferFrom( 31 | address from, 32 | address to, 33 | uint256 amount 34 | ) public { 35 | return token.safeTransferFrom(from, to, amount); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/utilities/BoringOwnable.js: -------------------------------------------------------------------------------- 1 | const { ADDRESS_ZERO, addr } = require(".") 2 | 3 | class BoringOwnable { 4 | constructor(hardhat) { 5 | this.BoringOwnable = hardhat.BoringOwnable 6 | } 7 | 8 | async deploy() { 9 | this.contract = await this.BoringOwnable.deploy() 10 | await this.contract.deployed() 11 | this.do = this.contract 12 | return this 13 | } 14 | 15 | as(from) { 16 | this.do = this.contract.connect(from) 17 | return this 18 | } 19 | 20 | owner() { 21 | return this.do.owner() 22 | } 23 | 24 | pendingOwner() { 25 | return this.do.pendingOwner() 26 | } 27 | 28 | pendingOwner() { 29 | return this.do.pendingOwner() 30 | } 31 | 32 | renounceOwnership() { 33 | return this.do.transferOwnership(ADDRESS_ZERO, true, true) 34 | } 35 | 36 | assignOwnership(newOwner) { 37 | return this.do.transferOwnership(addr(newOwner), true, false) 38 | } 39 | 40 | transferOwnership(newOwner) { 41 | return this.do.transferOwnership(addr(newOwner), false, false) 42 | } 43 | 44 | claimOwnership() { 45 | return this.do.claimOwnership() 46 | } 47 | } 48 | 49 | module.exports = { 50 | BoringOwnable, 51 | } 52 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "./IERC165.sol"; 4 | 5 | interface IERC1155 is IERC165 { 6 | event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value); 7 | event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values); 8 | event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); 9 | event URI(string _value, uint256 indexed _id); 10 | 11 | function safeTransferFrom( 12 | address _from, 13 | address _to, 14 | uint256 _id, 15 | uint256 _value, 16 | bytes calldata _data 17 | ) external; 18 | 19 | function safeBatchTransferFrom( 20 | address _from, 21 | address _to, 22 | uint256[] calldata _ids, 23 | uint256[] calldata _values, 24 | bytes calldata _data 25 | ) external; 26 | 27 | function balanceOf(address _owner, uint256 _id) external view returns (uint256); 28 | 29 | function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) external view returns (uint256[] memory); 30 | 31 | function setApprovalForAll(address _operator, bool _approved) external; 32 | 33 | function isApprovedForAll(address _owner, address _operator) external view returns (bool); 34 | } 35 | -------------------------------------------------------------------------------- /contracts/interfaces/IBoringGenerativeNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | import "../BoringMultipleNFT.sol"; 5 | 6 | struct GeneInfo { 7 | ITrait trait; 8 | string name; 9 | } 10 | 11 | interface ITrait { 12 | // Should return bytes4(keccak256("setName(uint8,string)")) 13 | function setName(uint8 trait, string calldata name) external returns (bytes4); 14 | 15 | // Should return bytes4(keccak256("addData(address,uint8,bytes)")) 16 | function addData(uint8 trait, bytes calldata data) external returns (bytes4); 17 | 18 | function renderTrait( 19 | IBoringGenerativeNFT nft, 20 | uint256 tokenId, 21 | uint8 trait, 22 | uint8 gene 23 | ) external view returns (string memory output); 24 | 25 | function renderSVG( 26 | IBoringGenerativeNFT nft, 27 | uint256 tokenId, 28 | uint8 trait, 29 | uint8 gene 30 | ) external view returns (string memory output); 31 | } 32 | 33 | interface IBoringGenerativeNFT { 34 | function traits(uint256 index) external view returns (ITrait trait); 35 | 36 | function traitsCount() external view returns (uint256 count); 37 | 38 | function addTrait(string calldata name, ITrait trait) external; 39 | 40 | function mint(TraitsData calldata genes, address to) external; 41 | 42 | function batchMint(TraitsData[] calldata genes, address[] calldata to) external; 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BoringSolidity 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/boringcrypto/BoringSolidity/badge.svg?branch=master)](https://coveralls.io/github/boringcrypto/BoringSolidity?branch=master) 4 | 5 | BoringSolidity is a collection of general purpose Solidity contracts that have been reasonably optimized, reviewed and tested. Still, they come with no guarantees and are provided as-is. 6 | 7 | ## BoringOwnable 8 | 9 | This is a combination of the well known Ownable and Claimable patterns. It's streamlined to reduce the amount of functions exposed for gas savings. 10 | 11 | ## BoringERC20 12 | 13 | This is not a full ERC20 implementation, as it's missing totalSupply. It's optimized for minimal gas usage while remaining easy to read. 14 | 15 | ## BoringFactory 16 | 17 | Simple universal factory to create minimal proxies from masterContracts. 18 | 19 | ## BoringBatchable 20 | 21 | Extension to be added to any contract to allow calling multiple functions on the contract in a batch (a single EOA call). 22 | The EIP 2612 permit proxy function is included because it's common to approve spending before calling other functions on a contract. 23 | 24 | ## BoringRebase 25 | 26 | The Rebase struct and RebaseLibary make it easy to track amounts and shares in a single storage slot. This will limit amounts and shares to 128 bits, 27 | but if used for token balances, this should be enough for pretty much all tokens that have real use. 28 | 29 | ## Licence 30 | 31 | MIT 32 | -------------------------------------------------------------------------------- /contracts/FixedTrait.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | import "./interfaces/IBoringGenerativeNFT.sol"; 5 | 6 | contract FixedTrait is ITrait { 7 | struct Option { 8 | string name; 9 | string svg; 10 | } 11 | 12 | mapping(IBoringGenerativeNFT => mapping(uint8 => string)) public names; 13 | mapping(IBoringGenerativeNFT => mapping(uint8 => Option[])) public options; 14 | 15 | function setName(uint8 trait, string calldata name) external override returns (bytes4) { 16 | names[IBoringGenerativeNFT(msg.sender)][trait] = name; 17 | 18 | return bytes4(keccak256("setName(uint8,string)")); 19 | } 20 | 21 | function addData(uint8 trait, bytes calldata data) external override returns (bytes4) { 22 | Option memory option = abi.decode(data, (Option)); 23 | options[IBoringGenerativeNFT(msg.sender)][trait].push(option); 24 | 25 | return bytes4(keccak256("addData(address,uint8,bytes)")); 26 | } 27 | 28 | function renderTrait( 29 | IBoringGenerativeNFT nft, 30 | uint256, 31 | uint8 trait, 32 | uint8 gene 33 | ) external view override returns (string memory output) { 34 | return string(abi.encodePacked('{"trait_type":"', names[nft][trait], '","value":"', options[nft][trait][gene].name, '"}')); 35 | } 36 | 37 | function renderSVG( 38 | IBoringGenerativeNFT nft, 39 | uint256, 40 | uint8 trait, 41 | uint8 gene 42 | ) external view override returns (string memory output) { 43 | return options[nft][trait][gene].svg; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /contracts/mocks/MockBoringRebase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "../libraries/BoringRebase.sol"; 4 | 5 | contract MockBoringRebase { 6 | using RebaseLibrary for Rebase; 7 | Rebase public total; 8 | 9 | function set(uint128 elastic, uint128 base) public { 10 | total.elastic = elastic; 11 | total.base = base; 12 | } 13 | 14 | function toBase(uint256 elastic, bool roundUp) public view returns (uint256 base) { 15 | base = total.toBase(elastic, roundUp); 16 | } 17 | 18 | function toElastic(uint256 base, bool roundUp) public view returns (uint256 elastic) { 19 | elastic = total.toElastic(base, roundUp); 20 | } 21 | 22 | function add(uint256 elastic, bool roundUp) public returns (uint256 base) { 23 | (total, base) = total.add(elastic, roundUp); 24 | } 25 | 26 | function sub(uint256 base, bool roundUp) public returns (uint256 elastic) { 27 | (total, elastic) = total.sub(base, roundUp); 28 | } 29 | 30 | function add2(uint256 base, uint256 elastic) public { 31 | total = total.add(base, elastic); 32 | } 33 | 34 | function sub2(uint256 base, uint256 elastic) public { 35 | total = total.sub(base, elastic); 36 | } 37 | 38 | function addElastic(uint256 elastic) public returns (uint256 newElastic) { 39 | newElastic = total.addElastic(elastic); 40 | require(newElastic == 150, "MockBoringRebase: test failed"); 41 | } 42 | 43 | function subElastic(uint256 elastic) public returns (uint256 newElastic) { 44 | newElastic = total.subElastic(elastic); 45 | require(newElastic == 110, "MockBoringRebase: test failed"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contracts/Domain.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Based on code and smartness by Ross Campbell and Keno 3 | // Uses immutable to store the domain separator to reduce gas usage 4 | // If the chain id changes due to a fork, the forked chain will calculate on the fly. 5 | pragma solidity ^0.8.0; 6 | 7 | // solhint-disable no-inline-assembly 8 | 9 | contract Domain { 10 | bytes32 private constant DOMAIN_SEPARATOR_SIGNATURE_HASH = keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"); 11 | // See https://eips.ethereum.org/EIPS/eip-191 12 | string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = "\x19\x01"; 13 | 14 | // solhint-disable var-name-mixedcase 15 | bytes32 private immutable _DOMAIN_SEPARATOR; 16 | uint256 private immutable DOMAIN_SEPARATOR_CHAIN_ID; 17 | 18 | /// @dev Calculate the DOMAIN_SEPARATOR 19 | function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) { 20 | return keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, chainId, address(this))); 21 | } 22 | 23 | constructor() { 24 | _DOMAIN_SEPARATOR = _calculateDomainSeparator(DOMAIN_SEPARATOR_CHAIN_ID = block.chainid); 25 | } 26 | 27 | /// @dev Return the DOMAIN_SEPARATOR 28 | // It's named internal to allow making it public from the contract that uses it by creating a simple view function 29 | // with the desired public name, such as DOMAIN_SEPARATOR or domainSeparator. 30 | // solhint-disable-next-line func-name-mixedcase 31 | function _domainSeparator() internal view returns (bytes32) { 32 | return block.chainid == DOMAIN_SEPARATOR_CHAIN_ID ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(block.chainid); 33 | } 34 | 35 | function _getDigest(bytes32 dataHash) internal view returns (bytes32 digest) { 36 | digest = keccak256(abi.encodePacked(EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, _domainSeparator(), dataHash)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /contracts/BoringOwnable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | // Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol + Claimable.sol 5 | // Simplified by BoringCrypto 6 | 7 | contract BoringOwnableData { 8 | address public owner; 9 | address public pendingOwner; 10 | } 11 | 12 | contract BoringOwnable is BoringOwnableData { 13 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 14 | 15 | /// @notice `owner` defaults to msg.sender on construction. 16 | constructor() { 17 | owner = msg.sender; 18 | emit OwnershipTransferred(address(0), msg.sender); 19 | } 20 | 21 | /// @notice Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner. 22 | /// Can only be invoked by the current `owner`. 23 | /// @param newOwner Address of the new owner. 24 | /// @param direct True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`. 25 | /// @param renounce Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise. 26 | function transferOwnership( 27 | address newOwner, 28 | bool direct, 29 | bool renounce 30 | ) public onlyOwner { 31 | if (direct) { 32 | // Checks 33 | require(newOwner != address(0) || renounce, "Ownable: zero address"); 34 | 35 | // Effects 36 | emit OwnershipTransferred(owner, newOwner); 37 | owner = newOwner; 38 | pendingOwner = address(0); 39 | } else { 40 | // Effects 41 | pendingOwner = newOwner; 42 | } 43 | } 44 | 45 | /// @notice Needs to be called by `pendingOwner` to claim ownership. 46 | function claimOwnership() public { 47 | address _pendingOwner = pendingOwner; 48 | 49 | // Checks 50 | require(msg.sender == _pendingOwner, "Ownable: caller != pending owner"); 51 | 52 | // Effects 53 | emit OwnershipTransferred(owner, _pendingOwner); 54 | owner = _pendingOwner; 55 | pendingOwner = address(0); 56 | } 57 | 58 | /// @notice Only allows the `owner` to execute the function. 59 | modifier onlyOwner() { 60 | require(msg.sender == owner, "Ownable: caller is not the owner"); 61 | _; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /contracts/libraries/Base64.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | // solhint-disable no-inline-assembly 5 | // solhint-disable no-empty-blocks 6 | 7 | /// @title Base64 8 | /// @author Brecht Devos - 9 | /// @notice Provides functions for encoding/decoding base64 10 | library Base64 { 11 | function encode(bytes memory data) internal pure returns (string memory) { 12 | if (data.length == 0) return ""; 13 | 14 | // load the table into memory 15 | string memory table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 16 | 17 | // multiply by 4/3 rounded up 18 | uint256 encodedLen = 4 * ((data.length + 2) / 3); 19 | 20 | // add some extra buffer at the end required for the writing 21 | string memory result = new string(encodedLen + 32); 22 | 23 | assembly { 24 | // set the actual output length 25 | mstore(result, encodedLen) 26 | 27 | // prepare the lookup table 28 | let tablePtr := add(table, 1) 29 | 30 | // input ptr 31 | let dataPtr := data 32 | let endPtr := add(dataPtr, mload(data)) 33 | 34 | // result ptr, jump over length 35 | let resultPtr := add(result, 32) 36 | 37 | // run over the input, 3 bytes at a time 38 | for { 39 | 40 | } lt(dataPtr, endPtr) { 41 | 42 | } { 43 | // read 3 bytes 44 | dataPtr := add(dataPtr, 3) 45 | let input := mload(dataPtr) 46 | 47 | // write 4 characters 48 | mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F)))) 49 | resultPtr := add(resultPtr, 1) 50 | mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F)))) 51 | resultPtr := add(resultPtr, 1) 52 | mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F)))) 53 | resultPtr := add(resultPtr, 1) 54 | mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) 55 | resultPtr := add(resultPtr, 1) 56 | } 57 | 58 | // padding with '=' 59 | switch mod(mload(data), 3) 60 | case 1 { 61 | mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) 62 | } 63 | case 2 { 64 | mstore(sub(resultPtr, 1), shl(248, 0x3d)) 65 | } 66 | } 67 | 68 | return result; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IERC20 { 5 | // transfer and tranferFrom have been removed, because they don't work on all tokens (some aren't ERC20 complaint). 6 | // By removing them you can't accidentally use them. 7 | // name, symbol and decimals have been removed, because they are optional and sometimes wrongly implemented (MKR). 8 | // Use BoringERC20 with `using BoringERC20 for IERC20` and call `safeTransfer`, `safeTransferFrom`, etc instead. 9 | function totalSupply() external view returns (uint256); 10 | 11 | function balanceOf(address account) external view returns (uint256); 12 | 13 | function allowance(address owner, address spender) external view returns (uint256); 14 | 15 | function approve(address spender, uint256 amount) external returns (bool); 16 | 17 | event Transfer(address indexed from, address indexed to, uint256 value); 18 | event Approval(address indexed owner, address indexed spender, uint256 value); 19 | 20 | /// @notice EIP 2612 21 | function permit( 22 | address owner, 23 | address spender, 24 | uint256 value, 25 | uint256 deadline, 26 | uint8 v, 27 | bytes32 r, 28 | bytes32 s 29 | ) external; 30 | } 31 | 32 | interface IStrictERC20 { 33 | // This is the strict ERC20 interface. Don't use this, certainly not if you don't control the ERC20 token you're calling. 34 | function name() external view returns (string memory); 35 | function symbol() external view returns (string memory); 36 | function decimals() external view returns (uint8); 37 | function totalSupply() external view returns (uint256); 38 | function balanceOf(address _owner) external view returns (uint256 balance); 39 | function transfer(address _to, uint256 _value) external returns (bool success); 40 | function transferFrom(address _from, address _to, uint256 _value) external returns (bool success); 41 | function approve(address _spender, uint256 _value) external returns (bool success); 42 | function allowance(address _owner, address _spender) external view returns (uint256 remaining); 43 | 44 | event Transfer(address indexed from, address indexed to, uint256 value); 45 | event Approval(address indexed owner, address indexed spender, uint256 value); 46 | 47 | /// @notice EIP 2612 48 | function permit( 49 | address owner, 50 | address spender, 51 | uint256 value, 52 | uint256 deadline, 53 | uint8 v, 54 | bytes32 r, 55 | bytes32 s 56 | ) external; 57 | } 58 | -------------------------------------------------------------------------------- /test/BoringERC20.js: -------------------------------------------------------------------------------- 1 | const { expect, assert } = require("chai") 2 | const { ADDRESS_ZERO, getApprovalDigest, getDomainSeparator, prepare } = require("./utilities") 3 | const { ecsign } = require("ethereumjs-util") 4 | 5 | describe("BoringERC20", function () { 6 | before(async function () { 7 | await prepare(this, ["MockBoringERC20", "MockERC20"]) 8 | }) 9 | 10 | beforeEach(async function () { 11 | this.token = await this.MockERC20.deploy(10000) 12 | await this.token.deployed() 13 | this.mock = await this.MockBoringERC20.deploy(this.token.address) 14 | await this.mock.deployed() 15 | }) 16 | 17 | it("Token address is set", async function () { 18 | expect(await this.mock.token()).to.equal(this.token.address) 19 | }) 20 | 21 | it("Get the symbol of a token that has no symbol", async function () { 22 | expect(await this.mock.safeSymbol()).to.equal("???") 23 | }) 24 | 25 | it("Get the name of a token that has no name", async function () { 26 | expect(await this.mock.safeName()).to.equal("???") 27 | }) 28 | 29 | it("Get the decimals of a token that has no decimals", async function () { 30 | expect(await this.mock.safeDecimals()).to.equal(18) 31 | }) 32 | 33 | it("Can send tokens with transfer", async function () { 34 | await expect(this.token.transfer(this.mock.address, 100)) 35 | .to.emit(this.token, "Transfer") 36 | .withArgs(this.alice.address, this.mock.address, 100) 37 | await expect(this.mock.safeTransfer(this.bob.address, 100)) 38 | .to.emit(this.token, "Transfer") 39 | .withArgs(this.mock.address, this.bob.address, 100) 40 | expect(await this.token.balanceOf(this.bob.address)).to.equal(100) 41 | }) 42 | 43 | it("Can send tokens with transferFrom", async function () { 44 | await this.token.approve(this.mock.address, 50) 45 | await expect(this.mock.safeTransferFrom(this.alice.address, this.carol.address, 50)) 46 | .to.emit(this.token, "Transfer") 47 | .withArgs(this.alice.address, this.carol.address, 50) 48 | expect(await this.token.balanceOf(this.carol.address)).to.equal(50) 49 | }) 50 | 51 | it("Reverts sending tokens with transfer", async function () { 52 | await expect(this.mock.safeTransfer(this.bob.address, 100)).to.revertedWith("BoringERC20: Transfer failed") 53 | }) 54 | 55 | it("Reverts sending tokens with transferFrom", async function () { 56 | await expect(this.mock.safeTransferFrom(this.alice.address, this.carol.address, 50)).to.revertedWith("BoringERC20: TransferFrom failed") 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /test/BoringFactory.js: -------------------------------------------------------------------------------- 1 | const { ADDRESS_ZERO, prepare, getApprovalDigest } = require("./utilities") 2 | const { expect } = require("chai") 3 | const { ecsign } = require("ethereumjs-util") 4 | 5 | describe("BoringFactory", function () { 6 | before(async function () { 7 | await prepare(this, ["BoringFactory", "MockMasterContract"]) 8 | this.contract = await this.BoringFactory.deploy() 9 | await this.contract.deployed() 10 | this.master = await this.MockMasterContract.deploy() 11 | await this.master.deployed() 12 | }) 13 | 14 | it("Can create clone", async function () { 15 | let initData = await this.master.getInitData(1234) 16 | await expect(this.contract.deploy(this.master.address, initData, false)).to.emit(this.contract, "LogDeploy") 17 | }) 18 | 19 | it("Can create a second clone", async function () { 20 | let initData = await this.master.getInitData(12345) 21 | await expect(this.contract.deploy(this.master.address, initData, false)).to.emit(this.contract, "LogDeploy") 22 | }) 23 | 24 | it("Can create the same clone twice", async function () { 25 | initData = await this.master.getInitData(1234) 26 | await expect(this.contract.deploy(this.master.address, initData, false)) 27 | }) 28 | 29 | it("Can create clone with CREATE2", async function () { 30 | let initData = await this.master.getInitData(1234) 31 | await expect(this.contract.deploy(this.master.address, initData, true)).to.emit(this.contract, "LogDeploy") 32 | }) 33 | 34 | it("Can create another clone with the same masterContract using CREATE2", async function () { 35 | let initData = await this.master.getInitData(12345) 36 | await expect(this.contract.deploy(this.master.address, initData, true)) 37 | }) 38 | 39 | it("Cannot create the same clone twice", async function () { 40 | initData = await this.master.getInitData(1234) 41 | await expect(this.contract.deploy(this.master.address, initData, true)).to.be.reverted 42 | }) 43 | 44 | it("Reverts on masterContract address 0", async function () { 45 | let initData = await this.master.getInitData(1234) 46 | await expect(this.contract.deploy(ADDRESS_ZERO, initData, false)).to.revertedWith("BoringFactory: No masterContract") 47 | }) 48 | 49 | it("Reverts on masterContract address 0 with CREATE2", async function () { 50 | let initData = await this.master.getInitData(1234) 51 | await expect(this.contract.deploy(ADDRESS_ZERO, initData, true)).to.revertedWith("BoringFactory: No masterContract") 52 | }) 53 | 54 | it("Reverts on masterContract with wrong data", async function () { 55 | await expect(this.contract.deploy(this.master.address, "0x", true)).to.be.reverted 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /contracts/BoringBatchable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | // solhint-disable avoid-low-level-calls 6 | // solhint-disable no-inline-assembly 7 | 8 | // WARNING!!! 9 | // Combining BoringBatchable with msg.value can cause double spending issues 10 | // https://www.paradigm.xyz/2021/08/two-rights-might-make-a-wrong/ 11 | 12 | import "./interfaces/IERC20.sol"; 13 | 14 | contract BaseBoringBatchable { 15 | error BatchError(bytes innerError); 16 | 17 | /// @dev Helper function to extract a useful revert message from a failed call. 18 | /// If the returned data is malformed or not correctly abi encoded then this call can fail itself. 19 | function _getRevertMsg(bytes memory _returnData) internal pure{ 20 | // If the _res length is less than 68, then 21 | // the transaction failed with custom error or silently (without a revert message) 22 | if (_returnData.length < 68) revert BatchError(_returnData); 23 | 24 | assembly { 25 | // Slice the sighash. 26 | _returnData := add(_returnData, 0x04) 27 | } 28 | revert(abi.decode(_returnData, (string))); // All that remains is the revert string 29 | } 30 | 31 | /// @notice Allows batched call to self (this contract). 32 | /// @param calls An array of inputs for each call. 33 | /// @param revertOnFail If True then reverts after a failed call and stops doing further calls. 34 | // F1: External is ok here because this is the batch function, adding it to a batch makes no sense 35 | // F2: Calls in the batch may be payable, delegatecall operates in the same context, so each call in the batch has access to msg.value 36 | // C3: The length of the loop is fully under user control, so can't be exploited 37 | // C7: Delegatecall is only used on the same contract, so it's safe 38 | function batch(bytes[] calldata calls, bool revertOnFail) external payable { 39 | for (uint256 i = 0; i < calls.length; i++) { 40 | (bool success, bytes memory result) = address(this).delegatecall(calls[i]); 41 | if (!success && revertOnFail) { 42 | _getRevertMsg(result); 43 | } 44 | } 45 | } 46 | } 47 | 48 | contract BoringBatchable is BaseBoringBatchable { 49 | /// @notice Call wrapper that performs `ERC20.permit` on `token`. 50 | /// Lookup `IERC20.permit`. 51 | // F6: Parameters can be used front-run the permit and the user's permit will fail (due to nonce or other revert) 52 | // if part of a batch this could be used to grief once as the second call would not need the permit 53 | function permitToken( 54 | IERC20 token, 55 | address from, 56 | address to, 57 | uint256 amount, 58 | uint256 deadline, 59 | uint8 v, 60 | bytes32 r, 61 | bytes32 s 62 | ) public { 63 | token.permit(from, to, amount, deadline, v, r, s); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/BoringBatchable.js: -------------------------------------------------------------------------------- 1 | const { ADDRESS_ZERO, prepare, getApprovalDigest } = require("./utilities") 2 | const { expect } = require("chai") 3 | const { ecsign } = require("ethereumjs-util") 4 | 5 | describe("BoringBatchable", function () { 6 | before(async function () { 7 | await prepare(this, ["MockBoringBatchable"]) 8 | this.contract = await this.MockBoringBatchable.deploy() 9 | await this.contract.deployed() 10 | }) 11 | 12 | it("TotalSupply is set", async function () { 13 | expect(await this.contract.totalSupply()).to.equal(10000) 14 | }) 15 | 16 | it("Batch of 2 transfers", async function () { 17 | await expect( 18 | this.contract.batch( 19 | [ 20 | this.contract.interface.encodeFunctionData("transfer", [this.bob.address, 100]), 21 | this.contract.interface.encodeFunctionData("transfer", [this.carol.address, 200]), 22 | ], 23 | true 24 | ) 25 | ) 26 | .to.emit(this.contract, "Transfer") 27 | .withArgs(this.alice.address, this.bob.address, 100) 28 | .to.emit(this.contract, "Transfer") 29 | .withArgs(this.alice.address, this.carol.address, 200) 30 | }) 31 | 32 | it("Batch of 2 transfers fails if one fails", async function () { 33 | await expect( 34 | this.contract.batch( 35 | [ 36 | this.contract.interface.encodeFunctionData("transfer", [this.bob.address, 100]), 37 | this.contract.interface.encodeFunctionData("transfer", [this.carol.address, 20000]), 38 | ], 39 | true 40 | ) 41 | ).to.revertedWith("ERC20: balance too low") 42 | }) 43 | 44 | it("Batch of 2 transfers succeeds even though one tx fails", async function () { 45 | await expect( 46 | this.contract.batch( 47 | [ 48 | this.contract.interface.encodeFunctionData("transfer", [this.bob.address, 100]), 49 | this.contract.interface.encodeFunctionData("transfer", [this.carol.address, 20000]), 50 | ], 51 | false 52 | ) 53 | ) 54 | .to.emit(this.contract, "Transfer") 55 | .withArgs(this.alice.address, this.bob.address, 100) 56 | }) 57 | 58 | it("Successfully executes a permit", async function () { 59 | const nonce = await this.contract.nonces(this.alice.address) 60 | 61 | const deadline = (await this.bob.provider._internalBlockNumber).respTime + 10000 62 | 63 | const digest = await getApprovalDigest( 64 | this.contract, 65 | { 66 | owner: this.alice.address, 67 | spender: this.bob.address, 68 | value: 1, 69 | }, 70 | nonce, 71 | deadline, 72 | this.bob.provider._network.chainId 73 | ) 74 | const { v, r, s } = ecsign(Buffer.from(digest.slice(2), "hex"), Buffer.from(this.alicePrivateKey.replace("0x", ""), "hex")) 75 | await this.contract.permitToken(this.contract.address, this.alice.address, this.bob.address, 1, deadline, v, r, s) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@boringcrypto/boring-solidity", 3 | "version": "2.0.3", 4 | "private": false, 5 | "description": "BoringSolidity", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "files": [ 10 | "contracts" 11 | ], 12 | "scripts": { 13 | "build": "hardhat compile", 14 | "console": "hardhat console", 15 | "mainnet:deploy": "hardhat --network mainnet deploy", 16 | "mainnet:verify": "hardhat --network mainnet etherscan-verify --solc-input --license UNLICENSED", 17 | "mainnet:export": "hardhat --network mainnet export", 18 | "ropsten:deploy": "hardhat --network ropsten deploy", 19 | "ropsten:verify": "hardhat --network ropsten etherscan-verify --solc-input --license UNLICENSED", 20 | "ropsten:export": "hardhat --network ropsten export", 21 | "kovan:deploy": "hardhat --network kovan deploy", 22 | "kovan:export": "hardhat --network kovan export", 23 | "kovan:verify": "hardhat --network kovan etherscan-verify --solc-input --license UNLICENSED", 24 | "coverage": ".\\node_modules\\.bin\\hardhat.cmd coverage", 25 | "test": "hardhat test --deploy-fixture", 26 | "test:coverage": "node --max-old-space-size=4096 ./node_modules/.bin/hardhat coverage", 27 | "test:gas": "REPORT_GAS=true yarn test", 28 | "certora-erc20": "certoraRun contracts\\ERC20.sol:ERC20WithSupply --verify ERC20WithSupply:test\\certora\\ERC20.spec --jar C:\\\\certora\\\\cvt.jar", 29 | "certora": "certoraRun contracts\\mocks\\MockSingleNFT.sol:MockSingleNFT --verify MockSingleNFT:test\\certora\\SingleNFT.spec --jar C:\\\\certora\\\\emv2.jar --optimistic_loop --rule_sanity", 30 | "prettier": "prettier --write test/**/*.js contracts/**/*.sol", 31 | "lint": "yarn prettier && solhint -c .solhint.json 'contracts/**/*.sol'" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/boringcrypto/BoringSolidity.git" 36 | }, 37 | "author": "", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/boringcrypto/BoringSolidity/issues" 41 | }, 42 | "homepage": "https://github.com/boringcrypto/BoringSolidity#readme", 43 | "devDependencies": { 44 | "@codechecks/client": "^0.1.10", 45 | "@nomiclabs/hardhat-ethers": "^2.0.1", 46 | "@nomiclabs/hardhat-etherscan": "^2.1.0", 47 | "@nomiclabs/hardhat-solhint": "^2.0.0", 48 | "@nomiclabs/hardhat-waffle": "^2.0.1", 49 | "@tenderly/hardhat-tenderly": "^1.0.6", 50 | "chai": "^4.2.0", 51 | "coveralls": "^3.1.0", 52 | "cross-env": "^7.0.3", 53 | "dotenv": "^8.2.0", 54 | "ethereum-waffle": "^3.2.1", 55 | "ethereumjs-util": "^7.0.7", 56 | "ethers": "^5.1.4", 57 | "hardhat": "^2.0.5", 58 | "hardhat-abi-exporter": "^2.0.6", 59 | "hardhat-dependency-compiler": "^1.0.0", 60 | "hardhat-deploy": "^0.7.0-beta.38", 61 | "hardhat-deploy-ethers": "^0.3.0-beta.7", 62 | "hardhat-gas-reporter": "^1.0.3", 63 | "hardhat-preprocessor": "^0.1.1", 64 | "hardhat-spdx-license-identifier": "^2.0.2", 65 | "hardhat-watcher": "^2.0.0", 66 | "husky": "^4.3.6", 67 | "mocha": "^8.2.1", 68 | "prettier": "^2.2.1", 69 | "prettier-plugin-solidity": "^1.0.0-beta.2", 70 | "solc": "0.8.9", 71 | "solhint": "^3.3.2", 72 | "solhint-plugin-prettier": "^0.0.5", 73 | "solidity-coverage": "^0.7.12" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /contracts/BoringFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "./interfaces/IMasterContract.sol"; 4 | 5 | // solhint-disable no-inline-assembly 6 | 7 | contract BoringFactory { 8 | event LogDeploy(address indexed masterContract, bytes data, address indexed cloneAddress); 9 | 10 | /// @notice Mapping from clone contracts to their masterContract. 11 | mapping(address => address) public masterContractOf; 12 | 13 | /// @notice Mapping from masterContract to an array of all clones 14 | /// On mainnet events can be used to get this list, but events aren't always easy to retrieve and 15 | /// barely work on sidechains. While this adds gas, it makes enumerating all clones much easier. 16 | mapping(address => address[]) public clonesOf; 17 | 18 | /// @notice Returns the count of clones that exists for a specific masterContract 19 | /// @param masterContract The address of the master contract. 20 | /// @return cloneCount total number of clones for the masterContract. 21 | function clonesOfCount(address masterContract) public view returns (uint256 cloneCount) { 22 | cloneCount = clonesOf[masterContract].length; 23 | } 24 | 25 | /// @notice Deploys a given master Contract as a clone. 26 | /// Any ETH transferred with this call is forwarded to the new clone. 27 | /// Emits `LogDeploy`. 28 | /// @param masterContract The address of the contract to clone. 29 | /// @param data Additional abi encoded calldata that is passed to the new clone via `IMasterContract.init`. 30 | /// @param useCreate2 Creates the clone by using the CREATE2 opcode, in this case `data` will be used as salt. 31 | /// @return cloneAddress Address of the created clone contract. 32 | function deploy( 33 | address masterContract, 34 | bytes calldata data, 35 | bool useCreate2 36 | ) public payable returns (address cloneAddress) { 37 | require(masterContract != address(0), "BoringFactory: No masterContract"); 38 | bytes20 targetBytes = bytes20(masterContract); // Takes the first 20 bytes of the masterContract's address 39 | 40 | if (useCreate2) { 41 | // each masterContract has different code already. So clones are distinguished by their data only. 42 | bytes32 salt = keccak256(data); 43 | 44 | // Creates clone, more info here: https://blog.openzeppelin.com/deep-dive-into-the-minimal-proxy-contract/ 45 | assembly { 46 | let clone := mload(0x40) 47 | mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) 48 | mstore(add(clone, 0x14), targetBytes) 49 | mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) 50 | cloneAddress := create2(0, clone, 0x37, salt) 51 | } 52 | } else { 53 | assembly { 54 | let clone := mload(0x40) 55 | mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) 56 | mstore(add(clone, 0x14), targetBytes) 57 | mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) 58 | cloneAddress := create(0, clone, 0x37) 59 | } 60 | } 61 | masterContractOf[cloneAddress] = masterContract; 62 | clonesOf[masterContract].push(cloneAddress); 63 | 64 | IMasterContract(cloneAddress).init{value: msg.value}(data); 65 | 66 | emit LogDeploy(masterContract, data, cloneAddress); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /scripts/create_interfaces.js: -------------------------------------------------------------------------------- 1 | function renderInput(input, contracts) { 2 | let line; 3 | if (input.internalType.startsWith("contract ")) { 4 | let newContract = input.internalType.substr(9); 5 | if (contracts.indexOf(newContract) == -1) { 6 | contracts.push(newContract); 7 | } 8 | line = newContract; 9 | } else { 10 | line = input.internalType; 11 | } 12 | if (input.type == 'string' || input.type == "bytes" || input.type.endsWith("[]")) { 13 | line += " calldata"; 14 | } 15 | if (input.name) { 16 | line += " " + input.name 17 | } 18 | return line; 19 | } 20 | 21 | function renderOutput(input, contracts) { 22 | let line; 23 | if (input.internalType.startsWith("contract ")) { 24 | let newContract = input.internalType.substr(9); 25 | if (contracts.indexOf(newContract) == -1) { 26 | contracts.push(newContract); 27 | } 28 | line = newContract; 29 | } else { 30 | line = input.internalType; 31 | } 32 | if (input.type == 'string' || input.type == "bytes" || input.type.endsWith("[]")) { 33 | line += " memory"; 34 | } 35 | if (input.name) { 36 | line += " " + input.name 37 | } 38 | return line; 39 | } 40 | 41 | function createInterface(name) { 42 | fs = require('fs'); 43 | fs.readFile('./build/abi/' + name + '.json', 'utf8', function (err, data) { 44 | let abi = JSON.parse(data); 45 | let lines = []; 46 | let header = []; 47 | let contracts = []; 48 | header.push("// SPDX-License-Identifier: MIT"); 49 | header.push("pragma solidity ^0.8.0;"); 50 | lines.push(""); 51 | lines.push("interface I" + name + " {"); 52 | for (let i in abi) { 53 | let item = abi[i]; 54 | if (item.type == "function") { 55 | let line = " function " + item.name + "("; 56 | line += item.inputs.map(input => renderInput(input, contracts)).join(", "); 57 | line += ") external"; 58 | if (item.stateMutability != 'nonpayable') { 59 | line += " " + item.stateMutability; 60 | } 61 | if (item.outputs.length) { 62 | line += " returns ("; 63 | line += item.outputs.map(output => renderOutput(output, contracts)).join(", "); 64 | line += ")"; 65 | } 66 | line += ";"; 67 | lines.push(line); 68 | } else if (item.type == "event") { 69 | let line = " event " + item.name + "("; 70 | line += item.inputs.map(input => input.type + (input.indexed ? " indexed" : "") + (input.name ? " " + input.name : "")).join(", "); 71 | line += ");"; 72 | lines.push(line); 73 | } else { 74 | console.log(item); 75 | } 76 | } 77 | lines.push("}"); 78 | for (let i in contracts) { 79 | if (contracts[i] == "IOracle") { 80 | header.push('import "./IOracle.sol";'); 81 | } 82 | if (contracts[i] == "ILendingPair") { 83 | header.push('import "./ILendingPair.sol";'); 84 | } 85 | } 86 | console.log(header.join("\r\n") + "\r\n" + lines.join("\r\n")); 87 | fs.writeFile('./contracts/interfaces/genI' + name + ".sol", 88 | header.join("\r\n") + "\r\n" + lines.join("\r\n"), 89 | function (err) { }); 90 | }); 91 | } 92 | 93 | createInterface("KashiPairMediumRiskV1"); 94 | -------------------------------------------------------------------------------- /test/certora/BoringMath.spec: -------------------------------------------------------------------------------- 1 | methods { 2 | add(uint256 a, uint256 b) returns (uint256) envfree 3 | sub(uint256 a, uint256 b) returns (uint256) envfree 4 | mul(uint256 a, uint256 b) returns (uint256) envfree 5 | to128(uint256 a) returns (uint128 c) envfree 6 | to64(uint256 a) returns (uint64 c) envfree 7 | to32(uint256 a) returns (uint32 c) envfree 8 | 9 | add128(uint128 a, uint128 b) returns (uint128 c) envfree 10 | sub128(uint128 a, uint128 b) returns (uint128 c) envfree 11 | 12 | add64(uint64 a, uint64 b) returns (uint64 c) envfree 13 | sub64(uint64 a, uint64 b) returns (uint64 c) envfree 14 | 15 | add32(uint32 a, uint32 b) returns (uint32 c) envfree 16 | sub32(uint32 a, uint32 b) returns (uint32 c) envfree 17 | } 18 | 19 | rule AddCorrect(uint256 a, uint256 b) { 20 | uint256 c = add@withrevert(a, b); 21 | bool reverted = lastReverted; 22 | // add must return the mathimatical addition of a and b, or revert 23 | assert reverted || c == a + b; 24 | // add can ONLY revert if the mathimatical addition of a and b overflows 25 | assert !reverted || a + b > max_uint256; 26 | } 27 | 28 | rule SubCorrect(uint256 a, uint256 b) { 29 | uint256 c = sub@withrevert(a, b); 30 | bool reverted = lastReverted; 31 | assert reverted || c == a - b; 32 | assert !reverted || a < b; 33 | } 34 | 35 | rule MulCorrectIfNotReverted(uint256 a, uint256 b) { 36 | require a * b <= max_uint256; 37 | uint256 c = mul@withrevert(a, b); 38 | bool reverted = lastReverted; 39 | assert reverted || c == a * b; 40 | assert !reverted || a * b > max_uint256; 41 | } 42 | 43 | rule CorrectCastTo128(uint256 a) { 44 | uint128 c = to128@withrevert(a); 45 | bool reverted = lastReverted; 46 | assert reverted || c == a; 47 | assert !reverted || a > max_uint128; 48 | } 49 | 50 | rule CorrectCastTo64(uint256 a) { 51 | uint64 c = to64@withrevert(a); 52 | bool reverted = lastReverted; 53 | assert reverted || c == a; 54 | assert !reverted || a > max_uint64; 55 | } 56 | 57 | rule CorrectCastTo32(uint256 a) { 58 | uint32 c = to32@withrevert(a); 59 | bool reverted = lastReverted; 60 | assert reverted || c == a; 61 | assert !reverted || a > max_uint32; 62 | } 63 | 64 | rule Add128Correct(uint128 a, uint128 b) { 65 | uint128 c = add128@withrevert(a, b); 66 | bool reverted = lastReverted; 67 | assert reverted || c == a + b; 68 | assert !reverted || a + b > max_uint128; 69 | } 70 | 71 | rule Sub128Correct(uint128 a, uint128 b) { 72 | uint128 c = sub128@withrevert(a, b); 73 | bool reverted = lastReverted; 74 | assert reverted || c == a - b; 75 | assert !reverted || a < b; 76 | } 77 | 78 | rule Add64Correct(uint64 a, uint64 b) { 79 | uint64 c = add64@withrevert(a, b); 80 | bool reverted = lastReverted; 81 | assert reverted || c == a + b; 82 | assert !reverted || a + b > max_uint64; 83 | } 84 | 85 | rule Sub64Correct(uint64 a, uint64 b) { 86 | uint64 c = sub64@withrevert(a, b); 87 | bool reverted = lastReverted; 88 | assert reverted || c == a - b; 89 | assert !reverted || a < b; 90 | } 91 | 92 | rule Add32Correct(uint32 a, uint32 b) { 93 | uint32 c = add32@withrevert(a, b); 94 | bool reverted = lastReverted; 95 | assert reverted || c == a + b; 96 | assert !reverted || a + b > max_uint32; 97 | } 98 | 99 | rule Sub32Correct(uint32 a, uint32 b) { 100 | uint32 c = sub32@withrevert(a, b); 101 | bool reverted = lastReverted; 102 | assert reverted || c == a - b; 103 | assert !reverted || a < b; 104 | } 105 | 106 | -------------------------------------------------------------------------------- /contracts/libraries/BoringRebase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | struct Rebase { 5 | uint128 elastic; 6 | uint128 base; 7 | } 8 | 9 | /// @notice A rebasing library using overflow-/underflow-safe math. 10 | library RebaseLibrary { 11 | /// @notice Calculates the base value in relationship to `elastic` and `total`. 12 | function toBase( 13 | Rebase memory total, 14 | uint256 elastic, 15 | bool roundUp 16 | ) internal pure returns (uint256 base) { 17 | if (total.elastic == 0) { 18 | base = elastic; 19 | } else { 20 | base = (elastic * total.base) / total.elastic; 21 | if (roundUp && (base * total.elastic) / total.base < elastic) { 22 | base++; 23 | } 24 | } 25 | } 26 | 27 | /// @notice Calculates the elastic value in relationship to `base` and `total`. 28 | function toElastic( 29 | Rebase memory total, 30 | uint256 base, 31 | bool roundUp 32 | ) internal pure returns (uint256 elastic) { 33 | if (total.base == 0) { 34 | elastic = base; 35 | } else { 36 | elastic = (base * total.elastic) / total.base; 37 | if (roundUp && (elastic * total.base) / total.elastic < base) { 38 | elastic++; 39 | } 40 | } 41 | } 42 | 43 | /// @notice Add `elastic` to `total` and doubles `total.base`. 44 | /// @return (Rebase) The new total. 45 | /// @return base in relationship to `elastic`. 46 | function add( 47 | Rebase memory total, 48 | uint256 elastic, 49 | bool roundUp 50 | ) internal pure returns (Rebase memory, uint256 base) { 51 | base = toBase(total, elastic, roundUp); 52 | total.elastic += uint128(elastic); 53 | total.base += uint128(base); 54 | return (total, base); 55 | } 56 | 57 | /// @notice Sub `base` from `total` and update `total.elastic`. 58 | /// @return (Rebase) The new total. 59 | /// @return elastic in relationship to `base`. 60 | function sub( 61 | Rebase memory total, 62 | uint256 base, 63 | bool roundUp 64 | ) internal pure returns (Rebase memory, uint256 elastic) { 65 | elastic = toElastic(total, base, roundUp); 66 | total.elastic -= uint128(elastic); 67 | total.base -= uint128(base); 68 | return (total, elastic); 69 | } 70 | 71 | /// @notice Add `elastic` and `base` to `total`. 72 | function add( 73 | Rebase memory total, 74 | uint256 elastic, 75 | uint256 base 76 | ) internal pure returns (Rebase memory) { 77 | total.elastic += uint128(elastic); 78 | total.base += uint128(base); 79 | return total; 80 | } 81 | 82 | /// @notice Subtract `elastic` and `base` to `total`. 83 | function sub( 84 | Rebase memory total, 85 | uint256 elastic, 86 | uint256 base 87 | ) internal pure returns (Rebase memory) { 88 | total.elastic -= uint128(elastic); 89 | total.base -= uint128(base); 90 | return total; 91 | } 92 | 93 | /// @notice Add `elastic` to `total` and update storage. 94 | /// @return newElastic Returns updated `elastic`. 95 | function addElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) { 96 | newElastic = total.elastic += uint128(elastic); 97 | } 98 | 99 | /// @notice Subtract `elastic` from `total` and update storage. 100 | /// @return newElastic Returns updated `elastic`. 101 | function subElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) { 102 | newElastic = total.elastic -= uint128(elastic); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /docs/checks.txt: -------------------------------------------------------------------------------- 1 | Variables 2 | V1 - Is visibility set (SWC-108) 3 | V2 - Can they be private 4 | V3 - Can it be constant? 5 | V4 - Can it be immutable? 6 | V5 - No unused variables (SWC-131) 7 | 8 | Structs 9 | S1 - Are the members split on 256 boundaries 10 | S2 - Can any members be a smaller type 11 | 12 | Functions 13 | F1 - Set visibility: Change external to public to support batching. Should it be private? (SWC-100) 14 | F2 - Should it be payable? 15 | F3 - Can it be combined with another similar function? 16 | F4 - Check behaviour for all function arguments when wrong or extreme 17 | F5 - Checks-Effects-Interactions pattern followed? (SWC-107) 18 | F6 - Check for front-running possibilities, such as the approve function (SWC-114) 19 | F7 - Avoid Insufficient gas grieving (SWC-126)? 20 | F8 - Gas Limit DoS via Block Stuffing 21 | F9 - Are the correct modifiers applied, such as onlyOwner 22 | F10 - Return arguments are always assigned 23 | 24 | Modifiers 25 | M1 - No state changes (except for a reentrancy lock) 26 | M2 - No external calls 27 | M3 - Checks only 28 | M4 - Are any unbounded loops/arrays used that can cause DoS? (SWC-128) 29 | M5 - Check behaviour for all function arguments when wrong or extreme 30 | 31 | Code 32 | C1 - All math done through BoringMath (SWC-101) 33 | C2 - Are any storage slots read multiple times? 34 | C3 - Are any unbounded loops/arrays used that can cause DoS? (SWC-128) 35 | C4 - Use block.timestamp only for long intervals (SWC-116) 36 | C5 - Don't use block.number for elapsed time (SWC-116) 37 | C6 - Don't use assert, tx.origin, address.transfer(), address.send() (SWC-115 SWC-134 SWC-110) 38 | C7 - delegatecall only used within the same contract, never external even if trusted (SWC-112) 39 | C8 - Don't use function types 40 | C9 - Don't use blockhash, etc for randomness (SWC-120) 41 | C10 - Protect signatures against replay, use nonce and chainId (SWC-121) 42 | C11 - All signatures strictly EIP-712 (SWC-117 SWC-122) 43 | C12 - abi.encodePacked can't contain variable length user input (SWC-133) 44 | C13 - Careful with assembly, don't allow any arbitrary use data (SWC-127) 45 | C14 - Don't assume a specific ETH balance (and token) (SWC-132) 46 | C15 - Avoid Insufficient gas grieving (SWC-126)? 47 | C16 - Private data ISN'T private (SWC-136) 48 | C17 - Don't use deprecated functions, they should turn red anyway (SWC-111) 49 | (suicide, block.blockhash, sha3, callcode, throw, msg.gas, constant for view, var) 50 | C18 - Never shadow state variables (SWC-119) 51 | C19 - No unused variables (SWC-131) 52 | C20 - Is calculation on the fly cheaper than storing the value 53 | C21 - Are all state variables read from the correct contract: master vs. clone 54 | C22 - Is > or < or >= or <= correct 55 | C23 - Are logical operators correct ==, !=, &&, ||, ! 56 | 57 | Calls in Functions 58 | X1 - Is the result checked and errors dealt with? (SWC-104) 59 | X2 - If there is an error, could it cause a DoS. Like balanceOf causing revert. (SWC-113) 60 | X3 - What if it uses all gas? 61 | X4 - Is an external contract call needed? 62 | X5 - Is a lock used? If so are the external calls protected? 63 | 64 | Staticcalls(view) in functions 65 | S1 - Is it actually marked as view in the interface 66 | S2 - If there is an error, could it cause a DoS. Like balanceOf causing revert. (SWC-113) 67 | S3 - What if it uses all gas? 68 | S4 - Is an external contract call needed? 69 | 70 | Events 71 | E1 - Should any argument be indexed 72 | 73 | Contract 74 | T1 - Are all event there? 75 | T2 - Right-To-Left-Override control character not used, duh (SWC-130) 76 | T3 - No SELFDESTRUCT (SWC-106) 77 | T4 - Check for correct inheritance, keep it simple and linear (SWC-125) 78 | 79 | File 80 | P1 - SPDX header 81 | P2 - Solidity version hardcoded to 0.6.12 (SWC-102 SWC-103 SWC-124 SWC-129) 82 | P3 - Remove solhints that aren't needed 83 | 84 | -------------------------------------------------------------------------------- /test/BoringOwnable.js: -------------------------------------------------------------------------------- 1 | const { ADDRESS_ZERO, prepare } = require("./utilities") 2 | const { expect } = require("chai") 3 | const { BoringOwnable } = require("./utilities/BoringOwnable") 4 | 5 | describe("BoringOwnable", function () { 6 | before(async function () { 7 | await prepare(this, ["BoringOwnable"]) 8 | }) 9 | 10 | beforeEach(async function () { 11 | this.ownable = await new BoringOwnable(this).deploy() 12 | }) 13 | 14 | describe("Deployment", function () { 15 | it("Assigns owner", async function () { 16 | expect(await this.ownable.owner()).to.equal(this.alice.address) 17 | }) 18 | }) 19 | 20 | describe("Renounce Ownership", function () { 21 | it("Prevents non-owners from renouncement", async function () { 22 | await expect(this.ownable.as(this.bob).renounceOwnership()).to.be.revertedWith("Ownable: caller is not the owner") 23 | }) 24 | 25 | it("Assigns owner to address zero", async function () { 26 | await expect(this.ownable.renounceOwnership()) 27 | .to.emit(this.ownable.contract, "OwnershipTransferred") 28 | .withArgs(this.alice.address, ADDRESS_ZERO) 29 | 30 | expect(await this.ownable.owner()).to.equal(ADDRESS_ZERO) 31 | }) 32 | }) 33 | 34 | describe("Transfer Ownership", function () { 35 | it("Prevents non-owners from transferring", async function () { 36 | await expect(this.ownable.as(this.bob).transferOwnership(this.bob)).to.be.revertedWith("Ownable: caller is not the owner") 37 | }) 38 | 39 | it("Changes pending owner after transfer", async function () { 40 | await this.ownable.transferOwnership(this.bob.address) 41 | 42 | expect(await this.ownable.pendingOwner()).to.equal(this.bob.address) 43 | }) 44 | }) 45 | 46 | describe("Transfer Ownership Direct", function () { 47 | it("Reverts given a zero address as newOwner argument", async function () { 48 | await expect(this.ownable.assignOwnership(ADDRESS_ZERO)).to.be.revertedWith("Ownable: zero address") 49 | }) 50 | 51 | it("Mutates owner", async function () { 52 | await this.ownable.assignOwnership(this.bob) 53 | 54 | expect(await this.ownable.owner()).to.equal(this.bob.address) 55 | }) 56 | 57 | it("Emit OwnershipTransferred event with expected arguments", async function () { 58 | await expect(this.ownable.assignOwnership(this.bob)) 59 | .to.emit(this.ownable.contract, "OwnershipTransferred") 60 | .withArgs(this.alice.address, this.bob.address) 61 | }) 62 | }) 63 | 64 | describe("Claim Ownership", function () { 65 | it("Mutates owner", async function () { 66 | await this.ownable.transferOwnership(this.bob) 67 | await this.ownable.as(this.bob).claimOwnership() 68 | 69 | expect(await this.ownable.owner()).to.equal(this.bob.address) 70 | }) 71 | 72 | it("Assigns previous owner to address zero", async function () { 73 | await this.ownable.transferOwnership(this.bob) 74 | await this.ownable.as(this.bob).claimOwnership() 75 | 76 | expect(await this.ownable.pendingOwner()).to.equal(ADDRESS_ZERO) 77 | }) 78 | 79 | it("Prevents anybody but pending owner from claiming ownership", async function () { 80 | await expect(this.ownable.as(this.bob).claimOwnership()).to.be.revertedWith("Ownable: caller != pending owner") 81 | }) 82 | 83 | it("Emit OwnershipTransferred event with expected arguments", async function () { 84 | await this.ownable.transferOwnership(this.bob) 85 | 86 | await expect(this.ownable.as(this.bob).claimOwnership()) 87 | .to.emit(this.ownable.contract, "OwnershipTransferred") 88 | .withArgs(this.alice.address, this.bob.address) 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /contracts/BoringSingleNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "./interfaces/IERC721.sol"; 6 | import "./libraries/BoringAddress.sol"; 7 | 8 | // solhint-disable avoid-low-level-calls 9 | 10 | interface ERC721TokenReceiver { 11 | function onERC721Received( 12 | address _operator, 13 | address _from, 14 | uint256 _tokenId, 15 | bytes calldata _data 16 | ) external returns (bytes4); 17 | } 18 | 19 | abstract contract BoringSingleNFT is IERC721, IERC721Metadata { 20 | /// This contract is an EIP-721 compliant contract that holds only a single NFT (totalSupply = 1) 21 | using BoringAddress for address; 22 | 23 | // hodler must be set in derived contract 24 | // Since there is only one NFT, we only track single holder and allowed 25 | address public hodler; 26 | address public allowed; 27 | // solhint-disable-next-line const-name-snakecase 28 | uint256 public constant totalSupply = 1; 29 | 30 | // operator mappings as per usual 31 | mapping(address => mapping(address => bool)) public operators; 32 | 33 | function supportsInterface(bytes4 interfaceID) external pure returns (bool) { 34 | return 35 | interfaceID == this.supportsInterface.selector || // EIP-165 36 | interfaceID == 0x80ac58cd; // EIP-721 37 | } 38 | 39 | function balanceOf(address user) public view returns (uint256) { 40 | require(user != address(0), "No zero address"); 41 | return user == hodler ? 1 : 0; 42 | } 43 | 44 | function ownerOf(uint256 tokenId) public view returns (address) { 45 | require(tokenId == 0, "Invalid token ID"); 46 | require(hodler != address(0), "No owner"); 47 | return hodler; 48 | } 49 | 50 | function approve(address approved, uint256 tokenId) public payable { 51 | require(tokenId == 0, "Invalid token ID"); 52 | require(msg.sender == hodler || operators[hodler][msg.sender], "Not allowed"); 53 | allowed = approved; 54 | emit Approval(msg.sender, approved, tokenId); 55 | } 56 | 57 | function setApprovalForAll(address operator, bool approved) public { 58 | operators[msg.sender][operator] = approved; 59 | emit ApprovalForAll(msg.sender, operator, approved); 60 | } 61 | 62 | function getApproved(uint256 tokenId) public view returns (address) { 63 | require(tokenId == 0, "Invalid token ID"); 64 | return allowed; 65 | } 66 | 67 | function isApprovedForAll(address owner, address operator) public view returns (bool) { 68 | return operators[owner][operator]; 69 | } 70 | 71 | function _transferBase(address to) internal { 72 | emit Transfer(hodler, to, 0); 73 | hodler = to; 74 | // EIP-721 seems to suggest not to emit the Approval event here as it is indicated by the Transfer event. 75 | allowed = address(0); 76 | } 77 | 78 | function _transfer( 79 | address from, 80 | address to, 81 | uint256 tokenId 82 | ) internal { 83 | require(tokenId == 0, "Invalid token ID"); 84 | require(from == hodler, "From not owner"); 85 | require(msg.sender == hodler || msg.sender == allowed || operators[hodler][msg.sender], "Transfer not allowed"); 86 | require(to != address(0), "No zero address"); 87 | _transferBase(to); 88 | } 89 | 90 | function transferFrom( 91 | address from, 92 | address to, 93 | uint256 tokenId 94 | ) public payable { 95 | _transfer(from, to, tokenId); 96 | } 97 | 98 | function safeTransferFrom( 99 | address from, 100 | address to, 101 | uint256 tokenId 102 | ) public payable { 103 | safeTransferFrom(from, to, tokenId, ""); 104 | } 105 | 106 | function safeTransferFrom( 107 | address from, 108 | address to, 109 | uint256 tokenId, 110 | bytes memory data 111 | ) public payable { 112 | _transfer(from, to, tokenId); 113 | if (to.isContract()) { 114 | require( 115 | ERC721TokenReceiver(to).onERC721Received(msg.sender, from, tokenId, data) == 116 | bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")), 117 | "Wrong return value" 118 | ); 119 | } 120 | } 121 | 122 | function tokenURI(uint256 tokenId) public pure returns (string memory) { 123 | require(tokenId == 0, "Invalid token ID"); 124 | return _tokenURI(); 125 | } 126 | 127 | function _tokenURI() internal pure virtual returns (string memory); 128 | } 129 | -------------------------------------------------------------------------------- /contracts/BoringCooker.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "./Domain.sol"; 6 | 7 | // solhint-disable no-inline-assembly 8 | // solhint-disable avoid-low-level-calls 9 | // solhint-disable not-rely-on-time 10 | 11 | // This is a work in progress 12 | 13 | contract CookTarget { 14 | function onCook(address, bytes calldata) public payable virtual returns (bool success, bytes memory result) { 15 | // Check that msg.sender is the BoringCooker. If so, you can trust sender to be verified. 16 | return (true, ""); 17 | } 18 | } 19 | 20 | contract BoringCooker is Domain { 21 | mapping(address => uint256) public nonces; 22 | 23 | /// @dev Helper function to extract a useful revert message from a failed call. 24 | /// If the returned data is malformed or not correctly abi encoded then this call can fail itself. 25 | function _getRevertMsg(bytes memory _returnData) internal pure returns (string memory) { 26 | // If the _res length is less than 68, then the transaction failed silently (without a revert message) 27 | if (_returnData.length < 68) return "Transaction reverted silently"; 28 | 29 | assembly { 30 | // Slice the sighash. 31 | _returnData := add(_returnData, 0x04) 32 | } 33 | return abi.decode(_returnData, (string)); // All that remains is the revert string 34 | } 35 | 36 | uint8 private constant ACTION_CALL = 1; 37 | uint8 private constant ACTION_COOK = 2; 38 | uint8 private constant ACTION_SIGNED_COOK = 3; 39 | 40 | // keccak256("Cook(address sender,address target,bytes data,uint256 value,uint256 nonce,uint256 deadline)"); 41 | bytes32 private constant COOK_SIGNATURE_HASH = 0x22efff3742eba32ab114c316a3e6dae791aea24d5d74f889a8f67bc7d4054f24; 42 | 43 | // Verify that the cook call was signed and pass on the cook call params. Split out for stack reasons. 44 | function _verifySignature(bytes memory data) 45 | internal 46 | returns ( 47 | address, 48 | CookTarget, 49 | bytes memory, 50 | uint256 51 | ) 52 | { 53 | (address sender, CookTarget target, bytes memory data_, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) = 54 | abi.decode(data, (address, CookTarget, bytes, uint256, uint256, uint8, bytes32, bytes32)); 55 | 56 | require(sender != address(0), "Cooker: Sender cannot be 0"); 57 | require(block.timestamp < deadline, "Cooker: Expired"); 58 | require( 59 | ecrecover(_getDigest(keccak256(abi.encode(COOK_SIGNATURE_HASH, data_, sender, nonces[sender]++, deadline))), v, r, s) == sender, 60 | "Cooker: Invalid Signature" 61 | ); 62 | return (sender, target, data_, value); 63 | } 64 | 65 | function cook(uint8[] calldata actions, bytes[] calldata datas) external payable { 66 | bytes memory result; 67 | for (uint256 i = 0; i < actions.length; i++) { 68 | uint8 action = actions[i]; 69 | if (action == ACTION_CALL) { 70 | // Do any call. msg.sender will be the Cooker. 71 | (address target, bytes4 signature, bytes memory data, uint256 value) = abi.decode(datas[i], (address, bytes4, bytes, uint256)); 72 | require(signature != CookTarget.onCook.selector, "Use action cook"); 73 | (bool success, bytes memory localResult) = target.call{value: value}(abi.encodePacked(signature, data)); 74 | if (!success) { 75 | revert(_getRevertMsg(localResult)); 76 | } 77 | result = localResult; 78 | } else if (action == ACTION_COOK) { 79 | // Contracts that support cooking can accept the passed in sender as the verified msg.sender. 80 | (CookTarget target, bytes memory data, uint256 value) = abi.decode(datas[i], (CookTarget, bytes, uint256)); 81 | (bool success, bytes memory localResult) = target.onCook{value: value}(msg.sender, data); 82 | if (!success) { 83 | revert(_getRevertMsg(localResult)); 84 | } 85 | result = localResult; 86 | } else if (action == ACTION_SIGNED_COOK) { 87 | // Contracts that support cooking can accept the passed in sender as the verified msg.sender (here verified by signed message). 88 | (address sender, CookTarget target, bytes memory data, uint256 value) = _verifySignature(datas[i]); 89 | (bool success, bytes memory localResult) = target.onCook{value: value}(sender, data); 90 | if (!success) { 91 | revert(_getRevertMsg(localResult)); 92 | } 93 | result = localResult; 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/BoringRebase.js: -------------------------------------------------------------------------------- 1 | const { ADDRESS_ZERO, prepare } = require("./utilities") 2 | const { expect } = require("chai") 3 | 4 | describe("BoringRebase", function () { 5 | before(async function () { 6 | await prepare(this, ["MockBoringRebase"]) 7 | this.contract = await this.MockBoringRebase.deploy() 8 | await this.contract.deployed() 9 | }) 10 | 11 | it("Calculates first toShare correctly", async function () { 12 | expect(await this.contract.toBase(100, false)).to.equal(100) 13 | }) 14 | 15 | it("Calculates first toElastic correctly", async function () { 16 | expect(await this.contract.toElastic(100, false)).to.equal(100) 17 | }) 18 | 19 | it("Sets elastic and base", async function () { 20 | this.contract.set(1000, 500) 21 | }) 22 | 23 | it("Calculates toShare correctly", async function () { 24 | expect(await this.contract.toBase(100, false)).to.equal(50) 25 | expect(await this.contract.toBase(100, true)).to.equal(50) 26 | expect(await this.contract.toBase(1, false)).to.equal(0) 27 | expect(await this.contract.toBase(1, true)).to.equal(1) 28 | expect(await this.contract.toBase(0, false)).to.equal(0) 29 | expect(await this.contract.toBase(0, true)).to.equal(0) 30 | }) 31 | 32 | it("Calculates toElastic correctly", async function () { 33 | expect(await this.contract.toElastic(100, false)).to.equal(200) 34 | expect(await this.contract.toElastic(100, true)).to.equal(200) 35 | expect(await this.contract.toElastic(1, false)).to.equal(2) 36 | expect(await this.contract.toElastic(1, true)).to.equal(2) 37 | expect(await this.contract.toElastic(0, false)).to.equal(0) 38 | expect(await this.contract.toElastic(0, true)).to.equal(0) 39 | }) 40 | 41 | it("Adds elastic correctly", async function () { 42 | await this.contract.add(100, false) 43 | const total = await this.contract.total() 44 | expect(total.elastic).to.equal(1100) 45 | expect(total.base).to.equal(550) 46 | }) 47 | 48 | it("Removes base correctly", async function () { 49 | await this.contract.sub(50, false) 50 | const total = await this.contract.total() 51 | expect(total.elastic).to.equal(1000) 52 | expect(total.base).to.equal(500) 53 | }) 54 | 55 | it("Add both correctly", async function () { 56 | await this.contract.add2(189, 12) 57 | const total = await this.contract.total() 58 | expect(total.elastic).to.equal(1189) 59 | expect(total.base).to.equal(512) 60 | }) 61 | 62 | it("Calculates toShare correctly (complex)", async function () { 63 | expect(await this.contract.toBase(100, false)).to.equal(43) 64 | expect(await this.contract.toBase(100, true)).to.equal(44) 65 | expect(await this.contract.toBase(1, false)).to.equal(0) 66 | expect(await this.contract.toBase(1, true)).to.equal(1) 67 | expect(await this.contract.toBase(0, false)).to.equal(0) 68 | expect(await this.contract.toBase(0, true)).to.equal(0) 69 | }) 70 | 71 | it("Calculates toElastic correctly (complex)", async function () { 72 | expect(await this.contract.toElastic(100, false)).to.equal(232) 73 | expect(await this.contract.toElastic(100, true)).to.equal(233) 74 | expect(await this.contract.toElastic(1, false)).to.equal(2) 75 | expect(await this.contract.toElastic(1, true)).to.equal(3) 76 | expect(await this.contract.toElastic(0, false)).to.equal(0) 77 | expect(await this.contract.toElastic(0, true)).to.equal(0) 78 | }) 79 | 80 | it("Remove both correctly", async function () { 81 | await this.contract.sub2(1189, 512) 82 | const total = await this.contract.total() 83 | expect(total.elastic).to.equal(0) 84 | expect(total.base).to.equal(0) 85 | }) 86 | 87 | it("Removes base correctly when empty", async function () { 88 | this.contract = await this.MockBoringRebase.deploy() 89 | await this.contract.deployed() 90 | 91 | await this.contract.sub(0, false) 92 | const total = await this.contract.total() 93 | expect(total.elastic).to.equal(0) 94 | expect(total.base).to.equal(0) 95 | }) 96 | 97 | it("Adds elastic correctly when empty", async function () { 98 | await this.contract.add(100, false) 99 | const total = await this.contract.total() 100 | expect(total.elastic).to.equal(100) 101 | expect(total.base).to.equal(100) 102 | }) 103 | 104 | it("Adds just elastic correctly when empty", async function () { 105 | await this.contract.addElastic(50) 106 | const total = await this.contract.total() 107 | expect(total.elastic).to.equal(150) 108 | expect(total.base).to.equal(100) 109 | }) 110 | 111 | it("Remove just elastic correctly when empty", async function () { 112 | await this.contract.subElastic(40) 113 | const total = await this.contract.total() 114 | expect(total.elastic).to.equal(110) 115 | expect(total.base).to.equal(100) 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /contracts/ERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./interfaces/IERC1155.sol"; 5 | import "./interfaces/IERC1155TokenReceiver.sol"; 6 | import "./libraries/BoringAddress.sol"; 7 | 8 | // Written by OreNoMochi (https://github.com/OreNoMochii), BoringCrypto 9 | 10 | contract ERC1155 is IERC1155 { 11 | using BoringAddress for address; 12 | 13 | // mappings 14 | mapping(address => mapping(address => bool)) public override isApprovedForAll; // map of operator approval 15 | mapping(address => mapping(uint256 => uint256)) public override balanceOf; // map of tokens owned by 16 | mapping(uint256 => uint256) public totalSupply; // totalSupply per token 17 | 18 | function supportsInterface(bytes4 interfaceID) public pure override virtual returns (bool) { 19 | return 20 | interfaceID == this.supportsInterface.selector || // EIP-165 21 | interfaceID == 0xd9b67a26 || // ERC-1155 22 | interfaceID == 0x0e89341c; // EIP-1155 Metadata 23 | } 24 | 25 | function balanceOfBatch(address[] calldata owners, uint256[] calldata ids) external view override returns (uint256[] memory balances) { 26 | uint256 len = owners.length; 27 | require(len == ids.length, "ERC1155: Length mismatch"); 28 | 29 | balances = new uint256[](len); 30 | 31 | for (uint256 i = 0; i < len; i++) { 32 | balances[i] = balanceOf[owners[i]][ids[i]]; 33 | } 34 | } 35 | 36 | function _mint( 37 | address to, 38 | uint256 id, 39 | uint256 value 40 | ) internal { 41 | require(to != address(0), "No 0 address"); 42 | 43 | balanceOf[to][id] += value; 44 | totalSupply[id] += value; 45 | 46 | emit TransferSingle(msg.sender, address(0), to, id, value); 47 | } 48 | 49 | function _burn( 50 | address from, 51 | uint256 id, 52 | uint256 value 53 | ) internal { 54 | require(from != address(0), "No 0 address"); 55 | 56 | balanceOf[from][id] -= value; 57 | totalSupply[id] -= value; 58 | 59 | emit TransferSingle(msg.sender, from, address(0), id, value); 60 | } 61 | 62 | function _transferSingle( 63 | address from, 64 | address to, 65 | uint256 id, 66 | uint256 value 67 | ) internal { 68 | require(to != address(0), "No 0 address"); 69 | 70 | balanceOf[from][id] -= value; 71 | balanceOf[to][id] += value; 72 | 73 | emit TransferSingle(msg.sender, from, to, id, value); 74 | } 75 | 76 | function _transferBatch( 77 | address from, 78 | address to, 79 | uint256[] calldata ids, 80 | uint256[] calldata values 81 | ) internal { 82 | require(to != address(0), "No 0 address"); 83 | 84 | for (uint256 i = 0; i < ids.length; i++) { 85 | uint256 id = ids[i]; 86 | uint256 value = values[i]; 87 | balanceOf[from][id] -= value; 88 | balanceOf[to][id] += value; 89 | } 90 | 91 | emit TransferBatch(msg.sender, from, to, ids, values); 92 | } 93 | 94 | function _requireTransferAllowed(address from) internal view virtual { 95 | require(from == msg.sender || isApprovedForAll[from][msg.sender] == true, "Transfer not allowed"); 96 | } 97 | 98 | function safeTransferFrom( 99 | address from, 100 | address to, 101 | uint256 id, 102 | uint256 value, 103 | bytes calldata data 104 | ) external override { 105 | _requireTransferAllowed(from); 106 | 107 | _transferSingle(from, to, id, value); 108 | 109 | if (to.isContract()) { 110 | require( 111 | IERC1155TokenReceiver(to).onERC1155Received(msg.sender, from, id, value, data) == 112 | bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")), 113 | "Wrong return value" 114 | ); 115 | } 116 | } 117 | 118 | function safeBatchTransferFrom( 119 | address from, 120 | address to, 121 | uint256[] calldata ids, 122 | uint256[] calldata values, 123 | bytes calldata data 124 | ) external override { 125 | require(ids.length == values.length, "ERC1155: Length mismatch"); 126 | _requireTransferAllowed(from); 127 | 128 | _transferBatch(from, to, ids, values); 129 | 130 | if (to.isContract()) { 131 | require( 132 | IERC1155TokenReceiver(to).onERC1155BatchReceived(msg.sender, from, ids, values, data) == 133 | bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)")), 134 | "Wrong return value" 135 | ); 136 | } 137 | } 138 | 139 | function setApprovalForAll(address operator, bool approved) external virtual override { 140 | isApprovedForAll[msg.sender][operator] = approved; 141 | 142 | emit ApprovalForAll(msg.sender, operator, approved); 143 | } 144 | 145 | function uri( 146 | uint256 /*assetId*/ 147 | ) external view virtual returns (string memory) { 148 | return ""; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /contracts/BoringGenerativeNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | import "./BoringMultipleNFT.sol"; 5 | import "./BoringOwnable.sol"; 6 | import "./libraries/Base64.sol"; 7 | import "./interfaces/IBoringGenerativeNFT.sol"; 8 | 9 | contract BoringGenerativeNFT is IBoringGenerativeNFT, BoringMultipleNFT, BoringOwnable { 10 | using Base64 for bytes; 11 | 12 | ITrait[] private _traits; 13 | 14 | function traits(uint256 index) external view override returns (ITrait trait) { 15 | return _traits[index]; 16 | } 17 | 18 | constructor(string memory name, string memory symbol) BoringMultipleNFT(name, symbol) { 19 | this; // Hide empty code block warning 20 | } 21 | 22 | function traitsCount() public view override returns (uint256 count) { 23 | count = _traits.length; 24 | } 25 | 26 | function addTrait(string calldata name, ITrait trait) public override onlyOwner { 27 | uint8 gene = uint8(_traits.length); 28 | require(_traits.length < 9, "Traits full"); 29 | _traits.push(trait); 30 | require(_traits[gene].setName(gene, name) == bytes4(keccak256("setName(uint8,string)")), "Bad return"); 31 | } 32 | 33 | function addTraitData(uint8 trait, bytes calldata data) public onlyOwner { 34 | // Return value is checked to ensure only real Traits contracts are called 35 | require(_traits[trait].addData(trait, data) == bytes4(keccak256("addData(address,uint8,bytes)")), "Bad return"); 36 | } 37 | 38 | function tokenSVG(uint256 tokenId) public view returns (string memory) { 39 | TraitsData memory genes = _tokens[tokenId].data; 40 | uint256 traitCount = _traits.length; 41 | 42 | return 43 | abi 44 | .encodePacked( 45 | '', 46 | traitCount > 0 ? _traits[0].renderSVG(this, tokenId, 0, genes.trait0) : "", 47 | traitCount > 1 ? _traits[1].renderSVG(this, tokenId, 1, genes.trait1) : "", 48 | traitCount > 2 ? _traits[2].renderSVG(this, tokenId, 2, genes.trait2) : "", 49 | traitCount > 3 ? _traits[3].renderSVG(this, tokenId, 3, genes.trait3) : "", 50 | traitCount > 4 ? _traits[4].renderSVG(this, tokenId, 4, genes.trait4) : "", 51 | traitCount > 5 ? _traits[5].renderSVG(this, tokenId, 5, genes.trait5) : "", 52 | traitCount > 6 ? _traits[6].renderSVG(this, tokenId, 6, genes.trait6) : "", 53 | traitCount > 7 ? _traits[7].renderSVG(this, tokenId, 7, genes.trait7) : "", 54 | traitCount > 8 ? _traits[8].renderSVG(this, tokenId, 8, genes.trait8) : "", 55 | "" 56 | ) 57 | .encode(); 58 | } 59 | 60 | function _renderTrait( 61 | uint256 tokenId, 62 | uint256 traitCount, 63 | uint8 trait, 64 | uint8 gene 65 | ) internal view returns (bytes memory) { 66 | return abi.encodePacked(traitCount > trait ? _traits[0].renderTrait(this, tokenId, trait, gene) : "", traitCount > trait + 1 ? "," : ""); 67 | } 68 | 69 | function _renderTraits(uint256 tokenId) internal view returns (bytes memory) { 70 | TraitsData memory genes = _tokens[tokenId].data; 71 | uint256 traitCount = _traits.length; 72 | 73 | return 74 | abi.encodePacked( 75 | _renderTrait(tokenId, traitCount, 0, genes.trait0), 76 | _renderTrait(tokenId, traitCount, 1, genes.trait1), 77 | _renderTrait(tokenId, traitCount, 2, genes.trait2), 78 | _renderTrait(tokenId, traitCount, 3, genes.trait3), 79 | _renderTrait(tokenId, traitCount, 4, genes.trait4), 80 | _renderTrait(tokenId, traitCount, 5, genes.trait5), 81 | _renderTrait(tokenId, traitCount, 6, genes.trait6), 82 | _renderTrait(tokenId, traitCount, 7, genes.trait7), 83 | traitCount > 8 ? _traits[8].renderTrait(this, tokenId, 8, genes.trait8) : "" 84 | ); 85 | } 86 | 87 | function _tokenURI(uint256 tokenId) internal view override returns (string memory) { 88 | return 89 | string( 90 | abi.encodePacked( 91 | "data:application/json;base64,", 92 | abi 93 | .encodePacked( 94 | '{"image":"data:image/svg+xml;base64,', 95 | tokenSVG(tokenId), 96 | '","attributes":[', 97 | _renderTraits(tokenId), 98 | "]}" 99 | ) 100 | .encode() 101 | ) 102 | ); 103 | } 104 | 105 | function mint(TraitsData calldata genes, address to) public override onlyOwner { 106 | _mint(to, genes); 107 | } 108 | 109 | function batchMint(TraitsData[] calldata genes, address[] calldata to) public override onlyOwner { 110 | uint256 len = genes.length; 111 | require(len == to.length, "Length mismatch"); 112 | for (uint256 i = 0; i < len; i++) { 113 | _mint(to[i], genes[i]); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/certora/SingleNFT.spec: -------------------------------------------------------------------------------- 1 | methods { 2 | balanceOf(address user) returns (uint256) envfree; 3 | totalSupply() returns (uint256) envfree; 4 | ownerOf(uint256 tokenId) returns (address) envfree; 5 | allowed() returns (address) envfree; 6 | getApproved(uint256 tokenId) returns (address) envfree; 7 | isApprovedForAll(address _owner, address _operator) returns (bool) envfree; 8 | } 9 | 10 | /* 11 | // To "proof" the contract, I'm trialing an opposite approach to start with the thing we don't 12 | // want happening and iterate until we either find a counter example or close all paths. 13 | 14 | // We want to make sure no unauthorized addresses can steal the NFT (or actually change the owner in any way) 15 | 16 | rule StealV1(method f) { 17 | address ownerBefore = ownerOf(0); 18 | 19 | env e; 20 | // If you're already the owner, it's not stealing... 21 | require e.msg.sender != ownerBefore; 22 | calldataarg args; 23 | f(e, args); 24 | 25 | address ownerAfter = ownerOf(0); 26 | assert ownerBefore == ownerAfter; 27 | 28 | // This gives us 3 places to look, these are the only places that can change the owner (and maybe let us steal it) 29 | 30 | // Failed on StealV1: Violated for: 31 | // transferFrom(address,address,uint256), 32 | // safeTransferFrom(address,address,uint256), 33 | // safeTransferFrom(address,address,uint256,bytes), 34 | } 35 | */ 36 | 37 | // By looking at those functions we can see the approved address and operators could change the owner, so we need 38 | // to do 3 things: 39 | // - Try to steal it when we're not the operator and not the approved address 40 | // - Try to change the approved address (which could lead to stealing) 41 | // - Try to change the operator (which could lead to stealing) 42 | 43 | rule StealV2_Part1(method f) { 44 | address ownerBefore = ownerOf(0); 45 | 46 | env e; 47 | // If you're already the owner, it's not stealing... 48 | require e.msg.sender != ownerBefore; 49 | // If you're the approved address, it's not stealing... 50 | require e.msg.sender != getApproved(0); 51 | // If you're an operator, it's not stealing... 52 | require !isApprovedForAll(ownerBefore, e.msg.sender); 53 | calldataarg args; 54 | f(e, args); 55 | 56 | address ownerAfter = ownerOf(0); 57 | assert ownerBefore == ownerAfter; 58 | 59 | // This passed, so we can close off this path as a way to steal 60 | } 61 | 62 | rule StealV2_Part2(method f) { 63 | address ownerBefore = ownerOf(0); 64 | address approvedBefore = getApproved(0); 65 | 66 | env e; 67 | // If you're already the owner, it's not stealing... 68 | require e.msg.sender != ownerBefore; 69 | // If you're the approved address, it's not stealing? According the the EIP is it, but we'll allow it 70 | require e.msg.sender != getApproved(0); 71 | // If you're an operator, it's not stealing... 72 | require !isApprovedForAll(ownerBefore, e.msg.sender); 73 | calldataarg args; 74 | f(e, args); 75 | 76 | address approvedAfter = getApproved(0); 77 | assert approvedBefore == approvedAfter; 78 | 79 | // This passed, so we can't manipulate the approved address as a first step towards stealing 80 | } 81 | 82 | rule StealV2_Part3(method f, address operator) { 83 | address ownerBefore = ownerOf(0); 84 | bool operatorBefore = isApprovedForAll(ownerBefore, operator); 85 | 86 | env e; 87 | // If you're already the owner, it's not stealing... 88 | require e.msg.sender != ownerBefore; 89 | calldataarg args; 90 | f(e, args); 91 | 92 | bool operatorAfter = isApprovedForAll(ownerBefore, operator); 93 | assert operatorBefore == operatorAfter; 94 | 95 | // This passed, so we can't manipulate the operators as a first step towards stealing 96 | } 97 | 98 | // All change paths that would lead to changing the owner of the NFT by an unauthorized party are covered 99 | // So it looks like the NFT cannot be stolen :D 100 | 101 | // Here's some other rules and the original proof that kind of tests the same, but wasn't deduced the same way: 102 | 103 | invariant TotalSupplyAlwaysOne() 104 | totalSupply() == 1 105 | 106 | invariant BalanceOfOwnerIsOne() 107 | balanceOf(ownerOf(0)) == 1 108 | 109 | rule NoExternalAllowedOrOwnerChange(method f) { 110 | address allowedBefore = getApproved(0); 111 | address ownerBefore = ownerOf(0); 112 | 113 | env e; 114 | require e.msg.sender != ownerOf(0); 115 | require e.msg.sender != getApproved(0); 116 | require !isApprovedForAll(ownerOf(0), e.msg.sender); 117 | calldataarg args; 118 | f(e, args); 119 | 120 | address allowedAfter = getApproved(0); 121 | address ownerAfter = ownerOf(0); 122 | 123 | assert allowedBefore == allowedAfter; 124 | assert ownerBefore == ownerAfter; 125 | } 126 | 127 | rule NoExternalOperatorChange(method f, address operator) { 128 | address ownerBefore = ownerOf(0); 129 | bool operatorBefore = isApprovedForAll(ownerBefore, operator); 130 | 131 | env e; 132 | require e.msg.sender != ownerOf(0); 133 | require e.msg.sender != getApproved(0); 134 | require !isApprovedForAll(ownerOf(0), e.msg.sender); 135 | calldataarg args; 136 | f(e, args); 137 | 138 | bool operatorAfter = isApprovedForAll(ownerBefore, operator); 139 | 140 | assert operatorBefore == operatorAfter; 141 | } -------------------------------------------------------------------------------- /contracts/libraries/BoringERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "../interfaces/IERC20.sol"; 4 | 5 | // solhint-disable avoid-low-level-calls 6 | 7 | library BoringERC20 { 8 | bytes4 private constant SIG_SYMBOL = 0x95d89b41; // symbol() 9 | bytes4 private constant SIG_NAME = 0x06fdde03; // name() 10 | bytes4 private constant SIG_DECIMALS = 0x313ce567; // decimals() 11 | bytes4 private constant SIG_BALANCE_OF = 0x70a08231; // balanceOf(address) 12 | bytes4 private constant SIG_TOTALSUPPLY = 0x18160ddd; // balanceOf(address) 13 | bytes4 private constant SIG_TRANSFER = 0xa9059cbb; // transfer(address,uint256) 14 | bytes4 private constant SIG_TRANSFER_FROM = 0x23b872dd; // transferFrom(address,address,uint256) 15 | 16 | function returnDataToString(bytes memory data) internal pure returns (string memory) { 17 | if (data.length >= 64) { 18 | return abi.decode(data, (string)); 19 | } else if (data.length == 32) { 20 | uint8 i = 0; 21 | while (i < 32 && data[i] != 0) { 22 | i++; 23 | } 24 | bytes memory bytesArray = new bytes(i); 25 | for (i = 0; i < 32 && data[i] != 0; i++) { 26 | bytesArray[i] = data[i]; 27 | } 28 | return string(bytesArray); 29 | } else { 30 | return "???"; 31 | } 32 | } 33 | 34 | /// @notice Provides a safe ERC20.symbol version which returns '???' as fallback string. 35 | /// @param token The address of the ERC-20 token contract. 36 | /// @return (string) Token symbol. 37 | function safeSymbol(IERC20 token) internal view returns (string memory) { 38 | (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_SYMBOL)); 39 | return success ? returnDataToString(data) : "???"; 40 | } 41 | 42 | /// @notice Provides a safe ERC20.name version which returns '???' as fallback string. 43 | /// @param token The address of the ERC-20 token contract. 44 | /// @return (string) Token name. 45 | function safeName(IERC20 token) internal view returns (string memory) { 46 | (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_NAME)); 47 | return success ? returnDataToString(data) : "???"; 48 | } 49 | 50 | /// @notice Provides a safe ERC20.decimals version which returns '18' as fallback value. 51 | /// @param token The address of the ERC-20 token contract. 52 | /// @return (uint8) Token decimals. 53 | function safeDecimals(IERC20 token) internal view returns (uint8) { 54 | (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_DECIMALS)); 55 | return success && data.length == 32 ? abi.decode(data, (uint8)) : 18; 56 | } 57 | 58 | /// @notice Provides a gas-optimized balance check to avoid a redundant extcodesize check in addition to the returndatasize check. 59 | /// @param token The address of the ERC-20 token. 60 | /// @param to The address of the user to check. 61 | /// @return amount The token amount. 62 | function safeBalanceOf(IERC20 token, address to) internal view returns (uint256 amount) { 63 | (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_BALANCE_OF, to)); 64 | require(success && data.length >= 32, "BoringERC20: BalanceOf failed"); 65 | amount = abi.decode(data, (uint256)); 66 | } 67 | 68 | /// @notice Provides a gas-optimized totalSupply to avoid a redundant extcodesize check in addition to the returndatasize check. 69 | /// @param token The address of the ERC-20 token. 70 | /// @return totalSupply The token totalSupply. 71 | function safeTotalSupply(IERC20 token) internal view returns (uint256 totalSupply) { 72 | (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_TOTALSUPPLY)); 73 | require(success && data.length >= 32, "BoringERC20: totalSupply failed"); 74 | totalSupply = abi.decode(data, (uint256)); 75 | } 76 | 77 | /// @notice Provides a safe ERC20.transfer version for different ERC-20 implementations. 78 | /// Reverts on a failed transfer. 79 | /// @param token The address of the ERC-20 token. 80 | /// @param to Transfer tokens to. 81 | /// @param amount The token amount. 82 | function safeTransfer( 83 | IERC20 token, 84 | address to, 85 | uint256 amount 86 | ) internal { 87 | (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER, to, amount)); 88 | require(success && (data.length == 0 || abi.decode(data, (bool))), "BoringERC20: Transfer failed"); 89 | } 90 | 91 | /// @notice Provides a safe ERC20.transferFrom version for different ERC-20 implementations. 92 | /// Reverts on a failed transfer. 93 | /// @param token The address of the ERC-20 token. 94 | /// @param from Transfer tokens from. 95 | /// @param to Transfer tokens to. 96 | /// @param amount The token amount. 97 | function safeTransferFrom( 98 | IERC20 token, 99 | address from, 100 | address to, 101 | uint256 amount 102 | ) internal { 103 | (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER_FROM, from, to, amount)); 104 | require(success && (data.length == 0 || abi.decode(data, (bool))), "BoringERC20: TransferFrom failed"); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | // hardhat.config.js 2 | require("dotenv/config") 3 | require("@nomiclabs/hardhat-etherscan") 4 | require("@nomiclabs/hardhat-solhint") 5 | // require("@nomiclabs/hardhat-solpp") 6 | require("@tenderly/hardhat-tenderly") 7 | require("@nomiclabs/hardhat-waffle") 8 | require("hardhat-abi-exporter") 9 | require("hardhat-deploy") 10 | require("hardhat-deploy-ethers") 11 | require("hardhat-gas-reporter") 12 | require("hardhat-spdx-license-identifier") 13 | require("hardhat-watcher") 14 | require("solidity-coverage") 15 | 16 | const { normalizeHardhatNetworkAccountsConfig } = require("hardhat/internal/core/providers/util") 17 | 18 | const { BN, bufferToHex, privateToAddress, toBuffer } = require("ethereumjs-util") 19 | 20 | const { removeConsoleLog } = require("hardhat-preprocessor") 21 | 22 | const accounts = { 23 | mnemonic: process.env.MNEMONIC || "test test test test test test test test test test test junk", 24 | accountsBalance: "990000000000000000000", 25 | } 26 | 27 | // This is a sample Hardhat task. To learn how to create your own go to 28 | // https://hardhat.org/guides/create-task.html 29 | task("accounts", "Prints the list of accounts", async (_, { config }) => { 30 | const networkConfig = config.networks["hardhat"] 31 | 32 | const accounts = normalizeHardhatNetworkAccountsConfig(networkConfig.accounts) 33 | 34 | console.log("Accounts") 35 | console.log("========") 36 | 37 | for (const [index, account] of accounts.entries()) { 38 | const address = bufferToHex(privateToAddress(toBuffer(account.privateKey))) 39 | const privateKey = bufferToHex(toBuffer(account.privateKey)) 40 | const balance = new BN(account.balance).div(new BN(10).pow(new BN(18))).toString(10) 41 | console.log(`Account #${index}: ${address} (${balance} ETH) 42 | Private Key: ${privateKey} 43 | `) 44 | } 45 | }) 46 | 47 | task("named-accounts", "Prints the list of named account", async () => { 48 | console.log({ namedAccounts: await getNamedAccounts() }) 49 | }) 50 | 51 | task("block", "Prints the current block", async (_, { ethers }) => { 52 | const block = await ethers.provider.getBlockNumber() 53 | 54 | console.log("Current block: " + block) 55 | }) 56 | 57 | task("pairs", "Prints the list of pairs", async () => { 58 | // ... 59 | }) 60 | 61 | /** 62 | * @type import('hardhat/config').HardhatUserConfig 63 | */ 64 | module.exports = { 65 | abiExporter: { 66 | path: "./build/abi", 67 | //clear: true, 68 | flat: true, 69 | // only: [], 70 | // except: [] 71 | }, 72 | defaultNetwork: "hardhat", 73 | etherscan: { 74 | // Your API key for Etherscan 75 | // Obtain one at https://etherscan.io/ 76 | apiKey: process.env.ETHERSCAN_API_KEY, 77 | }, 78 | gasReporter: { 79 | enabled: process.env.REPORT_GAS ? true : false, 80 | currency: "USD", 81 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 82 | excludeContracts: ["contracts/mocks/", "contracts/libraries/"], 83 | }, 84 | hardhat: { 85 | forking: { 86 | url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_API_KEY}`, 87 | }, 88 | }, 89 | // mocha: { 90 | // timeout: 0, 91 | // }, 92 | namedAccounts: { 93 | deployer: { 94 | default: 0, // here this will by default take the first account as deployer 95 | 1: 0, // similarly on mainnet it will take the first account as deployer. Note though that depending on how hardhat network are configured, the account 0 on one network can be different than on another 96 | }, 97 | alice: { 98 | default: 1, 99 | // hardhat: 0, 100 | }, 101 | bob: { 102 | default: 2, 103 | // hardhat: 0, 104 | }, 105 | carol: { 106 | default: 3, 107 | // hardhat: 0, 108 | }, 109 | fee: { 110 | // Default to 1 111 | default: 1, 112 | // Multi sig feeTo address 113 | // 1: "", 114 | }, 115 | dev: { 116 | // Default to 1 117 | default: 1, 118 | // Borings devTo address 119 | // 1: "", 120 | }, 121 | }, 122 | 123 | networks: { 124 | hardhat: { 125 | chainId: 31337, 126 | accounts, 127 | }, 128 | // mainnet: { 129 | // url: `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, 130 | // accounts: [process.env.PRIVATE_KEY], 131 | // gasPrice: 120 * 1000000000, 132 | // chainId: 1, 133 | // }, 134 | ropsten: { 135 | url: `https://ropsten.infura.io/v3/${process.env.INFURA_API_KEY}`, 136 | accounts, 137 | chainId: 3, 138 | live: true, 139 | saveDeployments: true, 140 | }, 141 | kovan: { 142 | url: `https://kovan.infura.io/v3/${process.env.INFURA_API_KEY}`, 143 | accounts, 144 | chainId: 42, 145 | live: true, 146 | saveDeployments: true, 147 | }, 148 | // rinkeby: { 149 | // url: `https://rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`, 150 | // accounts: [process.env.PRIVATE_KEY], 151 | // chainId: 4, 152 | // }, 153 | }, 154 | preprocess: { 155 | eachLine: removeConsoleLog((bre) => bre.network.name !== "hardhat" && bre.network.name !== "localhost"), 156 | }, 157 | solidity: { 158 | version: "0.8.9", 159 | settings: { 160 | optimizer: { 161 | enabled: true, 162 | runs: 500, 163 | }, 164 | }, 165 | }, 166 | spdxLicenseIdentifier: { 167 | overwrite: false, 168 | runOnCompile: true, 169 | }, 170 | tenderly: { 171 | project: process.env.TENDERLY_PROJECT, 172 | username: process.env.TENDERLY_USERNAME, 173 | }, 174 | watcher: { 175 | compile: { 176 | tasks: ["compile"], 177 | files: ["./contracts"], 178 | verbose: true, 179 | }, 180 | }, 181 | } 182 | -------------------------------------------------------------------------------- /contracts/ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "./interfaces/IERC20.sol"; 4 | import "./Domain.sol"; 5 | 6 | // solhint-disable no-inline-assembly 7 | // solhint-disable not-rely-on-time 8 | 9 | // Data part taken out for building of contracts that receive delegate calls 10 | contract ERC20Data { 11 | /// @notice owner > balance mapping. 12 | mapping(address => uint256) public balanceOf; 13 | /// @notice owner > spender > allowance mapping. 14 | mapping(address => mapping(address => uint256)) public allowance; 15 | /// @notice owner > nonce mapping. Used in `permit`. 16 | mapping(address => uint256) public nonces; 17 | } 18 | 19 | abstract contract ERC20 is IERC20, Domain { 20 | /// @notice owner > balance mapping. 21 | mapping(address => uint256) public override balanceOf; 22 | /// @notice owner > spender > allowance mapping. 23 | mapping(address => mapping(address => uint256)) public override allowance; 24 | /// @notice owner > nonce mapping. Used in `permit`. 25 | mapping(address => uint256) public nonces; 26 | 27 | /// @notice Transfers `amount` tokens from `msg.sender` to `to`. 28 | /// @param to The address to move the tokens. 29 | /// @param amount of the tokens to move. 30 | /// @return (bool) Returns True if succeeded. 31 | function transfer(address to, uint256 amount) public returns (bool) { 32 | // If `amount` is 0, or `msg.sender` is `to` nothing happens 33 | if (amount != 0 || msg.sender == to) { 34 | uint256 srcBalance = balanceOf[msg.sender]; 35 | require(srcBalance >= amount, "ERC20: balance too low"); 36 | if (msg.sender != to) { 37 | require(to != address(0), "ERC20: no zero address"); // Moved down so low balance calls safe some gas 38 | 39 | balanceOf[msg.sender] = srcBalance - amount; // Underflow is checked 40 | balanceOf[to] += amount; 41 | } 42 | } 43 | emit Transfer(msg.sender, to, amount); 44 | return true; 45 | } 46 | 47 | /// @notice Transfers `amount` tokens from `from` to `to`. Caller needs approval for `from`. 48 | /// @param from Address to draw tokens from. 49 | /// @param to The address to move the tokens. 50 | /// @param amount The token amount to move. 51 | /// @return (bool) Returns True if succeeded. 52 | function transferFrom( 53 | address from, 54 | address to, 55 | uint256 amount 56 | ) public returns (bool) { 57 | // If `amount` is 0, or `from` is `to` nothing happens 58 | if (amount != 0) { 59 | uint256 srcBalance = balanceOf[from]; 60 | require(srcBalance >= amount, "ERC20: balance too low"); 61 | 62 | if (from != to) { 63 | uint256 spenderAllowance = allowance[from][msg.sender]; 64 | // If allowance is infinite, don't decrease it to save on gas (breaks with EIP-20). 65 | if (spenderAllowance != type(uint256).max) { 66 | require(spenderAllowance >= amount, "ERC20: allowance too low"); 67 | allowance[from][msg.sender] = spenderAllowance - amount; // Underflow is checked 68 | } 69 | require(to != address(0), "ERC20: no zero address"); // Moved down so other failed calls safe some gas 70 | 71 | balanceOf[from] = srcBalance - amount; // Underflow is checked 72 | balanceOf[to] += amount; 73 | } 74 | } 75 | emit Transfer(from, to, amount); 76 | return true; 77 | } 78 | 79 | /// @notice Approves `amount` from sender to be spend by `spender`. 80 | /// @param spender Address of the party that can draw from msg.sender's account. 81 | /// @param amount The maximum collective amount that `spender` can draw. 82 | /// @return (bool) Returns True if approved. 83 | function approve(address spender, uint256 amount) public override returns (bool) { 84 | allowance[msg.sender][spender] = amount; 85 | emit Approval(msg.sender, spender, amount); 86 | return true; 87 | } 88 | 89 | // solhint-disable-next-line func-name-mixedcase 90 | function DOMAIN_SEPARATOR() external view returns (bytes32) { 91 | return _domainSeparator(); 92 | } 93 | 94 | // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 95 | bytes32 private constant PERMIT_SIGNATURE_HASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; 96 | 97 | /// @notice Approves `value` from `owner_` to be spend by `spender`. 98 | /// @param owner_ Address of the owner. 99 | /// @param spender The address of the spender that gets approved to draw from `owner_`. 100 | /// @param value The maximum collective amount that `spender` can draw. 101 | /// @param deadline This permit must be redeemed before this deadline (UTC timestamp in seconds). 102 | function permit( 103 | address owner_, 104 | address spender, 105 | uint256 value, 106 | uint256 deadline, 107 | uint8 v, 108 | bytes32 r, 109 | bytes32 s 110 | ) external override { 111 | require(owner_ != address(0), "ERC20: Owner cannot be 0"); 112 | require(block.timestamp < deadline, "ERC20: Expired"); 113 | require( 114 | ecrecover(_getDigest(keccak256(abi.encode(PERMIT_SIGNATURE_HASH, owner_, spender, value, nonces[owner_]++, deadline))), v, r, s) == 115 | owner_, 116 | "ERC20: Invalid Signature" 117 | ); 118 | allowance[owner_][spender] = value; 119 | emit Approval(owner_, spender, value); 120 | } 121 | } 122 | 123 | contract ERC20WithSupply is IERC20, ERC20 { 124 | uint256 public override totalSupply; 125 | 126 | function _mint(address user, uint256 amount) internal { 127 | uint256 newTotalSupply = totalSupply + amount; 128 | require(newTotalSupply >= totalSupply, "Mint overflow"); 129 | totalSupply = newTotalSupply; 130 | balanceOf[user] += amount; 131 | emit Transfer(address(0), user, amount); 132 | } 133 | 134 | function _burn(address user, uint256 amount) internal { 135 | require(balanceOf[user] >= amount, "Burn too much"); 136 | totalSupply -= amount; 137 | balanceOf[user] -= amount; 138 | emit Transfer(user, address(0), amount); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /contracts/BoringMultipleNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import "./interfaces/IERC721.sol"; 6 | import "./interfaces/IERC721TokenReceiver.sol"; 7 | import "./libraries/BoringAddress.sol"; 8 | 9 | // solhint-disable avoid-low-level-calls 10 | 11 | struct TraitsData { 12 | uint8 trait0; 13 | uint8 trait1; 14 | uint8 trait2; 15 | uint8 trait3; 16 | uint8 trait4; 17 | uint8 trait5; 18 | uint8 trait6; 19 | uint8 trait7; 20 | uint8 trait8; 21 | } 22 | 23 | abstract contract BoringMultipleNFT is IERC721, IERC721Metadata, IERC721Enumerable { 24 | /// This contract is an EIP-721 compliant contract with enumerable support 25 | /// To optimize for gas, tokenId is sequential and start at 0. Also, tokens can't be removed/burned. 26 | using BoringAddress for address; 27 | 28 | string public name; 29 | string public symbol; 30 | 31 | constructor(string memory name_, string memory symbol_) { 32 | name = name_; 33 | symbol = symbol_; 34 | } 35 | 36 | uint256 public totalSupply = 0; 37 | 38 | struct TokenInfo { 39 | // There 3 pack into a single storage slot 160 + 24 + 9*8 = 256 bits 40 | address owner; 41 | uint24 index; // index in the tokensOf array, one address can hold a maximum of 16,777,216 tokens 42 | TraitsData data; // data field can be used to store traits 43 | } 44 | 45 | // operator mappings as per usual 46 | mapping(address => mapping(address => bool)) public isApprovedForAll; 47 | mapping(address => uint256[]) public tokensOf; // Array of tokens owned by 48 | mapping(uint256 => TokenInfo) internal _tokens; // The index in the tokensOf array for the token, needed to remove tokens from tokensOf 49 | mapping(uint256 => address) internal _approved; // keep track of approved nft 50 | 51 | function supportsInterface(bytes4 interfaceID) external pure returns (bool) { 52 | return 53 | interfaceID == this.supportsInterface.selector || // EIP-165 54 | interfaceID == 0x80ac58cd || // EIP-721 55 | interfaceID == 0x5b5e139f || // EIP-721 metadata extension 56 | interfaceID == 0x780e9d63; // EIP-721 enumeration extension 57 | } 58 | 59 | function approve(address approved, uint256 tokenId) public payable { 60 | address owner = _tokens[tokenId].owner; 61 | require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "Not allowed"); 62 | _approved[tokenId] = approved; 63 | emit Approval(owner, approved, tokenId); 64 | } 65 | 66 | function getApproved(uint256 tokenId) public view returns (address approved) { 67 | require(tokenId < totalSupply, "Invalid tokenId"); 68 | return _approved[tokenId]; 69 | } 70 | 71 | function setApprovalForAll(address operator, bool approved) public { 72 | isApprovedForAll[msg.sender][operator] = approved; 73 | emit ApprovalForAll(msg.sender, operator, approved); 74 | } 75 | 76 | function ownerOf(uint256 tokenId) public view returns (address) { 77 | address owner = _tokens[tokenId].owner; 78 | require(owner != address(0), "No owner"); 79 | return owner; 80 | } 81 | 82 | function balanceOf(address owner) public view returns (uint256) { 83 | require(owner != address(0), "No 0 owner"); 84 | return tokensOf[owner].length; 85 | } 86 | 87 | function _transferBase( 88 | uint256 tokenId, 89 | address from, 90 | address to, 91 | TraitsData memory data 92 | ) internal { 93 | address owner = _tokens[tokenId].owner; 94 | require(from == owner, "From not owner"); 95 | 96 | uint24 index; 97 | // Remove the token from the current owner's tokensOf array 98 | if (from != address(0)) { 99 | index = _tokens[tokenId].index; // The index of the item to remove in the array 100 | data = _tokens[tokenId].data; 101 | uint256 last = tokensOf[from].length - 1; 102 | uint256 lastTokenId = tokensOf[from][last]; 103 | tokensOf[from][index] = lastTokenId; // Copy the last item into the slot of the one to be removed 104 | _tokens[lastTokenId].index = index; // Update the token index for the last item that was moved 105 | tokensOf[from].pop(); // Delete the last item 106 | } 107 | 108 | index = uint24(tokensOf[to].length); 109 | tokensOf[to].push(tokenId); 110 | _tokens[tokenId] = TokenInfo({owner: to, index: index, data: data}); 111 | 112 | // EIP-721 seems to suggest not to emit the Approval event here as it is indicated by the Transfer event. 113 | _approved[tokenId] = address(0); 114 | emit Transfer(from, to, tokenId); 115 | } 116 | 117 | function _transfer( 118 | address from, 119 | address to, 120 | uint256 tokenId 121 | ) internal { 122 | require(msg.sender == from || msg.sender == _approved[tokenId] || isApprovedForAll[from][msg.sender], "Transfer not allowed"); 123 | require(to != address(0), "No zero address"); 124 | // check for owner == from is in base 125 | _transferBase(tokenId, from, to, TraitsData(0, 0, 0, 0, 0, 0, 0, 0, 0)); 126 | } 127 | 128 | function transferFrom( 129 | address from, 130 | address to, 131 | uint256 tokenId 132 | ) public payable { 133 | _transfer(from, to, tokenId); 134 | } 135 | 136 | function safeTransferFrom( 137 | address from, 138 | address to, 139 | uint256 tokenId 140 | ) public payable { 141 | safeTransferFrom(from, to, tokenId, ""); 142 | } 143 | 144 | function safeTransferFrom( 145 | address from, 146 | address to, 147 | uint256 tokenId, 148 | bytes memory data 149 | ) public payable { 150 | _transfer(from, to, tokenId); 151 | if (to.isContract()) { 152 | require( 153 | IERC721TokenReceiver(to).onERC721Received(msg.sender, from, tokenId, data) == 154 | bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")), 155 | "Wrong return value" 156 | ); 157 | } 158 | } 159 | 160 | function tokenURI(uint256 tokenId) public view returns (string memory) { 161 | require(tokenId < totalSupply, "Not minted"); 162 | return _tokenURI(tokenId); 163 | } 164 | 165 | function _tokenURI(uint256 tokenId) internal view virtual returns (string memory); 166 | 167 | function tokenByIndex(uint256 index) public view returns (uint256) { 168 | require(index < totalSupply, "Out of bounds"); 169 | return index; // This works due the optimization of sequential tokenIds and no burning 170 | } 171 | 172 | function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) { 173 | return tokensOf[owner][index]; 174 | } 175 | 176 | // 177 | function _mint(address owner, TraitsData memory data) internal returns (uint256 tokenId) { 178 | tokenId = totalSupply; 179 | _transferBase(tokenId, address(0), owner, data); 180 | totalSupply++; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /test/certora/ERC20.spec: -------------------------------------------------------------------------------- 1 | methods { 2 | balanceOf(address user) returns (uint256) envfree; 3 | totalSupply() returns (uint256) envfree; 4 | allowance(address from, address spender) returns (uint256) envfree; 5 | } 6 | 7 | rule TransferCorrect(address to, uint256 amount) { 8 | env e; 9 | require e.msg.value == 0; 10 | uint256 fromBalanceBefore = balanceOf(e.msg.sender); 11 | uint256 toBalanceBefore = balanceOf(to); 12 | require fromBalanceBefore + toBalanceBefore <= max_uint256; 13 | 14 | transfer@withrevert(e, to, amount); 15 | bool reverted = lastReverted; 16 | if (!reverted) { 17 | if (e.msg.sender == to) { 18 | assert balanceOf(e.msg.sender) == fromBalanceBefore; 19 | } else { 20 | assert balanceOf(e.msg.sender) == fromBalanceBefore - amount; 21 | assert balanceOf(to) == toBalanceBefore + amount; 22 | } 23 | } else { 24 | assert amount > fromBalanceBefore || to == 0; 25 | } 26 | } 27 | 28 | rule TransferFromCorrect(address from, address to, uint256 amount) { 29 | env e; 30 | require e.msg.value == 0; 31 | uint256 fromBalanceBefore = balanceOf(from); 32 | uint256 toBalanceBefore = balanceOf(to); 33 | uint256 allowanceBefore = allowance(from, e.msg.sender); 34 | require fromBalanceBefore + toBalanceBefore <= max_uint256; 35 | 36 | transferFrom@withrevert(e, from, to, amount); 37 | bool reverted = lastReverted; 38 | if (!reverted) { 39 | if (from == to) { 40 | assert balanceOf(from) == fromBalanceBefore; 41 | assert allowance(from, e.msg.sender) == allowanceBefore; 42 | } else { 43 | assert balanceOf(from) == fromBalanceBefore - amount; 44 | assert balanceOf(to) == toBalanceBefore + amount; 45 | if (allowanceBefore == max_uint256) { 46 | assert allowance(from, e.msg.sender) == max_uint256; 47 | } else { 48 | assert allowance(from, e.msg.sender) == allowanceBefore - amount; 49 | } 50 | } 51 | } else { 52 | assert allowanceBefore < amount || amount > fromBalanceBefore || to == 0; 53 | } 54 | } 55 | 56 | invariant ZeroAddressNoBalance() 57 | balanceOf(0) == 0 58 | 59 | ghost sumOfBalances() returns uint256; 60 | 61 | hook Sstore balanceOf[KEY address a] uint256 balance (uint256 old_balance) STORAGE { 62 | havoc sumOfBalances assuming 63 | sumOfBalances@new() == sumOfBalances@old() + (balance - old_balance); 64 | } 65 | 66 | rule NoChangeTotalSupply(method f) { 67 | uint256 totalSupplyBefore = totalSupply(); 68 | env e; 69 | calldataarg args; 70 | f(e, args); 71 | assert totalSupply() == totalSupplyBefore; 72 | } 73 | 74 | rule ChangingAllowance(method f, address from, address spender) { 75 | uint256 allowanceBefore = allowance(from, spender); 76 | env e; 77 | if (f.selector == approve(address, uint256).selector) { 78 | address spender_; 79 | uint256 amount; 80 | approve(e, spender_, amount); 81 | if (from == e.msg.sender && spender == spender_) { 82 | assert allowance(from, spender) == amount; 83 | } else { 84 | assert allowance(from, spender) == allowanceBefore; 85 | } 86 | } else if (f.selector == permit(address,address,uint256,uint256,uint8,bytes32,bytes32).selector) { 87 | address from_; 88 | address spender_; 89 | uint256 amount; 90 | uint256 deadline; 91 | uint8 v; 92 | bytes32 r; 93 | bytes32 s; 94 | permit(e, from_, spender_, amount, deadline, v, r, s); 95 | if (from == from_ && spender == spender_) { 96 | assert allowance(from, spender) == amount; 97 | } else { 98 | assert allowance(from, spender) == allowanceBefore; 99 | } 100 | } else if (f.selector == transferFrom(address,address,uint256).selector) { 101 | address from_; 102 | address to; 103 | address amount; 104 | transferFrom(e, from_, to, amount); 105 | uint256 allowanceAfter = allowance(from, spender); 106 | if (from == from_ && spender == e.msg.sender) { 107 | assert from == to || allowanceBefore == max_uint256 || allowanceAfter == allowanceBefore - amount; 108 | } else { 109 | assert allowance(from, spender) == allowanceBefore; 110 | } 111 | } else { 112 | calldataarg args; 113 | f(e, args); 114 | assert allowance(from, spender) == allowanceBefore; 115 | } 116 | } 117 | 118 | rule TransferSumOfFromAndToBalancesStaySame(address to, uint256 amount) { 119 | env e; 120 | mathint sum = balanceOf(e.msg.sender) + balanceOf(to); 121 | require sum < max_uint256; 122 | transfer(e, to, amount); 123 | assert balanceOf(e.msg.sender) + balanceOf(to) == sum; 124 | } 125 | 126 | rule TransferFromSumOfFromAndToBalancesStaySame(address from, address to, uint256 amount) { 127 | env e; 128 | mathint sum = balanceOf(from) + balanceOf(to); 129 | require sum < max_uint256; 130 | transferFrom(e, from, to, amount); 131 | assert balanceOf(from) + balanceOf(to) == sum; 132 | } 133 | 134 | rule TransferDoesntChangeOtherBalance(address to, uint256 amount, address other) { 135 | env e; 136 | require other != e.msg.sender; 137 | require other != to; 138 | uint256 balanceBefore = balanceOf(other); 139 | transfer(e, to, amount); 140 | assert balanceBefore == balanceOf(other); 141 | } 142 | 143 | rule TransferFromDoesntChangeOtherBalance(address from, address to, uint256 amount, address other) { 144 | env e; 145 | require other != from; 146 | require other != to; 147 | uint256 balanceBefore = balanceOf(other); 148 | transferFrom(e, from, to, amount); 149 | assert balanceBefore == balanceOf(other); 150 | } 151 | 152 | rule SumOfBalancesIsTotalSupply(method f) { 153 | require sumOfBalances() == totalSupply(); 154 | 155 | env e; 156 | if (f.selector != transfer(address, uint256).selector && f.selector != transferFrom(address, address, uint256).selector) { 157 | calldataarg args; 158 | f(e, args); 159 | } 160 | 161 | if (f.selector == transfer(address, uint256).selector) { 162 | address to; 163 | uint256 amount; 164 | require balanceOf(e.msg.sender) + balanceOf(to) < max_uint256; 165 | transfer(e, to, amount); 166 | } 167 | 168 | if (f.selector == transferFrom(address, address, uint256).selector) { 169 | address from; 170 | address to; 171 | uint256 amount; 172 | require balanceOf(from) + balanceOf(to) < max_uint256; 173 | transferFrom(e, from, to, amount); 174 | } 175 | 176 | assert sumOfBalances() == totalSupply(); 177 | } 178 | 179 | rule OtherBalanceOnlyGoesUp(address other, method f) { 180 | env e; 181 | // totalSupply would have already overflowed in this case, so we can assume this 182 | uint256 balanceBefore = balanceOf(other); 183 | 184 | if (f.selector == transferFrom(address, address, uint256).selector) { 185 | address from; 186 | address to; 187 | uint256 amount; 188 | require(other != from); 189 | require balanceOf(from) + balanceBefore < max_uint256; 190 | 191 | transferFrom(e, from, to, amount); 192 | } else if (f.selector == transfer(address, uint256).selector) { 193 | require other != e.msg.sender; 194 | require balanceOf(e.msg.sender) + balanceBefore < max_uint256; 195 | calldataarg args; 196 | f(e, args); 197 | } else { 198 | require other != e.msg.sender; 199 | calldataarg args; 200 | f(e, args); 201 | } 202 | 203 | assert balanceOf(other) >= balanceBefore; 204 | } -------------------------------------------------------------------------------- /test/utilities/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | BigNumber, 3 | utils: { keccak256, defaultAbiCoder, toUtf8Bytes, solidityPack }, 4 | } = require("ethers") 5 | const { ecsign } = require("ethereumjs-util") 6 | 7 | const { BN } = require("bn.js") 8 | 9 | const ADDRESS_ZERO = "0x0000000000000000000000000000000000000000" 10 | 11 | const BASE_TEN = 10 12 | 13 | function roundBN(number) { 14 | return new BN(number.toString()).divRound(new BN("10000000000000000")).toString() 15 | } 16 | 17 | function encodePrice(reserve0, reserve1) { 18 | return [ 19 | reserve1.mul(BigNumber.from(2).pow(BigNumber.from(112))).div(reserve0), 20 | reserve0.mul(BigNumber.from(2).pow(BigNumber.from(112))).div(reserve1), 21 | ] 22 | } 23 | 24 | const PERMIT_TYPEHASH = keccak256(toUtf8Bytes("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")) 25 | 26 | function getDomainSeparator(tokenAddress, chainId) { 27 | return keccak256( 28 | defaultAbiCoder.encode( 29 | ["bytes32", "uint256", "address"], 30 | [keccak256(toUtf8Bytes("EIP712Domain(uint256 chainId,address verifyingContract)")), chainId, tokenAddress] 31 | ) 32 | ) 33 | } 34 | 35 | function getApprovalDigest(token, approve, nonce, deadline, chainId = 1) { 36 | const DOMAIN_SEPARATOR = getDomainSeparator(token.address, chainId) 37 | const msg = defaultAbiCoder.encode( 38 | ["bytes32", "address", "address", "uint256", "uint256", "uint256"], 39 | [PERMIT_TYPEHASH, approve.owner, approve.spender, approve.value, nonce, deadline] 40 | ) 41 | const pack = solidityPack(["bytes1", "bytes1", "bytes32", "bytes32"], ["0x19", "0x01", DOMAIN_SEPARATOR, keccak256(msg)]) 42 | return keccak256(pack) 43 | } 44 | 45 | function getApprovalMsg(tokenAddress, approve, nonce, deadline) { 46 | const DOMAIN_SEPARATOR = getDomainSeparator(tokenAddress) 47 | const msg = defaultAbiCoder.encode( 48 | ["bytes32", "address", "address", "uint256", "uint256", "uint256"], 49 | [PERMIT_TYPEHASH, approve.owner, approve.spender, approve.value, nonce, deadline] 50 | ) 51 | const pack = solidityPack(["bytes1", "bytes1", "bytes32", "bytes32"], ["0x19", "0x01", DOMAIN_SEPARATOR, keccak256(msg)]) 52 | return pack 53 | } 54 | 55 | const BENTOBOX_MASTER_APPROVAL_TYPEHASH = keccak256( 56 | toUtf8Bytes("SetMasterContractApproval(string warning,address user,address masterContract,bool approved,uint256 nonce)") 57 | ) 58 | 59 | function getBentoBoxDomainSeparator(address, chainId) { 60 | return keccak256( 61 | defaultAbiCoder.encode( 62 | ["bytes32", "string", "uint256", "address"], 63 | [keccak256(toUtf8Bytes("EIP712Domain(string name,uint256 chainId,address verifyingContract)")), "BentoBox V1", chainId, address] 64 | ) 65 | ) 66 | } 67 | 68 | function getBentoBoxApprovalDigest(bentoBox, user, masterContractAddress, approved, nonce, chainId = 1) { 69 | const DOMAIN_SEPARATOR = getBentoBoxDomainSeparator(bentoBox.address, chainId) 70 | const msg = defaultAbiCoder.encode( 71 | ["bytes32", "string", "address", "address", "bool", "uint256"], 72 | [ 73 | BENTOBOX_MASTER_APPROVAL_TYPEHASH, 74 | approved ? "Give FULL access to funds in (and approved to) BentoBox?" : "Revoke access to BentoBox?", 75 | user.address, 76 | masterContractAddress, 77 | approved, 78 | nonce, 79 | ] 80 | ) 81 | const pack = solidityPack(["bytes1", "bytes1", "bytes32", "bytes32"], ["0x19", "0x01", DOMAIN_SEPARATOR, keccak256(msg)]) 82 | return keccak256(pack) 83 | } 84 | 85 | async function setMasterContractApproval(bentoBox, user, privateKey, masterContractAddress, approved) { 86 | const nonce = await bentoBox.nonces(user.address) 87 | 88 | const digest = getBentoBoxApprovalDigest(bentoBox, user, masterContractAddress, approved, nonce, user.provider._network.chainId) 89 | const { v, r, s } = ecsign(Buffer.from(digest.slice(2), "hex"), Buffer.from(privateKey.replace("0x", ""), "hex")) 90 | 91 | return await bentoBox.connect(user).setMasterContractApproval(user.address, masterContractAddress, approved, v, r, s) 92 | } 93 | 94 | async function setLendingPairContractApproval(bentoBox, user, privateKey, lendingPair, approved) { 95 | const nonce = await bentoBox.nonces(user.address) 96 | 97 | const digest = getBentoBoxApprovalDigest(bentoBox, user, lendingPair.address, approved, nonce, user.provider._network.chainId) 98 | const { v, r, s } = ecsign(Buffer.from(digest.slice(2), "hex"), Buffer.from(privateKey.replace("0x", ""), "hex")) 99 | 100 | return await lendingPair.connect(user).setApproval(user.address, approved, v, r, s) 101 | } 102 | 103 | async function lendingPairPermit(bentoBox, token, user, privateKey, lendingPair, amount) { 104 | const nonce = await token.nonces(user.address) 105 | 106 | const deadline = (await user.provider._internalBlockNumber).respTime + 10000 107 | 108 | const digest = await getApprovalDigest( 109 | token, 110 | { 111 | owner: user.address, 112 | spender: bentoBox.address, 113 | value: amount, 114 | }, 115 | nonce, 116 | deadline, 117 | user.provider._network.chainId 118 | ) 119 | const { v, r, s } = ecsign(Buffer.from(digest.slice(2), "hex"), Buffer.from(privateKey.replace("0x", ""), "hex")) 120 | 121 | return await lendingPair.connect(user).permitToken(token.address, user.address, amount, deadline, v, r, s) 122 | } 123 | 124 | function sansBorrowFee(amount) { 125 | return amount.mul(BigNumber.from(2000)).div(BigNumber.from(2001)) 126 | } 127 | 128 | async function advanceTimeAndBlock(time, ethers) { 129 | await advanceTime(time, ethers) 130 | await advanceBlock(ethers) 131 | } 132 | 133 | async function advanceTime(time, ethers) { 134 | await ethers.provider.send("evm_increaseTime", [time]) 135 | } 136 | 137 | async function advanceBlock(ethers) { 138 | await ethers.provider.send("evm_mine") 139 | } 140 | 141 | // Defaults to e18 using amount * 10^18 142 | function getBigNumber(amount, decimals = 18) { 143 | return BigNumber.from(amount).mul(BigNumber.from(BASE_TEN).pow(decimals)) 144 | } 145 | 146 | async function prepare(thisObject, contracts) { 147 | for (let i in contracts) { 148 | let contract = contracts[i] 149 | thisObject[contract] = await ethers.getContractFactory(contract) 150 | } 151 | thisObject.signers = await ethers.getSigners() 152 | thisObject.alice = thisObject.signers[0] 153 | thisObject.bob = thisObject.signers[1] 154 | thisObject.carol = thisObject.signers[2] 155 | thisObject.alicePrivateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" 156 | thisObject.bobPrivateKey = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" 157 | thisObject.carolPrivateKey = "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" 158 | } 159 | 160 | async function deploy(thisObject, contracts) { 161 | for (let i in contracts) { 162 | let contract = contracts[i] 163 | thisObject[contract[0]] = await contract[1].deploy(...(contract[2] || [])) 164 | await thisObject[contract[0]].deployed() 165 | } 166 | } 167 | 168 | function addr(address) { 169 | if (typeof address == "object" && address.address) { 170 | address = address.address 171 | } 172 | return address 173 | } 174 | 175 | module.exports = { 176 | ADDRESS_ZERO, 177 | getDomainSeparator, 178 | getApprovalDigest, 179 | getApprovalMsg, 180 | getBentoBoxDomainSeparator, 181 | getBentoBoxApprovalDigest, 182 | lendingPairPermit, 183 | setMasterContractApproval, 184 | setLendingPairContractApproval, 185 | sansBorrowFee, 186 | encodePrice, 187 | roundBN, 188 | advanceTime, 189 | advanceBlock, 190 | advanceTimeAndBlock, 191 | getBigNumber, 192 | prepare, 193 | deploy, 194 | addr, 195 | } 196 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title ERC-721 Non-Fungible Token Standard 5 | /// @dev See https://eips.ethereum.org/EIPS/eip-721 6 | /// Note: the ERC-165 identifier for this interface is 0x80ac58cd. 7 | interface IERC721 /* is ERC165 */ { 8 | /// @dev This emits when ownership of any NFT changes by any mechanism. 9 | /// This event emits when NFTs are created (`from` == 0) and destroyed 10 | /// (`to` == 0). Exception: during contract creation, any number of NFTs 11 | /// may be created and assigned without emitting Transfer. At the time of 12 | /// any transfer, the approved address for that NFT (if any) is reset to none. 13 | event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); 14 | 15 | /// @dev This emits when the approved address for an NFT is changed or 16 | /// reaffirmed. The zero address indicates there is no approved address. 17 | /// When a Transfer event emits, this also indicates that the approved 18 | /// address for that NFT (if any) is reset to none. 19 | event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); 20 | 21 | /// @dev This emits when an operator is enabled or disabled for an owner. 22 | /// The operator can manage all NFTs of the owner. 23 | event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); 24 | 25 | /// @notice Count all NFTs assigned to an owner 26 | /// @dev NFTs assigned to the zero address are considered invalid, and this 27 | /// function throws for queries about the zero address. 28 | /// @param _owner An address for whom to query the balance 29 | /// @return The number of NFTs owned by `_owner`, possibly zero 30 | function balanceOf(address _owner) external view returns (uint256); 31 | 32 | /// @notice Find the owner of an NFT 33 | /// @dev NFTs assigned to zero address are considered invalid, and queries 34 | /// about them do throw. 35 | /// @param _tokenId The identifier for an NFT 36 | /// @return The address of the owner of the NFT 37 | function ownerOf(uint256 _tokenId) external view returns (address); 38 | 39 | /// @notice Transfers the ownership of an NFT from one address to another address 40 | /// @dev Throws unless `msg.sender` is the current owner, an authorized 41 | /// operator, or the approved address for this NFT. Throws if `_from` is 42 | /// not the current owner. Throws if `_to` is the zero address. Throws if 43 | /// `_tokenId` is not a valid NFT. When transfer is complete, this function 44 | /// checks if `_to` is a smart contract (code size > 0). If so, it calls 45 | /// `onERC721Received` on `_to` and throws if the return value is not 46 | /// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. 47 | /// @param _from The current owner of the NFT 48 | /// @param _to The new owner 49 | /// @param _tokenId The NFT to transfer 50 | /// @param data Additional data with no specified format, sent in call to `_to` 51 | function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata data) external payable; 52 | 53 | /// @notice Transfers the ownership of an NFT from one address to another address 54 | /// @dev This works identically to the other function with an extra data parameter, 55 | /// except this function just sets data to "". 56 | /// @param _from The current owner of the NFT 57 | /// @param _to The new owner 58 | /// @param _tokenId The NFT to transfer 59 | function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; 60 | 61 | /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE 62 | /// TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE 63 | /// THEY MAY BE PERMANENTLY LOST 64 | /// @dev Throws unless `msg.sender` is the current owner, an authorized 65 | /// operator, or the approved address for this NFT. Throws if `_from` is 66 | /// not the current owner. Throws if `_to` is the zero address. Throws if 67 | /// `_tokenId` is not a valid NFT. 68 | /// @param _from The current owner of the NFT 69 | /// @param _to The new owner 70 | /// @param _tokenId The NFT to transfer 71 | function transferFrom(address _from, address _to, uint256 _tokenId) external payable; 72 | 73 | /// @notice Change or reaffirm the approved address for an NFT 74 | /// @dev The zero address indicates there is no approved address. 75 | /// Throws unless `msg.sender` is the current NFT owner, or an authorized 76 | /// operator of the current owner. 77 | /// @param _approved The new approved NFT controller 78 | /// @param _tokenId The NFT to approve 79 | function approve(address _approved, uint256 _tokenId) external payable; 80 | 81 | /// @notice Enable or disable approval for a third party ("operator") to manage 82 | /// all of `msg.sender`'s assets 83 | /// @dev Emits the ApprovalForAll event. The contract MUST allow 84 | /// multiple operators per owner. 85 | /// @param _operator Address to add to the set of authorized operators 86 | /// @param _approved True if the operator is approved, false to revoke approval 87 | function setApprovalForAll(address _operator, bool _approved) external; 88 | 89 | /// @notice Get the approved address for a single NFT 90 | /// @dev Throws if `_tokenId` is not a valid NFT. 91 | /// @param _tokenId The NFT to find the approved address for 92 | /// @return The approved address for this NFT, or the zero address if there is none 93 | function getApproved(uint256 _tokenId) external view returns (address); 94 | 95 | /// @notice Query if an address is an authorized operator for another address 96 | /// @param _owner The address that owns the NFTs 97 | /// @param _operator The address that acts on behalf of the owner 98 | /// @return True if `_operator` is an approved operator for `_owner`, false otherwise 99 | function isApprovedForAll(address _owner, address _operator) external view returns (bool); 100 | } 101 | 102 | /// @title ERC-721 Non-Fungible Token Standard, optional metadata extension 103 | /// @dev See https://eips.ethereum.org/EIPS/eip-721 104 | /// Note: the ERC-165 identifier for this interface is 0x5b5e139f. 105 | interface IERC721Metadata /* is ERC721 */ { 106 | /// @notice A descriptive name for a collection of NFTs in this contract 107 | function name() external view returns (string memory _name); 108 | 109 | /// @notice An abbreviated name for NFTs in this contract 110 | function symbol() external view returns (string memory _symbol); 111 | 112 | /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. 113 | /// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC 114 | /// 3986. The URI may point to a JSON file that conforms to the "ERC721 115 | /// Metadata JSON Schema". 116 | function tokenURI(uint256 _tokenId) external view returns (string memory); 117 | } 118 | 119 | /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension 120 | /// @dev See https://eips.ethereum.org/EIPS/eip-721 121 | /// Note: the ERC-165 identifier for this interface is 0x780e9d63. 122 | interface IERC721Enumerable /* is ERC721 */ { 123 | /// @notice Count NFTs tracked by this contract 124 | /// @return A count of valid NFTs tracked by this contract, where each one of 125 | /// them has an assigned and queryable owner not equal to the zero address 126 | function totalSupply() external view returns (uint256); 127 | 128 | /// @notice Enumerate valid NFTs 129 | /// @dev Throws if `_index` >= `totalSupply()`. 130 | /// @param _index A counter less than `totalSupply()` 131 | /// @return The token identifier for the `_index`th NFT, 132 | /// (sort order not specified) 133 | function tokenByIndex(uint256 _index) external view returns (uint256); 134 | 135 | /// @notice Enumerate NFTs assigned to an owner 136 | /// @dev Throws if `_index` >= `balanceOf(_owner)` or if 137 | /// `_owner` is the zero address, representing invalid NFTs. 138 | /// @param _owner An address where we are interested in NFTs owned by them 139 | /// @param _index A counter less than `balanceOf(_owner)` 140 | /// @return The token identifier for the `_index`th NFT assigned to `_owner`, 141 | /// (sort order not specified) 142 | function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256); 143 | } 144 | -------------------------------------------------------------------------------- /test/BoringGenerativeNFT.js: -------------------------------------------------------------------------------- 1 | const { ADDRESS_ZERO, prepare, getApprovalDigest, deploy } = require("./utilities") 2 | const { expect } = require("chai") 3 | const { ecsign } = require("ethereumjs-util") 4 | const { AbiCoder } = require("ethers/lib/utils") 5 | 6 | describe("BoringGenerativeNFT", function () { 7 | before(async function () { 8 | await prepare(this, ["BoringGenerativeNFT", "FixedTrait"]) 9 | await deploy(this, [ 10 | ["contract", this.BoringGenerativeNFT, ["Gatos", "GATO"]], 11 | ["fixed", this.FixedTrait], 12 | ]) 13 | await this.contract.addTrait("Head", this.fixed.address) 14 | await this.contract.addTrait("Eyes", this.fixed.address) 15 | 16 | await this.contract.addTraitData(0, new AbiCoder().encode(["tuple(string, string)"], [["Base", "HEAD"]])) 17 | await this.contract.addTraitData(0, new AbiCoder().encode(["tuple(string, string)"], [["Big", "BIGHEAD"]])) 18 | await this.contract.addTraitData(1, new AbiCoder().encode(["tuple(string, string)"], [["Open", "EYES"]])) 19 | await this.contract.addTraitData(1, new AbiCoder().encode(["tuple(string, string)"], [["Closed", "CLOSEDEYES"]])) 20 | await this.contract.addTraitData(1, new AbiCoder().encode(["tuple(string, string)"], [["Wink", "WINKEYES"]])) 21 | }) 22 | 23 | it("TotalSupply is 0", async function () { 24 | expect(await this.contract.totalSupply()).to.equal(0) 25 | }) 26 | 27 | it("Mint token", async function () { 28 | await this.contract.mint( 29 | { 30 | trait0: 1, 31 | trait1: 2, 32 | trait2: 0, 33 | trait3: 0, 34 | trait4: 0, 35 | trait5: 0, 36 | trait6: 0, 37 | trait7: 0, 38 | trait8: 0, 39 | }, 40 | this.alice.address 41 | ) 42 | const base64 = (await this.contract.tokenURI(0)).substring(29) 43 | const json = Buffer.from(base64, "base64").toString("utf-8") 44 | const data = JSON.parse(json) 45 | const svg = Buffer.from(data.image.substring(26), "base64").toString("utf-8") 46 | 47 | //console.log(data) 48 | //console.log(svg) 49 | }) 50 | }) 51 | 52 | /*function renderHead(uint8 color) private view returns (string memory) { 53 | string[8] memory colorList = ["9b9c9e", "ffffff", "222222", "6b6c6e", "abcdef", "557799", "887755", "aa0000"]; 54 | return 55 | string( 56 | abi.encodePacked( 57 | '' 60 | ) 61 | ); 62 | } 63 | 64 | function renderEyes() private view returns (string memory) { 65 | return 66 | ''; 67 | }*/ 68 | 69 | /* 70 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | */ 169 | -------------------------------------------------------------------------------- /test/ERC20.js: -------------------------------------------------------------------------------- 1 | const { expect, assert } = require("chai") 2 | const { ADDRESS_ZERO, getApprovalDigest, getDomainSeparator, prepare } = require("./utilities") 3 | const { ecsign } = require("ethereumjs-util") 4 | 5 | describe("ERC20", function () { 6 | before(async function () { 7 | await prepare(this, ["MockERC20"]) 8 | }) 9 | 10 | beforeEach(async function () { 11 | this.token = await this.MockERC20.deploy(10000) 12 | await this.token.deployed() 13 | }) 14 | 15 | // You can nest describe calls to create subsections. 16 | describe("Deployment", function () { 17 | it("Assigns the total supply of tokens to the alice", async function () { 18 | const ownerBalance = await this.token.balanceOf(this.alice.address) 19 | expect(await this.token.totalSupply()).to.equal(ownerBalance) 20 | }) 21 | 22 | // TODO: Ask about this one (Why is it needed?) 23 | it("Succeeds in creating over 2^256 - 1 (max) tokens", async function () { 24 | // 2^256 - 1 25 | const token = await this.MockERC20.deploy("115792089237316195423570985008687907853269984665640564039457584007913129639935") 26 | await token.deployed() 27 | 28 | const totalSupply = await token.totalSupply() 29 | expect(totalSupply).to.be.equal("115792089237316195423570985008687907853269984665640564039457584007913129639935") 30 | }) 31 | }) 32 | 33 | describe("Transfer", function () { 34 | it("Succeeds transfering 10000 tokens from alice to bob", async function () { 35 | expect(() => this.token.transfer(this.bob.address, 10000)).to.changeTokenBalances( 36 | this.token, 37 | [this.alice, this.bob], 38 | [-10000, 10000] 39 | ) 40 | }) 41 | 42 | it("Returns true on success", async function () { 43 | expect(await this.token.callStatic.transfer(this.bob.address, 10000)).to.be.true 44 | }) 45 | 46 | it("Fails transfering 10001 tokens from alice to bob", async function () { 47 | await expect(this.token.transfer(this.bob.address, 10001)).to.be.revertedWith("ERC20: balance too low") 48 | }) 49 | 50 | it("Fails transfering to zero address", async function () { 51 | await expect(this.token.transfer(ADDRESS_ZERO, 10)).to.be.revertedWith("ERC20: no zero address") 52 | }) 53 | 54 | it("Succeeds transfering max tokens from alice to alice", async function () { 55 | // Since the sender and receiver are the same, this should actually work fine (most ERC-20 tokens will fail) 56 | const token = await this.MockERC20.deploy("115792089237316195423570985008687907853269984665640564039457584007913129639935") 57 | await token.deployed() 58 | await token.transfer(this.alice.address, "115792089237316195423570985008687907853269984665640564039457584007913129639935") 59 | }) 60 | 61 | it("transfering tokens from alice to alice", async function () { 62 | await expect(() => this.token.transfer(this.alice.address, "1000")).to.changeTokenBalances( 63 | this.token, 64 | [this.alice, this.alice], 65 | [0, 0] 66 | ) 67 | }) 68 | 69 | it("Succeeds for zero value transfer", async function () { 70 | await expect(() => this.token.transfer(this.bob.address, 0)).to.changeTokenBalances(this.token, [this.alice, this.bob], [-0, 0]) 71 | }) 72 | 73 | it("Emits Transfer event with expected arguments", async function () { 74 | await expect(this.token.transfer(this.bob.address, 2666)) 75 | .to.emit(this.token, "Transfer") 76 | .withArgs(this.alice.address, this.bob.address, 2666) 77 | }) 78 | 79 | it("Emits Transfer event with expected arguments for zero value transfer ", async function () { 80 | await expect(this.token.transfer(this.bob.address, 0)) 81 | .to.emit(this.token, "Transfer") 82 | .withArgs(this.alice.address, this.bob.address, 0) 83 | }) 84 | }) 85 | 86 | describe("TransferFrom", function () { 87 | it("transferFrom should fail if balance is too low", async function () { 88 | await expect(this.token.transferFrom(this.alice.address, this.bob.address, 10001)).to.be.revertedWith("ERC20: balance too low") 89 | }) 90 | 91 | it("transferFrom should fail to address zero", async function () { 92 | await this.token.approve(this.alice.address, "10") 93 | await expect(this.token.transferFrom(this.alice.address, ADDRESS_ZERO, 10)).to.be.revertedWith("ERC20: no zero address") 94 | }) 95 | 96 | it("Fails transferingFrom max tokens from alice to alice", async function () { 97 | // Since the sender and receiver are the same, this should actually work fine (most ERC-20 tokens will fail) 98 | const token = await this.MockERC20.deploy("115792089237316195423570985008687907853269984665640564039457584007913129639935") 99 | await token.deployed() 100 | await token.approve(this.alice.address, "115792089237316195423570985008687907853269984665640564039457584007913129639935") 101 | await token.transferFrom( 102 | this.alice.address, 103 | this.alice.address, 104 | "115792089237316195423570985008687907853269984665640564039457584007913129639935" 105 | ) 106 | }) 107 | 108 | it("transfering tokens from alice to alice", async function () { 109 | await this.token.approve(this.alice.address, 1000) 110 | await expect(() => this.token.transferFrom(this.alice.address, this.alice.address, "1000")).to.changeTokenBalances( 111 | this.token, 112 | [this.alice, this.alice], 113 | [0, 0] 114 | ) 115 | }) 116 | 117 | it("transfering tokens from alice to bob", async function () { 118 | await this.token.approve(this.alice.address, 1000) 119 | await expect(() => this.token.transferFrom(this.alice.address, this.bob.address, "1000")).to.changeTokenBalances( 120 | this.token, 121 | [this.alice, this.bob], 122 | [-1000, 1000] 123 | ) 124 | }) 125 | 126 | it("transfering 0 tokens from alice to bob", async function () { 127 | await this.token.approve(this.alice.address, 1000) 128 | await this.token.transferFrom(this.alice.address, this.bob.address, "0") 129 | }) 130 | }) 131 | 132 | describe("Approve", function () { 133 | it("approvals: msg.sender should approve 100 to this.bob.address", async function () { 134 | await this.token.approve(this.bob.address, 100) 135 | expect(await this.token.allowance(this.alice.address, this.bob.address)).to.equal(100) 136 | }) 137 | 138 | it("approvals: msg.sender approves this.bob.address of 100 & withdraws 20 once.", async function () { 139 | const balance0 = await this.token.balanceOf(this.alice.address) 140 | assert.strictEqual(balance0, 10000) 141 | 142 | await this.token.approve(this.bob.address, 100) // 100 143 | const balance2 = await this.token.balanceOf(this.carol.address) 144 | assert.strictEqual(balance2, 0, "balance2 not correct") 145 | 146 | await this.token.connect(this.bob).transferFrom(this.alice.address, this.carol.address, 20, { 147 | from: this.bob.address, 148 | }) // -20 149 | const allowance01 = await this.token.allowance(this.alice.address, this.bob.address) 150 | assert.strictEqual(allowance01, 80) // =80 151 | 152 | const balance22 = await this.token.balanceOf(this.carol.address) 153 | assert.strictEqual(balance22, 20) 154 | 155 | const balance02 = await this.token.balanceOf(this.alice.address) 156 | assert.strictEqual(balance02, 9980) 157 | }) 158 | 159 | // should approve 100 of msg.sender & withdraw 50, twice. (should succeed) 160 | it("approvals: msg.sender approves this.bob.address of 100 & withdraws 20 twice.", async function () { 161 | await this.token.approve(this.bob.address, 100) 162 | const allowance01 = await this.token.allowance(this.alice.address, this.bob.address) 163 | assert.strictEqual(allowance01, 100) 164 | 165 | await this.token.connect(this.bob).transferFrom(this.alice.address, this.carol.address, 20, { 166 | from: this.bob.address, 167 | }) 168 | const allowance012 = await this.token.allowance(this.alice.address, this.bob.address) 169 | assert.strictEqual(allowance012, 80) 170 | 171 | const balance2 = await this.token.balanceOf(this.carol.address) 172 | assert.strictEqual(balance2, 20) 173 | 174 | const balance0 = await this.token.balanceOf(this.alice.address) 175 | assert.strictEqual(balance0, 9980) 176 | 177 | // FIRST tx done. 178 | // onto next. 179 | await this.token.connect(this.bob).transferFrom(this.alice.address, this.carol.address, 20, { 180 | from: this.bob.address, 181 | }) 182 | const allowance013 = await this.token.allowance(this.alice.address, this.bob.address) 183 | assert.strictEqual(allowance013, 60) 184 | 185 | const balance22 = await this.token.balanceOf(this.carol.address) 186 | assert.strictEqual(balance22, 40) 187 | 188 | const balance02 = await this.token.balanceOf(this.alice.address) 189 | assert.strictEqual(balance02, 9960) 190 | }) 191 | 192 | // should approve 100 of msg.sender & withdraw 50 & 60 (should fail). 193 | it("approvals: msg.sender approves this.bob.address of 100 & withdraws 50 & 60 (2nd tx should fail)", async function () { 194 | await this.token.approve(this.bob.address, 100) 195 | const allowance01 = await this.token.allowance(this.alice.address, this.bob.address) 196 | assert.strictEqual(allowance01, 100) 197 | 198 | await this.token.connect(this.bob).transferFrom(this.alice.address, this.carol.address, 50, { 199 | from: this.bob.address, 200 | }) 201 | const allowance012 = await this.token.allowance(this.alice.address, this.bob.address) 202 | assert.strictEqual(allowance012, 50) 203 | 204 | const balance2 = await this.token.balanceOf(this.carol.address) 205 | assert.strictEqual(balance2, 50) 206 | 207 | let balance0 = await this.token.balanceOf(this.alice.address) 208 | assert.strictEqual(balance0, 9950) 209 | 210 | await expect( 211 | this.token.connect(this.bob).transferFrom(this.alice.address, this.carol.address, 60, { 212 | from: this.bob.address, 213 | }) 214 | ).to.be.revertedWith("ERC20: allowance too low") 215 | }) 216 | 217 | it("approvals: attempt withdrawal from account with no allowance (should fail)", async function () { 218 | await expect( 219 | this.token.connect(this.bob).transferFrom(this.alice.address, this.carol.address, 60, { 220 | from: this.bob.address, 221 | }) 222 | ).to.be.revertedWith("ERC20: allowance too low") 223 | }) 224 | 225 | it("approvals: allow this.bob.address 100 to withdraw from this.alice.address. Withdraw 60 and then approve 0 & attempt transfer.", async function () { 226 | await this.token.approve(this.bob.address, 100) 227 | await this.token.connect(this.bob).transferFrom(this.alice.address, this.carol.address, 60, { 228 | from: this.bob.address, 229 | }) 230 | await this.token.approve(this.bob.address, 0) 231 | 232 | await expect( 233 | this.token.connect(this.bob).transferFrom(this.alice.address, this.carol.address, 10, { 234 | from: this.bob.address, 235 | }) 236 | ).to.be.revertedWith("ERC20: allowance too low") 237 | }) 238 | 239 | it("approvals: approve max (2^256 - 1)", async function () { 240 | await this.token.approve(this.bob.address, "115792089237316195423570985008687907853269984665640564039457584007913129639935") 241 | 242 | expect(await this.token.allowance(this.alice.address, this.bob.address)).to.equal( 243 | "115792089237316195423570985008687907853269984665640564039457584007913129639935" 244 | ) 245 | }) 246 | 247 | // should approve max of msg.sender & withdraw 20 with changing allowance (should succeed). 248 | it("approvals: msg.sender approves this.bob.address of max (2^256 - 1) & withdraws 20", async function () { 249 | const balance0 = await this.token.balanceOf(this.alice.address) 250 | expect(balance0).to.equal(10000) 251 | 252 | const max = "115792089237316195423570985008687907853269984665640564039457584007913129639935" 253 | await this.token.approve(this.bob.address, max) 254 | const balance2 = await this.token.balanceOf(this.carol.address) 255 | expect(balance2).to.equal(0) 256 | 257 | await this.token.connect(this.bob).transferFrom(this.alice.address, this.carol.address, 20, { 258 | from: this.bob.address, 259 | }) 260 | 261 | const allowance01 = await this.token.allowance(this.alice.address, this.bob.address) 262 | const maxMinus20 = "115792089237316195423570985008687907853269984665640564039457584007913129639915" 263 | // Not ERC20 compliant, but a gas optimization. Infinite is now really infinite. 264 | expect(allowance01).to.equal(max) 265 | 266 | const balance22 = await this.token.balanceOf(this.carol.address) 267 | expect(balance22).to.equal(20) 268 | 269 | const balance02 = await this.token.balanceOf(this.alice.address) 270 | expect(balance02).to.equal(9980) 271 | }) 272 | 273 | it("Emits Approval event with expected arguments", async function () { 274 | await expect( 275 | this.token.connect(this.alice).approve(this.bob.address, "2666", { 276 | from: this.alice.address, 277 | }) 278 | ) 279 | .to.emit(this.token, "Approval") 280 | .withArgs(this.alice.address, this.bob.address, 2666) 281 | }) 282 | }) 283 | 284 | describe("Permit", function () { 285 | // This is a test of our utility function. 286 | it("Returns correct DOMAIN_SEPARATOR for token and chainId", async function () { 287 | expect(await this.token.DOMAIN_SEPARATOR()).to.be.equal(getDomainSeparator(this.token.address, this.bob.provider._network.chainId)) 288 | }) 289 | 290 | it("Reverts when address zero is passed as alice argument", async function () { 291 | const nonce = await this.token.nonces(this.carol.address) 292 | 293 | const deadline = (await this.bob.provider._internalBlockNumber).respTime + 10000 294 | 295 | const digest = await getApprovalDigest( 296 | this.token, 297 | { 298 | owner: this.carol.address, 299 | spender: this.bob.address, 300 | value: 1, 301 | }, 302 | nonce, 303 | deadline, 304 | this.bob.provider._network.chainId 305 | ) 306 | 307 | const { v, r, s } = ecsign(Buffer.from(digest.slice(2), "hex"), Buffer.from(this.carolPrivateKey.replace("0x", ""), "hex")) 308 | 309 | await expect( 310 | this.token.connect(this.carol).permit(ADDRESS_ZERO, this.bob.address, 1, deadline, v, r, s, { 311 | from: this.carol.address, 312 | }) 313 | ).to.be.revertedWith("Owner cannot be 0") 314 | }) 315 | 316 | it("Successfully executes a permit", async function () { 317 | const nonce = await this.token.nonces(this.carol.address) 318 | 319 | const deadline = (await this.bob.provider._internalBlockNumber).respTime + 10000 320 | 321 | const digest = await getApprovalDigest( 322 | this.token, 323 | { 324 | owner: this.carol.address, 325 | spender: this.bob.address, 326 | value: 1, 327 | }, 328 | nonce, 329 | deadline, 330 | this.bob.provider._network.chainId 331 | ) 332 | const { v, r, s } = ecsign(Buffer.from(digest.slice(2), "hex"), Buffer.from(this.carolPrivateKey.replace("0x", ""), "hex")) 333 | 334 | await this.token.connect(this.carol).permit(this.carol.address, this.bob.address, 1, deadline, v, r, s, { 335 | from: this.carol.address, 336 | }) 337 | }) 338 | 339 | it("Emits Approval event with expected arguments on successful execution of permit", async function () { 340 | const nonce = await this.token.nonces(this.carol.address) 341 | 342 | const deadline = (await this.bob.provider._internalBlockNumber).respTime + 10000 343 | 344 | const digest = await getApprovalDigest( 345 | this.token, 346 | { 347 | owner: this.carol.address, 348 | spender: this.bob.address, 349 | value: 1, 350 | }, 351 | nonce, 352 | deadline, 353 | this.bob.provider._network.chainId 354 | ) 355 | 356 | const { v, r, s } = ecsign(Buffer.from(digest.slice(2), "hex"), Buffer.from(this.carolPrivateKey.replace("0x", ""), "hex")) 357 | 358 | await expect( 359 | this.token.connect(this.carol).permit(this.carol.address, this.bob.address, 1, deadline, v, r, s, { 360 | from: this.carol.address, 361 | }) 362 | ) 363 | .to.emit(this.token, "Approval") 364 | .withArgs(this.carol.address, this.bob.address, 1) 365 | }) 366 | 367 | it("Reverts on expired deadline", async function () { 368 | let nonce = await this.token.nonces(this.carol.address) 369 | 370 | const deadline = 0 371 | 372 | const digest = await getApprovalDigest( 373 | this.token, 374 | { 375 | owner: this.carol.address, 376 | spender: this.bob.address, 377 | value: 1, 378 | }, 379 | nonce, 380 | deadline, 381 | this.bob.provider._network.chainId 382 | ) 383 | const { v, r, s } = ecsign(Buffer.from(digest.slice(2), "hex"), Buffer.from(this.carolPrivateKey.replace("0x", ""), "hex")) 384 | 385 | await expect( 386 | this.token.connect(this.carol).permit(this.carol.address, this.bob.address, 1, deadline, v, r, s, { 387 | from: this.carol.address, 388 | }) 389 | ).to.be.revertedWith("Expired") 390 | }) 391 | 392 | it("Reverts on invalid signiture", async function () { 393 | let nonce = await this.token.nonces(this.carol.address) 394 | 395 | const deadline = (await this.carol.provider._internalBlockNumber).respTime + 10000 396 | 397 | const digest = await getApprovalDigest( 398 | this.token, 399 | { 400 | owner: this.carol.address, 401 | spender: this.bob.address, 402 | value: 1, 403 | }, 404 | nonce, 405 | deadline, 406 | this.bob.provider._network.chainId 407 | ) 408 | const { v, r, s } = ecsign(Buffer.from(digest.slice(2), "hex"), Buffer.from(this.carolPrivateKey.replace("0x", ""), "hex")) 409 | 410 | await expect( 411 | this.token.connect(this.carol).permit(this.carol.address, this.bob.address, 10, deadline, v, r, s, { 412 | from: this.carol.address, 413 | }) 414 | ).to.be.revertedWith("Invalid Signature") 415 | }) 416 | }) 417 | }) 418 | -------------------------------------------------------------------------------- /test/BoringSingleNFT.js: -------------------------------------------------------------------------------- 1 | const { expect, assert } = require("chai") 2 | const { ADDRESS_ZERO, getApprovalDigest, getDomainSeparator, prepare } = require("./utilities") 3 | const { ecsign, Address } = require("ethereumjs-util") 4 | 5 | describe("BoringSingleNFT", async function () { 6 | before(async function () { 7 | await prepare(this, ["MockBoringSingleNFT"]) 8 | this.contract = await this.MockBoringSingleNFT.deploy() 9 | // alice is the deployer and hence the owner of the NFT. 10 | await this.contract.connect(this.alice).deployed() 11 | 12 | await prepare(this, ["MockERC721Receiver"]) 13 | this.receiver = await this.MockERC721Receiver.deploy() 14 | await this.receiver.deployed() 15 | 16 | await prepare(this, ["MockERC721ReceiverWrong"]) 17 | this.wrongReceiver = await this.MockERC721ReceiverWrong.deploy() 18 | await this.wrongReceiver.deployed() 19 | }) 20 | 21 | describe("deployment basic requirements", async function () { 22 | it("should not be null, empty, undefined, address 0", async function () { 23 | const contractAddress = await this.contract.address 24 | assert.notEqual(contractAddress, "") 25 | assert.notEqual(contractAddress, ADDRESS_ZERO) 26 | assert.notEqual(contractAddress, null) 27 | assert.notEqual(contractAddress, undefined) 28 | }) 29 | }) 30 | 31 | describe("supports interface", async function () { 32 | it("should support the interface EIP-165 and EIP-721 + extensions", async function () { 33 | assert.isTrue(await this.contract.supportsInterface("0x01ffc9a7", { gasLimit: 30000 })) // EIP-165 34 | assert.isTrue(await this.contract.supportsInterface("0x80ac58cd", { gasLimit: 30000 })) // EIP-721 35 | // assert.isTrue(await this.contract.supportsInterface("0x5b5e139f", { gasLimit: 30000 })) // EIP-721 metadata extension 36 | // assert.isTrue(await this.contract.supportsInterface("0x780e9d63", { gasLimit: 30000 })) // EIP-721 enumeration extension 37 | assert.isFalse(await this.contract.supportsInterface("0xffffffff", { gasLimit: 30000 })) // Must be false 38 | assert.isFalse(await this.contract.supportsInterface("0xabcdef12", { gasLimit: 30000 })) // Not implemented, so false 39 | assert.isFalse(await this.contract.supportsInterface("0x00000000", { gasLimit: 30000 })) // Not implemented, so false 40 | }) 41 | }) 42 | 43 | describe("balanceOf function", async function () { 44 | it("should revert for queries about the 0 address", async function () { 45 | await expect(this.contract.balanceOf(ADDRESS_ZERO)).to.be.revertedWith("No zero address") // not consistent with "No 0 owner" 46 | }) 47 | it("should count all NFTs assigned to an owner", async function () { 48 | assert.equal(Number(await this.contract.balanceOf(this.alice.address)), 1) 49 | assert.equal(Number(await this.contract.balanceOf(this.bob.address)), 0) 50 | assert.equal(Number(await this.contract.balanceOf(this.carol.address)), 0) 51 | // ---SUMMARY--- 52 | // alice owns the single NFT. 53 | // no one else does. 54 | }) 55 | }) 56 | 57 | describe("ownerOf function", async function () { 58 | it("should find the owner of an NFT", async function () { 59 | assert.equal(await this.contract.ownerOf(0), this.alice.address) 60 | // ---SUMMARY--- 61 | // alice owns the single NFT. 62 | // no one else does. 63 | }) 64 | 65 | it("should revert if the token owner is 0", async function () { 66 | await expect(this.contract.ownerOf(5)).to.be.revertedWith("Invalid token ID") 67 | }) 68 | }) 69 | 70 | describe("transferFrom function", async function () { 71 | it("should throw token Id is invalid", async function () { 72 | await expect(this.contract.transferFrom(this.alice.address, this.bob.address, 1)).to.be.revertedWith("Invalid token ID") 73 | }) 74 | 75 | it("should throw if from is not the current owner", async function () { 76 | await expect(this.contract.transferFrom(this.bob.address, this.alice.address, 0)).to.be.revertedWith("From not owner") 77 | }) 78 | 79 | it("should throw if msg.sender is not the owner ", async function () { 80 | await expect(this.contract.connect(this.bob).transferFrom(this.alice.address, this.bob.address, 0)).to.be.revertedWith( 81 | "Transfer not allowed" 82 | ) 83 | }) 84 | 85 | it("should throw if _to is the zero address", async function () { 86 | await expect(this.contract.transferFrom(this.alice.address, ADDRESS_ZERO, 0)).to.be.revertedWith("No zero address") 87 | }) 88 | 89 | it("should throw unauthorized operator", async function () { 90 | await expect(this.contract.connect(this.carol).transferFrom(this.alice.address, this.bob.address, 0)).to.be.revertedWith( 91 | "Transfer not allowed" 92 | ) 93 | }) 94 | 95 | it("should transfer when the operator is authorized by the original owner of the NFT", async function () { 96 | await expect(this.contract.connect(this.alice).setApprovalForAll(this.bob.address, true)) 97 | .to.emit(this.contract, "ApprovalForAll") 98 | .withArgs(this.alice.address, this.bob.address, true) 99 | 100 | await expect(this.contract.connect(this.bob).transferFrom(this.alice.address, this.carol.address, 0)) 101 | .to.emit(this.contract, "Transfer") 102 | .withArgs(this.alice.address, this.carol.address, 0) 103 | 104 | assert.equal(Number(await this.contract.balanceOf(this.carol.address)), 1) 105 | assert.equal(await this.contract.ownerOf(0), this.carol.address) 106 | // ---SUMMARY--- 107 | // carol is the hodler 108 | // bob is authorized to interact with alice's NFT if owned 109 | }) 110 | 111 | it("should transfer an nft from the owner to the receiver", async function () { 112 | await expect(this.contract.connect(this.carol).transferFrom(this.carol.address, this.alice.address, 0)) 113 | .to.emit(this.contract, "Transfer") 114 | .withArgs(this.carol.address, this.alice.address, 0) 115 | 116 | assert.equal(Number(await this.contract.balanceOf(this.alice.address)), 1) 117 | assert.equal(await this.contract.ownerOf(0), this.alice.address) 118 | // ---SUMMARY--- 119 | // alice is the hodler 120 | // bob is authorized to interact with alice's NFT if owned 121 | }) 122 | 123 | it("should not work after the operator is unapproved by the original owner of the NFT", async function () { 124 | await expect(this.contract.connect(this.alice).setApprovalForAll(this.bob.address, false)) 125 | .to.emit(this.contract, "ApprovalForAll") 126 | .withArgs(this.alice.address, this.bob.address, false) 127 | 128 | await expect(this.contract.connect(this.bob).transferFrom(this.alice.address, this.bob.address, 0)).to.be.revertedWith( 129 | "Transfer not allowed" 130 | ) 131 | // ---SUMMARY--- 132 | // alice is the hodler 133 | }) 134 | }) 135 | 136 | describe("tokenURI function", async function () { 137 | it("should return a distinct Uniform Resource Identifier (URI) for a given asset in our case nothing since it is supposed to be implemented by the user of the contract", async function () { 138 | assert.equal(await this.contract.tokenURI(0), "") 139 | }) 140 | it("should throw when token is invalid", async function () { 141 | await expect(this.contract.tokenURI(20)).to.be.revertedWith("Invalid token ID") 142 | }) 143 | }) 144 | 145 | describe("setApprovalForAll and isApprovedForAll function", async function () { 146 | it("should query if an address is an authorized operator for another address", async function () { 147 | assert.equal(await this.contract.isApprovedForAll(this.alice.address, this.bob.address), false) 148 | assert.equal(await this.contract.isApprovedForAll(this.alice.address, this.contract.address), false) 149 | assert.equal(await this.contract.isApprovedForAll(this.alice.address, this.carol.address), false) 150 | }) 151 | it('should enable or disable approval for multiple third party ("operator") to manage all of msg.sender assets', async function () { 152 | await expect(this.contract.connect(this.alice).setApprovalForAll(this.carol.address, true)) 153 | .to.emit(this.contract, "ApprovalForAll") 154 | .withArgs(this.alice.address, this.carol.address, true) 155 | 156 | await expect(this.contract.connect(this.alice).setApprovalForAll(this.bob.address, true)) 157 | .to.emit(this.contract, "ApprovalForAll") 158 | .withArgs(this.alice.address, this.bob.address, true) 159 | 160 | assert.equal(await this.contract.isApprovedForAll(this.alice.address, this.carol.address), true) 161 | assert.equal(await this.contract.isApprovedForAll(this.alice.address, this.bob.address), true) 162 | 163 | await expect(this.contract.connect(this.alice).setApprovalForAll(this.carol.address, false)) 164 | .to.emit(this.contract, "ApprovalForAll") 165 | .withArgs(this.alice.address, this.carol.address, false) 166 | 167 | await expect(this.contract.connect(this.alice).setApprovalForAll(this.bob.address, false)) 168 | .to.emit(this.contract, "ApprovalForAll") 169 | .withArgs(this.alice.address, this.bob.address, false) 170 | 171 | assert.equal(await this.contract.isApprovedForAll(this.alice.address, this.carol.address), false) 172 | assert.equal(await this.contract.isApprovedForAll(this.alice.address, this.bob.address), false) 173 | // ---SUMMARY--- 174 | // alice is the hodler 175 | }) 176 | }) 177 | 178 | describe("approve function", async function () { 179 | it("should throw if the msg.sender is not the owner of the NFT", async function () { 180 | await expect(this.contract.connect(this.carol).approve(this.bob.address, 0)).to.be.revertedWith("Not allowed") 181 | }) 182 | 183 | it("should throw if the operator is unauthorized", async function () { 184 | await expect(this.contract.connect(this.bob).approve(this.bob.address, 0)).to.be.revertedWith("Not allowed") 185 | }) // how do you test that ? already tested above maybe ? 186 | 187 | it("should change or reaffirm the approved address(es) for an NFT", async function () { 188 | await expect(this.contract.connect(this.alice).approve(this.bob.address, 0)) 189 | .to.emit(this.contract, "Approval") 190 | .withArgs(this.alice.address, this.bob.address, 0) 191 | 192 | assert.equal(await this.contract.getApproved(0), this.bob.address) 193 | 194 | await expect(this.contract.connect(this.bob).transferFrom(this.alice.address, this.carol.address, 0)) 195 | .to.emit(this.contract, "Transfer") 196 | .withArgs(this.alice.address, this.carol.address, 0) 197 | 198 | // assert if it was reset to none after transfer 199 | assert.equal(await this.contract.ownerOf(0), this.carol.address) 200 | assert.equal(await this.contract.getApproved(0), ADDRESS_ZERO) 201 | 202 | // is that normal that after bob approved and after I reset setApprovalForAll to false bob can still transfer stuff from alice to carol ? 203 | 204 | // await expect(this.contract.connect(this.alice).setApprovalForAll(this.bob.address, false)) 205 | // .to.emit(this.contract, "ApprovalForAll") 206 | // .withArgs(this.alice.address, this.bob.address, false) 207 | 208 | // await expect(this.contract.connect(this.bob).transferFrom(this.alice.address, this.carol.address, 0)) 209 | // .to.emit(this.contract, "Transfer") 210 | // .withArgs(this.alice.address, this.carol.address, 0) 211 | 212 | // ---SUMMARY--- 213 | // carol is the hodler 214 | }) 215 | }) 216 | 217 | describe("getApproved function", async function () { 218 | it("should throw if tokenId is invalid", async function () { 219 | await expect(this.contract.getApproved(1)).to.be.revertedWith("Invalid token ID") 220 | }) 221 | 222 | it("should get the approved address(es) for a single NFT", async function () { 223 | assert.equal(await this.contract.getApproved(0), ADDRESS_ZERO) 224 | }) 225 | 226 | it("should return the approved address after it was approved", async function () { 227 | await expect(this.contract.connect(this.carol).approve(this.bob.address, 0)) 228 | .to.emit(this.contract, "Approval") 229 | .withArgs(this.carol.address, this.bob.address, 0) 230 | 231 | assert.equal(await this.contract.getApproved(0), this.bob.address) 232 | }) 233 | 234 | it("should return the previous approved address after it was unapproved (i.e set Approval to address zero)", async function () { 235 | await expect(this.contract.connect(this.carol).approve(ADDRESS_ZERO, 0)) 236 | .to.emit(this.contract, "Approval") 237 | .withArgs(this.carol.address, ADDRESS_ZERO, 0) 238 | 239 | assert.equal(await this.contract.getApproved(0), ADDRESS_ZERO) 240 | // ---SUMMARY--- 241 | // carol is the hodler 242 | }) 243 | }) 244 | 245 | describe("safeTransferFrom function", async function () { 246 | it("should throw if from is not the current owner", async function () { 247 | await expect( 248 | this.contract.functions["safeTransferFrom(address,address,uint256)"](this.carol.address, this.alice.address, 0) 249 | ).to.be.revertedWith("Transfer not allowed") 250 | }) 251 | 252 | it("should throw if msg.sender is not the owner ", async function () { 253 | await expect(this.contract.connect(this.carol).transferFrom(this.bob.address, this.bob.address, 0)).to.be.revertedWith( 254 | "From not owner" 255 | ) 256 | }) 257 | 258 | it("should throw if _to is the zero address", async function () { 259 | await expect( 260 | this.contract.connect(this.carol).functions["safeTransferFrom(address,address,uint256)"](this.carol.address, ADDRESS_ZERO, 0) 261 | ).to.be.revertedWith("No zero address") 262 | }) 263 | 264 | it("should throw if token Id is invalid", async function () { 265 | await expect( 266 | this.contract.connect(this.carol).functions["safeTransferFrom(address,address,uint256)"](this.carol.address, this.bob.address, 2) 267 | ).to.be.revertedWith("Invalid token ID") 268 | }) 269 | 270 | it("should throw unauthorized operator", async function () { 271 | await expect( 272 | this.contract.connect(this.bob).functions["safeTransferFrom(address,address,uint256)"](this.carol.address, this.bob.address, 0) 273 | ).to.be.revertedWith("Transfer not allowed") 274 | }) 275 | 276 | it("should transfer when the operator is authorized by the original owner of the NFT", async function () { 277 | await expect(this.contract.connect(this.carol).setApprovalForAll(this.alice.address, true)) 278 | .to.emit(this.contract, "ApprovalForAll") 279 | .withArgs(this.carol.address, this.alice.address, true) 280 | 281 | await expect( 282 | this.contract 283 | .connect(this.alice) 284 | .functions["safeTransferFrom(address,address,uint256)"](this.carol.address, this.alice.address, 0) 285 | ) 286 | .to.emit(this.contract, "Transfer") 287 | .withArgs(this.carol.address, this.alice.address, 0) 288 | 289 | assert.equal(await this.contract.ownerOf(0), this.alice.address) 290 | // ---SUMMARY--- 291 | // alice is the hodler 292 | // alice apporved for all carol's asset. 293 | }) 294 | 295 | it("should transfer an nft from the owner to the receiver", async function () { 296 | await expect( 297 | this.contract 298 | .connect(this.alice) 299 | .functions["safeTransferFrom(address,address,uint256)"](this.alice.address, this.carol.address, 0) 300 | ) 301 | .to.emit(this.contract, "Transfer") 302 | .withArgs(this.alice.address, this.carol.address, 0) 303 | 304 | assert.equal(await this.contract.ownerOf(0), this.carol.address) 305 | // ---SUMMARY--- 306 | // carol is the hodler 307 | // alice apporved for all carol's asset. 308 | }) 309 | 310 | it("should not work after the operator is unapproved by the original owner of the NFT", async function () { 311 | await expect(this.contract.connect(this.carol).setApprovalForAll(this.alice.address, false)) 312 | .to.emit(this.contract, "ApprovalForAll") 313 | .withArgs(this.carol.address, this.alice.address, false) 314 | 315 | await expect( 316 | this.contract.connect(this.alice).functions["safeTransferFrom(address,address,uint256)"](this.carol.address, this.bob.address, 0) 317 | ).to.be.revertedWith("Transfer not allowed") 318 | // ---SUMMARY--- 319 | // carol is the hodler 320 | // alice apporved for all carol's asset. 321 | }) 322 | 323 | it("should call onERC721TokenReceived on the contract it was transferred to", async function () { 324 | await expect( 325 | this.contract 326 | .connect(this.carol) 327 | .functions["safeTransferFrom(address,address,uint256)"](this.carol.address, this.receiver.address, 0) 328 | ) 329 | .to.emit(this.contract, "Transfer") 330 | .withArgs(this.carol.address, this.receiver.address, 0) 331 | assert.equal(await this.receiver.operator(), this.carol.address) 332 | assert.equal(await this.receiver.from(), this.carol.address) 333 | assert.equal(await this.receiver.tokenId(), 0) 334 | assert.equal(await this.receiver.data(), "0x") 335 | assert.equal(await this.contract.ownerOf(0), this.receiver.address) 336 | await this.receiver.returnToken() 337 | assert.equal(await this.contract.ownerOf(0), this.carol.address) 338 | // ---SUMMARY--- 339 | // carol is the holder 340 | }) 341 | 342 | it("should call onERC721TokenReceived on the contract it was transferred to and throw as the contract returns the wrong value", async function () { 343 | await expect( 344 | this.contract 345 | .connect(this.carol) 346 | .functions["safeTransferFrom(address,address,uint256)"](this.carol.address, this.wrongReceiver.address, 0) 347 | ).to.be.revertedWith("Wrong return value") 348 | assert.equal(await this.wrongReceiver.operator(), ADDRESS_ZERO) 349 | assert.equal(await this.wrongReceiver.from(), ADDRESS_ZERO) 350 | assert.equal(await this.wrongReceiver.tokenId(), 0) 351 | assert.equal(await this.wrongReceiver.data(), "0x") 352 | 353 | await expect(this.wrongReceiver.returnToken()).to.be.revertedWith("Transaction reverted without a reason") 354 | }) 355 | }) 356 | 357 | describe("safeTransferFrom function with bytes of data", async function () { 358 | it("should throw if from is not the current owner", async function () { 359 | await expect( 360 | this.contract 361 | .connect(this.carol) 362 | .functions["safeTransferFrom(address,address,uint256,bytes)"]( 363 | this.bob.address, 364 | this.alice.address, 365 | 0, 366 | "0x32352342135123432532544353425345" 367 | ) 368 | ).to.be.revertedWith("From not owner") 369 | }) 370 | 371 | it("should throw if msg.sender is not the owner ", async function () { 372 | await expect( 373 | this.contract 374 | .connect(this.alice) 375 | .functions["safeTransferFrom(address,address,uint256,bytes)"]( 376 | this.carol.address, 377 | this.alice.address, 378 | 0, 379 | "0x32352342135123432532544353425345" 380 | ) 381 | ).to.be.revertedWith("Transfer not allowed") 382 | }) 383 | 384 | it("should throw if _to is the zero address", async function () { 385 | await expect( 386 | this.contract 387 | .connect(this.carol) 388 | .functions["safeTransferFrom(address,address,uint256,bytes)"]( 389 | this.carol.address, 390 | ADDRESS_ZERO, 391 | 0, 392 | "0x32352342135123432532544353425345" 393 | ) 394 | ).to.be.revertedWith("No zero address") 395 | }) 396 | 397 | it("should throw if token Id is invalid", async function () { 398 | await expect( 399 | this.contract 400 | .connect(this.carol) 401 | .functions["safeTransferFrom(address,address,uint256,bytes)"]( 402 | this.carol.address, 403 | this.bob.address, 404 | 100000000, 405 | "0x32352342135123432532544353425345" 406 | ) 407 | ).to.be.revertedWith("Invalid token ID") 408 | }) 409 | 410 | it("should throw unauthorized operator", async function () { 411 | await expect( 412 | this.contract 413 | .connect(this.alice) 414 | .functions["safeTransferFrom(address,address,uint256,bytes)"]( 415 | this.carol.address, 416 | this.alice.address, 417 | 0, 418 | "0x32352342135123432532544353425345" 419 | ) 420 | ).to.be.revertedWith("Transfer not allowed") 421 | }) 422 | 423 | it("should transfer when the operator is authorized by the original owner of the NFT", async function () { 424 | await expect(this.contract.connect(this.carol).setApprovalForAll(this.alice.address, true)) 425 | .to.emit(this.contract, "ApprovalForAll") 426 | .withArgs(this.carol.address, this.alice.address, true) 427 | 428 | await expect( 429 | this.contract 430 | .connect(this.alice) 431 | .functions["safeTransferFrom(address,address,uint256,bytes)"]( 432 | this.carol.address, 433 | this.alice.address, 434 | 0, 435 | "0x32352342135123432532544353425345" 436 | ) 437 | ) 438 | .to.emit(this.contract, "Transfer") 439 | .withArgs(this.carol.address, this.alice.address, 0) 440 | assert.equal(await this.contract.ownerOf(0), this.alice.address) 441 | // ---SUMMARY--- 442 | // alice is the holder 443 | // alice is approved to interact with all carol's asset. 444 | }) 445 | 446 | it("should transfer an nft from the owner to the receiver", async function () { 447 | await expect( 448 | this.contract 449 | .connect(this.alice) 450 | .functions["safeTransferFrom(address,address,uint256,bytes)"]( 451 | this.alice.address, 452 | this.carol.address, 453 | 0, 454 | "0x32352342135123432532544353425345" 455 | ) 456 | ) 457 | .to.emit(this.contract, "Transfer") 458 | .withArgs(this.alice.address, this.carol.address, 0) 459 | 460 | assert.equal(await this.contract.ownerOf(0), this.carol.address) 461 | // ---SUMMARY--- 462 | // carol is the holder 463 | // alice is approved to interact with all carol's asset. 464 | }) 465 | 466 | it("should not work after the operator is unapproved by the original owner of the NFT", async function () { 467 | await expect(this.contract.connect(this.carol).setApprovalForAll(this.alice.address, false)) 468 | .to.emit(this.contract, "ApprovalForAll") 469 | .withArgs(this.carol.address, this.alice.address, false) 470 | 471 | await expect( 472 | this.contract 473 | .connect(this.alice) 474 | .functions["safeTransferFrom(address,address,uint256,bytes)"]( 475 | this.carol.address, 476 | this.alice.address, 477 | 0, 478 | "0x32352342135123432532544353425345" 479 | ) 480 | ).to.be.revertedWith("Transfer not allowed") 481 | // ---SUMMARY--- 482 | // carol is the holder 483 | }) 484 | 485 | it("should call onERC721TokenReceived on the contract it was transferred to", async function () { 486 | await expect( 487 | this.contract 488 | .connect(this.carol) 489 | .functions["safeTransferFrom(address,address,uint256,bytes)"]( 490 | this.carol.address, 491 | this.receiver.address, 492 | 0, 493 | "0x32352342135123432532544353425345" 494 | ) 495 | ) 496 | .to.emit(this.contract, "Transfer") 497 | .withArgs(this.carol.address, this.receiver.address, 0) 498 | 499 | assert.equal(await this.receiver.operator(), this.carol.address) 500 | assert.equal(await this.receiver.from(), this.carol.address) 501 | assert.equal(await this.receiver.tokenId(), 0) 502 | assert.equal(await this.receiver.data(), "0x32352342135123432532544353425345") 503 | assert.equal(await this.contract.ownerOf(0), this.receiver.address) 504 | await this.receiver.returnToken() 505 | assert.equal(await this.contract.ownerOf(0), this.carol.address) 506 | // ---SUMMARY--- 507 | // carol is the holder 508 | }) 509 | 510 | it("should call onERC721TokenReceived on the contract it was transferred to and throw as the contract returns the wrong value", async function () { 511 | await expect( 512 | this.contract 513 | .connect(this.carol) 514 | .functions["safeTransferFrom(address,address,uint256,bytes)"]( 515 | this.carol.address, 516 | this.wrongReceiver.address, 517 | 0, 518 | "0x32352342135123432532544353425345" 519 | ) 520 | ).to.be.revertedWith("Wrong return value") 521 | 522 | assert.equal(await this.wrongReceiver.operator(), ADDRESS_ZERO) 523 | assert.equal(await this.wrongReceiver.from(), ADDRESS_ZERO) 524 | assert.equal(await this.wrongReceiver.tokenId(), 0) 525 | assert.equal(await this.wrongReceiver.data(), "0x") 526 | 527 | await expect(this.wrongReceiver.returnToken()).to.be.revertedWith("Transaction reverted without a reason") 528 | }) 529 | }) 530 | }) 531 | --------------------------------------------------------------------------------