├── .github └── workflows │ └── quality.yml ├── .gitignore ├── .gitmodules ├── .prettierrc ├── README.md ├── foundry.toml ├── lefthook.yml ├── package-lock.json ├── package.json └── src ├── test ├── ERC721.t.sol └── utils │ ├── DSInvariantTest.sol │ ├── DSTestPlus.sol │ ├── Hevm.sol │ ├── mocks │ └── MockERC721.sol │ └── users │ └── ERC721User.sol └── tokens └── ERC721L.sol /.github/workflows/quality.yml: -------------------------------------------------------------------------------- 1 | name: Quality checks 2 | 3 | on: [push] 4 | 5 | jobs: 6 | quality: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - uses: actions/setup-node@v2 12 | with: 13 | check-latest: true 14 | 15 | - name: Cache node modules 16 | uses: actions/cache@v2 17 | env: 18 | cache-name: cache-node-modules 19 | with: 20 | path: ~/.npm 21 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 22 | restore-keys: | 23 | ${{ runner.os }}-build-${{ env.cache-name }}- 24 | ${{ runner.os }}-build- 25 | ${{ runner.os }}- 26 | 27 | - name: Install dependencies 28 | run: npm ci 29 | 30 | - name: Prettier step 31 | run: npm run prettier 32 | 33 | check: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v2 37 | with: 38 | submodules: recursive 39 | 40 | - name: Install Foundry 41 | uses: onbjerg/foundry-toolchain@v1 42 | with: 43 | version: nightly 44 | 45 | - name: Run tests 46 | run: forge test -vvv 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cache/ 2 | out/ 3 | node_modules 4 | remappings.txt 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/ds-test"] 2 | path = lib/ds-test 3 | url = https://github.com/dapphub/ds-test 4 | [submodule "lib/solmate"] 5 | path = lib/solmate 6 | url = https://github.com/Rari-Capital/solmate 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "printWidth": 100, 4 | 5 | "overrides": [ 6 | { 7 | "files": "*.sol", 8 | "options": { 9 | "tabWidth": 4, 10 | "printWidth": 120, 11 | "bracketSpacing": true 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # innovation-contracts-solidity 2 | 3 | Collection of smart contracts written in Solidity 4 | 5 | This repository contains smart-contract used by Ledger. For example we might add a ERC721 ugradeable based on solmate that would be used by other project. 6 | 7 | ## Install 8 | 9 | To get all npm packages 10 | 11 | npm install 12 | 13 | To get all git modules dependencies 14 | 15 | forge update 16 | 17 | In case you have VSCode path issue. The file is in .gitignore because path are absolute 18 | 19 | forge remappings > remappings.txt 20 | 21 | ## Git hooks 22 | 23 | We use [lefthook](https://github.com/evilmartians/lefthook) to manage our git hooks. For the moment, the following hooks are configured: 24 | - Run `npm run prettier` before pushing to remote 25 | - Run `forge test` before pushing to remote 26 | 27 | ### How to install 28 | 29 | You only need to do this step once. Run `npx @arkweid/lefthook install` in the root of the repository to install the git hooks described in the `lefthook.yml` file. 30 | 31 | ## How to run manually 32 | 33 | You can manually run the installed git hooks by running `npx @arkweid/lefthook run pre-push` or by running the alias defined in the `package.json` file (`npm run lefthook`). 34 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | remappings = ['ds-test/=lib/ds-test/src/', 'solmate/=lib/solmate/src/'] 6 | gas_reports = ['MockERC721'] 7 | 8 | # See more config options https://github.com/gakonst/foundry/tree/master/config -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | # Automatically run quality checks before pushing code 2 | pre-push: 3 | parallel: true 4 | commands: 5 | linter: 6 | run: npm run prettier 7 | tests: 8 | run: forge test 9 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "innovation-contracts-solidity", 3 | "version": "0.1.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "innovation-contracts-solidity", 9 | "version": "0.1.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "prettier": "^2.6.0", 13 | "prettier-plugin-solidity": "^1.0.0-beta.19" 14 | } 15 | }, 16 | "node_modules/@solidity-parser/parser": { 17 | "version": "0.14.1", 18 | "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.1.tgz", 19 | "integrity": "sha512-eLjj2L6AuQjBB6s/ibwCAc0DwrR5Ge+ys+wgWo+bviU7fV2nTMQhU63CGaDKXg9iTmMxwhkyoggdIR7ZGRfMgw==", 20 | "dev": true, 21 | "dependencies": { 22 | "antlr4ts": "^0.5.0-alpha.4" 23 | } 24 | }, 25 | "node_modules/ansi-regex": { 26 | "version": "5.0.1", 27 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 28 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 29 | "dev": true, 30 | "engines": { 31 | "node": ">=8" 32 | } 33 | }, 34 | "node_modules/antlr4ts": { 35 | "version": "0.5.0-alpha.4", 36 | "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", 37 | "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", 38 | "dev": true 39 | }, 40 | "node_modules/emoji-regex": { 41 | "version": "10.0.1", 42 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.0.1.tgz", 43 | "integrity": "sha512-cLuZzriSWVE+rllzeq4zpUEoCPfYEbQ6ZVhIq+ed6ynWST7Bw9XnOr+bKWgCup4paq72DI21gw9M3aaFkm4HAw==", 44 | "dev": true 45 | }, 46 | "node_modules/escape-string-regexp": { 47 | "version": "4.0.0", 48 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 49 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 50 | "dev": true, 51 | "engines": { 52 | "node": ">=10" 53 | }, 54 | "funding": { 55 | "url": "https://github.com/sponsors/sindresorhus" 56 | } 57 | }, 58 | "node_modules/is-fullwidth-code-point": { 59 | "version": "3.0.0", 60 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 61 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 62 | "dev": true, 63 | "engines": { 64 | "node": ">=8" 65 | } 66 | }, 67 | "node_modules/lru-cache": { 68 | "version": "6.0.0", 69 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 70 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 71 | "dev": true, 72 | "dependencies": { 73 | "yallist": "^4.0.0" 74 | }, 75 | "engines": { 76 | "node": ">=10" 77 | } 78 | }, 79 | "node_modules/prettier": { 80 | "version": "2.6.0", 81 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.0.tgz", 82 | "integrity": "sha512-m2FgJibYrBGGgQXNzfd0PuDGShJgRavjUoRCw1mZERIWVSXF0iLzLm+aOqTAbLnC3n6JzUhAA8uZnFVghHJ86A==", 83 | "dev": true, 84 | "bin": { 85 | "prettier": "bin-prettier.js" 86 | }, 87 | "engines": { 88 | "node": ">=10.13.0" 89 | }, 90 | "funding": { 91 | "url": "https://github.com/prettier/prettier?sponsor=1" 92 | } 93 | }, 94 | "node_modules/prettier-plugin-solidity": { 95 | "version": "1.0.0-beta.19", 96 | "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.19.tgz", 97 | "integrity": "sha512-xxRQ5ZiiZyUoMFLE9h7HnUDXI/daf1tnmL1msEdcKmyh7ZGQ4YklkYLC71bfBpYU2WruTb5/SFLUaEb3RApg5g==", 98 | "dev": true, 99 | "dependencies": { 100 | "@solidity-parser/parser": "^0.14.0", 101 | "emoji-regex": "^10.0.0", 102 | "escape-string-regexp": "^4.0.0", 103 | "semver": "^7.3.5", 104 | "solidity-comments-extractor": "^0.0.7", 105 | "string-width": "^4.2.3" 106 | }, 107 | "engines": { 108 | "node": ">=12" 109 | }, 110 | "peerDependencies": { 111 | "prettier": "^2.3.0" 112 | } 113 | }, 114 | "node_modules/semver": { 115 | "version": "7.3.5", 116 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", 117 | "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", 118 | "dev": true, 119 | "dependencies": { 120 | "lru-cache": "^6.0.0" 121 | }, 122 | "bin": { 123 | "semver": "bin/semver.js" 124 | }, 125 | "engines": { 126 | "node": ">=10" 127 | } 128 | }, 129 | "node_modules/solidity-comments-extractor": { 130 | "version": "0.0.7", 131 | "resolved": "https://registry.npmjs.org/solidity-comments-extractor/-/solidity-comments-extractor-0.0.7.tgz", 132 | "integrity": "sha512-wciNMLg/Irp8OKGrh3S2tfvZiZ0NEyILfcRCXCD4mp7SgK/i9gzLfhY2hY7VMCQJ3kH9UB9BzNdibIVMchzyYw==", 133 | "dev": true 134 | }, 135 | "node_modules/string-width": { 136 | "version": "4.2.3", 137 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 138 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 139 | "dev": true, 140 | "dependencies": { 141 | "emoji-regex": "^8.0.0", 142 | "is-fullwidth-code-point": "^3.0.0", 143 | "strip-ansi": "^6.0.1" 144 | }, 145 | "engines": { 146 | "node": ">=8" 147 | } 148 | }, 149 | "node_modules/string-width/node_modules/emoji-regex": { 150 | "version": "8.0.0", 151 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 152 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 153 | "dev": true 154 | }, 155 | "node_modules/strip-ansi": { 156 | "version": "6.0.1", 157 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 158 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 159 | "dev": true, 160 | "dependencies": { 161 | "ansi-regex": "^5.0.1" 162 | }, 163 | "engines": { 164 | "node": ">=8" 165 | } 166 | }, 167 | "node_modules/yallist": { 168 | "version": "4.0.0", 169 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 170 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 171 | "dev": true 172 | } 173 | }, 174 | "dependencies": { 175 | "@solidity-parser/parser": { 176 | "version": "0.14.1", 177 | "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.1.tgz", 178 | "integrity": "sha512-eLjj2L6AuQjBB6s/ibwCAc0DwrR5Ge+ys+wgWo+bviU7fV2nTMQhU63CGaDKXg9iTmMxwhkyoggdIR7ZGRfMgw==", 179 | "dev": true, 180 | "requires": { 181 | "antlr4ts": "^0.5.0-alpha.4" 182 | } 183 | }, 184 | "ansi-regex": { 185 | "version": "5.0.1", 186 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 187 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 188 | "dev": true 189 | }, 190 | "antlr4ts": { 191 | "version": "0.5.0-alpha.4", 192 | "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", 193 | "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", 194 | "dev": true 195 | }, 196 | "emoji-regex": { 197 | "version": "10.0.1", 198 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.0.1.tgz", 199 | "integrity": "sha512-cLuZzriSWVE+rllzeq4zpUEoCPfYEbQ6ZVhIq+ed6ynWST7Bw9XnOr+bKWgCup4paq72DI21gw9M3aaFkm4HAw==", 200 | "dev": true 201 | }, 202 | "escape-string-regexp": { 203 | "version": "4.0.0", 204 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 205 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 206 | "dev": true 207 | }, 208 | "is-fullwidth-code-point": { 209 | "version": "3.0.0", 210 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 211 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 212 | "dev": true 213 | }, 214 | "lru-cache": { 215 | "version": "6.0.0", 216 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 217 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 218 | "dev": true, 219 | "requires": { 220 | "yallist": "^4.0.0" 221 | } 222 | }, 223 | "prettier": { 224 | "version": "2.6.0", 225 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.0.tgz", 226 | "integrity": "sha512-m2FgJibYrBGGgQXNzfd0PuDGShJgRavjUoRCw1mZERIWVSXF0iLzLm+aOqTAbLnC3n6JzUhAA8uZnFVghHJ86A==", 227 | "dev": true 228 | }, 229 | "prettier-plugin-solidity": { 230 | "version": "1.0.0-beta.19", 231 | "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.19.tgz", 232 | "integrity": "sha512-xxRQ5ZiiZyUoMFLE9h7HnUDXI/daf1tnmL1msEdcKmyh7ZGQ4YklkYLC71bfBpYU2WruTb5/SFLUaEb3RApg5g==", 233 | "dev": true, 234 | "requires": { 235 | "@solidity-parser/parser": "^0.14.0", 236 | "emoji-regex": "^10.0.0", 237 | "escape-string-regexp": "^4.0.0", 238 | "semver": "^7.3.5", 239 | "solidity-comments-extractor": "^0.0.7", 240 | "string-width": "^4.2.3" 241 | } 242 | }, 243 | "semver": { 244 | "version": "7.3.5", 245 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", 246 | "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", 247 | "dev": true, 248 | "requires": { 249 | "lru-cache": "^6.0.0" 250 | } 251 | }, 252 | "solidity-comments-extractor": { 253 | "version": "0.0.7", 254 | "resolved": "https://registry.npmjs.org/solidity-comments-extractor/-/solidity-comments-extractor-0.0.7.tgz", 255 | "integrity": "sha512-wciNMLg/Irp8OKGrh3S2tfvZiZ0NEyILfcRCXCD4mp7SgK/i9gzLfhY2hY7VMCQJ3kH9UB9BzNdibIVMchzyYw==", 256 | "dev": true 257 | }, 258 | "string-width": { 259 | "version": "4.2.3", 260 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 261 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 262 | "dev": true, 263 | "requires": { 264 | "emoji-regex": "^8.0.0", 265 | "is-fullwidth-code-point": "^3.0.0", 266 | "strip-ansi": "^6.0.1" 267 | }, 268 | "dependencies": { 269 | "emoji-regex": { 270 | "version": "8.0.0", 271 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 272 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 273 | "dev": true 274 | } 275 | } 276 | }, 277 | "strip-ansi": { 278 | "version": "6.0.1", 279 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 280 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 281 | "dev": true, 282 | "requires": { 283 | "ansi-regex": "^5.0.1" 284 | } 285 | }, 286 | "yallist": { 287 | "version": "4.0.0", 288 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 289 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 290 | "dev": true 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "innovation-contracts-solidity", 3 | "version": "0.1.0", 4 | "description": "Collection of smart contracts written in Solidity", 5 | "scripts": { 6 | "prettier": "prettier --check \"src/**/*.sol\"", 7 | "prettier:fix": "prettier --write \"src/**/*.sol\"", 8 | "lefthook": "npx @arkweid/lefthook run pre-push" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/LedgerHQ/innovation-contracts-solidity.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/LedgerHQ/innovation-contracts-solidity/issues" 18 | }, 19 | "homepage": "https://github.com/LedgerHQ/innovation-contracts-solidity#readme", 20 | "devDependencies": { 21 | "prettier": "^2.6.0", 22 | "prettier-plugin-solidity": "^1.0.0-beta.19" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/ERC721.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >0.8.12; 3 | 4 | import { DSTestPlus } from "./utils/DSTestPlus.sol"; 5 | import { DSInvariantTest } from "./utils/DSInvariantTest.sol"; 6 | 7 | import { MockERC721 } from "./utils/mocks/MockERC721.sol"; 8 | import { ERC721User } from "./utils/users/ERC721User.sol"; 9 | 10 | import { ERC721TokenReceiver } from "../tokens/ERC721L.sol"; 11 | 12 | contract ERC721Recipient is ERC721TokenReceiver { 13 | address public operator; 14 | address public from; 15 | uint256 public id; 16 | bytes public data; 17 | 18 | function onERC721Received( 19 | address _operator, 20 | address _from, 21 | uint256 _id, 22 | bytes calldata _data 23 | ) public virtual override returns (bytes4) { 24 | operator = _operator; 25 | from = _from; 26 | id = _id; 27 | data = _data; 28 | 29 | return ERC721TokenReceiver.onERC721Received.selector; 30 | } 31 | } 32 | 33 | contract RevertingERC721Recipient is ERC721TokenReceiver { 34 | function onERC721Received( 35 | address, 36 | address, 37 | uint256, 38 | bytes calldata 39 | ) public virtual override returns (bytes4) { 40 | revert(string(abi.encodePacked(ERC721TokenReceiver.onERC721Received.selector))); 41 | } 42 | } 43 | 44 | contract WrongReturnDataERC721Recipient is ERC721TokenReceiver { 45 | function onERC721Received( 46 | address, 47 | address, 48 | uint256, 49 | bytes calldata 50 | ) public virtual override returns (bytes4) { 51 | return 0xCAFEBEEF; 52 | } 53 | } 54 | 55 | contract NonERC721Recipient {} 56 | 57 | contract ERC721Test is DSTestPlus { 58 | MockERC721 token; 59 | 60 | function setUp() public { 61 | token = new MockERC721("Token", "TKN"); 62 | } 63 | 64 | function invariantMetadata() public { 65 | assertEq(token.name(), "Token"); 66 | assertEq(token.symbol(), "TKN"); 67 | } 68 | 69 | function testMint() public { 70 | token.mint(address(0xBEEF), 1337); 71 | 72 | assertEq(token.balanceOf(address(0xBEEF)), 1); 73 | assertEq(token.ownerOf(1337), address(0xBEEF)); 74 | } 75 | 76 | function testBurn() public { 77 | token.mint(address(0xBEEF), 1337); 78 | token.burn(1337); 79 | 80 | assertEq(token.balanceOf(address(0xBEEF)), 0); 81 | 82 | hevm.expectRevert("NOT_MINTED"); 83 | token.ownerOf(1337); 84 | } 85 | 86 | function testApprove() public { 87 | token.mint(address(this), 1337); 88 | 89 | token.approve(address(0xBEEF), 1337); 90 | 91 | assertEq(token.getApproved(1337), address(0xBEEF)); 92 | } 93 | 94 | function testApproveBurn() public { 95 | token.mint(address(this), 1337); 96 | 97 | token.approve(address(0xBEEF), 1337); 98 | 99 | token.burn(1337); 100 | 101 | assertEq(token.balanceOf(address(this)), 0); 102 | assertEq(token.getApproved(1337), address(0)); 103 | 104 | hevm.expectRevert("NOT_MINTED"); 105 | token.ownerOf(1337); 106 | } 107 | 108 | function testApproveAll() public { 109 | token.setApprovalForAll(address(0xBEEF), true); 110 | 111 | assertTrue(token.isApprovedForAll(address(this), address(0xBEEF))); 112 | } 113 | 114 | function testTransferFrom() public { 115 | ERC721User from = new ERC721User(token); 116 | 117 | token.mint(address(from), 1337); 118 | 119 | from.approve(address(this), 1337); 120 | 121 | token.transferFrom(address(from), address(0xBEEF), 1337); 122 | 123 | assertEq(token.getApproved(1337), address(0)); 124 | assertEq(token.ownerOf(1337), address(0xBEEF)); 125 | assertEq(token.balanceOf(address(0xBEEF)), 1); 126 | assertEq(token.balanceOf(address(from)), 0); 127 | } 128 | 129 | function testTransferFromSelf() public { 130 | token.mint(address(this), 1337); 131 | 132 | token.transferFrom(address(this), address(0xBEEF), 1337); 133 | 134 | assertEq(token.getApproved(1337), address(0)); 135 | assertEq(token.ownerOf(1337), address(0xBEEF)); 136 | assertEq(token.balanceOf(address(0xBEEF)), 1); 137 | assertEq(token.balanceOf(address(this)), 0); 138 | } 139 | 140 | function testTransferFromApproveAll() public { 141 | ERC721User from = new ERC721User(token); 142 | 143 | token.mint(address(from), 1337); 144 | 145 | from.setApprovalForAll(address(this), true); 146 | 147 | token.transferFrom(address(from), address(0xBEEF), 1337); 148 | 149 | assertEq(token.getApproved(1337), address(0)); 150 | assertEq(token.ownerOf(1337), address(0xBEEF)); 151 | assertEq(token.balanceOf(address(0xBEEF)), 1); 152 | assertEq(token.balanceOf(address(from)), 0); 153 | } 154 | 155 | function testSafeTransferFromToEOA() public { 156 | ERC721User from = new ERC721User(token); 157 | 158 | token.mint(address(from), 1337); 159 | 160 | from.setApprovalForAll(address(this), true); 161 | 162 | token.safeTransferFrom(address(from), address(0xBEEF), 1337); 163 | 164 | assertEq(token.getApproved(1337), address(0)); 165 | assertEq(token.ownerOf(1337), address(0xBEEF)); 166 | assertEq(token.balanceOf(address(0xBEEF)), 1); 167 | assertEq(token.balanceOf(address(from)), 0); 168 | } 169 | 170 | function testSafeTransferFromToERC721Recipient() public { 171 | ERC721User from = new ERC721User(token); 172 | ERC721Recipient recipient = new ERC721Recipient(); 173 | 174 | token.mint(address(from), 1337); 175 | 176 | from.setApprovalForAll(address(this), true); 177 | 178 | token.safeTransferFrom(address(from), address(recipient), 1337); 179 | 180 | assertEq(token.getApproved(1337), address(0)); 181 | assertEq(token.ownerOf(1337), address(recipient)); 182 | assertEq(token.balanceOf(address(recipient)), 1); 183 | assertEq(token.balanceOf(address(from)), 0); 184 | 185 | assertEq(recipient.operator(), address(this)); 186 | assertEq(recipient.from(), address(from)); 187 | assertEq(recipient.id(), 1337); 188 | assertBytesEq(recipient.data(), ""); 189 | } 190 | 191 | function testSafeTransferFromToERC721RecipientWithData() public { 192 | ERC721User from = new ERC721User(token); 193 | ERC721Recipient recipient = new ERC721Recipient(); 194 | 195 | token.mint(address(from), 1337); 196 | 197 | from.setApprovalForAll(address(this), true); 198 | 199 | token.safeTransferFrom(address(from), address(recipient), 1337, "testing 123"); 200 | 201 | assertEq(token.getApproved(1337), address(0)); 202 | assertEq(token.ownerOf(1337), address(recipient)); 203 | assertEq(token.balanceOf(address(recipient)), 1); 204 | assertEq(token.balanceOf(address(from)), 0); 205 | 206 | assertEq(recipient.operator(), address(this)); 207 | assertEq(recipient.from(), address(from)); 208 | assertEq(recipient.id(), 1337); 209 | assertBytesEq(recipient.data(), "testing 123"); 210 | } 211 | 212 | function testSafeMintToEOA() public { 213 | token.safeMint(address(0xBEEF), 1337); 214 | 215 | assertEq(token.ownerOf(1337), address(address(0xBEEF))); 216 | assertEq(token.balanceOf(address(address(0xBEEF))), 1); 217 | } 218 | 219 | function testSafeMintToERC721Recipient() public { 220 | ERC721Recipient to = new ERC721Recipient(); 221 | 222 | token.safeMint(address(to), 1337); 223 | 224 | assertEq(token.ownerOf(1337), address(to)); 225 | assertEq(token.balanceOf(address(to)), 1); 226 | 227 | assertEq(to.operator(), address(this)); 228 | assertEq(to.from(), address(0)); 229 | assertEq(to.id(), 1337); 230 | assertBytesEq(to.data(), ""); 231 | } 232 | 233 | function testSafeMintToERC721RecipientWithData() public { 234 | ERC721Recipient to = new ERC721Recipient(); 235 | 236 | token.safeMint(address(to), 1337, "testing 123"); 237 | 238 | assertEq(token.ownerOf(1337), address(to)); 239 | assertEq(token.balanceOf(address(to)), 1); 240 | 241 | assertEq(to.operator(), address(this)); 242 | assertEq(to.from(), address(0)); 243 | assertEq(to.id(), 1337); 244 | assertBytesEq(to.data(), "testing 123"); 245 | } 246 | 247 | function testFailMintToZero() public { 248 | token.mint(address(0), 1337); 249 | } 250 | 251 | function testFailDoubleMint() public { 252 | token.mint(address(0xBEEF), 1337); 253 | token.mint(address(0xBEEF), 1337); 254 | } 255 | 256 | function testFailBurnUnMinted() public { 257 | token.burn(1337); 258 | } 259 | 260 | function testFailDoubleBurn() public { 261 | token.mint(address(0xBEEF), 1337); 262 | 263 | token.burn(1337); 264 | token.burn(1337); 265 | } 266 | 267 | function testFailApproveUnMinted() public { 268 | token.approve(address(0xBEEF), 1337); 269 | } 270 | 271 | function testFailApproveUnAuthorized() public { 272 | token.mint(address(0xCAFE), 1337); 273 | 274 | token.approve(address(0xBEEF), 1337); 275 | } 276 | 277 | function testFailTransferFromUnOwned() public { 278 | token.transferFrom(address(0xFEED), address(0xBEEF), 1337); 279 | } 280 | 281 | function testFailTransferFromWrongFrom() public { 282 | token.mint(address(0xCAFE), 1337); 283 | 284 | token.transferFrom(address(0xFEED), address(0xBEEF), 1337); 285 | } 286 | 287 | function testFailTransferFromToZero() public { 288 | token.mint(address(this), 1337); 289 | 290 | token.transferFrom(address(this), address(0), 1337); 291 | } 292 | 293 | function testFailTransferFromNotOwner() public { 294 | token.mint(address(0xFEED), 1337); 295 | 296 | token.transferFrom(address(0xFEED), address(0xBEEF), 1337); 297 | } 298 | 299 | function testFailSafeTransferFromToNonERC721Recipient() public { 300 | token.mint(address(this), 1337); 301 | 302 | token.safeTransferFrom(address(this), address(new NonERC721Recipient()), 1337); 303 | } 304 | 305 | function testFailSafeTransferFromToNonERC721RecipientWithData() public { 306 | token.mint(address(this), 1337); 307 | 308 | token.safeTransferFrom(address(this), address(new NonERC721Recipient()), 1337, "testing 123"); 309 | } 310 | 311 | function testFailSafeTransferFromToRevertingERC721Recipient() public { 312 | token.mint(address(this), 1337); 313 | 314 | token.safeTransferFrom(address(this), address(new RevertingERC721Recipient()), 1337); 315 | } 316 | 317 | function testFailSafeTransferFromToRevertingERC721RecipientWithData() public { 318 | token.mint(address(this), 1337); 319 | 320 | token.safeTransferFrom(address(this), address(new RevertingERC721Recipient()), 1337, "testing 123"); 321 | } 322 | 323 | function testFailSafeTransferFromToERC721RecipientWithWrongReturnData() public { 324 | token.mint(address(this), 1337); 325 | 326 | token.safeTransferFrom(address(this), address(new WrongReturnDataERC721Recipient()), 1337); 327 | } 328 | 329 | function testFailSafeTransferFromToERC721RecipientWithWrongReturnDataWithData() public { 330 | token.mint(address(this), 1337); 331 | 332 | token.safeTransferFrom(address(this), address(new WrongReturnDataERC721Recipient()), 1337, "testing 123"); 333 | } 334 | 335 | function testFailSafeMintToNonERC721Recipient() public { 336 | token.safeMint(address(new NonERC721Recipient()), 1337); 337 | } 338 | 339 | function testFailSafeMintToNonERC721RecipientWithData() public { 340 | token.safeMint(address(new NonERC721Recipient()), 1337, "testing 123"); 341 | } 342 | 343 | function testFailSafeMintToRevertingERC721Recipient() public { 344 | token.safeMint(address(new RevertingERC721Recipient()), 1337); 345 | } 346 | 347 | function testFailSafeMintToRevertingERC721RecipientWithData() public { 348 | token.safeMint(address(new RevertingERC721Recipient()), 1337, "testing 123"); 349 | } 350 | 351 | function testFailSafeMintToERC721RecipientWithWrongReturnData() public { 352 | token.safeMint(address(new WrongReturnDataERC721Recipient()), 1337); 353 | } 354 | 355 | function testFailSafeMintToERC721RecipientWithWrongReturnDataWithData() public { 356 | token.safeMint(address(new WrongReturnDataERC721Recipient()), 1337, "testing 123"); 357 | } 358 | 359 | function testFailBalanceOfZeroAddress() public view { 360 | token.balanceOf(address(0)); 361 | } 362 | 363 | function testFailOwnerOfUnminted() public view { 364 | token.ownerOf(1337); 365 | } 366 | 367 | function testMetadata(string memory name, string memory symbol) public { 368 | MockERC721 tkn = new MockERC721(name, symbol); 369 | 370 | assertEq(tkn.name(), name); 371 | assertEq(tkn.symbol(), symbol); 372 | } 373 | 374 | function testMint(address to, uint256 id) public { 375 | if (to == address(0)) to = address(0xBEEF); 376 | 377 | token.mint(to, id); 378 | 379 | assertEq(token.balanceOf(to), 1); 380 | assertEq(token.ownerOf(id), to); 381 | } 382 | 383 | function testBurn(address to, uint256 id) public { 384 | if (to == address(0)) to = address(0xBEEF); 385 | 386 | token.mint(to, id); 387 | token.burn(id); 388 | 389 | assertEq(token.balanceOf(to), 0); 390 | hevm.expectRevert("NOT_MINTED"); 391 | token.ownerOf(id); 392 | } 393 | 394 | function testApprove(address to, uint256 id) public { 395 | if (to == address(0)) to = address(0xBEEF); 396 | 397 | token.mint(address(this), id); 398 | 399 | token.approve(to, id); 400 | 401 | assertEq(token.getApproved(id), to); 402 | } 403 | 404 | function testApproveBurn(address to, uint256 id) public { 405 | token.mint(address(this), id); 406 | 407 | token.approve(address(to), id); 408 | 409 | token.burn(id); 410 | 411 | assertEq(token.balanceOf(address(this)), 0); 412 | assertEq(token.getApproved(id), address(0)); 413 | 414 | hevm.expectRevert("NOT_MINTED"); 415 | token.ownerOf(id); 416 | } 417 | 418 | function testApproveAll(address to, bool approved) public { 419 | token.setApprovalForAll(to, approved); 420 | 421 | assertBoolEq(token.isApprovedForAll(address(this), to), approved); 422 | } 423 | 424 | function testTransferFrom(uint256 id, address to) public { 425 | ERC721User from = new ERC721User(token); 426 | 427 | if (to == address(0) || to == address(from)) to = address(0xBEEF); 428 | 429 | token.mint(address(from), id); 430 | 431 | from.approve(address(this), id); 432 | 433 | token.transferFrom(address(from), to, id); 434 | 435 | assertEq(token.getApproved(id), address(0)); 436 | assertEq(token.ownerOf(id), to); 437 | assertEq(token.balanceOf(to), 1); 438 | assertEq(token.balanceOf(address(from)), 0); 439 | } 440 | 441 | function testTransferFromSelf(uint256 id, address to) public { 442 | if (to == address(0) || to == address(this)) to = address(0xBEEF); 443 | 444 | token.mint(address(this), id); 445 | 446 | token.transferFrom(address(this), to, id); 447 | 448 | assertEq(token.getApproved(id), address(0)); 449 | assertEq(token.ownerOf(id), to); 450 | assertEq(token.balanceOf(to), 1); 451 | assertEq(token.balanceOf(address(this)), 0); 452 | } 453 | 454 | function testTransferFromApproveAll(uint256 id, address to) public { 455 | if (to == address(0) || to == address(this)) to = address(0xBEEF); 456 | 457 | ERC721User from = new ERC721User(token); 458 | 459 | token.mint(address(from), id); 460 | 461 | from.setApprovalForAll(address(this), true); 462 | 463 | token.transferFrom(address(from), to, id); 464 | 465 | assertEq(token.getApproved(id), address(0)); 466 | assertEq(token.ownerOf(id), to); 467 | assertEq(token.balanceOf(to), 1); 468 | assertEq(token.balanceOf(address(from)), 0); 469 | } 470 | 471 | function testSafeTransferFromToEOA(uint256 id, address to) public { 472 | ERC721User from = new ERC721User(token); 473 | 474 | if (to == address(0) || to == address(this)) to = address(0xBEEF); 475 | 476 | if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; 477 | 478 | token.mint(address(from), id); 479 | 480 | from.setApprovalForAll(address(this), true); 481 | 482 | token.safeTransferFrom(address(from), to, id); 483 | 484 | assertEq(token.getApproved(id), address(0)); 485 | assertEq(token.ownerOf(id), to); 486 | assertEq(token.balanceOf(to), 1); 487 | assertEq(token.balanceOf(address(from)), 0); 488 | } 489 | 490 | function testSafeTransferFromToERC721Recipient(uint256 id) public { 491 | ERC721User from = new ERC721User(token); 492 | ERC721Recipient recipient = new ERC721Recipient(); 493 | 494 | token.mint(address(from), id); 495 | 496 | from.setApprovalForAll(address(this), true); 497 | 498 | token.safeTransferFrom(address(from), address(recipient), id); 499 | 500 | assertEq(token.getApproved(id), address(0)); 501 | assertEq(token.ownerOf(id), address(recipient)); 502 | assertEq(token.balanceOf(address(recipient)), 1); 503 | assertEq(token.balanceOf(address(from)), 0); 504 | 505 | assertEq(recipient.operator(), address(this)); 506 | assertEq(recipient.from(), address(from)); 507 | assertEq(recipient.id(), id); 508 | assertBytesEq(recipient.data(), ""); 509 | } 510 | 511 | function testSafeTransferFromToERC721RecipientWithData(uint256 id, bytes calldata data) public { 512 | ERC721User from = new ERC721User(token); 513 | ERC721Recipient recipient = new ERC721Recipient(); 514 | 515 | token.mint(address(from), id); 516 | 517 | from.setApprovalForAll(address(this), true); 518 | 519 | token.safeTransferFrom(address(from), address(recipient), id, data); 520 | 521 | assertEq(token.getApproved(id), address(0)); 522 | assertEq(token.ownerOf(id), address(recipient)); 523 | assertEq(token.balanceOf(address(recipient)), 1); 524 | assertEq(token.balanceOf(address(from)), 0); 525 | 526 | assertEq(recipient.operator(), address(this)); 527 | assertEq(recipient.from(), address(from)); 528 | assertEq(recipient.id(), id); 529 | assertBytesEq(recipient.data(), data); 530 | } 531 | 532 | function testSafeMintToEOA(uint256 id, address to) public { 533 | if (to == address(0)) to = address(0xBEEF); 534 | 535 | if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; 536 | 537 | token.safeMint(to, id); 538 | 539 | assertEq(token.ownerOf(id), address(to)); 540 | assertEq(token.balanceOf(address(to)), 1); 541 | } 542 | 543 | function testSafeMintToERC721Recipient(uint256 id) public { 544 | ERC721Recipient to = new ERC721Recipient(); 545 | 546 | token.safeMint(address(to), id); 547 | 548 | assertEq(token.ownerOf(id), address(to)); 549 | assertEq(token.balanceOf(address(to)), 1); 550 | 551 | assertEq(to.operator(), address(this)); 552 | assertEq(to.from(), address(0)); 553 | assertEq(to.id(), id); 554 | assertBytesEq(to.data(), ""); 555 | } 556 | 557 | function testSafeMintToERC721RecipientWithData(uint256 id, bytes calldata data) public { 558 | ERC721Recipient to = new ERC721Recipient(); 559 | 560 | token.safeMint(address(to), id, data); 561 | 562 | assertEq(token.ownerOf(id), address(to)); 563 | assertEq(token.balanceOf(address(to)), 1); 564 | 565 | assertEq(to.operator(), address(this)); 566 | assertEq(to.from(), address(0)); 567 | assertEq(to.id(), id); 568 | assertBytesEq(to.data(), data); 569 | } 570 | 571 | function testFailMintToZero(uint256 id) public { 572 | token.mint(address(0), id); 573 | } 574 | 575 | function testFailDoubleMint(uint256 id, address to) public { 576 | if (to == address(0)) to = address(0xBEEF); 577 | 578 | token.mint(to, id); 579 | token.mint(to, id); 580 | } 581 | 582 | function testFailBurnUnMinted(uint256 id) public { 583 | token.burn(id); 584 | } 585 | 586 | function testFailDoubleBurn(uint256 id, address to) public { 587 | if (to == address(0)) to = address(0xBEEF); 588 | 589 | token.mint(to, id); 590 | 591 | token.burn(id); 592 | token.burn(id); 593 | } 594 | 595 | function testFailApproveUnMinted(uint256 id, address to) public { 596 | token.approve(to, id); 597 | } 598 | 599 | function testFailApproveUnAuthorized( 600 | address owner, 601 | uint256 id, 602 | address to 603 | ) public { 604 | if (owner == address(0) || owner == address(this)) owner = address(0xBEEF); 605 | 606 | token.mint(owner, id); 607 | 608 | token.approve(to, id); 609 | } 610 | 611 | function testFailTransferFromUnOwned( 612 | address from, 613 | address to, 614 | uint256 id 615 | ) public { 616 | token.transferFrom(from, to, id); 617 | } 618 | 619 | function testFailTransferFromWrongFrom( 620 | address owner, 621 | address from, 622 | address to, 623 | uint256 id 624 | ) public { 625 | if (owner == address(0)) to = address(0xBEEF); 626 | if (from == owner) revert(); 627 | 628 | token.mint(owner, id); 629 | 630 | token.transferFrom(from, to, id); 631 | } 632 | 633 | function testFailTransferFromToZero(uint256 id) public { 634 | token.mint(address(this), id); 635 | 636 | token.transferFrom(address(this), address(0), id); 637 | } 638 | 639 | function testFailTransferFromNotOwner( 640 | address from, 641 | address to, 642 | uint256 id 643 | ) public { 644 | if (from == address(this)) from = address(0xBEEF); 645 | 646 | token.mint(from, id); 647 | 648 | token.transferFrom(from, to, id); 649 | } 650 | 651 | function testFailSafeTransferFromToNonERC721Recipient(uint256 id) public { 652 | token.mint(address(this), id); 653 | 654 | token.safeTransferFrom(address(this), address(new NonERC721Recipient()), id); 655 | } 656 | 657 | function testFailSafeTransferFromToNonERC721RecipientWithData(uint256 id, bytes calldata data) public { 658 | token.mint(address(this), id); 659 | 660 | token.safeTransferFrom(address(this), address(new NonERC721Recipient()), id, data); 661 | } 662 | 663 | function testFailSafeTransferFromToRevertingERC721Recipient(uint256 id) public { 664 | token.mint(address(this), id); 665 | 666 | token.safeTransferFrom(address(this), address(new RevertingERC721Recipient()), id); 667 | } 668 | 669 | function testFailSafeTransferFromToRevertingERC721RecipientWithData(uint256 id, bytes calldata data) public { 670 | token.mint(address(this), id); 671 | 672 | token.safeTransferFrom(address(this), address(new RevertingERC721Recipient()), id, data); 673 | } 674 | 675 | function testFailSafeTransferFromToERC721RecipientWithWrongReturnData(uint256 id) public { 676 | token.mint(address(this), id); 677 | 678 | token.safeTransferFrom(address(this), address(new WrongReturnDataERC721Recipient()), id); 679 | } 680 | 681 | function testFailSafeTransferFromToERC721RecipientWithWrongReturnDataWithData(uint256 id, bytes calldata data) 682 | public 683 | { 684 | token.mint(address(this), id); 685 | 686 | token.safeTransferFrom(address(this), address(new WrongReturnDataERC721Recipient()), id, data); 687 | } 688 | 689 | function testFailSafeMintToNonERC721Recipient(uint256 id) public { 690 | token.safeMint(address(new NonERC721Recipient()), id); 691 | } 692 | 693 | function testFailSafeMintToNonERC721RecipientWithData(uint256 id, bytes calldata data) public { 694 | token.safeMint(address(new NonERC721Recipient()), id, data); 695 | } 696 | 697 | function testFailSafeMintToRevertingERC721Recipient(uint256 id) public { 698 | token.safeMint(address(new RevertingERC721Recipient()), id); 699 | } 700 | 701 | function testFailSafeMintToRevertingERC721RecipientWithData(uint256 id, bytes calldata data) public { 702 | token.safeMint(address(new RevertingERC721Recipient()), id, data); 703 | } 704 | 705 | function testFailSafeMintToERC721RecipientWithWrongReturnData(uint256 id) public { 706 | token.safeMint(address(new WrongReturnDataERC721Recipient()), id); 707 | } 708 | 709 | function testFailSafeMintToERC721RecipientWithWrongReturnDataWithData(uint256 id, bytes calldata data) public { 710 | token.safeMint(address(new WrongReturnDataERC721Recipient()), id, data); 711 | } 712 | 713 | function testFailOwnerOfUnminted(uint256 id) public view { 714 | token.ownerOf(id); 715 | } 716 | } 717 | -------------------------------------------------------------------------------- /src/test/utils/DSInvariantTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | contract DSInvariantTest { 5 | address[] private targets; 6 | 7 | function targetContracts() public view virtual returns (address[] memory) { 8 | require(targets.length > 0, "NO_TARGET_CONTRACTS"); 9 | 10 | return targets; 11 | } 12 | 13 | function addTargetContract(address newTargetContract) internal virtual { 14 | targets.push(newTargetContract); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/utils/DSTestPlus.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | import { DSTest } from "ds-test/test.sol"; 5 | 6 | import { Hevm } from "./Hevm.sol"; 7 | 8 | /// @notice Extended testing framework for DappTools projects. 9 | /// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/test/utils/DSTestPlus.sol) 10 | contract DSTestPlus is DSTest { 11 | Hevm internal constant hevm = Hevm(HEVM_ADDRESS); 12 | 13 | address internal constant DEAD_ADDRESS = 0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF; 14 | 15 | string private checkpointLabel; 16 | uint256 private checkpointGasLeft = 1; // Start the slot warm. 17 | 18 | function startMeasuringGas(string memory label) internal virtual { 19 | checkpointLabel = label; 20 | 21 | checkpointGasLeft = gasleft(); 22 | } 23 | 24 | function stopMeasuringGas() internal virtual { 25 | uint256 checkpointGasLeft2 = gasleft(); 26 | 27 | // Subtract 100 to account for the warm SLOAD in startMeasuringGas. 28 | uint256 gasDelta = checkpointGasLeft - checkpointGasLeft2 - 100; 29 | 30 | emit log_named_uint(string(abi.encodePacked(checkpointLabel, " Gas")), gasDelta); 31 | } 32 | 33 | function fail(string memory err) internal virtual { 34 | emit log_named_string("Error", err); 35 | fail(); 36 | } 37 | 38 | function assertFalse(bool data) internal virtual { 39 | assertTrue(!data); 40 | } 41 | 42 | function assertUint128Eq(uint128 a, uint128 b) internal virtual { 43 | assertEq(uint256(a), uint256(b)); 44 | } 45 | 46 | function assertUint64Eq(uint64 a, uint64 b) internal virtual { 47 | assertEq(uint256(a), uint256(b)); 48 | } 49 | 50 | function assertUint96Eq(uint96 a, uint96 b) internal virtual { 51 | assertEq(uint256(a), uint256(b)); 52 | } 53 | 54 | function assertUint32Eq(uint32 a, uint32 b) internal virtual { 55 | assertEq(uint256(a), uint256(b)); 56 | } 57 | 58 | function assertBoolEq(bool a, bool b) internal virtual { 59 | b ? assertTrue(a) : assertFalse(a); 60 | } 61 | 62 | function assertApproxEq( 63 | uint256 a, 64 | uint256 b, 65 | uint256 maxDelta 66 | ) internal virtual { 67 | uint256 delta = a > b ? a - b : b - a; 68 | 69 | if (delta > maxDelta) { 70 | emit log("Error: a ~= b not satisfied [uint]"); 71 | emit log_named_uint(" Expected", a); 72 | emit log_named_uint(" Actual", b); 73 | emit log_named_uint(" Max Delta", maxDelta); 74 | emit log_named_uint(" Delta", delta); 75 | fail(); 76 | } 77 | } 78 | 79 | function assertRelApproxEq( 80 | uint256 a, 81 | uint256 b, 82 | uint256 maxPercentDelta 83 | ) internal virtual { 84 | uint256 delta = a > b ? a - b : b - a; 85 | uint256 abs = a > b ? a : b; 86 | 87 | uint256 percentDelta = (delta * 1e18) / abs; 88 | 89 | if (percentDelta > maxPercentDelta) { 90 | emit log("Error: a ~= b not satisfied [uint]"); 91 | emit log_named_uint(" Expected", a); 92 | emit log_named_uint(" Actual", b); 93 | emit log_named_uint(" Max % Delta", maxPercentDelta); 94 | emit log_named_uint(" % Delta", percentDelta); 95 | fail(); 96 | } 97 | } 98 | 99 | function assertBytesEq(bytes memory a, bytes memory b) internal virtual { 100 | if (keccak256(a) != keccak256(b)) { 101 | emit log("Error: a == b not satisfied [bytes]"); 102 | emit log_named_bytes(" Expected", b); 103 | emit log_named_bytes(" Actual", a); 104 | fail(); 105 | } 106 | } 107 | 108 | function assertUintArrayEq(uint256[] memory a, uint256[] memory b) internal virtual { 109 | require(a.length == b.length, "LENGTH_MISMATCH"); 110 | 111 | for (uint256 i = 0; i < a.length; i++) { 112 | assertEq(a[i], b[i]); 113 | } 114 | } 115 | 116 | function bound( 117 | uint256 x, 118 | uint256 min, 119 | uint256 max 120 | ) internal pure returns (uint256 result) { 121 | require(max >= min, "MAX_LESS_THAN_MIN"); 122 | 123 | uint256 size = max - min; 124 | 125 | if (max != type(uint256).max) size++; // Make the max inclusive. 126 | if (size == 0) return min; // Using max would be equivalent as well. 127 | // Ensure max is inclusive in cases where x != 0 and max is at uint max. 128 | if (max == type(uint256).max && x != 0) x--; // Accounted for later. 129 | 130 | if (x < min) x += size * (((min - x) / size) + 1); 131 | result = min + ((x - min) % size); 132 | 133 | // Account for decrementing x to make max inclusive. 134 | if (max == type(uint256).max && x != 0) result++; 135 | } 136 | 137 | function min3( 138 | uint256 a, 139 | uint256 b, 140 | uint256 c 141 | ) internal pure returns (uint256) { 142 | return a > b ? (b > c ? c : b) : (a > c ? c : a); 143 | } 144 | 145 | function min2(uint256 a, uint256 b) internal pure returns (uint256) { 146 | return a > b ? b : a; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/test/utils/Hevm.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | interface Hevm { 5 | function warp(uint256) external; 6 | 7 | function roll(uint256) external; 8 | 9 | function fee(uint256) external; 10 | 11 | function load(address, bytes32) external returns (bytes32); 12 | 13 | function store( 14 | address, 15 | bytes32, 16 | bytes32 17 | ) external; 18 | 19 | function sign(uint256, bytes32) 20 | external 21 | returns ( 22 | uint8, 23 | bytes32, 24 | bytes32 25 | ); 26 | 27 | function addr(uint256) external returns (address); 28 | 29 | function ffi(string[] calldata) external returns (bytes memory); 30 | 31 | function prank(address) external; 32 | 33 | function startPrank(address) external; 34 | 35 | function prank(address, address) external; 36 | 37 | function startPrank(address, address) external; 38 | 39 | function stopPrank() external; 40 | 41 | function deal(address, uint256) external; 42 | 43 | function etch(address, bytes calldata) external; 44 | 45 | function expectRevert(bytes calldata) external; 46 | 47 | function expectRevert(bytes4) external; 48 | 49 | function record() external; 50 | 51 | function accesses(address) external returns (bytes32[] memory reads, bytes32[] memory writes); 52 | 53 | function expectEmit( 54 | bool, 55 | bool, 56 | bool, 57 | bool 58 | ) external; 59 | 60 | function mockCall( 61 | address, 62 | bytes calldata, 63 | bytes calldata 64 | ) external; 65 | 66 | function clearMockedCalls() external; 67 | 68 | function expectCall(address, bytes calldata) external; 69 | 70 | function getCode(string calldata) external returns (bytes memory); 71 | } 72 | -------------------------------------------------------------------------------- /src/test/utils/mocks/MockERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | import { ERC721L } from "../../../tokens/ERC721L.sol"; 5 | 6 | contract MockERC721 is ERC721L { 7 | constructor(string memory _name, string memory _symbol) ERC721L(_name, _symbol) {} 8 | 9 | function tokenURI(uint256) public pure virtual override returns (string memory) {} 10 | 11 | function mint(address to, uint256 tokenId) public virtual { 12 | _mint(to, tokenId); 13 | } 14 | 15 | function burn(uint256 tokenId) public virtual { 16 | _burn(tokenId); 17 | } 18 | 19 | function safeMint(address to, uint256 tokenId) public virtual { 20 | _safeMint(to, tokenId); 21 | } 22 | 23 | function safeMint( 24 | address to, 25 | uint256 tokenId, 26 | bytes memory data 27 | ) public virtual { 28 | _safeMint(to, tokenId, data); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/utils/users/ERC721User.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | import { ERC721L, ERC721TokenReceiver } from "../../../tokens/ERC721L.sol"; 5 | 6 | contract ERC721User is ERC721TokenReceiver { 7 | ERC721L token; 8 | 9 | constructor(ERC721L _token) { 10 | token = _token; 11 | } 12 | 13 | function onERC721Received( 14 | address, 15 | address, 16 | uint256, 17 | bytes calldata 18 | ) public virtual override returns (bytes4) { 19 | return ERC721TokenReceiver.onERC721Received.selector; 20 | } 21 | 22 | function approve(address spender, uint256 tokenId) public virtual { 23 | token.approve(spender, tokenId); 24 | } 25 | 26 | function setApprovalForAll(address operator, bool approved) public virtual { 27 | token.setApprovalForAll(operator, approved); 28 | } 29 | 30 | function transferFrom( 31 | address from, 32 | address to, 33 | uint256 tokenId 34 | ) public virtual { 35 | token.transferFrom(from, to, tokenId); 36 | } 37 | 38 | function safeTransferFrom( 39 | address from, 40 | address to, 41 | uint256 tokenId 42 | ) public virtual { 43 | token.safeTransferFrom(from, to, tokenId); 44 | } 45 | 46 | function safeTransferFrom( 47 | address from, 48 | address to, 49 | uint256 tokenId, 50 | bytes memory data 51 | ) public { 52 | token.safeTransferFrom(from, to, tokenId, data); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/tokens/ERC721L.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.4; 3 | 4 | import { ERC721 } from "solmate/tokens/ERC721.sol"; 5 | 6 | /*/////////////////////////////////////////////////////////////// 7 | ERRORS 8 | //////////////////////////////////////////////////////////////*/ 9 | error NotAuthorized(); 10 | error InvalidRecipient(); 11 | error WrongFrom(); 12 | error UnsafeRecipient(); 13 | error AlreadyMinted(); 14 | error NotMinted(); 15 | 16 | /// @notice Solmate ERC721 extended with custom errors 17 | abstract contract ERC721L is ERC721 { 18 | // We can delete this constructor and called instead ERC721 to save 13gas 19 | constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} 20 | 21 | /*/////////////////////////////////////////////////////////////// 22 | ERC721 LOGIC 23 | //////////////////////////////////////////////////////////////*/ 24 | function approve(address spender, uint256 id) public virtual override { 25 | address owner = ownerOf(id); 26 | 27 | if (msg.sender != owner && isApprovedForAll[owner][msg.sender] == false) revert NotAuthorized(); 28 | 29 | getApproved[id] = spender; 30 | 31 | emit Approval(owner, spender, id); 32 | } 33 | 34 | function transferFrom( 35 | address from, 36 | address to, 37 | uint256 id 38 | ) public virtual override { 39 | if (from != ownerOf(id)) revert WrongFrom(); 40 | if (to == address(0)) revert InvalidRecipient(); 41 | 42 | if (msg.sender != from && msg.sender != getApproved[id] && isApprovedForAll[from][msg.sender] == false) 43 | revert NotAuthorized(); 44 | 45 | // Underflow of the sender's balance is impossible because we check for 46 | // ownership above and the recipient's balance can't realistically overflow. 47 | unchecked { 48 | _balanceOf[from]--; 49 | 50 | _balanceOf[to]++; 51 | } 52 | 53 | _ownerOf[id] = to; 54 | 55 | delete getApproved[id]; 56 | 57 | emit Transfer(from, to, id); 58 | } 59 | 60 | function safeTransferFrom( 61 | address from, 62 | address to, 63 | uint256 id 64 | ) public virtual override { 65 | transferFrom(from, to, id); 66 | 67 | if ( 68 | to.code.length != 0 && 69 | ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") != 70 | ERC721TokenReceiver.onERC721Received.selector 71 | ) revert UnsafeRecipient(); 72 | } 73 | 74 | function safeTransferFrom( 75 | address from, 76 | address to, 77 | uint256 id, 78 | bytes calldata data 79 | ) public virtual override { 80 | transferFrom(from, to, id); 81 | 82 | if ( 83 | to.code.length != 0 && 84 | ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) != 85 | ERC721TokenReceiver.onERC721Received.selector 86 | ) revert UnsafeRecipient(); 87 | } 88 | 89 | /*/////////////////////////////////////////////////////////////// 90 | INTERNAL MINT/BURN LOGIC 91 | //////////////////////////////////////////////////////////////*/ 92 | 93 | function _mint(address to, uint256 id) internal virtual override { 94 | if (to == address(0)) revert InvalidRecipient(); 95 | if (_ownerOf[id] != address(0)) revert AlreadyMinted(); 96 | 97 | // Counter overflow is incredibly unrealistic. 98 | unchecked { 99 | _balanceOf[to]++; 100 | } 101 | 102 | _ownerOf[id] = to; 103 | 104 | emit Transfer(address(0), to, id); 105 | } 106 | 107 | function _burn(uint256 id) internal virtual override { 108 | address owner = _ownerOf[id]; 109 | if (owner == address(0)) revert NotMinted(); 110 | 111 | // Ownership check above ensures no underflow. 112 | unchecked { 113 | _balanceOf[owner]--; 114 | } 115 | 116 | delete _ownerOf[id]; 117 | 118 | delete getApproved[id]; 119 | 120 | emit Transfer(owner, address(0), id); 121 | } 122 | 123 | /*/////////////////////////////////////////////////////////////// 124 | INTERNAL SAFE MINT LOGIC 125 | //////////////////////////////////////////////////////////////*/ 126 | 127 | function _safeMint(address to, uint256 id) internal virtual override { 128 | _mint(to, id); 129 | 130 | if ( 131 | to.code.length != 0 && 132 | ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") != 133 | ERC721TokenReceiver.onERC721Received.selector 134 | ) revert UnsafeRecipient(); 135 | } 136 | 137 | function _safeMint( 138 | address to, 139 | uint256 id, 140 | bytes memory data 141 | ) internal virtual override { 142 | _mint(to, id); 143 | 144 | if ( 145 | to.code.length != 0 && 146 | ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, data) != 147 | ERC721TokenReceiver.onERC721Received.selector 148 | ) revert UnsafeRecipient(); 149 | } 150 | } 151 | 152 | /// @notice A generic interface for a contract which properly accepts ERC721 tokens. 153 | /// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol) 154 | interface ERC721TokenReceiver { 155 | function onERC721Received( 156 | address operator, 157 | address from, 158 | uint256 id, 159 | bytes calldata data 160 | ) external returns (bytes4); 161 | } 162 | --------------------------------------------------------------------------------