├── .gitignore ├── specs ├── README.md └── Gem.spec ├── tsconfig.json ├── pack ├── gemfab_arbitrum.dpack.json ├── gemfab_arbitrum_goerli.dpack.json └── gemfab_arbitrum_sepolia.dpack.json ├── NOTICE ├── package.json ├── hardhat.config.ts ├── index.ts ├── test ├── ERC20 │ ├── helpers.ts │ ├── draft-ERC20Permit.test.ts │ ├── ERC20.behavior.ts │ ├── ERC20.test.ts │ └── common-ERC20-issues.ts ├── bounds.ts ├── helpers.ts └── gemfab-test.ts ├── task └── deploy-gemfab.ts ├── README.md └── src └── gem.sol /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | -------------------------------------------------------------------------------- /specs/README.md: -------------------------------------------------------------------------------- 1 | ### Certora Specs 2 | 3 | Run like 4 | ``` 5 | $ certoraRun src/gem.sol:Gem --verify Gem:specs/Gem.spec 6 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "outDir": "dist", 5 | "esModuleInterop": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /pack/gemfab_arbitrum.dpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "format": "dpack-1", 3 | "network": "arbitrum", 4 | "types": { 5 | "GemFab": { 6 | "typename": "GemFab", 7 | "artifact": { 8 | "/": "bafybeid4zujx6upnggnf3kbvhkymqzs4jercf2yiypyg662alvxkytgm4y" 9 | } 10 | }, 11 | "Gem": { 12 | "typename": "Gem", 13 | "artifact": { 14 | "/": "bafybeiey7wh7j53xcmogh5kzy4tdrgvbfm3omyehbts5op5t7ay3w4vmsu" 15 | } 16 | } 17 | }, 18 | "objects": { 19 | "gemfab": { 20 | "objectname": "gemfab", 21 | "typename": "GemFab", 22 | "artifact": { 23 | "/": "bafybeid4zujx6upnggnf3kbvhkymqzs4jercf2yiypyg662alvxkytgm4y" 24 | }, 25 | "address": "0x5C635933743B93BC1C51B4798C984867Fc31BFC7" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /pack/gemfab_arbitrum_goerli.dpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "format": "dpack-1", 3 | "network": "arbitrum_goerli", 4 | "types": { 5 | "GemFab": { 6 | "typename": "GemFab", 7 | "artifact": { 8 | "/": "bafybeid4zujx6upnggnf3kbvhkymqzs4jercf2yiypyg662alvxkytgm4y" 9 | } 10 | }, 11 | "Gem": { 12 | "typename": "Gem", 13 | "artifact": { 14 | "/": "bafybeiey7wh7j53xcmogh5kzy4tdrgvbfm3omyehbts5op5t7ay3w4vmsu" 15 | } 16 | } 17 | }, 18 | "objects": { 19 | "gemfab": { 20 | "objectname": "gemfab", 21 | "typename": "GemFab", 22 | "artifact": { 23 | "/": "bafybeid4zujx6upnggnf3kbvhkymqzs4jercf2yiypyg662alvxkytgm4y" 24 | }, 25 | "address": "0xE36395165d64ef8130ceFD54024A1CCA149D57EE" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /pack/gemfab_arbitrum_sepolia.dpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "format": "dpack-1", 3 | "network": "arbitrum_sepolia", 4 | "types": { 5 | "GemFab": { 6 | "typename": "GemFab", 7 | "artifact": { 8 | "/": "bafybeid4zujx6upnggnf3kbvhkymqzs4jercf2yiypyg662alvxkytgm4y" 9 | } 10 | }, 11 | "Gem": { 12 | "typename": "Gem", 13 | "artifact": { 14 | "/": "bafybeiey7wh7j53xcmogh5kzy4tdrgvbfm3omyehbts5op5t7ay3w4vmsu" 15 | } 16 | } 17 | }, 18 | "objects": { 19 | "gemfab": { 20 | "objectname": "gemfab", 21 | "typename": "GemFab", 22 | "artifact": { 23 | "/": "bafybeid4zujx6upnggnf3kbvhkymqzs4jercf2yiypyg662alvxkytgm4y" 24 | }, 25 | "address": "0x708a8cB4Fe21717a827EE6De133B1Ad9a954B60a" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | NOTICE 2 | 3 | Copyrights and Attributions 4 | 5 | `gemfab` is dedicated in loving memory to Nikolai Mushegian, co-founder, core contributor, and dear friend. Nikolai intended this work to be used for the good of humanity. 6 | 7 | `gemfab` is owned by the Free Software Foundation. 8 | 9 | Individual contributions from Rico Credit System days are owned by the Free Software Foundation while attributed to the following people: 10 | 11 | ☀️ nikolai mushegian 2021-2022 12 | 🥝_🥝aaron 2021-2024 13 | 🍠kbrav 2021-2024 14 | 🎱dmfxyz 2022-2023 15 | 16 | See this project's version control history for specific contributions. 17 | 18 | Individual contributions from Dai Stablecoin System days were made by and are owned by: 19 | 20 | dbrock 2017-2019 21 | rain 2017-2019 22 | mrchico 2017-2019 23 | 24 | See Dai Stablecoin System version control history for specific contributions. 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gemfab", 3 | "version": "0.0.11", 4 | "main": "./dist/index.js", 5 | "scripts": { 6 | "init": "npm i", 7 | "build": "npm run build:sol && npm run build:ts", 8 | "build:ts": "npx tsc -b", 9 | "build:sol": "hardhat compile", 10 | "pretest": "npm run build", 11 | "test": "ts-mocha 'test/**'", 12 | "fmt": "ts-standard --fix task test index.ts", 13 | "verify": "certoraRun src/gem.sol:Gem --verify Gem:specs/Gem.spec" 14 | }, 15 | "devDependencies": { 16 | "@etherpacks/dpack": "^0.0.24", 17 | "@nomiclabs/hardhat-ethers": "^2.0.2", 18 | "@types/mocha": "^9.0.0", 19 | "debug": "^4.3.2", 20 | "ethers": "^5.5.3", 21 | "hardhat": "^2.10.1", 22 | "minihat": "^0.0.6", 23 | "ts-mocha": "^10.0.0", 24 | "ts-node": "^10.4.0", 25 | "ts-standard": "^10.0.0", 26 | "typescript": "^4.4.3" 27 | }, 28 | "dependencies": { 29 | "ethers-eip712": "^0.2.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import '@nomiclabs/hardhat-ethers' 2 | 3 | import './task/deploy-gemfab' 4 | 5 | /** 6 | * @type import('hardhat/config').HardhatUserConfig 7 | */ 8 | export default { 9 | paths: { 10 | sources: "./src" 11 | }, 12 | solidity: { 13 | version: '0.8.25', 14 | settings: { 15 | optimizer: { 16 | enabled: true, 17 | runs: 20000 18 | }, 19 | } 20 | }, 21 | networks: { 22 | arbitrum_goerli: { 23 | url: process.env["ARB_GOERLI_RPC_URL"], 24 | accounts: { 25 | mnemonic: process.env["ARB_GOERLI_MNEMONIC"] 26 | }, 27 | chainId: 421613 28 | }, 29 | arbitrum: { 30 | url: process.env["ARB_RPC_URL"], 31 | accounts: { 32 | mnemonic: process.env["ARB_MNEMONIC"] 33 | }, 34 | chainId: 42161 35 | }, 36 | arbitrum_sepolia: { 37 | url: process.env["ARB_SEPOLIA_RPC_URL"], 38 | accounts: { 39 | mnemonic: process.env["ARB_SEPOLIA_MNEMONIC"] 40 | }, 41 | chainId: 421614 42 | }, 43 | sepolia: { 44 | url: process.env["SEPOLIA_RPC_URL"], 45 | accounts: { 46 | mnemonic: process.env["SEPOLIA_MNEMONIC"], 47 | }, 48 | chainId: 11155111 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { TypedDataUtils } from 'ethers-eip712' 2 | 3 | // obj: { 4 | // chainId: u256, 5 | // gem: address, 6 | // owner: address, 7 | // spender: address, 8 | // value: u256, 9 | // nonce: u256, 10 | // deadline: u256, 11 | // } 12 | export function makeGemPermitDigest (obj: any) { 13 | const typedData = { 14 | types: { 15 | EIP712Domain: [ 16 | { name: 'name', type: 'string' }, 17 | { name: 'version', type: 'string' }, 18 | { name: 'chainId', type: 'uint256' }, 19 | { name: 'verifyingContract', type: 'address' } 20 | ], 21 | GemPermit: [ 22 | { name: 'owner', type: 'address' }, 23 | { name: 'spender', type: 'address' }, 24 | { name: 'value', type: 'uint256' }, 25 | { name: 'nonce', type: 'uint256' }, 26 | { name: 'deadline', type: 'bytes32' } 27 | ] 28 | }, 29 | primaryType: 'GemPermit', 30 | domain: { 31 | name: 'GemPermit', 32 | version: '0', 33 | chainId: obj.chainId, 34 | verifyingContract: obj.gem 35 | }, 36 | message: { 37 | owner: obj.owner, 38 | spender: obj.spender, 39 | value: obj.value, 40 | nonce: obj.nonce, 41 | deadline: obj.deadline 42 | } 43 | } 44 | // debug('encoding digest...') 45 | return Buffer.from(TypedDataUtils.encodeDigest(typedData)) 46 | } 47 | -------------------------------------------------------------------------------- /test/ERC20/helpers.ts: -------------------------------------------------------------------------------- 1 | // redo expectEvent to work with ethers 2 | // https://github.com/OpenZeppelin/openzeppelin-test-helpers/blob/master/src/expectEvent.js 3 | // 4 | // The MIT License (MIT) 5 | // Copyright (c) 2018 OpenZeppelin 6 | // https://github.com/OpenZeppelin/openzeppelin-test-helpers/blob/master/LICENSE 7 | 8 | const {expect} = require("chai"); 9 | 10 | // matches eventName 11 | // matches data if defined 12 | function expectEvent (receipt, eventName, eventArgs = {}, data = undefined) { 13 | const args = Object.keys(eventArgs).map((key) => {return eventArgs[key]}) 14 | let found = false 15 | receipt.events.forEach(event => { 16 | if( event.event == eventName && (data == undefined || data == event.data) ) { 17 | let match = true 18 | Object.keys(eventArgs).forEach(key => { 19 | try { 20 | if( eventName == undefined ) { 21 | expect(eventArgs[key]).to.eql(event.topics[key]) 22 | } else { 23 | expect(eventArgs[key]).to.eql(event.args[key]) 24 | } 25 | } catch { 26 | match = false 27 | } 28 | }) 29 | found = found || match 30 | } 31 | }) 32 | 33 | expect(found).to.equal(true, `No '${eventName}' events found with args ${args}`); 34 | } 35 | 36 | module.exports = { expectEvent } 37 | -------------------------------------------------------------------------------- /task/deploy-gemfab.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | const debug = Debug('gemfab:task') 3 | 4 | const dpack = require('@etherpacks/dpack') 5 | 6 | const { task } = require('hardhat/config') 7 | 8 | task('deploy-gemfab', 'deploy GemFab') 9 | .addFlag('stdout', 'print the dpack to stdout') 10 | .addOptionalParam('writepack', 'save the pack') 11 | .addOptionalParam('gasLimit', 'gemfab deploy tx gas limit') 12 | .setAction(async (args, hre) => { 13 | const { ethers, network } = hre 14 | 15 | const [acct] = await hre.ethers.getSigners() 16 | const deployer = acct.address 17 | 18 | debug(`Deploying contracts using ${deployer} to ${network.name}`) 19 | 20 | const GemArtifact = require('../artifacts/src/gem.sol/Gem.json') 21 | const GemFabArtifact = require('../artifacts/src/gem.sol/GemFab.json') 22 | const GemFabDeployer = ethers.ContractFactory.fromSolidity(GemFabArtifact, acct) 23 | const gf = await GemFabDeployer.deploy({gasLimit: args.gasLimit}) 24 | await gf.deployed() 25 | debug('GemFab deployed to : ', gf.address) 26 | 27 | const pb = new dpack.PackBuilder(network.name) 28 | await pb.packObject({ 29 | objectname: 'gemfab', 30 | typename: 'GemFab', 31 | artifact: GemFabArtifact, 32 | address: gf.address 33 | }, true) // alsoPackType 34 | await pb.packType({ 35 | typename: 'Gem', 36 | artifact: GemArtifact 37 | }) 38 | 39 | const pack = await pb.build() 40 | const str = JSON.stringify(pack, null, 2) 41 | if (args.stdout) { 42 | console.log(str) 43 | } 44 | if (args.writepack) { 45 | const outfile = require('path').join( 46 | __dirname, `../pack/gemfab_${hre.network.name}.dpack.json` 47 | ) 48 | const packstr = JSON.stringify(pack, null, 2) 49 | require('fs').writeFileSync(outfile, packstr) 50 | } 51 | return pack 52 | }) 53 | -------------------------------------------------------------------------------- /test/bounds.ts: -------------------------------------------------------------------------------- 1 | export const bounds = { 2 | gem: { 3 | decimals: [21291, 21291], 4 | mint: { 5 | 0: {0: [30481, 30481], 1: [70293, 70293]}, 6 | 1: { 1: [30481, 30481], 2: [36093, 36093]}, 7 | }, 8 | transfer: { 9 | 0: { 10 | 0: {0: [28385, 28385]}, 11 | 1: {0: [46297, 46297], 1: [28385, 28385]}, 12 | 2: {1: [51097, 51097]} 13 | }, 14 | 1: { 15 | 0: {0: [28385, 28385]}, 16 | 1: {0: [29197, 29197], 1: [28385, 28385]}, 17 | 2: {1: [33997, 33997]} 18 | }, 19 | 2: { 20 | 0: {0: [28385, 28385]}, 21 | 1: {0: [29197, 29197], 1: [28385, 28385]}, 22 | 2: {1: [33997, 33997]} 23 | } 24 | }, 25 | transferFrom: { 26 | notMaxAllowance: { 27 | 0: { 28 | 0: {0: [33301, 33301]}, 29 | 1: {0: [54013, 54013], 1: [33301, 33301]}, 30 | 2: {1: [58813, 58813]} 31 | }, 32 | 1: { 33 | 0: {0: [33301, 33301]}, 34 | 1: {0: [36913, 36913], 1: [33301, 33301]}, 35 | 2: {1: [41713, 41713]} 36 | }, 37 | 2: { 38 | 0: {0: [33301, 33301]}, 39 | 1: {0: [36913, 36913], 1: [33301, 33301]}, 40 | 2: {1: [41713, 41713]} 41 | } 42 | }, 43 | maxAllowance: { 44 | 0: { 45 | 0: {0: [33301, 33301]}, 46 | 1: {0: [49105, 49105], 1: [33301, 33301]}, 47 | 2: {1: [53905, 53905]} 48 | }, 49 | 1: { 50 | 0: {0: [33301, 33301]}, 51 | 1: {0: [32005, 32005], 1: [33301, 33301]}, 52 | 2: {1: [36805, 36805]} 53 | }, 54 | 2: { 55 | 0: {0: [33301, 33301]}, 56 | 1: {0: [32005, 32005], 1: [33301, 33301]}, 57 | 2: {1: [36805, 36805]} 58 | } 59 | } 60 | }, 61 | burn: { 62 | 0: {0: [30473, 30473]}, 63 | 1: {0: [28868, 28868], 1: [30473, 30473]}, 64 | 2: {1: [36085, 36085]} 65 | }, 66 | approve: { 67 | 0: {0: [26134, 26134], 1: [46046, 46046]}, 68 | 1: {0: [24134, 24134], 1: [26146, 26146], 2: [28946, 28946]}, 69 | 2: {1: [28946, 28946]} 70 | }, 71 | permit: { 72 | 0: {0: [54301, 54313], 1: [74201, 74225]}, 73 | 1: {0: [35177, 35213], 1: [37213, 37225], 2: [40013, 40025]}, 74 | 2: {1: [40013, 40025]} 75 | }, 76 | ward: { 77 | 0: {0: [28302, 28302], 1: [48214, 48214]}, 78 | 1: {0: [26302, 26302], 1: [28314, 28314]}, 79 | }, 80 | } 81 | /* 82 | 0: {0: [0, 0], 1: [0, 0]}, 83 | 1: {0: [0, 0], 1: [0, 0], 2: [0, 0]}, 84 | 2: { 1: [0, 0]} 85 | */ 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `Gem` is an ERC20 token implementation. `GemFab` is a token factory that builds `Gem`s. 2 | 3 | The idea is to stop deploying tokens directly, and use a factory for all tokens. 4 | 5 | `Gem` is *not safe for extension via inheritance*. Instead of customizing your gem via inheritance, you should use the built-in `mint` and `burn`, with multi-owner `ward` for authentication. 6 | 7 | Minting and burning from a controller contract which defines the rules for when those can occur is the most 'hygenic' way to implement all forms of tokenomics. 8 | 9 | If you check `gemfab.built(gem)`, you know that `gem` is a `Gem` -- no further audit needed. 10 | An independently deployed `Gem` will not appear in the factory's record of valid gems, which will complicate verification for no reason. 11 | More importantly, having a record that the gem was built from the factory allows *other contracts* to infer that certain invariants are maintained, 12 | which a codehash check cannot satisfy because contracts can write to their state during construction time. 13 | 14 | 15 | Here are some other implementation choices made for `Gem`. 16 | 17 | * `name` and `symbol` are `bytes32` instead of `string`, this prevents "return data bombs" when gemfab is composed into other systems 18 | * Infinite allowance via `approve(code, type(uint256).max);`. This avoids a useless store and is a major gas savings. 19 | * `permit` -- There are several minor variations in the wild; this one uses EIP-2612 (notably, has a small difference from earlier permit in Dai). 20 | * Custom error types for all possible error conditions, with a consistent error API. 21 | * Invariants preserved with controlled mint/burn means `unchecked` blocks can be used to save gas in every function. 22 | * Functions are `payable`, saving a little bit of gas. The contract as a whole will still reject ether sent to invalid ABIs, including regular ether "send" (call with no calldata), which covers the most common mistake. 23 | 24 | Working with gemfab 25 | --- 26 | 27 | (*Note: No packs are published yet -- watch for 'release' branch*). See `pack/gemfab_.dpack.json` to get the `gemfab` object plus `Gem` and `GemFab` types. See [`dpack`](https://github.com/dapphub/dpack) for docs on how to use these packs. 28 | 29 | Discussion 30 | --- 31 | 32 | "ERC20" is an ABI definition masquerading as a semantic spec. There is no "standard ERC20". 33 | As a result, the token ecosystem is a disaster. In an ideal world, this would have been the lesson that taught EIP enthusiasts to stop doing design by committee. 34 | 35 | Packs 36 | --- 37 | Arbitrum One bafkreibhkoqc5nda6dlubyvsylethk5nxqynjltnsxnlmpc7inpcmiveom 38 | Sepolia 39 | Arbitrum Sepolia bafybeigm6cpsg4jfnh3sqezpcp77i7ted4hawmj2bybqsqgh24ot7aijva 40 | Arbitrum Goerli bafybeifbn66p32bgd36kgf5xg67fthdyjhywfywjwwg357xnlvgxr4ne5a 41 | 42 | ### Notice 43 | 44 | See NOTICE file for copyrights and credits. 45 | -------------------------------------------------------------------------------- /test/ERC20/draft-ERC20Permit.test.ts: -------------------------------------------------------------------------------- 1 | // modified version openzeppelin-contracts draft-ERC20Permit.test.js 2 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/token/ERC20/extensions/draft-ERC20Permit.test.js 3 | // 4 | // The MIT License (MIT) 5 | // Copyright (c) 2016-2020 zOS Global Limited 6 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/LICENSE 7 | 8 | /* eslint-disable */ 9 | import {ethers} from "hardhat"; 10 | import * as hh from "hardhat"; 11 | import {snapshot, revert, send} from 'minihat' 12 | 13 | const { expect } = require('chai'); 14 | const expectRevert = async (f, msg) => { await expect(f).rejectedWith(msg) } 15 | const { BN } = require('bn.js') 16 | const { constants, BigNumber, utils } = ethers 17 | 18 | const Permit = [ 19 | { name: 'owner', type: 'address' }, 20 | { name: 'spender', type: 'address' }, 21 | { name: 'value', type: 'uint256' }, 22 | { name: 'nonce', type: 'uint256' }, 23 | { name: 'deadline', type: 'uint256' }, 24 | ]; 25 | 26 | const hre = require('hardhat'); 27 | 28 | describe('ERC20Permit', () => { 29 | let initialHolder, spender, recipient, other; 30 | 31 | const name = utils.formatBytes32String('GoodCoin'); 32 | const symbol = utils.formatBytes32String('GCN'); 33 | const version = '0'; 34 | 35 | const initialSupply = BigNumber.from(100); 36 | 37 | let chainId; 38 | let gem; 39 | let gem_type 40 | let gemfab; 41 | let gemfab_type 42 | 43 | before(async () => { 44 | [initialHolder, spender, recipient, other] = await ethers.getSigners(); 45 | gem_type = await ethers.getContractFactory('Gem', initialHolder) 46 | gemfab_type = await ethers.getContractFactory('GemFab', initialHolder) 47 | 48 | gemfab = await gemfab_type.deploy() 49 | const gemaddr = await gemfab.callStatic.build(name, symbol) 50 | await send(gemfab.build, name, symbol) 51 | gem = gem_type.attach(gemaddr) 52 | 53 | await snapshot(hh) 54 | 55 | chainId = await hh.network.config.chainId; 56 | //domain.chainId = chainId; 57 | //domain.verifyingContract = gem.address; 58 | }) 59 | 60 | 61 | beforeEach(async function () { 62 | await revert(hh) 63 | this.token = gem; 64 | 65 | // We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id 66 | // from within the EVM as from the JSON RPC interface. 67 | // See https://github.com/trufflesuite/ganache-core/issues/515 68 | 69 | //this.chainId = await this.token.getChainId(); 70 | //Gem doesn't have getChainId...hh env has same chainid 71 | this.chainId = await hh.network.config.chainId; 72 | }); 73 | 74 | it('initial nonce is 0', async function () { 75 | expect(await this.token.nonces(initialHolder.address)).to.eql(constants.Zero); 76 | }); 77 | 78 | it('domain separator', async function () { 79 | expect( 80 | await this.token.DOMAIN_SEPARATOR(), 81 | ).to.equal( 82 | ethers.utils._TypedDataEncoder.hashDomain( 83 | { 84 | name: 'GemPermit', 85 | version, 86 | chainId: this.chainId, 87 | verifyingContract: this.token.address 88 | } 89 | ) 90 | ); 91 | }); 92 | 93 | describe('permit', function () { 94 | const types = { 95 | Permit: [ 96 | { name: 'owner', type: 'address' }, 97 | { name: 'spender', type: 'address' }, 98 | { name: 'value', type: 'uint256' }, 99 | { name: 'nonce', type: 'uint256' }, 100 | { name: 'deadline', type: 'uint256' } 101 | ] 102 | }; 103 | 104 | const domain = { 105 | name: 'GemPermit', 106 | version: '0', 107 | chainId: undefined, 108 | verifyingContract: undefined 109 | }; 110 | 111 | const nonce = 0; 112 | const deadline = constants.MaxUint256; 113 | let value; 114 | before(async () => { 115 | value = { 116 | owner: initialHolder.address, 117 | spender: spender.address, 118 | value: 42, 119 | nonce: nonce, 120 | deadline: deadline 121 | }; 122 | domain.chainId = chainId; 123 | domain.verifyingContract = gem.address; 124 | }) 125 | 126 | it('accepts owner signature', async function () { 127 | const signature = await initialHolder._signTypedData(domain, types, value) 128 | const { v, r, s } = ethers.utils.splitSignature(signature); 129 | 130 | const receipt = await this.token.permit(initialHolder.address, spender.address, value.value, value.deadline, v, r, s); 131 | 132 | expect(await this.token.nonces(initialHolder.address)).to.eql(ethers.constants.One); 133 | expect(await this.token.allowance(initialHolder.address, spender.address)).to.eql(BigNumber.from(value.value)); 134 | }); 135 | 136 | it('rejects reused signature', async function () { 137 | const signature = await initialHolder._signTypedData(domain, types, value) 138 | const { v, r, s } = ethers.utils.splitSignature(signature); 139 | 140 | await this.token.permit(initialHolder.address, spender.address, value.value, value.deadline, v, r, s); 141 | 142 | await expectRevert( 143 | this.token.permit(initialHolder.address, spender.address, value.value, value.deadline, v, r, s), 144 | 'ErrPermitSignature', 145 | ); 146 | }); 147 | 148 | it('rejects other signature', async function () { 149 | const signature = await other._signTypedData(domain, types, value) 150 | const { v, r, s } = ethers.utils.splitSignature(signature); 151 | 152 | await expectRevert( 153 | this.token.permit(initialHolder.address, spender.address, value.value, value.deadline, v, r, s), 154 | 'ErrPermitSignature', 155 | ); 156 | }); 157 | 158 | it('rejects expired permit', async function () { 159 | value.deadline = Math.floor(Date.now() / 1000) - 10; 160 | 161 | const signature = await initialHolder._signTypedData(domain, types, value) 162 | const { v, r, s } = ethers.utils.splitSignature(signature); 163 | 164 | await expectRevert( 165 | this.token.permit(initialHolder.address, spender.address, value.value, value.deadline, v, r, s), 166 | 'ErrPermitDeadline', 167 | ); 168 | }); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { constants, BigNumber, utils } from 'ethers' 2 | import { chai, send, want } from 'minihat' 3 | import {describe} from "mocha"; 4 | 5 | export { snapshot, revert, send, wad, ray, rad, apy, N, BANKYEAR, WAD, RAY, RAD, U256_MAX } from 'minihat' 6 | 7 | const debug = require('debug')('rico:test') 8 | const ramp_members = ['vel', 'rel', 'bel', 'cel'] 9 | 10 | export async function check_gas (gas, minGas, maxGas) { 11 | await want(gas.toNumber()).to.be.at.most(maxGas); 12 | if( gas.toNumber() < minGas ) { 13 | console.log("gas reduction: previous min=", minGas, " gas used=", gas.toNumber()); 14 | } 15 | } 16 | 17 | // 18 | // for move patterns 19 | // verify takes previous dst value 20 | export function test2D( 21 | name : string, 22 | init : () => void, 23 | fillDst : (prev, next) => Promise, 24 | fill : (prev, next) => Promise, 25 | clear : (prev, next) => Promise, 26 | stay : (prev) => Promise, 27 | one : Buffer | boolean | BigNumber | number, 28 | two : Buffer | boolean | BigNumber | number, 29 | bounds : object, 30 | verify? : (dstPrev) => (srcPrev, srcNext) => Promise 31 | ) { 32 | let ZERO : any 33 | if( Buffer.isBuffer(one) ) { 34 | ZERO = Buffer.from('00'.repeat(32), 'hex') 35 | } else if( typeof(one) == 'boolean' ) { 36 | ZERO = false 37 | } else if( BigNumber.isBigNumber(one) ) { 38 | ZERO = constants.Zero 39 | } else if( typeof(one) == 'number' ) { 40 | ZERO = 0 41 | } 42 | 43 | const makeTests = (subTest, next) => { 44 | const initAndFill = async () => { 45 | await init() 46 | await fillDst(ZERO, next) 47 | } 48 | const testName = name + ' dst at ' + subTest 49 | if( verify ) { 50 | test1D(testName, initAndFill, fill, clear, stay, one, two, bounds[subTest], verify(next)) 51 | } else { 52 | test1D(testName, initAndFill, fill, clear, stay, one, two, bounds[subTest]) 53 | } 54 | } 55 | 56 | makeTests(0, constants.Zero) 57 | makeTests(1, one) 58 | makeTests(2, two) 59 | } 60 | 61 | 62 | // generate 1d tests 63 | // takes three functions that manipulate state 64 | // fill: increase value by one 65 | // clear: decrease value by one 66 | // stay: keep value the same, using the method being profiled 67 | // fill, clear and stay should return undefined if they don't use the method being tested 68 | // verify: verify the new src and dst values 69 | export function test1D( 70 | name : string, 71 | init : () => void, 72 | fill : (prev, next) => Promise, 73 | clear : (prev, next) => Promise, 74 | stay : (prev) => Promise, 75 | one : Buffer | boolean | BigNumber | number, 76 | two : Buffer | boolean | BigNumber | number, 77 | bounds : object, 78 | verify? : (srcPrev, srcNext) => Promise 79 | ) { 80 | function assert_def(gas) { 81 | chai.assert( 82 | gas != undefined, 83 | "Testing fill/clear/stay, but it returned undefined. This test can be removed." 84 | ) 85 | } 86 | describe(name, () => { 87 | let ZERO : any 88 | if( Buffer.isBuffer(one) ) { 89 | ZERO = Buffer.from('00'.repeat(32), 'hex') 90 | } else if( typeof(one) == 'boolean' ) { 91 | ZERO = false 92 | } else if( BigNumber.isBigNumber(one) ) { 93 | ZERO = constants.Zero 94 | } else if( typeof(one) == 'number' ) { 95 | ZERO = 0 96 | } 97 | //let ZERO = typeof(one) == 'boolean' ? false : typeof(one) == 'Buffer' ? Buffer.from(0) 98 | // : BigNumber.from(0) 99 | beforeEach(init) 100 | describe('no change', () => { 101 | if( bounds[0] != undefined && bounds[0][0] != undefined ) { 102 | it('0->0', async () => { 103 | const tx = await stay(ZERO) 104 | assert_def(tx) 105 | const gas = tx.gasUsed 106 | if( verify ) await verify(ZERO, ZERO) 107 | const bound = bounds[0][0] 108 | await check_gas(gas, bound[0], bound[1]) 109 | }) 110 | } 111 | if( bounds[1] != undefined && bounds[1][1] != undefined ) { 112 | it('1->1', async () => { 113 | await fill(ZERO, one) 114 | const tx = await stay(one) 115 | assert_def(tx) 116 | if( verify ) await verify(one, one) 117 | const gas = tx.gasUsed 118 | const bound = bounds[1][1] 119 | await check_gas(gas, bound[0], bound[1]) 120 | }) 121 | } 122 | }) 123 | describe('change', () => { 124 | if( bounds[0] != undefined && bounds[0][1] != undefined ) { 125 | it('0->1', async () => { 126 | const tx = await fill(ZERO, one) 127 | assert_def(tx) 128 | const gas = tx.gasUsed 129 | if( verify ) await verify(ZERO, one) 130 | const bound = bounds[0][1] 131 | await check_gas(gas, bound[0], bound[1]) 132 | }) 133 | } 134 | if( bounds[1] != undefined && bounds[1][0] != undefined ) { 135 | it('1->0', async () => { 136 | await fill(ZERO, one) 137 | const tx = await clear(one, ZERO) 138 | assert_def(tx) 139 | const gas = tx.gasUsed 140 | if( verify ) await verify(one, ZERO) 141 | const bound = bounds[1][0] 142 | await check_gas(gas, bound[0], bound[1]) 143 | }) 144 | } 145 | if( bounds[1] != undefined && bounds[1][2] != undefined ) { 146 | it('1->2', async () => { 147 | // 1->2 invalid for bools 148 | want(one).to.not.be.a('boolean') 149 | await fill(ZERO, one) 150 | const tx = await fill(one, two) 151 | assert_def(tx) 152 | const gas = tx.gasUsed 153 | if( verify ) await verify(one, two) 154 | const bound = bounds[1][2] 155 | await check_gas(gas, bound[0], bound[1]) 156 | }) 157 | } 158 | if( bounds[2] != undefined && bounds[2][1] != undefined ) { 159 | it('2->1', async () => { 160 | // 1->2 invalid for bools 161 | want(one).to.not.be.a('boolean') 162 | await fill(ZERO, two) 163 | const tx = await clear(two, one) 164 | assert_def(tx) 165 | const gas = tx.gasUsed 166 | if( verify ) await verify(two, one) 167 | const bound = bounds[2][1] 168 | await check_gas(gas, bound[0], bound[1]) 169 | }) 170 | } 171 | }) 172 | }) 173 | } 174 | -------------------------------------------------------------------------------- /src/gem.sol: -------------------------------------------------------------------------------- 1 | /// SPDX-License-Identifier: AGPL-3.0-only 2 | 3 | // Copyright (C) 2024 Free Software Foundation, in memoriam of Nikolai Mushegian 4 | // Copyright (C) 2017, 2018, 2019 dbrock, rain, mrchico 5 | 6 | // This program is free software: you can redistribute it and/or modify 7 | // it under the terms of the GNU Affero General Public License as published by 8 | // the Free Software Foundation, version 3. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | pragma solidity ^0.8.25; 19 | 20 | contract Gem { 21 | bytes32 public name; 22 | bytes32 public symbol; 23 | uint256 public totalSupply; 24 | uint8 public constant decimals = 18; 25 | 26 | mapping (address => uint) public balanceOf; 27 | mapping (address => mapping (address => uint)) public allowance; 28 | mapping (address => uint) public nonces; 29 | mapping (address => bool) public wards; 30 | 31 | bytes32 constant DOMAIN_SUBHASH = keccak256( 32 | 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' 33 | ); 34 | bytes32 constant PERMIT_TYPEHASH = keccak256( 35 | 'Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)' 36 | ); 37 | function DOMAIN_SEPARATOR() external view returns (bytes32) { 38 | return keccak256(abi.encode( DOMAIN_SUBHASH, 39 | keccak256("GemPermit"), keccak256("0"), 40 | block.chainid, address(this)) 41 | ); 42 | } 43 | 44 | event Approval(address indexed src, address indexed usr, uint256 wad); 45 | event Transfer(address indexed src, address indexed dst, uint256 wad); 46 | event Ward(address indexed setter, address indexed user, bool authed); 47 | 48 | error ErrPermitDeadline(); 49 | error ErrPermitSignature(); 50 | error ErrOverflow(); 51 | error ErrUnderflow(); 52 | error ErrZeroDst(); 53 | error ErrWard(); 54 | 55 | constructor(bytes32 name_, bytes32 symbol_) 56 | payable 57 | { 58 | name = name_; 59 | symbol = symbol_; 60 | 61 | wards[msg.sender] = true; 62 | emit Ward(msg.sender, msg.sender, true); 63 | } 64 | 65 | function ward(address usr, bool authed) 66 | payable external 67 | { 68 | if (!wards[msg.sender]) revert ErrWard(); 69 | wards[usr] = authed; 70 | emit Ward(msg.sender, usr, authed); 71 | } 72 | 73 | function mint(address usr, uint wad) 74 | payable external 75 | { 76 | if (!wards[msg.sender]) revert ErrWard(); 77 | unchecked { 78 | // only need to check totalSupply for overflow 79 | uint256 prev = totalSupply; 80 | if (prev + wad < prev) revert ErrOverflow(); 81 | 82 | balanceOf[usr] += wad; 83 | totalSupply = prev + wad; 84 | emit Transfer(address(0), usr, wad); 85 | 86 | if (usr == address(0)) revert ErrZeroDst(); 87 | } 88 | } 89 | 90 | function burn(address usr, uint wad) 91 | payable external 92 | { 93 | if (!wards[msg.sender]) revert ErrWard(); 94 | unchecked { 95 | // only need to check balanceOf[usr] for underflow 96 | uint256 prev = balanceOf[usr]; 97 | balanceOf[usr] = prev - wad; 98 | totalSupply -= wad; 99 | emit Transfer(usr, address(0), wad); 100 | 101 | if (prev < wad) revert ErrUnderflow(); 102 | } 103 | } 104 | 105 | function transfer(address dst, uint wad) 106 | payable external returns (bool ok) 107 | { 108 | unchecked { 109 | ok = true; 110 | uint256 prev = balanceOf[msg.sender]; 111 | balanceOf[msg.sender] = prev - wad; 112 | balanceOf[dst] += wad; 113 | emit Transfer(msg.sender, dst, wad); 114 | 115 | if (prev < wad) revert ErrUnderflow(); 116 | if (dst == address(0)) revert ErrZeroDst(); 117 | } 118 | } 119 | 120 | function transferFrom(address src, address dst, uint wad) 121 | payable external returns (bool ok) 122 | { 123 | unchecked { 124 | ok = true; 125 | uint256 prevB = balanceOf[src]; 126 | balanceOf[src] = prevB - wad; 127 | balanceOf[dst] += wad; 128 | uint256 prevA = allowance[src][msg.sender]; 129 | 130 | if (prevA != type(uint256).max) { 131 | allowance[src][msg.sender] = prevA - wad; 132 | emit Approval(src, msg.sender, prevA - wad); 133 | 134 | if (prevA < wad) revert ErrUnderflow(); 135 | } 136 | emit Transfer(src, dst, wad); 137 | 138 | if (prevB < wad) revert ErrUnderflow(); 139 | if (dst == address(0)) revert ErrZeroDst(); 140 | } 141 | } 142 | 143 | function approve(address usr, uint wad) 144 | payable external returns (bool ok) 145 | { 146 | ok = true; 147 | allowance[msg.sender][usr] = wad; 148 | emit Approval(msg.sender, usr, wad); 149 | } 150 | 151 | // EIP-2612 152 | function permit(address owner, address spender, uint256 value, uint256 deadline, 153 | uint8 v, bytes32 r, bytes32 s) 154 | payable external 155 | { 156 | allowance[owner][spender] = value; 157 | emit Approval(owner, spender, value); 158 | 159 | address signer; 160 | unchecked { 161 | signer = ecrecover( 162 | keccak256(abi.encodePacked( "\x19\x01", 163 | keccak256(abi.encode( DOMAIN_SUBHASH, 164 | keccak256("GemPermit"), keccak256("0"), 165 | block.chainid, address(this))), 166 | keccak256(abi.encode( PERMIT_TYPEHASH, owner, spender, 167 | value, nonces[owner]++, deadline )))), 168 | v, r, s 169 | ); 170 | } 171 | 172 | if (signer == address(0)) revert ErrPermitSignature(); 173 | if (owner != signer) revert ErrPermitSignature(); 174 | if (block.timestamp > deadline) revert ErrPermitDeadline(); 175 | } 176 | } 177 | 178 | contract GemFab { 179 | mapping(address=>bool) public built; 180 | 181 | event Build(address indexed caller, address indexed gem); 182 | 183 | function build(bytes32 name, bytes32 symbol) 184 | payable external returns (Gem gem) 185 | { 186 | gem = new Gem(name, symbol); 187 | built[address(gem)] = true; 188 | emit Build(msg.sender, address(gem)); 189 | gem.ward(msg.sender, true); 190 | gem.ward(address(this), false); 191 | return gem; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /specs/Gem.spec: -------------------------------------------------------------------------------- 1 | methods { 2 | balanceOf(address) returns(uint) envfree 3 | allowance(address,address) returns(uint) envfree 4 | totalSupply() returns(uint) envfree 5 | wards(address) returns(bool) envfree 6 | transferFrom(address,address,uint) returns(bool) 7 | mint(address,uint) 8 | burn(address,uint) 9 | } 10 | 11 | ghost uint ghostSupply; 12 | 13 | hook Sstore balanceOf[KEY address own] uint256 new_balance (uint256 old_balance) STORAGE { 14 | // holds for transfers because net value of writes on balanceOf should be 0 15 | ghostSupply = ghostSupply + (new_balance - old_balance); 16 | } 17 | 18 | rule transferMustDecreaseSenderBalanceAndIncreaseRecipientBalance(address recip, uint amt) { 19 | 20 | env e; 21 | address sender = e.msg.sender; 22 | 23 | require(balanceOf(e.msg.sender) + balanceOf(recip) < totalSupply()); 24 | 25 | // mathint type that represents an integer of any size; 26 | mathint balance_sender_before = balanceOf(sender); 27 | mathint balance_recip_before = balanceOf(recip); 28 | 29 | transfer(e, recip, amt); 30 | 31 | mathint balance_sender_after = balanceOf(sender); 32 | mathint balance_recip_after = balanceOf(recip); 33 | 34 | // operations on mathints can never overflow or underflow. 35 | assert recip != sender => balance_sender_after == balance_sender_before - amt, 36 | "transfer must decrease sender's gem balance by amount sent"; 37 | 38 | assert recip != sender => balance_recip_after == balance_recip_before + amt, 39 | "transfer must increase recipient's gem balance by amount sent"; 40 | 41 | assert recip == sender => balance_sender_after == balance_sender_before, 42 | "transfer must not change sender's gem balance when recip is self"; 43 | } 44 | 45 | rule transferMustRevertWithInsufficientBalance(address recip, uint amt) { 46 | 47 | env e; 48 | address sender = e.msg.sender; 49 | 50 | require(balanceOf(sender) < amt); 51 | 52 | transfer@withrevert(e, recip, amt); 53 | assert lastReverted, "transfer did not revert with insufficient sender balance"; 54 | } 55 | 56 | rule mintMustIncreaseBalanceAndTotalSupply(address recip, uint amt) { 57 | 58 | env e; 59 | require(balanceOf(e.msg.sender) + balanceOf(recip) < totalSupply()); 60 | mathint balance_recip_before = balanceOf(recip); 61 | mathint total_supply_before = totalSupply(); 62 | 63 | mint(e, recip, amt); 64 | 65 | mathint balance_recip_after = balanceOf(recip); 66 | mathint total_supply_after = totalSupply(); 67 | 68 | assert balance_recip_after == balance_recip_before + amt, "recip balance did not increase by mint amount"; 69 | assert total_supply_after == total_supply_before + amt, "total supply did not increase by mint amount"; 70 | 71 | } 72 | 73 | rule mintMustRevertOnOverflow(address recip, uint amt) { 74 | 75 | env e; 76 | mathint total_supply = totalSupply(); 77 | require(total_supply + amt > max_uint256); 78 | 79 | mint@withrevert(e, recip, amt); 80 | assert lastReverted, "mint must revert if total supply overflows"; 81 | } 82 | 83 | rule burnMustDecreaseBalanceAndTotalSupply(address burn, uint amt) { 84 | 85 | env e; 86 | require(balanceOf(burn) <= amt); 87 | require(balanceOf(burn) <= totalSupply()); 88 | 89 | mathint total_supply_before = totalSupply(); 90 | mathint balance_before = balanceOf(burn); 91 | burn(e, burn, amt); 92 | 93 | mathint total_supply_after = totalSupply(); 94 | mathint balance_after = balanceOf(burn); 95 | 96 | assert total_supply_after == total_supply_before - amt, "totalSupply did not decrease by burn amount"; 97 | assert balance_after == balance_before - amt, "usr balance did not decrease by burn amount"; 98 | } 99 | 100 | rule burnMustRevertOnUnderflow(address recip, uint amt) { 101 | 102 | env e; 103 | require(balanceOf(recip) < amt); 104 | 105 | burn@withrevert(e, recip, amt); 106 | assert lastReverted, "burn must revert if total supply underflows"; 107 | } 108 | 109 | rule mintAndBurnRequireWard(address other, uint amt) { 110 | 111 | env e; 112 | address sender = e.msg.sender; 113 | ward(e, sender, false); 114 | 115 | mint@withrevert(e, other, amt); 116 | assert lastReverted, "mint did not revert with non-ward sender"; 117 | 118 | burn@withrevert(e, other, amt); 119 | assert lastReverted, "burn did not revert with non-ward sender"; 120 | } 121 | 122 | 123 | rule totalSupplyOnlyChangedByMintAndBurn(address target, uint amt) { 124 | 125 | env e; method f; calldataarg args; 126 | mathint total_supply_before = totalSupply(); 127 | 128 | f@withrevert(e, args); 129 | 130 | mathint total_supply_after = totalSupply(); 131 | 132 | assert total_supply_before != total_supply_after 133 | => 134 | (f.selector == mint(address,uint).selector || f.selector == burn(address,uint).selector) 135 | && wards(e.msg.sender) == true, 136 | "function other than warded mint or burn changed total supply!"; 137 | } 138 | 139 | rule wardUpdatesUsrAuthedStatus(address other_ward) { 140 | 141 | env e; 142 | address sender = e.msg.sender; 143 | require(wards(sender) == true); // for this spec, assume sender is already a ward 144 | 145 | ward(e, other_ward, true); 146 | assert wards(other_ward) == true; 147 | 148 | ward(e, other_ward, false); 149 | assert wards(other_ward) == false; 150 | 151 | } 152 | 153 | rule approveUpdatesAllowanceForSpender(address spender, uint amt) { 154 | 155 | env e; 156 | approve(e, spender, amt); 157 | assert allowance(e.msg.sender, spender) == amt, "spender allowance does not match intended amount"; 158 | } 159 | 160 | rule transferFromUpdatesBalanceAndAllowanceOrIsSelfTransfer(address src, address dst, uint amt) { 161 | 162 | env e; 163 | address sender = e.msg.sender; 164 | require(allowance(src, sender) >= amt); 165 | require(balanceOf(src) >= amt); 166 | require(balanceOf(src) <= max_uint256); 167 | require(balanceOf(dst) <= max_uint256 - amt); 168 | 169 | mathint balance_src_before = balanceOf(src); 170 | mathint balance_dst_before = balanceOf(dst); 171 | mathint allowance_before = allowance(src, sender); 172 | 173 | transferFrom(e, src, dst, amt); 174 | 175 | // true to matter what 176 | assert allowance(src, sender) == allowance_before - amt 177 | || (allowance_before == max_uint256 && allowance_before == allowance(src, sender)), 178 | "allowance did not decrease by transferFrom amt"; 179 | 180 | // conditionals 181 | if(src != dst){ 182 | assert balanceOf(src) == balance_src_before - amt, "src balance did not decrease by transferFrom amt"; 183 | assert balanceOf(dst) == balance_dst_before + amt, "dst balance did not increase by transferFrom amt"; 184 | } else { 185 | assert balanceOf(src) == balanceOf(dst) 186 | && balanceOf(src) == balance_src_before 187 | && balanceOf(dst) == balance_dst_before 188 | && balance_dst_before == balance_src_before, 189 | "balances changed despite transferring to self"; 190 | } 191 | } 192 | 193 | rule transferFromRevertsWithInsufficientBalanceOrAllowanceOrOverflow(address src, address dst, uint amt) { 194 | env e; 195 | address sender = e.msg.sender; 196 | mathint balance_src = balanceOf(src); 197 | mathint balance_dst = balanceOf(dst); 198 | 199 | require(balance_src + balance_dst <= max_uint256); 200 | require(allowance(src, sender) < amt || balanceOf(src) < amt || balanceOf(dst) > max_uint256 - amt); 201 | 202 | transferFrom@withrevert(e, src, dst, amt); 203 | 204 | assert lastReverted, "transferFrom did not revert when expected to"; 205 | 206 | 207 | } 208 | 209 | rule totalSupplyIsSumOfAllBalanceOfValues(method f, calldataarg args) { 210 | 211 | env e; 212 | mathint total_supply_before = totalSupply(); 213 | require(total_supply_before == ghostSupply); // must be true at all times 214 | 215 | f(e, args); 216 | 217 | mathint total_supply_after = totalSupply(); 218 | assert total_supply_after == ghostSupply, "total_supply diverged from balanceOf storage writes"; 219 | 220 | } -------------------------------------------------------------------------------- /test/gemfab-test.ts: -------------------------------------------------------------------------------- 1 | import * as hh from 'hardhat' 2 | import { ethers, artifacts, network } from 'hardhat' 3 | import { want, send, fail, snapshot, revert, b32 } from 'minihat' 4 | const { constants, BigNumber, utils } = ethers 5 | 6 | import { test1D, test2D } from './helpers' 7 | import { bounds as _bounds } from './bounds' 8 | const bounds = _bounds.gem 9 | import { TypedDataUtils } from 'ethers-eip712' 10 | 11 | const { expectEvent } = require('./ERC20/helpers') 12 | 13 | const dpack = require('@etherpacks/dpack') 14 | 15 | const debug = require('debug')('gemfab:test') 16 | 17 | const types = { 18 | Permit: [ 19 | { name: 'owner', type: 'address' }, 20 | { name: 'spender', type: 'address' }, 21 | { name: 'value', type: 'uint256' }, 22 | { name: 'nonce', type: 'uint256' }, 23 | { name: 'deadline', type: 'uint256' } 24 | ] 25 | }; 26 | 27 | const domain = { 28 | name: 'GemPermit', 29 | version: '0', 30 | chainId: undefined, 31 | verifyingContract: undefined 32 | }; 33 | 34 | describe('gemfab', () => { 35 | let chainId; 36 | let ali, bob, cat 37 | let ALI, BOB, CAT 38 | let gem; let gem_type 39 | let gemfab 40 | before(async () => { 41 | [ali, bob, cat] = await ethers.getSigners(); 42 | [ALI, BOB, CAT] = [ali, bob, cat].map(signer => signer.address) 43 | 44 | const pack = await hh.run('deploy-gemfab', {writepack: 'true'}) 45 | const dapp = await dpack.load(pack, ethers, ali) 46 | gem_type = dapp._types.Gem 47 | 48 | gemfab = dapp.gemfab 49 | const name = utils.formatBytes32String('Mock Cash'); 50 | const symbol = utils.formatBytes32String('CASH'); 51 | const gemaddr = await gemfab.callStatic.build(name, symbol) 52 | const rx = await send(gemfab.build, name, symbol) 53 | expectEvent(rx, 'Build', {caller: ALI, gem: gemaddr}) 54 | want(await gemfab.built(gemaddr)).true 55 | 56 | gem = gem_type.attach(gemaddr) 57 | 58 | await snapshot(hh) 59 | 60 | chainId = await hh.network.config.chainId; 61 | 62 | domain.chainId = chainId; 63 | domain.verifyingContract = gem.address; 64 | 65 | }) 66 | beforeEach(async () => { 67 | await revert(hh) 68 | }) 69 | 70 | it('mint ward', async () => { 71 | await send(gem.mint, ALI, 100) 72 | const bal = await gem.balanceOf(ALI) 73 | want(bal.toNumber()).equal(100) 74 | 75 | const gembob = gem.connect(bob) 76 | await fail('ErrWard', gembob.mint, BOB, 100) 77 | }) 78 | 79 | it('burn underflow', async () => { 80 | await send(gem.mint, ALI, 100); 81 | await send(gem.mint, BOB, 100); // totalSupply wont be cause of underflow 82 | await fail('ErrUnderflow', gem.burn, ALI, 101); 83 | }); 84 | 85 | it('transferFrom self insufficient bal', async () => { 86 | await send(gem.mint, BOB, 100); 87 | const balbob = await gem.balanceOf(BOB); 88 | const gembob = gem.connect(bob) 89 | await send(gembob.approve, ALI, balbob + 1); 90 | const alibob = gem.connect(ali) 91 | await fail('ErrUnderflow', alibob.transferFrom, BOB, BOB, balbob + 1); 92 | }); 93 | 94 | it('transfer self insufficient bal', async () => { 95 | await send(gem.mint, ALI, 100); 96 | await fail('ErrUnderflow', gem.transfer, ALI, await gem.balanceOf(ALI) + 1) 97 | }) 98 | 99 | it('transferFrom self sufficient bal', async () => { 100 | await send(gem.mint, BOB, 100); 101 | const balbob = await gem.balanceOf(BOB); 102 | const gembob = gem.connect(bob) 103 | await send(gembob.approve, ALI, balbob); 104 | const alibob = gem.connect(ali) 105 | await send(alibob.transferFrom, BOB, BOB, balbob); 106 | want((await gem.balanceOf(BOB)).toNumber()).equal(balbob.toNumber()); 107 | }); 108 | 109 | it('transferFrom max allowance no approval event', async () => { 110 | await send(gem.mint, BOB, 100); 111 | const amt = await gem.balanceOf(BOB); 112 | await send(gem.connect(bob).approve, ALI, constants.MaxUint256); 113 | const rx = await send(gem.connect(ali).transferFrom, BOB, CAT, amt); 114 | try { 115 | expectEvent(rx, 'Approval') 116 | throw Error('transferFrom w max allowance emitted approval event') 117 | } catch {} 118 | }) 119 | 120 | describe('coverage', () => { 121 | describe('mint', () => { 122 | it('overflow', async function () { 123 | await send(gem.mint, ALI, constants.MaxUint256.div(2)); 124 | await send(gem.mint, BOB, constants.MaxUint256.div(2)) 125 | await send(gem.mint, CAT, 1) 126 | await fail('ErrOverflow', gem.mint, CAT, 1); 127 | }); 128 | }); 129 | 130 | describe('approve', () => { 131 | it('nonzero', async function () { 132 | await send(gem.approve, BOB, 0); 133 | want((await gem.allowance(ALI, BOB)).toNumber()).to.equal(0); 134 | await send(gem.approve, BOB, 1); 135 | want((await gem.allowance(ALI, BOB)).toNumber()).to.equal(1); 136 | }); 137 | }); 138 | }); 139 | 140 | describe(' gas cost', () => { 141 | async function check(gas, minGas, maxGas) { 142 | await want(gas.toNumber()).to.be.at.most(maxGas); 143 | if (gas.toNumber() < minGas) { 144 | console.log("gas reduction: previous min=", minGas, " gas used=", gas.toNumber()); 145 | } 146 | } 147 | 148 | it('decimals', async () => { // checking constant vs immutable -- no difference 149 | const gas = await gem.estimateGas.decimals(); 150 | await check(gas, bounds.decimals[0], bounds.decimals[1]) 151 | }); 152 | 153 | const NOP = async () => { 154 | } 155 | { 156 | const fill = async (prev, next) => { 157 | return send(gem.mint, ALI, next - prev) 158 | } 159 | const clear = async (prev, next) => { 160 | await send(gem.burn, ALI, prev - next) 161 | } 162 | const stay = async (prev) => { 163 | return fill(prev, prev) 164 | } 165 | test1D('mint', NOP, fill, clear, stay, 1, 2, bounds.mint) 166 | } 167 | 168 | { 169 | const fillDst = async (prev, next) => { 170 | await send(gem.mint, BOB, next - prev) 171 | } 172 | const fill = async (prev, next) => { 173 | await send(gem.mint, ALI, next - prev) 174 | } 175 | const clear = async (prev, next) => { 176 | return send(gem.transfer, BOB, prev - next) 177 | } 178 | const stay = async (prev) => { 179 | return clear(prev, prev) 180 | } 181 | test2D('transfer', NOP, fillDst, fill, clear, stay, 1, 2, bounds.transfer) 182 | } 183 | 184 | { 185 | const fillDst = async (prev, next) => { 186 | await send(gem.mint, BOB, next - prev) 187 | } 188 | const fill = async (prev, next) => { 189 | await send(gem.mint, ALI, next - prev) 190 | } 191 | const clear = (maxAllow) => async (prev, next) => { 192 | const max = constants.MaxUint256 193 | // approve is always nonzero->nonzero for now 194 | await send(gem.approve, BOB, maxAllow ? max : max.sub(1)); 195 | // tx with msg.sender == bob to account for tokens that treat allowance(a, a) == inf 196 | return send(gem.connect(bob).transferFrom, ALI, BOB, prev - next) 197 | } 198 | const stay = async (prev) => { 199 | return send(gem.connect(bob).transferFrom, ALI, BOB, 0) 200 | } 201 | test2D( 202 | 'transferFrom sub-max allowance', NOP, fillDst, 203 | fill, clear(false), stay, 204 | 1, 2, bounds.transferFrom.notMaxAllowance 205 | ) 206 | test2D( 207 | 'transferFrom max allowance', NOP, fillDst, 208 | fill, clear(true), stay, 209 | 1, 2, bounds.transferFrom.maxAllowance 210 | ) 211 | } 212 | 213 | { 214 | const fill = async (prev, next) => { 215 | await send(gem.mint, ALI, next - prev) 216 | } 217 | const clear = async (prev, next) => { 218 | return send(gem.burn, ALI, prev - next) 219 | } 220 | const stay = async (prev) => { 221 | return clear(prev, prev) 222 | } 223 | test1D('burn', NOP, fill, clear, stay, 1, 2, bounds.burn) 224 | } 225 | 226 | { 227 | const fill = async (prev, next) => { 228 | return send(gem.approve, ALI, next) 229 | } 230 | const clear = fill 231 | const stay = async (prev) => { 232 | return fill(prev, prev) 233 | } 234 | test1D('approve', NOP, fill, clear, stay, 1, 2, bounds.approve) 235 | } 236 | 237 | { 238 | const deadline = Math.floor(Date.now() / 1000) * 2; 239 | const fill = async (prev, next) => { 240 | const value = { 241 | owner: ALI, 242 | spender: BOB, 243 | value: next, 244 | nonce: await gem.nonces(ALI), 245 | deadline: deadline 246 | }; 247 | const signature = await ali._signTypedData(domain, types, value); 248 | const sig = ethers.utils.splitSignature(signature) 249 | 250 | return send(gem.permit, ALI, BOB, value.value, deadline, sig.v, sig.r, sig.s); 251 | } 252 | const clear = fill 253 | const stay = async (prev) => { 254 | return fill(prev, prev) 255 | } 256 | test1D('permit', NOP, fill, clear, stay, 1, 2, bounds.permit) 257 | } 258 | 259 | { 260 | const fill = async (prev, next) => { 261 | return send(gem.ward, BOB, next) 262 | } 263 | const clear = fill 264 | const stay = async (prev) => { 265 | return fill(prev, prev) 266 | } 267 | test1D('ward', NOP, fill, clear, stay, true, undefined, bounds.ward) 268 | } 269 | }); 270 | 271 | describe('rely/deny', () => { 272 | it('rely/deny permissions', async function () { 273 | await fail('ErrWard', gem.connect(bob).ward, ALI, false); 274 | await fail('ErrWard', gem.connect(bob).ward, BOB, false); 275 | await fail('ErrWard', gem.connect(bob).ward, ALI, true); 276 | await fail('ErrWard', gem.connect(bob).ward, BOB, true); 277 | want(await gem.wards(ALI)).to.equal(true); 278 | await send(gem.ward, BOB, false); 279 | want(await gem.wards(ALI)).to.equal(true); 280 | want(await gem.wards(BOB)).to.equal(false); 281 | await send(gem.ward, BOB, true); 282 | want(await gem.wards(ALI)).to.equal(true); 283 | want(await gem.wards(BOB)).to.equal(true); 284 | await send(gem.ward, BOB, false); 285 | want(await gem.wards(ALI)).to.equal(true); 286 | want(await gem.wards(BOB)).to.equal(false); 287 | await send(gem.ward, ALI, false); 288 | //lockout 289 | want(await gem.wards(ALI)).to.equal(false); 290 | want(await gem.wards(BOB)).to.equal(false); 291 | await fail('ErrWard', gem.ward, ALI, true); 292 | await fail('ErrWard', gem.connect(bob).ward, ALI, true); 293 | }); 294 | 295 | it('lockout example', async function () { 296 | await send(gem.mint, ALI, 1); 297 | await gem.connect(bob).ward(ALI, false).then((res) => {}, (err) => {}); 298 | await send(gem.mint, ALI, 1); 299 | }); 300 | 301 | it('burn', async function () { 302 | await send(gem.mint, ALI, 1); 303 | await fail('ErrWard', gem.connect(bob).burn, ALI, 1); 304 | await send(gem.ward, BOB, true); 305 | await send(gem.connect(bob).burn, ALI, 1); 306 | }); 307 | 308 | it('mint', async function () { 309 | await fail('ErrWard', gem.connect(bob).burn, ALI, 1); 310 | await send(gem.ward, BOB, true); 311 | await send(gem.connect(bob).mint, ALI, 1); 312 | }); 313 | 314 | it('public methods', async function () { 315 | const amt = 42; 316 | const nonce = 0; 317 | const deadline = Math.floor(Date.now() / 1000) * 2; 318 | const value = { 319 | owner: ALI, 320 | spender: BOB, 321 | value: amt, 322 | nonce: nonce, 323 | deadline: deadline 324 | }; 325 | await send(gem.mint, ALI, 100); 326 | await send(gem.transfer, BOB, 100); 327 | const gembob = gem.connect(bob); 328 | 329 | // pass with bob denied 330 | await send(gem.ward, BOB, false); 331 | await send(gembob.transfer, ALI, 1); 332 | await send(gembob.approve, ALI, 1); 333 | await send(gembob.approve, BOB, 1); 334 | await send(gembob.transferFrom, BOB, ALI, 1); 335 | let signature = await ali._signTypedData(domain, types, value); 336 | let sig = ethers.utils.splitSignature(signature) 337 | await send(gem.connect(bob).permit, ALI, BOB, amt, deadline, sig.v, sig.r, sig.s); 338 | 339 | // pass with bob relied 340 | await send(gem.ward, BOB, true); 341 | await send(gembob.transfer, ALI, 1); 342 | await send(gembob.approve, ALI, 1); 343 | await send(gembob.approve, BOB, 1); 344 | await send(gembob.transferFrom, BOB, ALI, 1); 345 | value.nonce++; 346 | signature = await ali._signTypedData(domain, types, value); 347 | sig = ethers.utils.splitSignature(signature) 348 | await send(gem.connect(bob).permit, ALI, BOB, amt, deadline, sig.v, sig.r, sig.s); 349 | }); 350 | 351 | }); 352 | 353 | it('code doesnt change bc we dont use any immutable (in-code) vars', async()=>{ 354 | const name = utils.formatBytes32String('other'); 355 | const symbol = utils.formatBytes32String('OTHER'); 356 | const gem2addr = await gemfab.callStatic.build(name, symbol) 357 | await send(gemfab.build, name, symbol) 358 | const gem2 = gem_type.attach(gem2addr) 359 | const gem_code = await ethers.provider.getCode(gem.address); 360 | const gem2_code = await ethers.provider.getCode(gem2.address); 361 | want(gem_code).eq(gem2_code); 362 | }) 363 | }) 364 | -------------------------------------------------------------------------------- /test/ERC20/ERC20.behavior.ts: -------------------------------------------------------------------------------- 1 | // modified version of openzeppelin-contracts ERC20.behavior.js 2 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/token/ERC20/ERC20.behavior.js 3 | // 4 | // The MIT License (MIT) 5 | // Copyright (c) 2016-2020 zOS Global Limited 6 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/LICENSE 7 | 8 | const { expect } = require('chai'); 9 | const expectRevert = async (f, msg) => { await expect(f).rejectedWith(msg) } 10 | const { expectEvent } = require('./helpers') 11 | import {BigNumber, constants} from 'ethers' 12 | const ZERO_ADDRESS = constants.AddressZero 13 | 14 | function shouldBehaveLikeERC20 (errorPrefix, initialSupply, _initialHolder, _recipient, _anotherAccount) { 15 | { 16 | let initialHolder, recipient, anotherAccount 17 | beforeEach(async function () { 18 | initialHolder = await _initialHolder 19 | recipient = await _recipient 20 | anotherAccount = await _anotherAccount 21 | }) 22 | 23 | describe('total supply', function () { 24 | it('returns the total amount of tokens', async function () { 25 | expect(await this.token.totalSupply()).to.eql(initialSupply); 26 | }); 27 | }); 28 | 29 | describe('balanceOf', function () { 30 | describe('when the requested account has no tokens', function () { 31 | it('returns zero', async function () { 32 | expect(await this.token.balanceOf(anotherAccount.address)).to.eql(constants.Zero); 33 | }); 34 | }); 35 | 36 | describe('when the requested account has some tokens', function () { 37 | it('returns the total amount of tokens', async function () { 38 | expect(await this.token.balanceOf(initialHolder.address)).to.eql(initialSupply); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('transfer', function () { 44 | shouldBehaveLikeERC20Transfer(errorPrefix, _initialHolder, _recipient, initialSupply, 45 | function (from, to, value) { 46 | return this.token.connect(from).transfer(to, value); 47 | }, 48 | ); 49 | }); 50 | 51 | describe('transfer from', function () { 52 | let spender; 53 | beforeEach(async function () { 54 | spender = await _recipient 55 | }) 56 | 57 | describe('when the token owner is not the zero address', function () { 58 | let tokenOwner 59 | beforeEach(async function () { 60 | tokenOwner = await _initialHolder 61 | }) 62 | 63 | describe('when the recipient is not the zero address', function () { 64 | let to 65 | beforeEach(async function () { 66 | to = await _anotherAccount 67 | }) 68 | 69 | describe('when the spender has enough approved balance', function () { 70 | beforeEach(async function () { 71 | await this.token.connect(initialHolder).approve(spender.address, initialSupply); 72 | }); 73 | 74 | describe('when the token owner has enough balance', function () { 75 | const amount = initialSupply; 76 | 77 | it('transfers the requested amount', async function () { 78 | await this.token.connect(spender).transferFrom(tokenOwner.address, to.address, amount); 79 | 80 | expect(await this.token.balanceOf(tokenOwner.address)).to.eql(constants.Zero); 81 | 82 | expect(await this.token.balanceOf(to.address)).to.eql(amount); 83 | }); 84 | 85 | it('decreases the spender allowance', async function () { 86 | await this.token.connect(spender).transferFrom(tokenOwner.address, to.address, amount); 87 | 88 | expect(await this.token.allowance(tokenOwner.address, spender.address)).to.eql(constants.Zero); 89 | }); 90 | 91 | it('emits a transfer event', async function () { 92 | const tx = await this.token.connect(spender).transferFrom(tokenOwner.address, to.address, amount); 93 | const rx = await tx.wait() 94 | 95 | expectEvent(rx, 'Transfer', { 96 | src: tokenOwner.address, 97 | dst: to.address, 98 | wad: amount, 99 | }); 100 | }); 101 | 102 | it('emits an approval event', async function () { 103 | const tx = await this.token.connect(spender).transferFrom(tokenOwner.address, to.address, amount); 104 | const rx = await tx.wait() 105 | 106 | expectEvent(rx, 'Approval', { 107 | src: tokenOwner.address, 108 | usr: spender.address, 109 | wad: await this.token.allowance(tokenOwner.address, spender.address), 110 | }); 111 | }); 112 | }); 113 | 114 | describe('when the token owner does not have enough balance', function () { 115 | const amount = initialSupply.add(1); 116 | 117 | it('reverts', async function () { 118 | await expectRevert(this.token.connect(spender).transferFrom( 119 | tokenOwner.address, to.address, amount), `ErrUnderflow`, 120 | ); 121 | }); 122 | }); 123 | }); 124 | 125 | describe('when the spender does not have enough approved balance', function () { 126 | beforeEach(async function () { 127 | await this.token.connect(tokenOwner).approve(spender.address, initialSupply.sub(1)); 128 | }); 129 | 130 | describe('when the token owner has enough balance', function () { 131 | const amount = initialSupply; 132 | 133 | it('reverts', async function () { 134 | await expectRevert(this.token.connect(spender).transferFrom( 135 | tokenOwner.address, to.address, amount), `ErrUnderflow`, 136 | ); 137 | }); 138 | }); 139 | 140 | describe('when the token owner does not have enough balance', function () { 141 | const amount = initialSupply.add(1); 142 | 143 | it('reverts', async function () { 144 | await expectRevert(this.token.connect(spender).transferFrom( 145 | tokenOwner.address, to.address, amount), `ErrUnderflow`, 146 | ); 147 | }); 148 | }); 149 | }); 150 | }); 151 | 152 | describe('when the recipient is the zero address', function () { 153 | const amount = initialSupply; 154 | const to = ZERO_ADDRESS; 155 | 156 | beforeEach(async function () { 157 | await this.token.connect(tokenOwner).approve(spender.address, amount); 158 | }); 159 | 160 | it('reverts', async function () { 161 | await expectRevert(this.token.connect(spender).transferFrom( 162 | tokenOwner.address, to, amount), `ErrZeroDst()`, 163 | ); 164 | }); 165 | }); 166 | }); 167 | 168 | /* 169 | describe('when the token owner is the zero address', function () { 170 | const amount = 0; 171 | const tokenOwner = ZERO_ADDRESS; 172 | const to = recipient; 173 | 174 | it('reverts', async function () { 175 | await expectRevert(this.token.transferFrom( 176 | tokenOwner, to, amount, { from: spender }), `${errorPrefix}: transfer from the zero address`, 177 | ); 178 | }); 179 | }); 180 | */ 181 | }); 182 | 183 | describe('approve', function () { 184 | shouldBehaveLikeERC20Approve(errorPrefix, _initialHolder, _recipient, initialSupply, 185 | function (owner, spender, amount) { 186 | return this.token.connect(owner).approve(spender.address, amount); 187 | }, 188 | ); 189 | }); 190 | } 191 | } 192 | 193 | function shouldBehaveLikeERC20Transfer (errorPrefix, _from, _to, balance, transfer) { 194 | { 195 | let from, to 196 | beforeEach(async function () { 197 | from = await _from 198 | to = await _to 199 | }) 200 | 201 | describe('when the recipient is not the zero address', function () { 202 | describe('when the sender does not have enough balance', function () { 203 | const amount = balance.add(1); 204 | 205 | it('reverts', async function () { 206 | await expectRevert(transfer.call(this, from, to.address, amount), `ErrUnderflow`, 207 | ); 208 | }); 209 | }); 210 | 211 | describe('when the sender transfers all balance', function () { 212 | const amount = balance; 213 | 214 | it('transfers the requested amount', async function () { 215 | await transfer.call(this, from, to.address, amount); 216 | 217 | expect(await this.token.balanceOf(from.address)).to.eql(constants.Zero); 218 | 219 | expect(await this.token.balanceOf(to.address)).to.eql(amount); 220 | }); 221 | 222 | it('emits a transfer event', async function () { 223 | const tx = await transfer.call(this, from, to.address, amount); 224 | const rx = await tx.wait() 225 | 226 | expectEvent(rx, 'Transfer', { 227 | src: from.address, 228 | dst: to.address, 229 | wad: amount, 230 | }); 231 | }); 232 | }); 233 | 234 | describe('when the sender transfers zero tokens', function () { 235 | const amount = constants.Zero; 236 | 237 | it('transfers the requested amount', async function () { 238 | await transfer.call(this, from, to.address, amount); 239 | 240 | expect(await this.token.balanceOf(from.address)).to.eql(balance); 241 | 242 | expect(await this.token.balanceOf(to.address)).to.eql(constants.Zero); 243 | }); 244 | 245 | it('emits a transfer event', async function () { 246 | const tx = await transfer.call(this, from, to.address, amount); 247 | const rx = await tx.wait() 248 | 249 | expectEvent(rx, 'Transfer', { 250 | src: from.address, 251 | dst: to.address, 252 | wad: amount, 253 | }); 254 | }); 255 | }); 256 | }); 257 | describe('when the recipient is the zero address', function () { 258 | it('reverts', async function () { 259 | await expectRevert(transfer.call(this, from, ZERO_ADDRESS, balance), `ErrZeroDst()`); 260 | }); 261 | }); 262 | 263 | } 264 | 265 | } 266 | 267 | function shouldBehaveLikeERC20Approve (errorPrefix, _owner, _spender, supply, approve) { 268 | { 269 | let owner, spender 270 | beforeEach(async function () { 271 | owner = await _owner 272 | spender = await _spender 273 | }) 274 | 275 | describe('when the spender is not the zero address', function () { 276 | describe('when the sender has enough balance', function () { 277 | const amount = supply; 278 | 279 | it('emits an approval event', async function () { 280 | const tx = await approve.call(this, owner, spender, amount); 281 | const rx = await tx.wait() 282 | 283 | expectEvent(rx, 'Approval', { 284 | src: owner.address, 285 | usr: spender.address, 286 | wad: amount, 287 | }); 288 | }); 289 | 290 | describe('when there was no approved amount before', function () { 291 | it('approves the requested amount', async function () { 292 | await approve.call(this, owner, spender, amount); 293 | 294 | expect(await this.token.allowance(owner.address, spender.address)).to.eql(amount); 295 | }); 296 | }); 297 | 298 | describe('when the spender had an approved amount', function () { 299 | beforeEach(async function () { 300 | await approve.call(this, owner, spender, BigNumber.from(1)); 301 | }); 302 | 303 | it('approves the requested amount and replaces the previous one', async function () { 304 | await approve.call(this, owner, spender, amount); 305 | 306 | expect(await this.token.allowance(owner.address, spender.address)).to.eql(amount); 307 | }); 308 | }); 309 | }); 310 | 311 | describe('when the sender does not have enough balance', function () { 312 | const amount = supply.add(1); 313 | 314 | it('emits an approval event', async function () { 315 | const tx = await approve.call(this, owner, spender, amount); 316 | const rx = await tx.wait() 317 | 318 | expectEvent(rx, 'Approval', { 319 | src: owner.address, 320 | usr: spender.address, 321 | wad: amount, 322 | }); 323 | }); 324 | 325 | describe('when there was no approved amount before', function () { 326 | it('approves the requested amount', async function () { 327 | await approve.call(this, owner, spender, amount); 328 | 329 | expect(await this.token.allowance(owner.address, spender.address)).to.eql(amount); 330 | }); 331 | }); 332 | 333 | describe('when the spender had an approved amount', function () { 334 | beforeEach(async function () { 335 | await approve.call(this, owner, spender, constants.One); 336 | }); 337 | 338 | it('approves the requested amount and replaces the previous one', async function () { 339 | await approve.call(this, owner, spender, amount); 340 | 341 | expect(await this.token.allowance(owner.address, spender.address)).to.eql(amount); 342 | }); 343 | }); 344 | }); 345 | }); 346 | } 347 | 348 | /* 349 | describe('when the spender is the zero address', function () { 350 | it('reverts', async function () { 351 | await expectRevert(approve.call(this, owner, ZERO_ADDRESS, supply), 352 | `${errorPrefix}: approve to the zero address`, 353 | ); 354 | }); 355 | }); 356 | */ 357 | } 358 | 359 | module.exports = { 360 | shouldBehaveLikeERC20, 361 | shouldBehaveLikeERC20Transfer, 362 | shouldBehaveLikeERC20Approve, 363 | }; 364 | -------------------------------------------------------------------------------- /test/ERC20/ERC20.test.ts: -------------------------------------------------------------------------------- 1 | // modified version of openzeppelin-contracts ERC20.test.js 2 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/token/ERC20/ERC20.test.js 3 | // 4 | // The MIT License (MIT) 5 | // Copyright (c) 2016-2020 zOS Global Limited 6 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/LICENSE 7 | 8 | import {ethers} from "hardhat"; 9 | import * as hh from "hardhat"; 10 | import {snapshot, revert, send} from 'minihat' 11 | import {Signer, constants, utils} from "ethers"; 12 | 13 | const { expectEvent } = require('./helpers') 14 | const { expect } = require('chai'); 15 | const expectRevert = async (f, msg) => { await expect(f).rejectedWith(msg) } 16 | const { BigNumber } = ethers 17 | const ZERO_ADDRESS = ethers.constants.AddressZero 18 | 19 | const { 20 | shouldBehaveLikeERC20, 21 | shouldBehaveLikeERC20Transfer, 22 | shouldBehaveLikeERC20Approve, 23 | } = require('./ERC20.behavior'); 24 | 25 | //const Gem = artifacts.require('Gem'); 26 | //const GemFab = artifacts.require('GemFab'); 27 | //const ERC20DecimalsMock = artifacts.require('ERC20DecimalsMock'); 28 | // 29 | async function decreaseAllowance (token, ali, bob, amount) { 30 | const allowance = await token.allowance(ali.address, bob.address); 31 | const tx = await token.connect(ali).approve(bob.address, allowance.sub(amount)); 32 | return tx; 33 | } 34 | 35 | async function increaseAllowance (token, ali, bob, amount) { 36 | const allowance = await token.allowance(ali.address, bob.address); 37 | const tx = await token.connect(ali).approve(bob.address, allowance.add(amount)); 38 | return tx; 39 | } 40 | 41 | let _initialHolder, _recipient, _anotherAccount : Promise; 42 | let initialHolder, recipient, anotherAccount : Signer; 43 | 44 | describe('ERC20', () => { 45 | 46 | const name = utils.formatBytes32String('Gem'); 47 | const symbol = utils.formatBytes32String('GEM'); 48 | 49 | const initialSupply = BigNumber.from(1000); 50 | 51 | let gem; 52 | let gem_type 53 | let gemfab; 54 | let gemfab_type 55 | const signers = ethers.getSigners(); 56 | _initialHolder = signers.then((s) => {return s[0]}) 57 | _recipient = signers.then((s) => {return s[1]}) 58 | _anotherAccount = signers.then((s) => {return s[2]}) 59 | before(async function () { 60 | //[initialHolder, recipient, anotherAccount] = [ali, bob, cat].map(signer => signer.address) 61 | initialHolder = await _initialHolder 62 | recipient = await _recipient 63 | anotherAccount = await _anotherAccount 64 | 65 | const ali = initialHolder 66 | gem_type = await ethers.getContractFactory('Gem', ali) 67 | gemfab_type = await ethers.getContractFactory('GemFab', ali) 68 | 69 | gemfab = await gemfab_type.deploy() 70 | const gemaddr = await gemfab.callStatic.build(name, symbol) 71 | await send(gemfab.build, name, symbol) 72 | gem = gem_type.attach(gemaddr) 73 | 74 | await snapshot(hh) 75 | }) 76 | 77 | 78 | beforeEach(async function () { 79 | await revert(hh) 80 | this.token = gem; 81 | await this.token.mint(initialHolder.address, initialSupply); 82 | }); 83 | 84 | it('has a name', async function () { 85 | expect(await this.token.name()).to.equal(name); 86 | }); 87 | 88 | it('has a symbol', async function () { 89 | expect(await this.token.symbol()).to.equal(symbol); 90 | }); 91 | 92 | it('has 18 decimals', async function () { 93 | expect(await this.token.decimals()).to.equal(18); 94 | }); 95 | 96 | /* 97 | describe('set decimals', function () { 98 | const decimals = new BN(6); 99 | 100 | it('can set decimals during construction', async function () { 101 | const token = await ERC20DecimalsMock.new(name, symbol, decimals); 102 | expect(await token.decimals()).to.be.bignumber.equal(decimals); 103 | }); 104 | }); 105 | */ 106 | 107 | shouldBehaveLikeERC20('ERC20', initialSupply, _initialHolder, _recipient, _anotherAccount); 108 | 109 | describe('decrease allowance', function () { 110 | describe('when the spender is not the zero address', function () { 111 | let spender; 112 | beforeEach(async function () { 113 | spender = await _recipient 114 | }) 115 | 116 | function shouldDecreaseApproval (amount) { 117 | /* // no decreaseAllowance contract method 118 | describe('when there was no approved amount before', function () { 119 | it('reverts', async function () { 120 | await expectRevert(decreaseAllowance(this.token, initialHolder, spender, amount), 'GEM/allowance underflow.', 121 | ); 122 | }); 123 | }); 124 | */ 125 | 126 | describe('when the spender had an approved amount', function () { 127 | const approvedAmount = amount; 128 | 129 | beforeEach(async function () { 130 | ({ logs: this.logs } = await this.token.approve(spender.address, approvedAmount)); 131 | }); 132 | 133 | it('emits an approval event', async function () { 134 | const tx = await decreaseAllowance(this.token, initialHolder, spender, approvedAmount); 135 | const rx = await tx.wait() 136 | 137 | expectEvent(rx, 'Approval', { 138 | src: initialHolder.address, 139 | usr: spender.address, 140 | wad: constants.Zero, 141 | }); 142 | }); 143 | 144 | it('decreases the spender allowance subtracting the requested amount', async function () { 145 | await decreaseAllowance(this.token, initialHolder, spender, approvedAmount.sub(1)); 146 | 147 | expect(await this.token.allowance(initialHolder.address, spender.address)).to.eql(constants.One); 148 | }); 149 | 150 | it('sets the allowance to zero when all allowance is removed', async function () { 151 | await decreaseAllowance(this.token, initialHolder, spender, approvedAmount); 152 | expect(await this.token.allowance(initialHolder.address, spender.address)).to.eql(constants.Zero); 153 | }); 154 | 155 | /* // no decreaseAllowance contract method 156 | it('reverts when more than the full allowance is removed', async function () { 157 | await expectRevert( 158 | decreaseAllowance(this.token, initialHolder, spender, approvedAmount.add(1), { from: initialHolder }), 159 | 'Reverted, check reason', 160 | ); 161 | }); 162 | */ 163 | }); 164 | } 165 | 166 | describe('when the sender has enough balance', function () { 167 | const amount = initialSupply; 168 | 169 | shouldDecreaseApproval(amount); 170 | }); 171 | 172 | describe('when the sender does not have enough balance', function () { 173 | const amount = initialSupply.add(1); 174 | 175 | shouldDecreaseApproval(amount); 176 | }); 177 | }); 178 | 179 | /* // null checks not part of spec 180 | describe('when the spender is the zero address', function () { 181 | const amount = initialSupply; 182 | const spender = ZERO_ADDRESS; 183 | 184 | it('reverts', async function () { 185 | await expectRevert(decreaseAllowance( 186 | this.token, initialHolder, spender, amount), 'Reverted, check reason', 187 | ); 188 | }); 189 | }); 190 | */ 191 | }); 192 | 193 | describe('increase allowance', function () { 194 | const amount = initialSupply; 195 | 196 | describe('when the spender is not the zero address', function () { 197 | let spender 198 | beforeEach(async function () { 199 | spender = await _recipient 200 | }) 201 | 202 | describe('when the sender has enough balance', function () { 203 | it('emits an approval event', async function () { 204 | const tx = await increaseAllowance(this.token, initialHolder, spender, amount); 205 | const rx = await tx.wait() 206 | 207 | expectEvent(rx, 'Approval', { 208 | src: initialHolder.address, 209 | usr: spender.address, 210 | wad: amount, 211 | }); 212 | }); 213 | 214 | describe('when there was no approved amount before', function () { 215 | it('approves the requested amount', async function () { 216 | await increaseAllowance(this.token, initialHolder, spender, amount); 217 | 218 | expect(await this.token.allowance(initialHolder.address, spender.address)).to.eql(amount); 219 | }); 220 | }); 221 | 222 | describe('when the spender had an approved amount', function () { 223 | beforeEach(async function () { 224 | await this.token.connect(initialHolder).approve(spender.address, constants.One); 225 | }); 226 | 227 | it('increases the spender allowance adding the requested amount', async function () { 228 | await increaseAllowance(this.token, initialHolder, spender, amount); 229 | 230 | expect(await this.token.allowance(initialHolder.address, spender.address)).to.eql(amount.add(1)); 231 | }); 232 | }); 233 | }); 234 | 235 | describe('when the sender does not have enough balance', function () { 236 | const amount = initialSupply.add(1); 237 | 238 | it('emits an approval event', async function () { 239 | const tx = await increaseAllowance(this.token, initialHolder, spender, amount); 240 | const rx = await tx.wait() 241 | 242 | expectEvent(rx, 'Approval', { 243 | src: initialHolder.address, 244 | usr: spender.address, 245 | wad: amount, 246 | }); 247 | }); 248 | 249 | describe('when there was no approved amount before', function () { 250 | it('approves the requested amount', async function () { 251 | await increaseAllowance(this.token, initialHolder, spender, amount); 252 | 253 | expect(await this.token.allowance(initialHolder.address, spender.address)).to.eql(amount); 254 | }); 255 | }); 256 | 257 | describe('when the spender had an approved amount', function () { 258 | beforeEach(async function () { 259 | await this.token.connect(initialHolder).approve(spender.address, constants.One); 260 | }); 261 | 262 | it('increases the spender allowance adding the requested amount', async function () { 263 | await increaseAllowance(this.token, initialHolder, spender, amount); 264 | 265 | expect(await this.token.allowance(initialHolder.address, spender.address)).to.eql(amount.add(1)); 266 | }); 267 | }); 268 | }); 269 | }); 270 | 271 | /* // null checks not part of spec 272 | describe('when the spender is the zero address', function () { 273 | const spender = ZERO_ADDRESS; 274 | 275 | it('reverts', async function () { 276 | await expectRevert( 277 | increaseAllowance(this.token, initialHolder, spender, amount), 'ERC20: approve to the zero address', 278 | ); 279 | }); 280 | }); 281 | */ 282 | }); 283 | 284 | describe('_mint', function () { 285 | const amount = BigNumber.from(50); 286 | it('rejects a null account', async function () { 287 | await expectRevert( 288 | this.token.mint(ZERO_ADDRESS, amount), 'ErrZeroDst()', 289 | ); 290 | }); 291 | 292 | describe('for a non zero account', function () { 293 | beforeEach('minting', async function () { 294 | const tx = await this.token.mint(recipient.address, amount); 295 | this.rx = await tx.wait(); 296 | }); 297 | 298 | it('increments totalSupply', async function () { 299 | const expectedSupply = initialSupply.add(amount); 300 | expect(await this.token.totalSupply()).to.eql(expectedSupply); 301 | }); 302 | 303 | it('increments recipient balance', async function () { 304 | expect(await this.token.balanceOf(recipient.address)).to.eql(amount); 305 | }); 306 | 307 | it('emits Transfer event', async function () { 308 | expectEvent(this.rx, 'Transfer', { 309 | src: constants.AddressZero, 310 | dst: recipient.address, 311 | wad: amount 312 | }); 313 | }); 314 | }); 315 | }); 316 | 317 | describe('_burn', function () { 318 | it('rejects a null account', async function () { 319 | // difference from OZ: underflow because nothing can be minted there either 320 | await expectRevert(this.token.burn(ZERO_ADDRESS, BigNumber.from(1)), 321 | 'ErrUnderflow()'); 322 | }); 323 | 324 | describe('for a non zero account', function () { 325 | it('rejects burning more than balance', async function () { 326 | await expectRevert(this.token.burn( 327 | initialHolder.address, initialSupply.add(1)), 'ErrUnderflow', 328 | ); 329 | }); 330 | 331 | const describeBurn = function (description, amount) { 332 | describe(description, function () { 333 | beforeEach('burning', async function () { 334 | const tx = await this.token.connect(initialHolder).burn(initialHolder.address, amount); 335 | this.rx = await tx.wait(); 336 | }); 337 | 338 | it('decrements totalSupply', async function () { 339 | const expectedSupply = initialSupply.sub(amount); 340 | expect(await this.token.totalSupply()).to.eql(expectedSupply); 341 | }); 342 | 343 | it('decrements initialHolder balance', async function () { 344 | const expectedBalance = initialSupply.sub(amount); 345 | expect(await this.token.balanceOf(initialHolder.address)).to.be.eql(expectedBalance); 346 | }); 347 | 348 | it('emits Transfer event', async function () { 349 | expectEvent(this.rx, 'Transfer', { 350 | src: initialHolder.address, 351 | dst: constants.AddressZero, 352 | wad: amount 353 | }); 354 | }); 355 | }); 356 | }; 357 | 358 | describeBurn('for entire balance', initialSupply); 359 | describeBurn('for less amount than balance', initialSupply.sub(1)); 360 | }); 361 | }); 362 | 363 | /* 364 | describe('_transfer', function () { 365 | shouldBehaveLikeERC20Transfer('ERC20', initialHolder, recipient, initialSupply, function (from, to, amount) { 366 | return this.token.transferInternal(from, to, amount, {from: initialHolder}); 367 | }); 368 | 369 | describe('when the sender is the zero address', function () { 370 | it('reverts', async function () { 371 | await expectRevert(this.token.transferInternal(ZERO_ADDRESS, recipient, initialSupply), 372 | 'ERC20: transfer from the zero address', 373 | ); 374 | }); 375 | }); 376 | }); 377 | */ 378 | 379 | /* 380 | describe('_approve', function () { 381 | shouldBehaveLikeERC20Approve('ERC20', initialHolder, recipient, initialSupply, function (owner, spender, amount) { 382 | return this.token.approveInternal(owner, spender, amount, {from: initialHolder}); 383 | }); 384 | 385 | describe('when the owner is the zero address', function () { 386 | it('reverts', async function () { 387 | await expectRevert(this.token.approveInternal(ZERO_ADDRESS, recipient, initialSupply), 388 | 'ERC20: approve from the zero address', 389 | ); 390 | }); 391 | }); 392 | }); 393 | */ 394 | }); 395 | -------------------------------------------------------------------------------- /test/ERC20/common-ERC20-issues.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as hh from 'hardhat' 3 | import { ethers } from 'hardhat' 4 | import { want, send, fail, snapshot, revert } from 'minihat' 5 | const { formatBytes32String } = ethers.utils 6 | 7 | const debug = require('debug')('gemfab:test') 8 | 9 | const types = { 10 | Permit: [ 11 | { name: 'owner', type: 'address' }, 12 | { name: 'spender', type: 'address' }, 13 | { name: 'value', type: 'uint256' }, 14 | { name: 'nonce', type: 'uint256' }, 15 | { name: 'deadline', type: 'uint256' } 16 | ] 17 | }; 18 | 19 | const domain = { 20 | name: 'GemPermit', 21 | version: '0', 22 | chainId: undefined, 23 | verifyingContract: undefined 24 | }; 25 | 26 | // investigate potential issues from awesome-buggy-erc20-tokens and weird-erc20 27 | // issues taken from: 28 | // https://github.com/sec-bit/awesome-buggy-erc20-tokens/blob/master/ERC20_token_issue_list.md 29 | // commit: 3e86725136585a33b5315ce1bd455f1062661005 30 | // https://github.com/d-xo/weird-erc20 31 | // commit: 438a180eb073fa451a8a9fc734942d6ad1874120 32 | describe('common-erc20-issues', () => { 33 | let chainId; 34 | let ali, bob, cat 35 | let ALI, BOB, CAT 36 | let gem; 37 | let gem_type 38 | let gemfab; 39 | let gemfab_type 40 | before(async () => { 41 | [ali, bob, cat] = await ethers.getSigners(); 42 | [ALI, BOB, CAT] = [ali, bob, cat].map(signer => signer.address) 43 | gem_type = await ethers.getContractFactory('Gem', ali) 44 | gemfab_type = await ethers.getContractFactory('GemFab', ali) 45 | 46 | gemfab = await gemfab_type.deploy() 47 | const name = formatBytes32String('Mock Cash'); 48 | const symbol = formatBytes32String('CASH'); 49 | const gemaddr = await gemfab.callStatic.build(name, symbol) 50 | await send(gemfab.build, name, symbol) 51 | gem = gem_type.attach(gemaddr) 52 | 53 | await snapshot(hh) 54 | 55 | chainId = await hh.network.config.chainId; 56 | domain.chainId = chainId; 57 | domain.verifyingContract = gem.address; 58 | }) 59 | beforeEach(async () => { 60 | await revert(hh) 61 | }) 62 | 63 | describe('awesome-buggy-erc20-tokens', () => { 64 | // TH = tested here 65 | // A13. approveProxy-keccak256 66 | // A14. constructor-case-insensitive 67 | // A17. setowner-anyone 68 | // A18. allowAnyone 69 | // A19. approve-with-balance-verify 70 | // A21. check-effect-inconsistency 71 | // A22. constructor-mistyping 72 | // A25. constructor-naming-error 73 | // B1. transfer-no-return 74 | // B2. approve-no-return 75 | // B3. transferFrom-no-return 76 | // AT = already tested 77 | // A2. totalSupply-overflow 78 | // A5. owner-overweight-token-by-overflow 79 | // A6. owner-decrease-balance-by-mint-overflow 80 | // A10. verify-reverse-in-transferFrom 81 | // A11. pauseTransfer-anyone 82 | // B4. no-decimals 83 | // B5. no-name 84 | // B6. no-symbol 85 | // B7. no-Approval 86 | // NA = not applicable 87 | // A1. batchTransfer-overflow 88 | // A3. verify-invalid-by-overflow 89 | // A4. owner-control-sell-price-for-overflow 90 | // A7. excess-allocation-by-overflow 91 | // A8. excess-mint-token-by-overflow 92 | // A9. excess-buy-token-by-overflow 93 | // A12. transferProxy-keccak256 94 | // A15. custom-fallback-bypass-ds-auth 95 | // A23. fake-burn 96 | // A24. getToken-anyone 97 | // C1. centralAccount-transfer-anyone 98 | // wontfix 99 | // A20. re-approve 100 | // ------------------------------------------------------------------ 101 | 102 | describe('A. List of Bugs in Implementation', () => { 103 | // A1. batchTransfer-overflow (NA) 104 | // batchTransfer() makes multiple transactions simultaneously. After passing several transferring addresses and 105 | // amounts by the caller, the function would conduct some checks then transfer tokens by modifying balances, while 106 | // overflow might occur in uint256 amount = uint256(cnt) * _value if _value is a huge number. It results in 107 | // passing the sender's balance check in require( _value > 0 && balances[msg.sender] >= amount) due to making 108 | // amount become a small value rather than cnt times of _value, then transfers out tokens exceeding 109 | // balances[msg.sender]. (CVE-2018-10299) 110 | // 111 | // Gem does not implement batch transfer 112 | 113 | // A2. totalSupply-overflow (AT) 114 | // totalSupply usually represents the sum of all tokens in the contract. The contract would add or decrease 115 | // totalSupply without any check or using SafeMath when the sum of tokens changes, making overflow possible in 116 | // totalSupply. 117 | // 118 | // test: gemfab->coverage->mint->overflow (gemfab-test.ts) 119 | 120 | // A3. verify-invalid-by-overflow (NA) 121 | // The contract checks the balance when doing operations like transferring and the hacker could bypass this check 122 | // making use of overflow by passing a great value. 123 | // 124 | // burn (NA) 125 | // balance underflow check does not use addition or subtraction 126 | // mint (NA) 127 | // no overflow check on balance (checks totalSupply instead (A2)) 128 | // transfer (NA) 129 | // balance underflow check does not use addition or subtraction 130 | 131 | // A4. owner-control-sell-price-for-overflow (NA) 132 | // Some contracts let owner control the price of transferring between ethers and tokens by users, yet owner could 133 | // maliciously set a huge sellPrice to make an overflow in computing equivalent ethers. The original number of 134 | // ethers becomes a small value, causing the user receiving insufficient ethers. (CVE-2018-11811) 135 | // 136 | // no price mechanics 137 | 138 | // A5. owner-overweight-token-by-overflow (AT) 139 | // owner could bring about an underflow to increase its holding arbitrarily by transferring tokens more than its 140 | // remaining tokens when transferring to other accounts. (CVE-2018-11687) 141 | // 142 | // by definition only applies to warded functions 143 | // mint (NA) 144 | // doesn't use subtraction 145 | // burn (AT) 146 | // gemfab->burn underflow 147 | // rely/deny (NA) 148 | // doesn't modify any uint values 149 | 150 | // A6. owner-decrease-balance-by-mint-overflow (AT) 151 | // owner with minting authority could control an account's balance at will by sending numerous tokens to the 152 | // account and leading its balance overflowing to a small figure. (CVE-2018-11812) 153 | // 154 | // test: gemfab->coverage->mint->overflow proves balance can't overflow 155 | // test checks totalSupply overflow 156 | // balance is always <= totalSupply 157 | // assume at start of call sum(balances) <= totalSupply 158 | // mint 159 | // balance' = balance + wad 160 | // totalSupply' = totalSupply + wad 161 | // balance <= totalSupply --> balance + wad <= totalSupply + wad 162 | // totalSupply ErrOverflow check 163 | // --> sum(balances') = sum(balances) + wad <= totalSupply + wad = totalSupply' 164 | // burn 165 | // balance' = balance - wad 166 | // totalSupply' = totalSupply - wad 167 | // balance <= totalSupply --> balance - wad < totalSupply - wad 168 | // balance ErrUnderflow check 169 | // --> sum(balances') = sum(balances) - wad <= totalSupply - wad = totalSupply' 170 | // transfer 171 | // balance[src]' = balance[src] - wad 172 | // balance[dst]' = balance[dst] + wad 173 | // totalSupply' = totalSupply 174 | // sum(balances') = sum(balances) - balance[src] + balance[src]' - balance[dst] + balance[dst]' 175 | // = sum(balances) - wad + wad = sum(balances) 176 | // --> sum(balances') < totalSupply' 177 | // transferFrom 178 | // same as transfer 179 | // --> sum(balances) <= totalSupply 180 | // --> balance <= totalSupply 181 | // --> balance can't overflow in mint 182 | 183 | // A7. excess-allocation-by-overflow (NA) 184 | // owner could allocate more tokens to an address via bypassing the upper bound with overflow when allocate tokens 185 | // to accounts. (CVE-2018-11810) 186 | // 187 | // mint doesn't set any upper bounds on balances to bypass, only checks totalSupply overflow (A2) 188 | 189 | // A8. excess-mint-token-by-overflow (NA) 190 | // owner can bring about an overflow and issue random amounts of tokens by passing a great value and pass the 191 | // check of max minting value. (CVE-2018-11809) 192 | // 193 | // mint doesn't set any upper bounds on balances to bypass, only checks totalSupply overflow (A2) 194 | 195 | // A9. excess-buy-token-by-overflow (NA) 196 | // If the user possesses an enormous amount of ethers when transferring to tokens, he or she could buy so many 197 | // tokens as an overflow would occur to pass TOTAL_SOLD_TOKEN_SUPPLY_LIMIT, thus gets more tokens. 198 | // (CVE-2018-11809) 199 | // 200 | // Gem has no price data, doesn't support buying/selling tokens 201 | 202 | // A10. verify-reverse-in-transferFrom (AT) 203 | // The developer wrote the opposite comparing sign when checking allowance in transferFrom(), thus there would be 204 | // an overflow or anyone could transfer out balances of any accounts. (CVE-2018-10468) 205 | // 206 | // test: ERC20->transfer from->when the token owner is not the zero address 207 | // ->when the receipient is not the zero address->when the spender does not have enough approved balance 208 | 209 | // A11. pauseTransfer-anyone (AT) 210 | // onlyFromWallet mistakingly replaced == with !=, causing anyone except walletAddress could call 211 | // enableTokenTransfer() and disableTokenTransfer(). 212 | // 213 | // test: gemfab->rely/deny 214 | 215 | // A12. transferProxy-keccak256 (NA) 216 | // Both keccak256() and ecrecover() are built-in functions. keccak256() computes the signature of public key and ecrecover recovers public key with signature. If the passed value is correct, we can verify the address by these two functions. (CVE-2018-10376) 217 | // bytes32 hash = keccak256(_from,_spender,_value,nonce,name); 218 | // if(_from != ecrecover(hash,_v,_r,_s)) revert(); 219 | // When the parameter of ecrecover() is incorrect, it would return the address of 0x0. Suppose _from passes 220 | // 0x0 address as well, the check got bypassed, meaning that anyone could transfer out the balance of 0x0 address. 221 | // 222 | // Gem does not have a transfer proxy, only approve proxy through EIP-2612 permit 223 | 224 | // A13. approveProxy-keccak256 (TH) 225 | // Both keccak256() and ecrecover() are built-in functions. keccak256() computes the signature of public key and 226 | // ecrecover recovers public key with signature. If the passed value is correct, we can verify the address by 227 | // these two functions. (CVE-2018-10376) 228 | // bytes32 hash = keccak256(_from,_spender,_value,nonce,name); 229 | // if(_from != ecrecover(hash,_v,_r,_s)) revert(); 230 | // When the parameter of ecrecover() is incorrect, it would return the address of 0x0. Suppose _from passes 0x0 231 | // address as well, the check got bypassed, meaning that anyone could get approved by 0x0 address. 232 | // 233 | it('A13. approveProxy-keccak256', async () => { 234 | // already cover the same code with repeated nonce test 235 | // however, this is a case where permit would pass if not for 236 | // signer == address(0) 237 | // note that current implementation differs from recommended implementation, 238 | // which would handle 0 address owner as its own failure case 239 | // (meaning, if(owner == address(0)) revert ErrPermitSignature;) 240 | // however, it still handles the case where ecrecover(...) == owner == 0 241 | const value = { 242 | owner: ethers.constants.AddressZero, 243 | spender: BOB, 244 | value: 1, 245 | nonce: 0, 246 | deadline: Math.floor(Date.now() / 1000) * 2 247 | }; 248 | 249 | const signature = await ali._signTypedData(domain, types, value); 250 | const sig = ethers.utils.splitSignature(signature) 251 | 252 | await fail('ErrPermitSignature', gem.permit, value.owner, value.spender, value.value, value.deadline, 253 | sig.v, sig.r, sig.s); 254 | }) 255 | 256 | // A14. constructor-case-insensitive (TH) 257 | // The developer made a mistake spelling the constructor's name, making it inconsistent with the contract's name 258 | // such that anyone could call this function. 259 | // 260 | // As of Solidity 0.4.22, constructors are named constructor. Gem uses Solidity version 0.8.10. 261 | // 262 | it('A14. constructor-case-insensitive', async () => { 263 | const constructor = gem.interface.deploy 264 | want(constructor.type).to.equal('constructor') 265 | want(constructor.payable).to.equal(true) 266 | want(constructor.inputs.length).to.equal(2) 267 | want(constructor.inputs[0].type).to.equal('bytes32') 268 | want(constructor.inputs[1].type).to.equal('bytes32') 269 | want(gem.functions.Gem).to.equal(undefined) 270 | want(gem.functions.gem).to.equal(undefined) 271 | }) 272 | 273 | // A15. custom-fallback-bypass-ds-auth (NA) 274 | // Token contract calls ERC223's Recommended branch code and ds-auth library simultaneously, thus the hacker could 275 | // make use of passing custom fallback functions in ERC223 contracts along with ds-auth approving check. When the 276 | // fallback function in ERC223 contracts gets triggered, the hacker could call the contract itself to deactivate 277 | // internal authorization control. 278 | // 279 | // Gem accepts no custom callbacks to exploit 280 | 281 | // A16. custom-call-abuse (N/A) 282 | // It is a really bad practice to allow the abuse of CUSTOM_CALL in token standard. 283 | // Attackers could call any contract in the name of vulnerable contract with CUSTOM_CALL. 284 | // This vulnerability will make these attacking scenarios possible: 285 | // Attackers could steal almost each kind of tokens belong to the vulnerable contract 286 | // Attackers could steal almost each kind of tokens approved to the vulnerable contract 287 | // Attackers could bypass the auth check in vulnerable contract by proxy of contract itself in special situation 288 | // Attackers could pass fake values as parameter to cheat with receiver contract 289 | // 290 | // Gem has no custom calls 291 | 292 | // A17. setowner-anyone (TH) 293 | // setOwner() could change owner and only the current owner may call it usually. However, the snippet below allows 294 | // anyone calling setOwner() to set contract's owner. (CVE-2018-10705) 295 | // 296 | it('A17. setowner-anyone', async () => { 297 | await fail('ErrWard', gem.connect(bob).ward, BOB, true); 298 | await send(gem.ward, BOB, true); 299 | }) 300 | 301 | // A18. allowAnyone (TH) 302 | // Description transferFrom() missed a check on allowed, then anyone could transfer balances from any accounts. A 303 | // hacker could make use of it to grab others' tokens. In the mean time, if the transferred sum surpasses allowed, 304 | // allowed[_from][msg.sender] -= _value; would lead to an underflow. 305 | // 306 | // same as A10 (spender does not have enough approved balance) 307 | 308 | // A19. approve-with-balance-verify (TH) 309 | // Several Token contracts add balance check in standard approve() requiring _amount not greater than the current 310 | // balance. 311 | // In one way, this check cannot assure that the approved account would transfer out tokens of this amount: 312 | // The token holder transfers out tokens after approval, making the balance smaller than allowance. 313 | // After approving multiple users, one of them calls transferFrom() and the balance could be smaller than the approved value. 314 | // On the another way, this check might prevent external contracts(e.g. decentralized exchanges based on 0x protocol) from normal calling, before the Token developing team transferring a tremendous amount of tokens to the intermediate account. 315 | // 316 | it('A19. approve-with-balance-verify', async () => { 317 | // nothing minted 318 | await send(gem.approve, BOB, 1); 319 | }) 320 | 321 | // A20. re-approve wontfix 322 | // approve() allows the spender account using a given number of tokens by updating the value of allowance. 323 | // Suppose the spender account is able to control miners' confirming order of transferring, then spender could use 324 | // up all allowance before approve comes into effect. After approve() is effective, spender has access to the new 325 | // allowance, causing total tokens spent greater than expected and resulting in Re-approve attack. 326 | // This attack is only possible when the spender has approval, the approved account changes the approved amount, 327 | // the balance is sufficient and the spender could control confirming order of transferring. 328 | // It would only cause the spender using more tokens than expected or the approved tokens less than expectation, 329 | // not affecting the account balance and sum of tokens. 330 | // 331 | // Applies to most ERC20 tokens, it's app dev/user's problem. wontfix. 332 | 333 | // A21. check-effect-inconsistency (TH) 334 | // The condition verification and the variable modification logic is inconsistent, which fails the verification 335 | // and could further leads to other vulnerabilities like integer underflow. For example, the contract checks the 336 | // balance of A but updates the balance of B. 337 | // 338 | describe('A21. check-effect-inconsistency', () => { 339 | async function checkUnchanged() { 340 | want((await gem.allowance(ALI, ALI)).toNumber()).to.be.equal(100); 341 | want((await gem.allowance(ALI, BOB)).toNumber()).to.be.equal(42); 342 | want((await gem.allowance(BOB, ALI)).toNumber()).to.be.equal(0); 343 | want((await gem.allowance(BOB, BOB)).toNumber()).to.be.equal(0); 344 | want((await gem.allowance(BOB, CAT)).toNumber()).to.be.equal(0); 345 | want((await gem.allowance(CAT, ALI)).toNumber()).to.be.equal(0); 346 | want((await gem.allowance(CAT, BOB)).toNumber()).to.be.equal(0); 347 | want((await gem.allowance(CAT, CAT)).toNumber()).to.be.equal(0); 348 | want((await gem.balanceOf(CAT)).toNumber()).to.be.equal(0); 349 | } 350 | 351 | beforeEach(async () => { 352 | await send(gem.mint, ALI, 1); 353 | await send(gem.approve, ALI, 100); 354 | await send(gem.approve, BOB, 42); 355 | await send(gem.approve, CAT, 1); 356 | await checkUnchanged(); 357 | want((await gem.allowance(ALI, CAT)).toNumber()).to.be.equal(1); 358 | want((await gem.balanceOf(ALI)).toNumber()).to.be.equal(1); 359 | want((await gem.balanceOf(BOB)).toNumber()).to.be.equal(0); 360 | }) 361 | 362 | it('transferFrom', async () => { 363 | await send(gem.connect(cat).transferFrom, ALI, BOB, 1); 364 | await checkUnchanged(); 365 | want((await gem.allowance(ALI, CAT)).toNumber()).to.be.equal(0); 366 | want((await gem.balanceOf(ALI)).toNumber()).to.be.equal(0); 367 | want((await gem.balanceOf(BOB)).toNumber()).to.be.equal(1); 368 | }) 369 | it('transfer', async () => { 370 | await send(gem.transfer, BOB, 1) 371 | await checkUnchanged(); 372 | want((await gem.allowance(ALI, CAT)).toNumber()).to.be.equal(1); 373 | want((await gem.balanceOf(ALI)).toNumber()).to.be.equal(0); 374 | want((await gem.balanceOf(BOB)).toNumber()).to.be.equal(1); 375 | }) 376 | }) 377 | 378 | // A22. constructor-mistyping (TH) 379 | // Description When declaring function constructors, one should write code like constructor(). However, some 380 | // mistyped this declaration, using function constructor(), thus the Solidity compiler would view it as an average 381 | // public function that anyone could access, not a constructor called just once when deploying. 382 | // 383 | // gem has its own defined constructor (as shown in A14) 384 | // 385 | it('A22. constructor-mistyping', async () => { 386 | // if gem uses a public `function constructor`, it should be part of its interface 387 | let constructor = gem.interface.functions["constructor(string,string)"] 388 | want(constructor).to.equal(undefined) 389 | constructor = gem.interface.functions["constructor()"] 390 | want(constructor).to.equal(undefined) 391 | }) 392 | 393 | // A23. fake-burn (NA) 394 | // Some token contracts have integer overflow bugs (CVE-2018-13151 fake-burn). The power method in burning might 395 | // lead to an integer overflow by passing specific parameters, resulting in burning 0 token, not the intended 396 | // value. 397 | // 398 | // burn does not use power, burn takes the exact amount to burn (wad) as an argument 399 | 400 | // A24. getToken-anyone (NA) 401 | // Function getToken() is used to add value to caller's token balance. The amount value is defined by caller as 402 | // input argument. This function works as mint token but allows anyone to call it. As a result, anyone could add 403 | // arbitrary amount of token to one's own balance by calling getToken(). This is very ridiculous to ERC20 token 404 | // contract. 405 | // 406 | // Gem does not have a getToken function 407 | 408 | // A25. constructor-naming-error (TH) 409 | // When declaring the constructor, neither did the developer used constructor() or the function with the contract 410 | // name. Instead, the developer mistakingly declared it with another method name, causing that anyone has access 411 | // to it. 412 | // 413 | // Gem's constructor is correctly named (shown in A14) 414 | }) 415 | 416 | describe('B. List of Incompatibilities', () => { 417 | // B1. transfer-no-return (TH) 418 | // transfer() should return a bool value according to ERC20, while it is left out in many deployed Token 419 | // contracts, not following EIP20. Suppose an external contract following EIP20 uses an ABI interface(with a 420 | // return value) to call transfer() without a return value, the Solidity compiler would not throw an exception 421 | // in versions before 0.4.22. However, transfer() calls would revert after the compiler is upgraded to 0.4.22 422 | // version. 423 | // 424 | it('B1. transfer-no-return', async () => { 425 | const ok = await gem.callStatic.transfer(BOB, 0); 426 | want(ok).to.be.equal(true); 427 | const outputs = gem.interface.functions["transfer(address,uint256)"].outputs; 428 | want(outputs.length).to.equal(1); 429 | want(outputs[0].type).to.equal('bool'); 430 | }) 431 | 432 | // B2. approve-no-return (TH) 433 | // approve() should return a bool value according to ERC20, while it is left out in many deployed Token 434 | // contracts, not following EIP20. Suppose an external contract following EIP20 uses an ABI interface(with a 435 | // return value) to call approve() without a return value, the Solidity compiler would not throw an exception in 436 | // versions before 0.4.22. However, approve() calls would revert after the compiler is upgraded to 0.4.22 437 | // version. 438 | // 439 | it('B2. approve-no-return', async () => { 440 | const ok = await gem.callStatic.approve(BOB, 0); 441 | want(ok).to.be.equal(true); 442 | const outputs = gem.interface.functions["approve(address,uint256)"].outputs; 443 | want(outputs.length).to.equal(1); 444 | want(outputs[0].type).to.equal('bool'); 445 | }) 446 | 447 | // B3. transferFrom-no-return (TH) 448 | // transferFrom() should return a bool value according to ERC20, while it is left out in many deployed Token 449 | // contracts, not following EIP20. Suppose an external contract following EIP20 uses an ABI interface(with a 450 | // return value) to call transferFrom() without a return value, the Solidity compiler would not throw an 451 | // exception in versions before 0.4.22. However, transferFrom() calls would revert after the compiler is 452 | // upgraded to 0.4.22 version. 453 | // 454 | it('B3. transferFrom-no-return', async () => { 455 | const ok = await gem.callStatic.transferFrom(ALI, BOB, 0); 456 | want(ok).to.be.equal(true); 457 | const outputs = gem.interface.functions["transferFrom(address,address,uint256)"].outputs; 458 | want(outputs.length).to.equal(1); 459 | want(outputs[0].type).to.equal('bool'); 460 | }) 461 | 462 | // B4. no-decimals (AT) 463 | // Usually a token contract employs decimals to represent digits after the token's decimal point, while some of 464 | // them does not define this variable properly, e.g. a case-insensitive decimals, making them incompatible with 465 | // external contract calls. 466 | // 467 | // test: ERC20->has 18 decimals (ERC20.test.js) 468 | 469 | // B5. no-name (AT) 470 | // Usually a token contract employs name as a token name, while some of them does not define this variable 471 | // properly, e.g. a case-insensitive name, making them incompatible with external contract calls. 472 | // 473 | // test: ERC20->has a name (ERC20.test.js) 474 | 475 | // B6. no-symbol (AT) 476 | // Usually a token contract employs symbol as a token alias, while some of them does not define this variable 477 | // properly, e.g. a case-insensitive symbol, making them incompatible with external contract calls. 478 | // 479 | // test: ERC20->no-symbol (ERC20.test.js) 480 | 481 | // B7. no-Approval (AT) 482 | // Two events - Transfer and Approval should get fired under certain circumstances as described by ERC20 483 | // specification. However, many Token contracts missed Approval event triggering, referred to an implementation 484 | // on official Ethereum website (which has been fixed). 485 | // 486 | // test: ERC20->approve->when the spender is not the zero address 487 | // ->when the sender has enough balance->emits an approval event (ERC20.behavior.js) 488 | // test: ERC20->approve->when the spender is not the zero address 489 | // ->when the sender does not have enough balance->emits an approval event (ERC20.behavior.js) 490 | }) 491 | 492 | // C. List of Excessive Authorities 493 | // C1. centralAccount-transfer-anyone N/A 494 | // onlycentralAccount could transfer out other account's balances randomly. (CVE-2018-1000203) 495 | // 496 | // wards can mint and burn but not transfer out 497 | }) 498 | describe('weird-erc20', async () => { 499 | // TH = tested here 500 | // Approval Race Protections 501 | // Revert on Approval to Zero Address 502 | // Revert on Zero Value Transfers 503 | // transferFrom with src == msg.sender 504 | // Non string metadata 505 | // Revert on Transfer to the Zero Address 506 | // Revert on Large Approvals & Transfers 507 | // AT = already tested 508 | // Missing Return Values 509 | // Low Decimals 510 | // High Decimals 511 | // No Revert on Failure 512 | // NA = not applicable 513 | // Reentrant calls 514 | // Fee on Transfer 515 | // Upgradable Tokens 516 | // Flash Mintable Tokens 517 | // Pausable Tokens 518 | // Multiple Token Addresses 519 | // Code Injection Via Token Name 520 | // Tokens with Blocklists 521 | // wontfix 522 | // Balance Modifications Outside of Transfers (rebasing / airdrops) 523 | // ------------------------------------------------------------------ 524 | 525 | // Reentrant calls (NA) 526 | // Some tokens allow reentract calls on transfer (e.g. ERC777 tokens). 527 | // This has been exploited in the wild on multiple occasions (e.g. imBTC uniswap pool drained, lendf.me drained) 528 | // 529 | // Gem has no external contract calls 530 | 531 | // Missing Return Values (AT) 532 | // Some tokens do not return a bool (e.g. USDT, BNB, OMG) on ERC20 methods. see here for a comprehensive (if 533 | // somewhat outdated) list. 534 | // Some tokens (e.g. BNB) may return a bool for some methods, but fail to do so for others. This resulted in stuck 535 | // BNB tokens in Uniswap v1 (details). 536 | // Some particulary pathological tokens (e.g. Tether Gold) declare a bool return, but then return false even when 537 | // the transfer was successful (code). 538 | // 539 | // see awesome-erc20 B1-B3, approval transfer and transferFrom return true or revert 540 | 541 | // Fee on Transfer (NA) 542 | // Some tokens take a transfer fee (e.g. STA, PAXG), some do not currently charge a fee but may do so in the 543 | // future (e.g. USDT, USDC). 544 | // 545 | // Gem has no fees 546 | 547 | // Balance Modifications Outside of Transfers (rebasing / airdrops) (wontfix) 548 | // Some tokens may make arbitrary balance modifications outside of transfers (e.g. Ampleforth style rebasing 549 | // tokens, Compound style airdrops of governance tokens, mintable / burnable tokens). 550 | // Some smart contract systems cache token balances (e.g. Balancer, Uniswap-V2), and arbitrary modifications to 551 | // underlying balances can mean that the contract is operating with outdated information. 552 | // 553 | // Need to make sure gem controller can't burn balances for some kinds of apps 554 | 555 | // Upgradable Tokens (NA) 556 | // Some tokens (e.g. USDC, USDT) are upgradable, allowing the token owners to make arbitrary modifications to the 557 | // logic of the token at any point in time. 558 | // A change to the token semantics can break any smart contract that depends on the past behaviour. 559 | // 560 | // Gem is not upgradable 561 | 562 | // Flash Mintable Tokens (NA) 563 | // Some tokens (e.g. DAI) allow for so called "flash minting", which allows tokens to be minted for the duration 564 | // of one transaction only, provided they are returned to the token contract by the end of the transaction. 565 | // This is similar to a flash loan, but does not require the tokens that are to be lent to exist before the start 566 | // of the transaction. A token that can be flash minted could potentially have a total supply of max uint256. 567 | // 568 | // Gem has no flash mint 569 | 570 | // Tokens with Blocklists (NA) 571 | // Some tokens (e.g. USDC, USDT) have a contract level admin controlled address blocklist. If an address is 572 | // blocked, then transfers to and from that address are forbidden. 573 | // Malicious or compromised token owners can trap funds in a contract by adding the contract address to the 574 | // blocklist. This could potentially be the result of regulatory action against the contract itself, against a 575 | // single user of the contract (e.g. a Uniswap LP), or could also be a part of an extortion attempt against users 576 | // of the blocked contract. 577 | // 578 | // Gem has no blocklist 579 | 580 | // Pausable Tokens (NA) 581 | // Some tokens can be paused by an admin (e.g. BNB, ZIL). 582 | // Similary to the blocklist issue above, an admin controlled pause feature opens users of the token to risk from 583 | // a malicious or compromised token owner. 584 | // 585 | // Gem is not not pausable 586 | 587 | // Approval Race Protections (TH) 588 | // Some tokens (e.g. USDT, KNC) do not allow approving an amount M > 0 when an existing amount N > 0 is already 589 | // approved. This is to protect from an ERC20 attack vector described here. 590 | // 591 | // test: awesome-erc20 A19 592 | 593 | // Revert on Approval to Zero Address (TH) 594 | // Some tokens (e.g. OpenZeppelin) will revert if trying to approve the zero address to spend tokens (i.e. a call 595 | // to approve(address(0), amt)). 596 | // 597 | it('don\'t revert on Approval to Zero Address', async () => { 598 | await send(gem.approve, ethers.constants.AddressZero, 1); 599 | }) 600 | 601 | // Revert on Zero Value Transfers (TH) 602 | // Some tokens (e.g. LEND) revert when transfering a zero value amount. 603 | // 604 | it('don\'t revert on Zero Value Transfers', async () => { 605 | await send(gem.transfer, BOB, 0); 606 | }) 607 | 608 | // Multiple Token Addresses (NA) 609 | // Some proxied tokens have multiple addresses 610 | // calling transfer on either affects your balance on both 611 | // 612 | // Gem has one address 613 | 614 | // Low Decimals (AT) 615 | // Some tokens have low decimals (e.g. USDC has 6). Even more extreme, some tokens like Gemini USD only have 616 | // 2 decimals. 617 | // This may result in larger than expected precision loss. 618 | // 619 | // test: ERC20->has 18 decimals (ERC20.test.js) 620 | 621 | // High Decimals (AT) 622 | // Some tokens have more than 18 decimals (e.g. YAM-V2 has 24). 623 | // This may result in larger than expected precision loss. 624 | // 625 | // test: ERC20->has 18 decimals (ERC20.test.js) 626 | 627 | // transferFrom with src == msg.sender (TH) 628 | // Some token implementations (e.g. DSToken) will not attempt to decrease the caller's allowance if the sender is 629 | // the same as the caller. This gives transferFrom the same semantics as transfer in this case. Other 630 | // implementations (e.g. OpenZeppelin, Uniswap-v2) will attempt to decrease the caller's allowance from the sender 631 | // in transferFrom even if the caller and the sender are the same address, giving transfer(dst, amt) and 632 | // transferFrom(address(this), dst, amt) a different semantics in this case. 633 | // 634 | it('transferFrom with src == msg.sender', async () => { 635 | await send(gem.mint, ALI, 1); 636 | await send(gem.approve, ALI, 1); 637 | want((await gem.balanceOf(ALI)).toNumber()).to.equal(1); 638 | want((await gem.balanceOf(BOB)).toNumber()).to.equal(0); 639 | want((await gem.allowance(ALI, ALI)).toNumber()).to.equal(1); 640 | await send(gem.transferFrom, ALI, BOB, 1); 641 | want((await gem.balanceOf(ALI)).toNumber()).to.equal(0); 642 | want((await gem.balanceOf(BOB)).toNumber()).to.equal(1); 643 | want((await gem.allowance(ALI, ALI)).toNumber()).to.equal(0); 644 | }) 645 | 646 | // No Revert on Failure (AT) 647 | // Some tokens do not revert on failure, but instead return false (e.g. ZRX). 648 | // While this is technicaly compliant with the ERC20 standard, it goes against common solidity coding practices 649 | // and may be overlooked by developers who forget to wrap their calls to transfer in a require. 650 | // 651 | // OZ ERC20 tests use expectRevert rather than testing for false (ERC20.test.js, ERC20.behavior.js) 652 | 653 | // Revert on Transfer to the Zero Address (TH) 654 | // Some tokens (e.g. openzeppelin) revert when attempting to transfer to address(0). 655 | // This may break systems that expect to be able to burn tokens by transfering them to address(0). 656 | // We agree with OZ 657 | 658 | // Revert on Large Approvals & Transfers (TH) 659 | // Some tokens (e.g. UNI, COMP) revert if the value passed to approve or transfer is larger than uint96. 660 | // Both of the above tokens have special case logic in approve that sets allowance to type(uint96).max if the 661 | // approval amount is uint256(-1), which may cause issues with systems that expect the value passed to approve 662 | // to be reflected in the allowances mapping. 663 | // 664 | it('doesn\'t revert on Large Approvals and Transfers', async () => { 665 | await send(gem.mint, ALI, ethers.constants.MaxUint256) 666 | await send(gem.approve, BOB, ethers.constants.MaxUint256) 667 | await send(gem.connect(bob).transferFrom, ALI, BOB, ethers.constants.MaxUint256) 668 | want(await gem.allowance(ALI, BOB)).to.eql(ethers.constants.MaxUint256) 669 | want(await gem.balanceOf(ALI)).to.eql(ethers.constants.Zero) 670 | want(await gem.balanceOf(BOB)).to.eql(ethers.constants.MaxUint256) 671 | await send(gem.connect(bob).transfer, ALI, ethers.constants.MaxUint256) 672 | want(await gem.balanceOf(ALI)).to.eql(ethers.constants.MaxUint256) 673 | want(await gem.balanceOf(BOB)).to.eql(ethers.constants.Zero) 674 | }) 675 | 676 | // Code Injection Via Token Name (NA) 677 | // Some malicious tokens have been observed to include malicious javascript in their name attribute, allowing 678 | // attackers to extract private keys from users who choose to interact with these tokens via vulnerable frontends. 679 | // 680 | // Gem will have a normal name 681 | }) 682 | }) 683 | --------------------------------------------------------------------------------