├── codecov.yml ├── .prettierrc.yml ├── test ├── token │ ├── erc20 │ │ ├── name │ │ │ ├── name.tree │ │ │ └── name.t.sol │ │ ├── symbol │ │ │ ├── symbol.tree │ │ │ └── symbol.t.sol │ │ ├── decimals │ │ │ ├── decimals.tree │ │ │ └── decimals.t.sol │ │ ├── balance-of │ │ │ ├── balanceOf.tree │ │ │ └── balanceOf.t.sol │ │ ├── decrease-allowance │ │ │ ├── decreaseAllowance.tree │ │ │ └── decreaseAllowance.t.sol │ │ ├── increase-allowance │ │ │ ├── increaseAllowance.tree │ │ │ └── increaseAllowance.t.sol │ │ ├── transfer-from │ │ │ ├── transferFrom.tree │ │ │ └── transferFrom.t.sol │ │ ├── approve │ │ │ ├── approve.tree │ │ │ └── approve.t.sol │ │ ├── burn │ │ │ ├── burn.tree │ │ │ └── burn.t.sol │ │ ├── mint │ │ │ ├── mint.tree │ │ │ └── mint.t.sol │ │ ├── transfer │ │ │ ├── transfer.tree │ │ │ └── transfer.t.sol │ │ └── ERC20.t.sol │ ├── erc20-permit │ │ ├── version │ │ │ ├── version.tree │ │ │ └── version.t.sol │ │ ├── permit-typehash │ │ │ ├── permitTypehash.tree │ │ │ └── permitTypehash.t.sol │ │ ├── domain-separator │ │ │ ├── domainSeparator.tree │ │ │ └── domainSeparator.t.sol │ │ ├── permit │ │ │ ├── permit.tree │ │ │ └── permit.t.sol │ │ └── ERC20Permit.t.sol │ ├── erc20-recover │ │ ├── is-token-denylist-set │ │ │ ├── isTokenDenylistSet.tree │ │ │ └── isTokenDenylistSet.t.sol │ │ ├── get-token-denylist │ │ │ ├── getTokenDenylist.tree │ │ │ └── getTokenDenylist.t.sol │ │ ├── set-token-denylist │ │ │ ├── setTokenDenylist.tree │ │ │ └── setTokenDenylist.t.sol │ │ ├── recover │ │ │ ├── recover.tree │ │ │ └── recover.t.sol │ │ └── ERC20Recover.t.sol │ └── erc20-normalizer │ │ ├── compute-scalar │ │ ├── computeScalar.tree │ │ └── computeScalar.t.sol │ │ ├── normalize │ │ ├── normalize.tree │ │ └── normalize.t.sol │ │ ├── denormalize │ │ ├── denormalize.tree │ │ └── denormalize.t.sol │ │ └── ERC20Normalizer.t.sol ├── shared │ ├── ERC20NormalizerMock.t.sol │ ├── ERC20RecoverMock.t.sol │ └── SymbollessERC20.t.sol ├── access │ └── adminable │ │ ├── renounce-admin │ │ ├── renounceAdmin.tree │ │ └── renounceAdmin.t.sol │ │ ├── transfer-admin │ │ ├── transferAdmin.tree │ │ └── transferAdmin.t.sol │ │ └── Adminable.t.sol └── Base.t.sol ├── remappings.txt ├── .env.example ├── .github ├── FUNDING.yml └── workflows │ ├── release.yml │ ├── sync.yml │ ├── deploy-test-token.yml │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .vscode └── settings.json ├── .gitmodules ├── .editorconfig ├── script ├── DeployTestToken.s.sol └── Base.s.sol ├── .solhint.json ├── LICENSE.md ├── src ├── access │ ├── Orchestratable.sol │ ├── Adminable.sol │ ├── IAdminable.sol │ └── IOrchestratable.sol ├── utils │ ├── Address.sol │ └── ReentrancyGuard.sol └── token │ └── erc20 │ ├── ERC20GodMode.sol │ ├── ERC20MissingReturn.sol │ ├── IERC20Normalizer.sol │ ├── IERC20Permit.sol │ ├── ERC20Normalizer.sol │ ├── IERC20Recover.sol │ ├── ERC20Permit.sol │ ├── ERC20Recover.sol │ ├── SafeERC20.sol │ ├── IERC20.sol │ └── ERC20.sol ├── foundry.toml ├── package.json ├── README.md ├── CHANGELOG.md └── pnpm-lock.yaml /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | ignore: 3 | - "script" 4 | - "test" 5 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | proseWrap: "always" 3 | trailingComma: "all" 4 | -------------------------------------------------------------------------------- /test/token/erc20/name/name.tree: -------------------------------------------------------------------------------- 1 | name.t.sol 2 | └── it should return the ERC-20 name 3 | -------------------------------------------------------------------------------- /test/token/erc20/symbol/symbol.tree: -------------------------------------------------------------------------------- 1 | symbol.t.sol 2 | └── it should return the ERC-20 symbol 3 | -------------------------------------------------------------------------------- /test/token/erc20/decimals/decimals.tree: -------------------------------------------------------------------------------- 1 | decimals.t.sol 2 | └── it should return the ERC-20 decimals 3 | -------------------------------------------------------------------------------- /test/token/erc20-permit/version/version.tree: -------------------------------------------------------------------------------- 1 | version.t.sol 2 | └── it should return the correct EIP-2612 version 3 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @prb/math/=lib/prb-math/contracts/ 2 | @prb/test/=lib/prb-test/src/ 3 | forge-std/=lib/forge-std/src/ 4 | -------------------------------------------------------------------------------- /test/token/erc20-permit/permit-typehash/permitTypehash.tree: -------------------------------------------------------------------------------- 1 | permitTypehash.t.sol 2 | └── it should return the correct EIP-2612 permit typehash 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | export API_KEY_ETHERSCAN="YOUR_API_KEY_ETHERSCAN" 2 | export API_KEY_INFURA="YOUR_API_KEY_INFURA" 3 | export MNEMONIC="YOUR_MNEMONIC" 4 | -------------------------------------------------------------------------------- /test/token/erc20-permit/domain-separator/domainSeparator.tree: -------------------------------------------------------------------------------- 1 | domainSeparator.t.sol 2 | └── it should return the correct EIP-2612 domain separator 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: "https://3cities.xyz/#/pay?c=CAESFAKY9DMuOFdjE4Wzl2YyUFipPiSfIgICATICCAJaFURvbmF0aW9uIHRvIFBhdWwgQmVyZw" 2 | github: "PaulRBerg" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # directories 2 | broadcast 3 | cache 4 | node_modules 5 | out 6 | 7 | # files 8 | *.env 9 | *.log 10 | .DS_Store 11 | .pnp.* 12 | lcov.info 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /test/token/erc20-recover/is-token-denylist-set/isTokenDenylistSet.tree: -------------------------------------------------------------------------------- 1 | isTokenDenylistSet.t.sol 2 | ├── when the token denylist was not set 3 | │ └── it should return false 4 | └── when the token denylist was set 5 | └── it should return true 6 | -------------------------------------------------------------------------------- /test/token/erc20/balance-of/balanceOf.tree: -------------------------------------------------------------------------------- 1 | balanceOf.t.sol 2 | ├── when the provided address has a zero balance 3 | │ └── it should return zero 4 | └── when the provided address has a non-zero balance 5 | └── it should return the correct balance 6 | -------------------------------------------------------------------------------- /test/shared/ERC20NormalizerMock.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20Normalizer } from "src/token/erc20/ERC20Normalizer.sol"; 5 | 6 | contract ERC20NormalizerMock is ERC20Normalizer { } 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .github/workflows 3 | broadcast 4 | cache 5 | lib 6 | node_modules 7 | out 8 | 9 | # files 10 | *.env 11 | *.log 12 | .DS_Store 13 | .pnp.* 14 | lcov.info 15 | package-lock.json 16 | pnpm-lock.json 17 | yarn.lock 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[solidity]": { 3 | "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" 4 | }, 5 | "[toml]": { 6 | "editor.defaultFormatter": "tamasfe.even-better-toml" 7 | }, 8 | "solidity.formatter": "forge" 9 | } 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | branch = "v1" 3 | path = "lib/forge-std" 4 | url = "https://github.com/foundry-rs/forge-std" 5 | [submodule "lib/prb-test"] 6 | branch = "release-v0" 7 | path = "lib/prb-test" 8 | url = "https://github.com/PaulRBerg/prb-test" 9 | -------------------------------------------------------------------------------- /test/access/adminable/renounce-admin/renounceAdmin.tree: -------------------------------------------------------------------------------- 1 | renounceAdmin.t.sol 2 | ├── when the caller is not the admin 3 | │ └── it should revert 4 | └── when the caller is the admin 5 | └── it should set the new admin 6 | └── it should emit a {TransferAdmin} event and set the new admin 7 | -------------------------------------------------------------------------------- /test/token/erc20/decrease-allowance/decreaseAllowance.tree: -------------------------------------------------------------------------------- 1 | decreaseAllowance.t.sol 2 | ├── when the calculation underflows uint256 3 | │ └── it should revert 4 | └── when the calculation does not underflow uint256 5 | ├── it should decrease the allowance 6 | └── it should emit an Approval event 7 | -------------------------------------------------------------------------------- /test/token/erc20/increase-allowance/increaseAllowance.tree: -------------------------------------------------------------------------------- 1 | increaseAllowance.t.sol 2 | ├── when the calculation overflows uint256 3 | │ └── it should revert 4 | └── when the calculation does not overflow uint256 5 | ├── it should increase the allowance 6 | └── it should emit an Approval event 7 | -------------------------------------------------------------------------------- /test/token/erc20/transfer-from/transferFrom.tree: -------------------------------------------------------------------------------- 1 | ├── when the spender's allowance is not enough 2 | │ └── it should revert 3 | └── when the spender's allowance is enough 4 | ├── it should decrease the owner's balance 5 | ├── it should increase the receiver's balance 6 | └── it should emit an Approval and a Transfer event 7 | 8 | -------------------------------------------------------------------------------- /test/shared/ERC20RecoverMock.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20Recover } from "src/token/erc20/ERC20Recover.sol"; 5 | 6 | contract ERC20RecoverMock is ERC20Recover { 7 | function __godMode_setIsTokenDenylistSet(bool newState) external { 8 | isTokenDenylistSet = newState; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # All files 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.sol] 16 | indent_size = 4 17 | 18 | [*.tree] 19 | indent_size = 1 20 | -------------------------------------------------------------------------------- /test/token/erc20/approve/approve.tree: -------------------------------------------------------------------------------- 1 | approve.t.sol 2 | ├── when the owner is the zero address 3 | │ └── it should revert 4 | └── when the owner is not the zero address 5 | ├── when the spender is the zero address 6 | │ └── it should revert 7 | └── when the spender is not the zero address 8 | ├── it should make the approval 9 | └── it should emit an Approval event 10 | -------------------------------------------------------------------------------- /test/token/erc20-recover/get-token-denylist/getTokenDenylist.tree: -------------------------------------------------------------------------------- 1 | getTokenDenylist.t.sol 2 | ├── when the token denylist was not set 3 | │ └── it should return an empty array 4 | └── when the token denylist was set 5 | ├── when the token denylist was set to an empty array 6 | │ └── it should return an empty array 7 | └── when the token denylist was set to a non- empty array 8 | └── it should return the token denylist 9 | -------------------------------------------------------------------------------- /test/token/erc20/name/name.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20_Test } from "../ERC20.t.sol"; 5 | 6 | contract Name_Test is ERC20_Test { 7 | function test_Name() external { 8 | string memory actualName = dai.name(); 9 | string memory expectedName = "Dai Stablecoin"; 10 | assertEq(actualName, expectedName, "name"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/token/erc20/decimals/decimals.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20_Test } from "../ERC20.t.sol"; 5 | 6 | contract Decimals_Test is ERC20_Test { 7 | function test_Decimals() external { 8 | uint8 actualDecimals = dai.decimals(); 9 | uint8 expectedDecimals = 18; 10 | assertEq(actualDecimals, expectedDecimals, "decimals"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/token/erc20/symbol/symbol.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20_Test } from "../ERC20.t.sol"; 5 | 6 | contract Symbol_Test is ERC20_Test { 7 | function test_Symbol() external { 8 | string memory actualSymbol = dai.symbol(); 9 | string memory expectedSymbol = "DAI"; 10 | assertEq(actualSymbol, expectedSymbol, "symbol"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | 8 | jobs: 9 | release: 10 | runs-on: "ubuntu-latest" 11 | steps: 12 | - name: "Check out the repo" 13 | uses: "actions/checkout@v3" 14 | 15 | - name: "Release" 16 | uses: "docker://antonyurchenko/git-release:v5" 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /test/token/erc20-normalizer/compute-scalar/computeScalar.tree: -------------------------------------------------------------------------------- 1 | computeScalar.t.sol 2 | ├── when the token decimals are zero 3 | │ └── it should revert 4 | └── when the token decimals are not zero 5 | ├── when the token decimals are greater than 18 6 | │ └── it should revert 7 | ├── when the token decimals are equal to 18 8 | │ └── it should compute the scalar 9 | └── when the token decimals are less than 18 10 | └── it should compute the scalar 11 | -------------------------------------------------------------------------------- /test/token/erc20-permit/version/version.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20Permit_Test } from "../ERC20Permit.t.sol"; 5 | 6 | contract Version_Test is ERC20Permit_Test { 7 | function test_Version() external { 8 | string memory actualVersion = erc20Permit.version(); 9 | string memory expectedVersion = version; 10 | assertEq(actualVersion, expectedVersion, "version"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/token/erc20/burn/burn.tree: -------------------------------------------------------------------------------- 1 | burn.t.sol 2 | ├── when the holder is the zero address 3 | │ └── it should revert 4 | └── when the holder is not the zero address 5 | ├── when the holder balance calculation underflows uint256 6 | │ └── it should revert 7 | └── when the holder balance calculation does not underflow uint256 8 | ├── it should decrease the balance of the holder 9 | ├── it should decrease the total supply 10 | └── it should emit a Transfer event 11 | -------------------------------------------------------------------------------- /test/token/erc20-permit/permit-typehash/permitTypehash.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20Permit_Test } from "../ERC20Permit.t.sol"; 5 | 6 | contract PermitTypehash_Test is ERC20Permit_Test { 7 | function test_PermitTypehash() external { 8 | bytes32 actualPermitTypehash = erc20Permit.PERMIT_TYPEHASH(); 9 | bytes32 expectedPermitTypehash = PERMIT_TYPEHASH; 10 | assertEq(actualPermitTypehash, expectedPermitTypehash, "permitTypehash"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/token/erc20-recover/set-token-denylist/setTokenDenylist.tree: -------------------------------------------------------------------------------- 1 | setTokenDenylist.t.sol 2 | ├── when the caller is not the admin 3 | │ └── it should revert 4 | └── when the caller is the admin 5 | ├── when the token denylist was already set 6 | │ └── it should revert 7 | └── when the token denylist was not already set 8 | ├── when some tokens don't have a symbol 9 | │ └── it should revert 10 | └── when all tokens have a symbol 11 | ├── it initializes the contract 12 | └── it emits a SetTokenDenylist event 13 | -------------------------------------------------------------------------------- /test/token/erc20-permit/domain-separator/domainSeparator.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20Permit_Test } from "../ERC20Permit.t.sol"; 5 | 6 | contract DomainSeparator_Test is ERC20Permit_Test { 7 | function test_DomainSeparator() external { 8 | bytes32 actualDomainSeparator = erc20Permit.DOMAIN_SEPARATOR(); 9 | bytes32 expectedDomainSeparator = DOMAIN_SEPARATOR; 10 | assertEq(actualDomainSeparator, expectedDomainSeparator, "domainSeparator"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /script/DeployTestToken.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.19 <=0.9.0; 3 | 4 | import { Script } from "forge-std/Script.sol"; 5 | 6 | import { ERC20GodMode } from "../src/token/erc20/ERC20GodMode.sol"; 7 | 8 | import { BaseScript } from "./Base.s.sol"; 9 | 10 | /// @notice Deploys a test ERC-20 token with infinite minting and burning capabilities. 11 | contract DeployTestToken is Script, BaseScript { 12 | function run() public virtual broadcast returns (ERC20GodMode token) { 13 | token = new ERC20GodMode("Test token", "TKN", 18); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/access/adminable/transfer-admin/transferAdmin.tree: -------------------------------------------------------------------------------- 1 | transferAdmin.t.sol 2 | ├── when the caller is not the admin 3 | │ └── it should revert 4 | └── when the caller is the admin 5 | ├── when the new admin is the zero address 6 | │ └── it should revert 7 | └── when the new admin is not the zero address 8 | ├── when the admin is the same as the current admin 9 | │ ├── it should re-set the admin 10 | │ └── it should emit a TransferAdmin event 11 | └── when the admin is not the same as the current admin 12 | ├── it should set the new admin 13 | └── it should emit a TransferAdmin event 14 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "code-complexity": ["error", 7], 5 | "compiler-version": ["error", ">=0.8.4"], 6 | "contract-name-camelcase": "off", 7 | "const-name-snakecase": "off", 8 | "constructor-syntax": "error", 9 | "func-name-mixedcase": "off", 10 | "func-visibility": ["error", { "ignoreConstructors": true }], 11 | "max-line-length": ["error", 120], 12 | "no-empty-blocks": "off", 13 | "no-inline-assembly": "off", 14 | "not-rely-on-time": "off", 15 | "reason-string": ["warn", { "maxLength": 64 }], 16 | "var-name-mixedcase": "off" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/token/erc20-recover/recover/recover.tree: -------------------------------------------------------------------------------- 1 | recover.tree 2 | ├── when the caller is not the owner 3 | │ └── it should revert 4 | └── when the caller is the owner 5 | ├── when the token denylist was not set 6 | │ └── it should revert 7 | └── when the token denylist was set 8 | ├── when the recover amount is zero 9 | │ └── it should revert 10 | └── when the recover amount is not zero 11 | ├── when the token is not recoverable 12 | │ └── it should revert 13 | └── when the token is recoverable 14 | ├── it should recover the tokens 15 | └── it should emit a Recover event 16 | -------------------------------------------------------------------------------- /test/token/erc20-normalizer/normalize/normalize.tree: -------------------------------------------------------------------------------- 1 | normalize.t.sol 2 | ├── when the scalar has not been computed 3 | │ └── it should return the normalized amount 4 | └── when the scalar has been computed 5 | ├── when the scalar is one 6 | │ └── it should return the normalized amount 7 | └── when the scalar is not one 8 | ├── when the calculation overflows uint256 9 | │ └── it should revert 10 | └── when the calculation does not overflow uint256 11 | ├── when the amount is zero 12 | │ └── it should return zero 13 | └── when the amount is not zero 14 | └── it should return the normalized amount 15 | -------------------------------------------------------------------------------- /test/token/erc20/mint/mint.tree: -------------------------------------------------------------------------------- 1 | mint.t.sol 2 | ├── when the beneficiary is the zero address 3 | │ └── it should revert 4 | └── when the beneficiary is not the zero address 5 | ├── when the beneficiary balance calculation overflows uint256 6 | │ └── it should revert 7 | └── when the beneficiary balance calculation does not overflow uint256 8 | ├── when the total supply calculation overflows uint256 9 | │ └── it should revert 10 | └── when the total supply calculation does not overflow uint256 11 | ├── it should increase the balance of the beneficiary 12 | ├── it should increase the total supply 13 | └── it should emit a Transfer event 14 | -------------------------------------------------------------------------------- /test/token/erc20-normalizer/denormalize/denormalize.tree: -------------------------------------------------------------------------------- 1 | denormalize.t.sol 2 | ├── when the scalar has not been computed 3 | │ └── it should return the denormalized amount 4 | └── when the scalar has been computed 5 | ├── when the scalar is 1 6 | │ └── it should return the denormalized amount 7 | └── when the scalar is not 1 8 | ├── when the amount is 0 9 | │ └── it should return 0 10 | └── when the amount is not 0 11 | ├── when the amount is smaller than the scalar 12 | │ └── it should return 0 13 | ├── when the amount is equal to the scalar 14 | │ └── it should return 1 15 | └── when the amount is bigger than the scalar 16 | └── it should return the denormalized amount 17 | -------------------------------------------------------------------------------- /test/token/erc20/transfer/transfer.tree: -------------------------------------------------------------------------------- 1 | transfer.t.sol 2 | ├── when the sender is the zero address 3 | │ └── it should revert 4 | └── when the sender is not the zero address 5 | ├── when the receiver is the zero address 6 | │ └── it should revert 7 | └── when the receiver is not the zero address 8 | ├── when the receiver is the sender 9 | │ └── it should make the transfer 10 | └── when the receiver is not the sender 11 | ├── when the sender does not have enough balance 12 | │ └── it should revert 13 | └── when the sender has enough balance 14 | ├── it should decrease the sender's balance 15 | ├── it should decrease the recipient's balance 16 | └── it should emit a Transfer event 17 | -------------------------------------------------------------------------------- /test/token/erc20-recover/is-token-denylist-set/isTokenDenylistSet.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20Recover_Test } from "../ERC20Recover.t.sol"; 5 | 6 | contract IsTokenDenylistSet_Test is ERC20Recover_Test { 7 | function test_IsTokenDenylistSet_TokenDenylistNotSet() external { 8 | bool isTokenDenylistSet = erc20Recover.isTokenDenylistSet(); 9 | assertFalse(isTokenDenylistSet, "isTokenDenylistSet"); 10 | } 11 | 12 | function test_IsTokenDenylistSet_TokenDenylistSet() external { 13 | erc20Recover.setTokenDenylist(TOKEN_DENYLIST); 14 | bool isTokenDenylistSet = erc20Recover.isTokenDenylistSet(); 15 | assertTrue(isTokenDenylistSet, "isTokenDenylistSet"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: "Sync" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v5.x" 7 | 8 | jobs: 9 | sync-release-branch: 10 | runs-on: "ubuntu-latest" 11 | if: "startsWith(github.ref, 'refs/tags/v5.')" 12 | steps: 13 | - name: "Check out the repo" 14 | uses: "actions/checkout@v3" 15 | with: 16 | fetch-depth: 0 17 | ref: "release-v5" 18 | 19 | - name: "Configure Git" 20 | run: | 21 | git config user.name github-actions[bot] 22 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 23 | 24 | - name: "Sync Release Branch" 25 | run: | 26 | git fetch --tags 27 | git checkout release-v5 28 | git reset --hard ${GITHUB_REF} 29 | git push --force 30 | -------------------------------------------------------------------------------- /test/token/erc20-permit/permit/permit.tree: -------------------------------------------------------------------------------- 1 | permit.t.sol 2 | ├── when the owner is the zero address 3 | │ └── it should revert 4 | └── when the owner is not the zero address 5 | ├── when the spender is the zero address 6 | │ └── it should revert 7 | └── when the spender is not the zero address 8 | ├── when the deadline is in the past 9 | │ └── it should revert 10 | └── when the deadline is not in the past 11 | ├── when the recovered owner is the zero address 12 | │ └── it should revert 13 | └── when the recovered owner is not the zero address 14 | ├── when the signature is not valid 15 | │ └── it should revert 16 | └── when the signature is valid 17 | ├── it should update the spender's allowance 18 | ├── it should increase the nonce of the owner 19 | └── it should emit an Approval event 20 | -------------------------------------------------------------------------------- /test/token/erc20-normalizer/ERC20Normalizer.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20NormalizerMock } from "../../shared/ERC20NormalizerMock.t.sol"; 5 | import { Base_Test } from "../../Base.t.sol"; 6 | 7 | /// @notice Common logic needed by all {ERC20Normalizer} unit tests. 8 | abstract contract ERC20Normalizer_Test is Base_Test { 9 | /*////////////////////////////////////////////////////////////////////////// 10 | CONSTANTS 11 | //////////////////////////////////////////////////////////////////////////*/ 12 | 13 | uint256 internal constant STANDARD_DECIMALS = 18; 14 | uint256 internal constant USDC_SCALAR = 10 ** 12; 15 | 16 | /*////////////////////////////////////////////////////////////////////////// 17 | TEST CONTRACTS 18 | //////////////////////////////////////////////////////////////////////////*/ 19 | 20 | ERC20NormalizerMock internal erc20Normalizer = new ERC20NormalizerMock(); 21 | } 22 | -------------------------------------------------------------------------------- /test/token/erc20/balance-of/balanceOf.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20_Test } from "../ERC20.t.sol"; 5 | 6 | contract BalanceOf_Test is ERC20_Test { 7 | function test_BalanceOf_DoesNotHaveBalance(address foo) external { 8 | uint256 actualBalance = dai.balanceOf(foo); 9 | uint256 expectedBalance = 0; 10 | assertEq(actualBalance, expectedBalance, "balance"); 11 | } 12 | 13 | modifier whenBalanceNonZero() { 14 | _; 15 | } 16 | 17 | function testFuzz_BalanceOf(address foo, uint256 amount) external whenBalanceNonZero { 18 | vm.assume(foo != address(0)); 19 | amount = _bound(amount, 1, ONE_MILLION_DAI); 20 | 21 | // Mint `amount` tokens to `foo`. 22 | dai.mint({ beneficiary: foo, amount: amount }); 23 | 24 | // Run the test. 25 | uint256 actualBalance = dai.balanceOf(foo); 26 | uint256 expectedBalance = amount; 27 | assertEq(actualBalance, expectedBalance, "balance"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Paul Razvan Berg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /src/access/Orchestratable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | import { Adminable } from "./Adminable.sol"; 5 | import { IOrchestratable } from "./IOrchestratable.sol"; 6 | 7 | /// @title Orchestratable 8 | /// @author Paul Razvan Berg 9 | contract Orchestratable is 10 | IOrchestratable, // 1 inherited component 11 | Adminable // 1 inherited component 12 | { 13 | /// @inheritdoc IOrchestratable 14 | address public override conductor; 15 | 16 | /// @inheritdoc IOrchestratable 17 | mapping(address => mapping(bytes4 => bool)) public override orchestration; 18 | 19 | /// @notice Restricts usage to authorized accounts. 20 | modifier onlyOrchestrated() { 21 | if (!orchestration[msg.sender][msg.sig]) { 22 | revert Orchestratable_NotOrchestrated(msg.sender, msg.sig); 23 | } 24 | _; 25 | } 26 | 27 | /// @inheritdoc IOrchestratable 28 | function orchestrate(address account, bytes4 signature) public override onlyAdmin { 29 | orchestration[account][signature] = true; 30 | emit GrantAccess(account); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/deploy-test-token.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy Test Token" 2 | 3 | env: 4 | API_KEY_ETHERSCAN: ${{ secrets.API_KEY_ETHERSCAN }} 5 | API_KEY_INFURA: ${{ secrets.API_KEY_INFURA }} 6 | MNEMONIC: ${{ secrets.MNEMONIC }} 7 | 8 | on: 9 | workflow_dispatch: 10 | inputs: 11 | chain: 12 | default: "goerli" 13 | description: "Chain name as defined in the Foundry config." 14 | required: false 15 | 16 | jobs: 17 | deploy-test-token: 18 | runs-on: "ubuntu-latest" 19 | steps: 20 | - name: "Check out the repo" 21 | uses: "actions/checkout@v3" 22 | with: 23 | submodules: "recursive" 24 | 25 | - name: "Install Foundry" 26 | uses: "foundry-rs/foundry-toolchain@v1" 27 | 28 | - name: "Deploy a test ERC-20 token contract" 29 | run: >- 30 | forge script script/DeployTestToken.s.sol 31 | --broadcast 32 | --rpc-url "${{github.event.inputs.chain }}" 33 | --verify 34 | -vvvv 35 | 36 | - name: "Add summary" 37 | run: | 38 | echo "## Deployment result" >> $GITHUB_STEP_SUMMARY 39 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 40 | -------------------------------------------------------------------------------- /test/access/adminable/renounce-admin/renounceAdmin.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { IAdminable } from "src/access/IAdminable.sol"; 5 | 6 | import { AdminableTest } from "../Adminable.t.sol"; 7 | 8 | contract RenounceAdmin_Test is AdminableTest { 9 | function testFuzz_RevertWhen_CallerNotAdmin(address eve) external { 10 | vm.assume(eve != address(0) && eve != users.admin); 11 | 12 | // Make Eve the caller in this test. 13 | changePrank(eve); 14 | 15 | // Run the test. 16 | vm.expectRevert(abi.encodeWithSelector(IAdminable.Adminable_CallerNotAdmin.selector, users.admin, eve)); 17 | adminable.renounceAdmin(); 18 | } 19 | 20 | modifier whenCallerAdmin() { 21 | _; 22 | } 23 | 24 | function test_RenounceAdmin() external whenCallerAdmin { 25 | vm.expectEmit({ emitter: address(adminable) }); 26 | emit TransferAdmin({ oldAdmin: users.admin, newAdmin: address(0) }); 27 | adminable.renounceAdmin(); 28 | address actualAdmin = adminable.admin(); 29 | address expectedAdmin = address(0); 30 | assertEq(actualAdmin, expectedAdmin, "admin"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | # Full reference https://github.com/foundry-rs/foundry/tree/master/config 2 | 3 | [profile.default] 4 | auto_detect_solc = false 5 | bytecode_hash = "none" 6 | cbor_metadata = false 7 | evm_version = "paris" 8 | gas_reports = ["*"] 9 | libs = ["lib"] 10 | optimizer = true 11 | optimizer_runs = 10_000 12 | out = "out" 13 | script = "script" 14 | solc = "0.8.19" 15 | src = "src" 16 | test = "test" 17 | 18 | [profile.default.fuzz] 19 | max_test_rejects = 100_000 20 | runs = 1_000 21 | 22 | [profile.ci] 23 | fuzz = { runs = 10_000 } 24 | 25 | [etherscan] 26 | goerli = { key = "${API_KEY_ETHERSCAN}" } 27 | mainnet = { key = "${API_KEY_ETHERSCAN}" } 28 | sepolia = { key = "${API_KEY_ETHERSCAN}" } 29 | 30 | [fmt] 31 | bracket_spacing = true 32 | int_types = "long" 33 | line_length = 120 34 | multiline_func_header = "all" 35 | number_underscore = "thousands" 36 | quote_style = "double" 37 | tab_width = 4 38 | wrap_comments = true 39 | 40 | [rpc_endpoints] 41 | goerli = "https://goerli.infura.io/v3/${API_KEY_INFURA}" 42 | localhost = "http://localhost:8545" 43 | mainnet = "https://mainnet.infura.io/v3/${API_KEY_INFURA}" 44 | sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}" 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prb/contracts", 3 | "description": "Off-the-shelf Solidity smart contracts", 4 | "version": "5.0.6", 5 | "author": { 6 | "name": "Paul Razvan Berg", 7 | "url": "https://github.com/PaulRBerg" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/PaulRBerg/prb-contracts/issues" 11 | }, 12 | "devDependencies": { 13 | "prettier": "^2.8.7", 14 | "solhint-community": "^3.5.2" 15 | }, 16 | "files": [ 17 | "src", 18 | "CHANGELOG.md" 19 | ], 20 | "homepage": "https://github.com/PaulRBerg/prb-contracts#readme", 21 | "keywords": [ 22 | "blockchain", 23 | "decentralized-finance", 24 | "erc20", 25 | "erc20-permit", 26 | "ethereum", 27 | "library", 28 | "smart-contracts", 29 | "solidity" 30 | ], 31 | "license": "MIT", 32 | "publishConfig": { 33 | "access": "public" 34 | }, 35 | "repository": "github:PaulRBerg/prb-contracts", 36 | "scripts": { 37 | "build": "forge build", 38 | "clean": "rm -rf broadcast cache out", 39 | "lint": "pnpm lint:sol && pnpm prettier:check", 40 | "lint:sol": "forge fmt --check && pnpm solhint \"{script,src,test}/**/*.sol\"", 41 | "prettier:check": "prettier --check \"**/*.{json,md,yml}\"", 42 | "prettier:write": "prettier --write \"**/*.{json,md,yml}\"" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/token/erc20-recover/get-token-denylist/getTokenDenylist.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { IERC20 } from "src/token/erc20/IERC20.sol"; 5 | 6 | import { ERC20Recover_Test } from "../ERC20Recover.t.sol"; 7 | 8 | contract GetTokenDenylist_Test is ERC20Recover_Test { 9 | function test_GetTokenDenylist_TokenDenylistNotSet() external { 10 | IERC20[] memory actualTokenDenylist = erc20Recover.getTokenDenylist(); 11 | IERC20[] memory expectedTokenDenylist; 12 | assertEq(actualTokenDenylist, expectedTokenDenylist, "tokenDenylist"); 13 | } 14 | 15 | function test_GetTokenDenylist_EmptyArray() external { 16 | IERC20[] memory tokenDenylist; 17 | erc20Recover.setTokenDenylist(tokenDenylist); 18 | 19 | IERC20[] memory actualTokenDenylist = erc20Recover.getTokenDenylist(); 20 | IERC20[] memory expectedTokenDenylist; 21 | assertEq(actualTokenDenylist, expectedTokenDenylist, "tokenDenylist"); 22 | } 23 | 24 | function test_GetTokenDenylist_NonEmptyArray() external { 25 | erc20Recover.setTokenDenylist(TOKEN_DENYLIST); 26 | IERC20[] memory actualTokenDenylist = erc20Recover.getTokenDenylist(); 27 | IERC20[] memory expectedTokenDenylist = TOKEN_DENYLIST; 28 | assertEq(actualTokenDenylist, expectedTokenDenylist, "tokenDenylist"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/access/adminable/Adminable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { Adminable } from "src/access/Adminable.sol"; 5 | import { IAdminable } from "src/access/IAdminable.sol"; 6 | 7 | import { Base_Test } from "../../Base.t.sol"; 8 | 9 | /// @notice Common logic needed by all ERC20Recover unit tests. 10 | abstract contract AdminableTest is Base_Test { 11 | /*////////////////////////////////////////////////////////////////////////// 12 | EVENTS 13 | //////////////////////////////////////////////////////////////////////////*/ 14 | 15 | event TransferAdmin(address indexed oldAdmin, address indexed newAdmin); 16 | 17 | /*////////////////////////////////////////////////////////////////////////// 18 | TEST CONTRACTS 19 | //////////////////////////////////////////////////////////////////////////*/ 20 | 21 | IAdminable internal adminable; 22 | 23 | /*////////////////////////////////////////////////////////////////////////// 24 | SETUP FUNCTION 25 | //////////////////////////////////////////////////////////////////////////*/ 26 | 27 | /// @dev A setup function invoked before each test case. 28 | function setUp() public virtual override { 29 | Base_Test.setUp(); 30 | adminable = new Adminable(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/Address.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | /// @title Address 5 | /// @author Paul Razvan Berg 6 | /// @notice Collection of functions related to the address type. 7 | /// @dev Forked from OpenZeppelin 8 | /// https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/v3.4.0/contracts/utils/Address.sol 9 | library Address { 10 | /// @dev Returns true if `account` is a contract. 11 | /// 12 | /// IMPORTANT: It is unsafe to assume that an address for which this function returns false is an 13 | /// externally-owned account (EOA) and not a contract. 14 | /// 15 | /// Among others, `isContract` will return false for the following types of addresses: 16 | /// 17 | /// - An externally-owned account 18 | /// - A contract in construction 19 | /// - An address where a contract will be created 20 | /// - An address where a contract lived, but was destroyed 21 | function isContract(address account) internal view returns (bool) { 22 | // According to EIP-1052, 0x0 is the value returned for not-yet created accounts 23 | // and 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is returned 24 | // for accounts without code, i.e. `keccak256('')`. 25 | bytes32 codehash; 26 | bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; 27 | // solhint-disable-next-line no-inline-assembly 28 | assembly { 29 | codehash := extcodehash(account) 30 | } 31 | return (codehash != accountHash && codehash != 0x0); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/token/erc20/ERC20.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { Base_Test } from "../../Base.t.sol"; 5 | 6 | /// @notice Common logic needed by all {ERC20} tests. 7 | abstract contract ERC20_Test is Base_Test { 8 | /*////////////////////////////////////////////////////////////////////////// 9 | EVENTS 10 | //////////////////////////////////////////////////////////////////////////*/ 11 | 12 | event Approval(address indexed owner, address indexed spender, uint256 amount); 13 | event Transfer(address indexed from, address indexed to, uint256 amount); 14 | 15 | /*////////////////////////////////////////////////////////////////////////// 16 | CONSTANTS 17 | //////////////////////////////////////////////////////////////////////////*/ 18 | 19 | uint256 internal constant TRANSFER_AMOUNT = 100e18; 20 | 21 | /*////////////////////////////////////////////////////////////////////////// 22 | SETUP FUNCTION 23 | //////////////////////////////////////////////////////////////////////////*/ 24 | 25 | function setUp() public virtual override { 26 | Base_Test.setUp(); 27 | 28 | // Burn the token balances so that they do not interfere with the tests. 29 | dai.burn(users.alice, ONE_MILLION_DAI); 30 | dai.burn(users.admin, ONE_MILLION_DAI); 31 | dai.burn(users.bob, ONE_MILLION_DAI); 32 | dai.burn(users.eve, ONE_MILLION_DAI); 33 | 34 | // Make Alice the default caller in all subsequent tests. 35 | changePrank(users.alice); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /script/Base.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.19 <=0.9.0; 3 | 4 | import { Script } from "forge-std/Script.sol"; 5 | 6 | abstract contract BaseScript is Script { 7 | /// @dev Included to enable compilation of the script without a $MNEMONIC environment variable. 8 | string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk"; 9 | 10 | /// @dev Needed for the deterministic deployments. 11 | bytes32 internal constant ZERO_SALT = bytes32(0); 12 | 13 | /// @dev The address of the transaction broadcaster. 14 | address internal broadcaster; 15 | 16 | /// @dev Used to derive the broadcaster's address if $ETH_FROM is not defined. 17 | string internal mnemonic; 18 | 19 | /// @dev Initializes the transaction broadcaster like this: 20 | /// 21 | /// - If $ETH_FROM is defined, use it. 22 | /// - Otherwise, derive the broadcaster address from $MNEMONIC. 23 | /// - If $MNEMONIC is not defined, default to a test mnemonic. 24 | /// 25 | /// The use case for $ETH_FROM is to specify the broadcaster key and its address via the command line. 26 | constructor() { 27 | address from = vm.envOr({ name: "ETH_FROM", defaultValue: address(0) }); 28 | if (from != address(0)) { 29 | broadcaster = from; 30 | } else { 31 | mnemonic = vm.envOr({ name: "MNEMONIC", defaultValue: TEST_MNEMONIC }); 32 | (broadcaster,) = deriveRememberKey({ mnemonic: mnemonic, index: 0 }); 33 | } 34 | } 35 | 36 | modifier broadcast() { 37 | vm.startBroadcast(broadcaster); 38 | _; 39 | vm.stopBroadcast(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/token/erc20/ERC20GodMode.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | import { ERC20 } from "./ERC20.sol"; 5 | 6 | /// @title ERC20GodMode 7 | /// @author Paul Razvan Berg 8 | /// @notice Allows anyone to mint or burn any amount of tokens to any account. 9 | contract ERC20GodMode is ERC20 { 10 | /*////////////////////////////////////////////////////////////////////////// 11 | EVENTS 12 | //////////////////////////////////////////////////////////////////////////*/ 13 | 14 | event Burn(address indexed holder, uint256 amount); 15 | 16 | event Mint(address indexed beneficiary, uint256 amount); 17 | 18 | /*////////////////////////////////////////////////////////////////////////// 19 | CONSTRUCTOR 20 | //////////////////////////////////////////////////////////////////////////*/ 21 | 22 | constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_, decimals_) { } 23 | 24 | /*////////////////////////////////////////////////////////////////////////// 25 | USER-FACING NON-CONSTANT FUNCTIONS 26 | //////////////////////////////////////////////////////////////////////////*/ 27 | 28 | /// @notice Destroys `amount` tokens from `holder`, decreasing the token supply. 29 | /// @param holder The account whose tokens to burn. 30 | /// @param amount The amount of tokens to destroy. 31 | function burn(address holder, uint256 amount) public { 32 | _burn(holder, amount); 33 | } 34 | 35 | /// @notice Prints new tokens into existence and assigns them to `beneficiary`, increasing the 36 | /// total supply. 37 | /// @param beneficiary The account for which to mint the tokens. 38 | /// @param amount The amount of tokens to print into existence. 39 | function mint(address beneficiary, uint256 amount) public { 40 | _mint(beneficiary, amount); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/token/erc20/approve/approve.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { IERC20 } from "src/token/erc20/IERC20.sol"; 5 | 6 | import { ERC20_Test } from "../ERC20.t.sol"; 7 | 8 | contract Approve_Test is ERC20_Test { 9 | function test_RevertWhen_OwnerZeroAddress() external { 10 | // Make the zero address the caller in this test. 11 | changePrank(address(0)); 12 | 13 | // Run the test. 14 | vm.expectRevert(IERC20.ERC20_ApproveOwnerZeroAddress.selector); 15 | dai.approve({ spender: users.alice, value: ONE_MILLION_DAI }); 16 | } 17 | 18 | modifier whenOwnerNotZeroAddress() { 19 | _; 20 | } 21 | 22 | function test_RevertWhen_SpenderZeroAddress() external whenOwnerNotZeroAddress { 23 | vm.expectRevert(IERC20.ERC20_ApproveSpenderZeroAddress.selector); 24 | dai.approve({ spender: address(0), value: ONE_MILLION_DAI }); 25 | } 26 | 27 | modifier whenSpenderNotZeroAddress() { 28 | _; 29 | } 30 | 31 | function testFuzz_Approve( 32 | address spender, 33 | uint256 amount 34 | ) 35 | external 36 | whenOwnerNotZeroAddress 37 | whenSpenderNotZeroAddress 38 | { 39 | vm.assume(spender != address(0)); 40 | dai.approve(spender, amount); 41 | uint256 actualAllowance = dai.allowance({ owner: users.alice, spender: spender }); 42 | uint256 expectedAllowance = amount; 43 | assertEq(actualAllowance, expectedAllowance, "allowance"); 44 | } 45 | 46 | function testFuzz_Approve_Event( 47 | address spender, 48 | uint256 amount 49 | ) 50 | external 51 | whenOwnerNotZeroAddress 52 | whenSpenderNotZeroAddress 53 | { 54 | vm.assume(spender != address(0)); 55 | vm.expectEmit({ emitter: address(dai) }); 56 | emit Approval({ owner: users.alice, spender: spender, amount: amount }); 57 | dai.approve(spender, amount); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/access/adminable/transfer-admin/transferAdmin.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { IAdminable } from "src/access/IAdminable.sol"; 5 | 6 | import { AdminableTest } from "../Adminable.t.sol"; 7 | 8 | contract TransferAdmin_Test is AdminableTest { 9 | function testFuzz_RevertWhen_CallerNotAdmin(address eve) external { 10 | vm.assume(eve != address(0) && eve != users.admin); 11 | 12 | // Make Eve the caller in this test. 13 | changePrank(eve); 14 | 15 | // Run the test. 16 | vm.expectRevert(abi.encodeWithSelector(IAdminable.Adminable_CallerNotAdmin.selector, users.admin, eve)); 17 | adminable.transferAdmin(eve); 18 | } 19 | 20 | modifier whenCallerAdmin() { 21 | _; 22 | } 23 | 24 | function test_RevertWhen_NewAdminZeroAddress() external whenCallerAdmin { 25 | vm.expectRevert(IAdminable.Adminable_AdminZeroAddress.selector); 26 | adminable.transferAdmin(address(0)); 27 | } 28 | 29 | modifier whenAdminNotZeroAddress() { 30 | _; 31 | } 32 | 33 | function test_TransferAdmin_SameAdmin() external whenCallerAdmin whenAdminNotZeroAddress { 34 | vm.expectEmit({ emitter: address(adminable) }); 35 | emit TransferAdmin({ oldAdmin: users.admin, newAdmin: users.admin }); 36 | adminable.transferAdmin(users.admin); 37 | address actualAdmin = adminable.admin(); 38 | address expectedAdmin = users.admin; 39 | assertEq(actualAdmin, expectedAdmin, "admin"); 40 | } 41 | 42 | function testFuzz_TransferAdmin_NewAdmin(address newAdmin) external whenCallerAdmin whenAdminNotZeroAddress { 43 | vm.assume(newAdmin != address(0) && newAdmin != users.admin); 44 | vm.expectEmit({ emitter: address(adminable) }); 45 | emit TransferAdmin({ oldAdmin: users.admin, newAdmin: newAdmin }); 46 | adminable.transferAdmin(newAdmin); 47 | address actualAdmin = adminable.admin(); 48 | address expectedAdmin = newAdmin; 49 | assertEq(actualAdmin, expectedAdmin, "admin"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/token/erc20/increase-allowance/increaseAllowance.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { stdError } from "forge-std/StdError.sol"; 5 | 6 | import { ERC20_Test } from "../ERC20.t.sol"; 7 | 8 | contract IncreaseAllowance_Test is ERC20_Test { 9 | function test_RevertWhen_CalculationOverflowsUint256(address spender, uint256 amount0, uint256 amount1) external { 10 | vm.assume(spender != address(0)); 11 | vm.assume(amount0 > 0); 12 | amount1 = _bound(amount1, MAX_UINT256 - amount0 + 1, MAX_UINT256); 13 | 14 | // Increase the allowance. 15 | dai.increaseAllowance(spender, amount0); 16 | 17 | // Expect an arithmetic error. 18 | vm.expectRevert(stdError.arithmeticError); 19 | 20 | // Increase the allowance again. 21 | dai.increaseAllowance(spender, amount1); 22 | } 23 | 24 | modifier whenCalculationDoesNotOverflowUint256() { 25 | _; 26 | } 27 | 28 | function testFuzz_IncreaseAllowance( 29 | address spender, 30 | uint256 value 31 | ) 32 | external 33 | whenCalculationDoesNotOverflowUint256 34 | { 35 | vm.assume(spender != address(0)); 36 | 37 | // Increase the allowance. 38 | dai.increaseAllowance(spender, value); 39 | 40 | // Assert that the allowance has been increased. 41 | uint256 actualAllowance = dai.allowance(users.alice, spender); 42 | uint256 expectedAllowance = value; 43 | assertEq(actualAllowance, expectedAllowance, "allowance"); 44 | } 45 | 46 | function testFuzz_IncreaseAllowance_Event( 47 | address spender, 48 | uint256 value 49 | ) 50 | external 51 | whenCalculationDoesNotOverflowUint256 52 | { 53 | vm.assume(spender != address(0)); 54 | vm.assume(value > 0); 55 | 56 | // Expect an {Approval} event to be emitted. 57 | vm.expectEmit({ emitter: address(dai) }); 58 | emit Approval(users.alice, spender, value); 59 | 60 | // Increase the allowance. 61 | dai.increaseAllowance(spender, value); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/token/erc20-recover/ERC20Recover.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { IERC20 } from "src/token/erc20/IERC20.sol"; 5 | 6 | import { ERC20RecoverMock } from "../../shared/ERC20RecoverMock.t.sol"; 7 | import { Base_Test } from "../../Base.t.sol"; 8 | import { SymbollessERC20 } from "../../shared/SymbollessERC20.t.sol"; 9 | 10 | /// @notice Common logic needed by all {ERC20Recover} unit tests. 11 | abstract contract ERC20Recover_Test is Base_Test { 12 | /*////////////////////////////////////////////////////////////////////////// 13 | EVENTS 14 | //////////////////////////////////////////////////////////////////////////*/ 15 | 16 | event Recover(address indexed owner, IERC20 indexed token, uint256 amount); 17 | event SetTokenDenylist(address indexed owner, IERC20[] tokenDenylist); 18 | 19 | /*////////////////////////////////////////////////////////////////////////// 20 | CONSTANTS 21 | //////////////////////////////////////////////////////////////////////////*/ 22 | 23 | IERC20[] internal TOKEN_DENYLIST = [dai]; 24 | uint256 internal constant DEFAULT_RECOVER_AMOUNT = 100e6; 25 | 26 | /*////////////////////////////////////////////////////////////////////////// 27 | TEST CONTRACTS 28 | //////////////////////////////////////////////////////////////////////////*/ 29 | 30 | ERC20RecoverMock internal erc20Recover; 31 | SymbollessERC20 internal symbollessToken; 32 | 33 | /*////////////////////////////////////////////////////////////////////////// 34 | SETUP FUNCTION 35 | //////////////////////////////////////////////////////////////////////////*/ 36 | 37 | function setUp() public virtual override { 38 | Base_Test.setUp(); 39 | 40 | // Deploy the contracts. 41 | erc20Recover = new ERC20RecoverMock(); 42 | symbollessToken = new SymbollessERC20("Symbolless Token", 18); 43 | 44 | // Give 100 USDC to the recover contract. 45 | usdc.mint(address(erc20Recover), DEFAULT_RECOVER_AMOUNT); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/token/erc20-normalizer/compute-scalar/computeScalar.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20GodMode } from "src/token/erc20/ERC20GodMode.sol"; 5 | import { IERC20Normalizer } from "src/token/erc20/IERC20Normalizer.sol"; 6 | 7 | import { ERC20Normalizer_Test } from "../ERC20Normalizer.t.sol"; 8 | 9 | contract ComputeScalar_Test is ERC20Normalizer_Test { 10 | ERC20GodMode internal tkn19 = new ERC20GodMode("Token 19", "TKN19", 19); 11 | ERC20GodMode internal tkn255 = new ERC20GodMode("Token 255", "TKN18", 255); 12 | 13 | function test_RevertWhen_TokenDecimalsZero() external { 14 | vm.expectRevert(abi.encodeWithSelector(IERC20Normalizer.IERC20Normalizer_TokenDecimalsZero.selector, tkn0)); 15 | erc20Normalizer.computeScalar({ token: tkn0 }); 16 | } 17 | 18 | modifier whenTokenDecimalsNotZero() { 19 | _; 20 | } 21 | 22 | function test_RevertWhen_TokenDecimalsGreaterThan18_TokenDecimals19() external whenTokenDecimalsNotZero { 23 | uint256 decimals = 19; 24 | vm.expectRevert( 25 | abi.encodeWithSelector( 26 | IERC20Normalizer.IERC20Normalizer_TokenDecimalsGreaterThan18.selector, tkn19, decimals 27 | ) 28 | ); 29 | erc20Normalizer.computeScalar({ token: tkn19 }); 30 | } 31 | 32 | function test_RevertWhen_TokenDecimalsGreaterThan18_TokenDecimals255() external whenTokenDecimalsNotZero { 33 | uint256 decimals = 255; 34 | vm.expectRevert( 35 | abi.encodeWithSelector( 36 | IERC20Normalizer.IERC20Normalizer_TokenDecimalsGreaterThan18.selector, tkn255, decimals 37 | ) 38 | ); 39 | erc20Normalizer.computeScalar({ token: tkn255 }); 40 | } 41 | 42 | function test_ComputeScalar_TokenDecimalsEqualTo18() external { 43 | erc20Normalizer.computeScalar({ token: dai }); 44 | uint256 actualScalar = erc20Normalizer.getScalar({ token: dai }); 45 | uint256 expectedScalar = 1; 46 | assertEq(actualScalar, expectedScalar, "scalar"); 47 | } 48 | 49 | function test_ComputeScalar_TokenDecimalsLessThan18() external { 50 | erc20Normalizer.computeScalar({ token: usdc }); 51 | uint256 actualScalar = erc20Normalizer.getScalar({ token: usdc }); 52 | uint256 expectedScalar = 10 ** (18 - usdc.decimals()); 53 | assertEq(actualScalar, expectedScalar, "scalar"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/shared/SymbollessERC20.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | contract SymbollessERC20 { 5 | uint8 public decimals; 6 | 7 | string public name; 8 | 9 | uint256 public totalSupply; 10 | 11 | mapping(address => mapping(address => uint256)) internal _allowances; 12 | 13 | mapping(address => uint256) internal _balances; 14 | 15 | event Transfer(address indexed from, address indexed to, uint256 amount); 16 | 17 | event Approval(address indexed owner, address indexed spender, uint256 amount); 18 | 19 | constructor(string memory name_, uint8 decimals_) { 20 | name = name_; 21 | decimals = decimals_; 22 | } 23 | 24 | function allowance(address owner, address spender) public view returns (uint256) { 25 | return _allowances[owner][spender]; 26 | } 27 | 28 | function balanceOf(address account) public view returns (uint256) { 29 | return _balances[account]; 30 | } 31 | 32 | function approve(address spender, uint256 amount) public returns (bool) { 33 | _approve(msg.sender, spender, amount); 34 | return true; 35 | } 36 | 37 | function burn(address holder, uint256 amount) public { 38 | _balances[holder] -= amount; 39 | totalSupply -= amount; 40 | emit Transfer(holder, address(0), amount); 41 | } 42 | 43 | function mint(address beneficiary, uint256 amount) public { 44 | _balances[beneficiary] += amount; 45 | totalSupply += amount; 46 | emit Transfer(address(0), beneficiary, amount); 47 | } 48 | 49 | function transfer(address to, uint256 amount) public returns (bool) { 50 | _transfer(msg.sender, to, amount); 51 | return true; 52 | } 53 | 54 | function transferFrom(address from, address to, uint256 amount) public returns (bool) { 55 | _transfer(from, to, amount); 56 | _approve(from, msg.sender, _allowances[from][msg.sender] - amount); 57 | return true; 58 | } 59 | 60 | function _transfer(address from, address to, uint256 amount) internal virtual { 61 | _balances[from] = _balances[from] - amount; 62 | _balances[to] = _balances[to] + amount; 63 | emit Transfer(from, to, amount); 64 | } 65 | 66 | function _approve(address owner, address spender, uint256 amount) internal virtual { 67 | _allowances[owner][spender] = amount; 68 | emit Approval(owner, spender, amount); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/token/erc20/decrease-allowance/decreaseAllowance.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { stdError } from "forge-std/StdError.sol"; 5 | 6 | import { ERC20_Test } from "../ERC20.t.sol"; 7 | 8 | contract DecreaseAllowance_Test is ERC20_Test { 9 | function test_RevertWhen_CalculationUnderflowsUint256(address spender, uint256 value) external { 10 | vm.assume(spender != address(0)); 11 | vm.assume(value > 0); 12 | 13 | // Expect an arithmetic error. 14 | vm.expectRevert(stdError.arithmeticError); 15 | 16 | // Decrease the allowance. 17 | dai.decreaseAllowance(spender, value); 18 | } 19 | 20 | modifier whenCalculationDoesNotUnderflowUint256() { 21 | _; 22 | } 23 | 24 | /// @dev Check common assumptions for the tests below. 25 | function checkAssumptions(address spender, uint256 value0) internal pure { 26 | vm.assume(spender != address(0)); 27 | vm.assume(value0 > 0); 28 | } 29 | 30 | function testFuzz_DecreaseAllowance( 31 | address spender, 32 | uint256 value0, 33 | uint256 value1 34 | ) 35 | external 36 | whenCalculationDoesNotUnderflowUint256 37 | { 38 | checkAssumptions(spender, value0); 39 | value1 = _bound(value1, 0, value0); 40 | 41 | // Increase the allowance so that we have what to decrease below. 42 | dai.increaseAllowance(spender, value0); 43 | 44 | // Decrease the allowance. 45 | dai.decreaseAllowance(spender, value1); 46 | 47 | // Assert that the allowance has been decreased. 48 | uint256 actualAllowance = dai.allowance(users.alice, spender); 49 | uint256 expectedAllowance = value0 - value1; 50 | assertEq(actualAllowance, expectedAllowance, "allowance"); 51 | } 52 | 53 | function testFuzz_DecreaseAllowance_Event( 54 | address spender, 55 | uint256 value0, 56 | uint256 value1 57 | ) 58 | external 59 | whenCalculationDoesNotUnderflowUint256 60 | { 61 | checkAssumptions(spender, value0); 62 | value1 = _bound(value1, 0, value0); 63 | 64 | // Increase the allowance so that we have what to decrease below. 65 | dai.increaseAllowance(spender, value0); 66 | 67 | // Expect an {Approval} event to be emitted. 68 | vm.expectEmit({ emitter: address(dai) }); 69 | uint256 expectedAllowance = value0 - value1; 70 | emit Approval(users.alice, spender, expectedAllowance); 71 | 72 | // Decrease the allowance. 73 | dai.decreaseAllowance(spender, value1); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/access/Adminable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | import { IAdminable } from "./IAdminable.sol"; 5 | 6 | /// @title Adminable 7 | /// @author Paul Razvan Berg 8 | contract Adminable is IAdminable { 9 | /*////////////////////////////////////////////////////////////////////////// 10 | USER-FACING STORAGE 11 | //////////////////////////////////////////////////////////////////////////*/ 12 | 13 | /// @inheritdoc IAdminable 14 | address public override admin; 15 | 16 | /*////////////////////////////////////////////////////////////////////////// 17 | CONSTRUCTOR 18 | //////////////////////////////////////////////////////////////////////////*/ 19 | 20 | /// @notice Initializes the contract setting the deployer as the initial admin. 21 | constructor() { 22 | _transferAdmin({ newAdmin: msg.sender }); 23 | } 24 | 25 | /*////////////////////////////////////////////////////////////////////////// 26 | MODIFIERS 27 | //////////////////////////////////////////////////////////////////////////*/ 28 | 29 | /// @notice Reverts if called by any account other than the admin. 30 | modifier onlyAdmin() { 31 | if (admin != msg.sender) { 32 | revert Adminable_CallerNotAdmin({ admin: admin, caller: msg.sender }); 33 | } 34 | _; 35 | } 36 | 37 | /*////////////////////////////////////////////////////////////////////////// 38 | USER-FACING FUNCTIONS 39 | //////////////////////////////////////////////////////////////////////////*/ 40 | 41 | /// @inheritdoc IAdminable 42 | function renounceAdmin() public virtual override onlyAdmin { 43 | _transferAdmin({ newAdmin: address(0) }); 44 | } 45 | 46 | /// @inheritdoc IAdminable 47 | function transferAdmin(address newAdmin) public virtual override onlyAdmin { 48 | if (newAdmin == address(0)) { 49 | revert Adminable_AdminZeroAddress(); 50 | } 51 | _transferAdmin(newAdmin); 52 | } 53 | 54 | /*////////////////////////////////////////////////////////////////////////// 55 | INTERNAL FUNCTIONS 56 | //////////////////////////////////////////////////////////////////////////*/ 57 | 58 | /// @dev Transfers the admin of the contract to a new account (`newAdmin`). 59 | /// Internal function without access restriction. 60 | function _transferAdmin(address newAdmin) internal virtual { 61 | address oldAdmin = admin; 62 | admin = newAdmin; 63 | emit TransferAdmin(oldAdmin, newAdmin); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/token/erc20-recover/set-token-denylist/setTokenDenylist.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { IAdminable } from "src/access/IAdminable.sol"; 5 | import { IERC20 } from "src/token/erc20/IERC20.sol"; 6 | import { IERC20Recover } from "src/token/erc20/IERC20Recover.sol"; 7 | 8 | import { ERC20Recover_Test } from "../ERC20Recover.t.sol"; 9 | 10 | contract SetTokenDenylist_Test is ERC20Recover_Test { 11 | function test_RevertWhen_CallerNotAdmin() external { 12 | // Make Eve the caller in this test. 13 | address caller = users.eve; 14 | changePrank(caller); 15 | 16 | // Run the test. 17 | vm.expectRevert(abi.encodeWithSelector(IAdminable.Adminable_CallerNotAdmin.selector, users.admin, caller)); 18 | erc20Recover.setTokenDenylist(TOKEN_DENYLIST); 19 | } 20 | 21 | modifier whenCallerAdmin() { 22 | _; 23 | } 24 | 25 | function test_RevertWhen_TokenDenylistAlreadySet() external whenCallerAdmin { 26 | erc20Recover.setTokenDenylist(TOKEN_DENYLIST); 27 | vm.expectRevert(IERC20Recover.ERC20Recover_TokenDenylistAlreadySet.selector); 28 | erc20Recover.setTokenDenylist(TOKEN_DENYLIST); 29 | } 30 | 31 | modifier whenTokenDenylistNotAlreadySet() { 32 | _; 33 | } 34 | 35 | function test_RevertWhen_SomeTokensDontHaveASymbol() external whenCallerAdmin whenTokenDenylistNotAlreadySet { 36 | vm.expectRevert(); 37 | IERC20[] memory tokenDenylist = new IERC20[](2); 38 | tokenDenylist[0] = dai; 39 | tokenDenylist[1] = IERC20(address(symbollessToken)); 40 | erc20Recover.setTokenDenylist(tokenDenylist); 41 | } 42 | 43 | modifier whenAllTokensHaveASymbol() { 44 | _; 45 | } 46 | 47 | function test_SetTokenDenylist() external whenCallerAdmin whenTokenDenylistNotAlreadySet whenAllTokensHaveASymbol { 48 | erc20Recover.setTokenDenylist(TOKEN_DENYLIST); 49 | 50 | // Compare the token denylists. 51 | IERC20[] memory actualTokenDenylist = erc20Recover.getTokenDenylist(); 52 | IERC20[] memory expectedTokenDenylist = TOKEN_DENYLIST; 53 | assertEq(actualTokenDenylist, expectedTokenDenylist, "tokenDenylist"); 54 | 55 | // Check that the flag has been set to true. 56 | bool isTokenDenylistSet = erc20Recover.isTokenDenylistSet(); 57 | assertTrue(isTokenDenylistSet, "isTokenDenylistSet"); 58 | } 59 | 60 | function test_SetTokenDenylist_Event() 61 | external 62 | whenCallerAdmin 63 | whenTokenDenylistNotAlreadySet 64 | whenAllTokensHaveASymbol 65 | { 66 | vm.expectEmit({ emitter: address(erc20Recover) }); 67 | emit SetTokenDenylist({ owner: users.admin, tokenDenylist: TOKEN_DENYLIST }); 68 | erc20Recover.setTokenDenylist(TOKEN_DENYLIST); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/token/erc20-recover/recover/recover.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { IAdminable } from "src/access/IAdminable.sol"; 5 | import { IERC20 } from "src/token/erc20/IERC20.sol"; 6 | import { IERC20Recover } from "src/token/erc20/IERC20Recover.sol"; 7 | 8 | import { ERC20Recover_Test } from "../ERC20Recover.t.sol"; 9 | 10 | contract Recover_Test is ERC20Recover_Test { 11 | function test_RevertWhen_CallerNotOwner() external { 12 | // Make Eve the caller in this test. 13 | address caller = users.eve; 14 | changePrank(caller); 15 | 16 | // Run the test. 17 | vm.expectRevert(abi.encodeWithSelector(IAdminable.Adminable_CallerNotAdmin.selector, users.admin, caller)); 18 | erc20Recover.recover({ token: dai, amount: DEFAULT_RECOVER_AMOUNT }); 19 | } 20 | 21 | modifier whenCallerOwner() { 22 | _; 23 | } 24 | 25 | function test_RevertWhen_TokenDenylistNotSet() external whenCallerOwner { 26 | vm.expectRevert(IERC20Recover.ERC20Recover_TokenDenylistNotSet.selector); 27 | erc20Recover.recover({ token: dai, amount: DEFAULT_RECOVER_AMOUNT }); 28 | } 29 | 30 | modifier whenTokenDenylistSet() { 31 | erc20Recover.setTokenDenylist(TOKEN_DENYLIST); 32 | _; 33 | } 34 | 35 | function test_RevertWhen_RecoverAmountZero() external whenCallerOwner whenTokenDenylistSet { 36 | vm.expectRevert(IERC20Recover.ERC20Recover_RecoverAmountZero.selector); 37 | erc20Recover.recover({ token: dai, amount: 0 }); 38 | } 39 | 40 | modifier whenRecoverAmountNotZero() { 41 | _; 42 | } 43 | 44 | function test_RevertWhen_TokenNotRecoverable() 45 | external 46 | whenCallerOwner 47 | whenTokenDenylistSet 48 | whenRecoverAmountNotZero 49 | { 50 | IERC20 nonRecoverableToken = dai; 51 | vm.expectRevert( 52 | abi.encodeWithSelector(IERC20Recover.ERC20Recover_RecoverNonRecoverableToken.selector, nonRecoverableToken) 53 | ); 54 | erc20Recover.recover({ token: nonRecoverableToken, amount: 1e18 }); 55 | } 56 | 57 | modifier whenTokenRecoverable() { 58 | _; 59 | } 60 | 61 | function test_Recover() 62 | external 63 | whenCallerOwner 64 | whenTokenDenylistSet 65 | whenRecoverAmountNotZero 66 | whenTokenRecoverable 67 | { 68 | erc20Recover.recover({ token: usdc, amount: DEFAULT_RECOVER_AMOUNT }); 69 | } 70 | 71 | function test_Recover_Event() 72 | external 73 | whenCallerOwner 74 | whenTokenDenylistSet 75 | whenRecoverAmountNotZero 76 | whenTokenRecoverable 77 | { 78 | vm.expectEmit({ emitter: address(erc20Recover) }); 79 | emit Recover({ owner: users.admin, token: usdc, amount: DEFAULT_RECOVER_AMOUNT }); 80 | erc20Recover.recover({ token: usdc, amount: DEFAULT_RECOVER_AMOUNT }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/token/erc20-normalizer/denormalize/denormalize.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20Normalizer_Test } from "../ERC20Normalizer.t.sol"; 5 | 6 | contract Denormalize_Test is ERC20Normalizer_Test { 7 | function test_Denormalize_ScalarNotComputed() external { 8 | uint256 amount = bn(100, STANDARD_DECIMALS); 9 | uint256 actualDenormalizedAmount = erc20Normalizer.denormalize({ token: usdc, amount: amount }); 10 | uint256 expectedDenormalizedAmount = bn(100, usdc.decimals()); 11 | assertEq(actualDenormalizedAmount, expectedDenormalizedAmount, "normalizedAmount"); 12 | } 13 | 14 | modifier whenScalarComputed() { 15 | _; 16 | } 17 | 18 | function test_Denormalize_Scalar1() external whenScalarComputed { 19 | erc20Normalizer.computeScalar({ token: usdc }); 20 | uint256 amount = bn(100, STANDARD_DECIMALS); 21 | uint256 actualDenormalizedAmount = erc20Normalizer.denormalize({ token: usdc, amount: amount }); 22 | uint256 expectedDenormalizedAmount = bn(100, usdc.decimals()); 23 | assertEq(actualDenormalizedAmount, expectedDenormalizedAmount, "normalizedAmount"); 24 | } 25 | 26 | modifier whenScalarNot1() { 27 | _; 28 | } 29 | 30 | function test_Denormalize_AmountZero() external whenScalarComputed whenScalarNot1 { 31 | erc20Normalizer.computeScalar({ token: usdc }); 32 | uint256 amount = 0; 33 | uint256 actualDenormalizedAmount = erc20Normalizer.denormalize({ token: usdc, amount: amount }); 34 | uint256 expectedDenormalizedAmount = 0; 35 | assertEq(actualDenormalizedAmount, expectedDenormalizedAmount, "normalizedAmount"); 36 | } 37 | 38 | modifier whenAmountNotZero() { 39 | _; 40 | } 41 | 42 | function test_Denormalize_AmountSmallerThanScalar() external whenScalarComputed whenScalarNot1 whenAmountNotZero { 43 | erc20Normalizer.computeScalar({ token: usdc }); 44 | uint256 amount = USDC_SCALAR - 1; 45 | uint256 actualDenormalizedAmount = erc20Normalizer.denormalize({ token: usdc, amount: amount }); 46 | uint256 expectedDenormalizedAmount = 0; 47 | assertEq(actualDenormalizedAmount, expectedDenormalizedAmount, "normalizedAmount"); 48 | } 49 | 50 | function testFuzz_Denormalize_AmountBiggerThanScalar(uint256 amount) 51 | external 52 | whenScalarComputed 53 | whenScalarNot1 54 | whenAmountNotZero 55 | { 56 | erc20Normalizer.computeScalar({ token: usdc }); 57 | amount = _bound(amount, USDC_SCALAR + 1, MAX_UINT256); 58 | 59 | uint256 actualDenormalizedAmount = erc20Normalizer.denormalize({ token: usdc, amount: amount }); 60 | uint256 expectedDenormalizedAmount; 61 | unchecked { 62 | expectedDenormalizedAmount = amount / USDC_SCALAR; 63 | } 64 | assertEq(actualDenormalizedAmount, expectedDenormalizedAmount, "normalizedAmount"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/token/erc20/ERC20MissingReturn.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | /// @title ERC20MissingReturn 5 | /// @author Paul Razvan Berg 6 | /// @notice An implementation of ERC-20 that does not return a boolean in {transfer} and {transferFrom}. 7 | /// @dev Strictly for test purposes. Do not use in production. 8 | /// https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca 9 | contract ERC20MissingReturn { 10 | uint8 public decimals; 11 | 12 | string public name; 13 | 14 | string public symbol; 15 | 16 | uint256 public totalSupply; 17 | 18 | mapping(address => mapping(address => uint256)) internal _allowances; 19 | 20 | mapping(address => uint256) internal _balances; 21 | 22 | event Transfer(address indexed from, address indexed to, uint256 amount); 23 | 24 | event Approval(address indexed owner, address indexed spender, uint256 amount); 25 | 26 | constructor(string memory name_, string memory symbol_, uint8 decimals_) { 27 | name = name_; 28 | symbol = symbol_; 29 | decimals = decimals_; 30 | } 31 | 32 | function allowance(address owner, address spender) public view returns (uint256) { 33 | return _allowances[owner][spender]; 34 | } 35 | 36 | function balanceOf(address account) public view returns (uint256) { 37 | return _balances[account]; 38 | } 39 | 40 | function approve(address spender, uint256 value) public returns (bool) { 41 | _approve(msg.sender, spender, value); 42 | return true; 43 | } 44 | 45 | function burn(address holder, uint256 amount) public { 46 | _balances[holder] -= amount; 47 | totalSupply -= amount; 48 | emit Transfer(holder, address(0), amount); 49 | } 50 | 51 | function mint(address beneficiary, uint256 amount) public { 52 | _balances[beneficiary] += amount; 53 | totalSupply += amount; 54 | emit Transfer(address(0), beneficiary, amount); 55 | } 56 | 57 | function _approve(address owner, address spender, uint256 value) internal virtual { 58 | _allowances[owner][spender] = value; 59 | emit Approval(owner, spender, value); 60 | } 61 | 62 | /// @dev This function does not return a value, although the ERC-20 standard mandates that it should. 63 | function transfer(address to, uint256 amount) public { 64 | _transfer(msg.sender, to, amount); 65 | } 66 | 67 | /// @dev This function does not return a value, although the ERC-20 standard mandates that it should. 68 | function transferFrom(address from, address to, uint256 amount) public { 69 | _transfer(from, to, amount); 70 | _approve(from, msg.sender, _allowances[from][msg.sender] - amount); 71 | } 72 | 73 | function _transfer(address from, address to, uint256 amount) internal virtual { 74 | _balances[from] = _balances[from] - amount; 75 | _balances[to] = _balances[to] + amount; 76 | emit Transfer(from, to, amount); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/token/erc20/IERC20Normalizer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | import { IERC20 } from "./IERC20.sol"; 5 | 6 | /// @title IERC20Normalizer 7 | /// @author Paul Razvan Berg 8 | /// @notice Caches ERC-20 token decimals and scales the amounts up or down using 18 decimals as a frame of reference. 9 | /// @dev Does not support ERC-20 tokens with decimals greater than 18. 10 | interface IERC20Normalizer { 11 | /*////////////////////////////////////////////////////////////////////////// 12 | ERRORS 13 | //////////////////////////////////////////////////////////////////////////*/ 14 | 15 | /// @notice Thrown when attempting to compute the scalar for a token whose decimals are zero. 16 | error IERC20Normalizer_TokenDecimalsZero(IERC20 token); 17 | 18 | /// @notice Thrown when attempting to compute the scalar for a token whose decimals are greater than 18. 19 | error IERC20Normalizer_TokenDecimalsGreaterThan18(IERC20 token, uint256 decimals); 20 | 21 | /*////////////////////////////////////////////////////////////////////////// 22 | CONSTANT FUNCTIONS 23 | //////////////////////////////////////////////////////////////////////////*/ 24 | 25 | /// @notice Returns the scalar $10^(18 - decimals)$ for the given token. 26 | /// @dev Returns zero when there is no cached scalar for the given token. 27 | /// @param token The ERC-20 token to make the query for. 28 | /// @return scalar The scalar for the given token. 29 | function getScalar(IERC20 token) external view returns (uint256 scalar); 30 | 31 | /*////////////////////////////////////////////////////////////////////////// 32 | NON-CONSTANT FUNCTIONS 33 | //////////////////////////////////////////////////////////////////////////*/ 34 | 35 | /// @notice Computes the scalar $10^(18 - decimals)$ for the given token. 36 | /// @param token The ERC-20 token to make the query for. 37 | /// @return scalar The newly computed scalar for the given token. 38 | function computeScalar(IERC20 token) external returns (uint256 scalar); 39 | 40 | /// @notice Denormalize the amount by diving by the token's scalar. 41 | /// @param token The ERC-20 token whose decimals are the units of the `amount` argumnet. 42 | /// @param amount The amount to denormalize, in units of the token's decimals. 43 | /// @param denormalizedAmount The amount denormalized with respect to `scalar`. 44 | function denormalize(IERC20 token, uint256 amount) external returns (uint256 denormalizedAmount); 45 | 46 | /// @notice Normalize the amount by multiplying by the token's scalar. 47 | /// @param token The ERC-20 token whose decimals are the units of `amount` argument. 48 | /// @param amount The amount to normalize, in units of the token's decimals. 49 | /// @param normalizedAmount The amount normalized with respect to `scalar`. 50 | function normalize(IERC20 token, uint256 amount) external returns (uint256 normalizedAmount); 51 | } 52 | -------------------------------------------------------------------------------- /test/token/erc20-normalizer/normalize/normalize.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { stdError } from "forge-std/StdError.sol"; 5 | 6 | import { ERC20Normalizer_Test } from "../ERC20Normalizer.t.sol"; 7 | 8 | contract Normalize_Test is ERC20Normalizer_Test { 9 | function test_Normalize_ScalarNotComputed() external { 10 | uint256 amount = bn(100, usdc.decimals()); 11 | uint256 actualNormalizedAmount = erc20Normalizer.normalize({ token: usdc, amount: amount }); 12 | uint256 expectedNormalizedAmount = bn(100, STANDARD_DECIMALS); 13 | assertEq(actualNormalizedAmount, expectedNormalizedAmount, "normalizedAmount"); 14 | } 15 | 16 | modifier whenScalarComputed() { 17 | _; 18 | } 19 | 20 | function test_Normalize_Scalar1() external whenScalarComputed { 21 | erc20Normalizer.computeScalar({ token: usdc }); 22 | uint256 amount = bn(100, usdc.decimals()); 23 | uint256 actualNormalizedAmount = erc20Normalizer.normalize({ token: usdc, amount: amount }); 24 | uint256 expectedNormalizedAmount = bn(100, STANDARD_DECIMALS); 25 | assertEq(actualNormalizedAmount, expectedNormalizedAmount, "normalizedAmount"); 26 | } 27 | 28 | modifier whenScalarNot1() { 29 | _; 30 | } 31 | 32 | function testFuzz_RevertWhen_CalculationOverflows(uint256 amount) external whenScalarComputed whenScalarNot1 { 33 | vm.assume(amount > (MAX_UINT256 / USDC_SCALAR) + 1); // 10^12 is the scalar for USDC. 34 | erc20Normalizer.computeScalar({ token: usdc }); 35 | vm.expectRevert(stdError.arithmeticError); 36 | erc20Normalizer.normalize({ token: usdc, amount: amount }); 37 | } 38 | 39 | modifier whenCalculationDoesNotOverflow() { 40 | _; 41 | } 42 | 43 | function test_Normalize_AmountZero() external whenScalarComputed whenScalarNot1 whenCalculationDoesNotOverflow { 44 | erc20Normalizer.computeScalar({ token: usdc }); 45 | uint256 amount = 0; 46 | uint256 actualNormalizedAmount = erc20Normalizer.normalize({ token: usdc, amount: amount }); 47 | uint256 expectedNormalizedAmount = 0; 48 | assertEq(actualNormalizedAmount, expectedNormalizedAmount, "normalizedAmount"); 49 | } 50 | 51 | modifier whenAmountNotZero() { 52 | _; 53 | } 54 | 55 | function testFuzz_Normalize(uint256 amount) 56 | external 57 | whenScalarComputed 58 | whenScalarNot1 59 | whenCalculationDoesNotOverflow 60 | whenAmountNotZero 61 | { 62 | amount = _bound(amount, 1, MAX_UINT256 / USDC_SCALAR); // 10^12 is the scalar for USDC 63 | erc20Normalizer.computeScalar({ token: usdc }); 64 | uint256 actualNormalizedAmount = erc20Normalizer.normalize({ token: usdc, amount: amount }); 65 | uint256 expectedNormalizedAmount; 66 | unchecked { 67 | expectedNormalizedAmount = amount * USDC_SCALAR; 68 | } 69 | assertEq(actualNormalizedAmount, expectedNormalizedAmount, "normalizedAmount"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | env: 4 | FOUNDRY_PROFILE: "ci" 5 | 6 | on: 7 | workflow_dispatch: 8 | pull_request: 9 | push: 10 | branches: 11 | - "main" 12 | 13 | jobs: 14 | lint: 15 | runs-on: "ubuntu-latest" 16 | steps: 17 | - name: "Check out the repo" 18 | uses: "actions/checkout@v3" 19 | 20 | - name: "Install Foundry" 21 | uses: "foundry-rs/foundry-toolchain@v1" 22 | 23 | - name: "Install Pnpm" 24 | uses: "pnpm/action-setup@v2" 25 | with: 26 | version: "8" 27 | 28 | - name: "Install Node.js" 29 | uses: "actions/setup-node@v3" 30 | with: 31 | cache: "pnpm" 32 | node-version: "lts/*" 33 | 34 | - name: "Install the Node.js dependencies" 35 | run: "pnpm install" 36 | 37 | - name: "Lint the contracts" 38 | run: "pnpm lint" 39 | 40 | - name: "Add lint summary" 41 | run: | 42 | echo "## Lint result" >> $GITHUB_STEP_SUMMARY 43 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 44 | 45 | build: 46 | runs-on: "ubuntu-latest" 47 | steps: 48 | - name: "Check out the repo" 49 | uses: "actions/checkout@v3" 50 | with: 51 | submodules: "recursive" 52 | 53 | - name: "Install Foundry" 54 | uses: "foundry-rs/foundry-toolchain@v1" 55 | 56 | - name: "Build the contracts and print their size" 57 | run: "forge build --sizes" 58 | 59 | - name: "Add build summary" 60 | run: | 61 | echo "## Build result" >> $GITHUB_STEP_SUMMARY 62 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 63 | 64 | test: 65 | needs: ["lint", "build"] 66 | runs-on: "ubuntu-latest" 67 | steps: 68 | - name: "Check out the repo" 69 | uses: "actions/checkout@v3" 70 | with: 71 | submodules: "recursive" 72 | 73 | - name: "Install Foundry" 74 | uses: "foundry-rs/foundry-toolchain@v1" 75 | 76 | - name: "Run the tests" 77 | run: "forge test" 78 | 79 | - name: "Add test summary" 80 | run: | 81 | echo "## Tests result" >> $GITHUB_STEP_SUMMARY 82 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 83 | 84 | coverage: 85 | needs: ["lint", "build"] 86 | runs-on: "ubuntu-latest" 87 | steps: 88 | - name: "Check out the repo" 89 | uses: "actions/checkout@v3" 90 | with: 91 | submodules: "recursive" 92 | 93 | - name: "Install Foundry" 94 | uses: "foundry-rs/foundry-toolchain@v1" 95 | 96 | - name: "Generate the coverage report" 97 | run: "forge coverage --report lcov" 98 | 99 | - name: "Upload coverage report to Codecov" 100 | uses: "codecov/codecov-action@v3" 101 | with: 102 | files: "./lcov.info" 103 | 104 | - name: "Add coverage summary" 105 | run: | 106 | echo "## Coverage result" >> $GITHUB_STEP_SUMMARY 107 | echo "✅ Uploaded to Codecov" >> $GITHUB_STEP_SUMMARY 108 | -------------------------------------------------------------------------------- /src/access/IAdminable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | /// @title IAdminable 5 | /// @author Paul Razvan Berg 6 | /// @notice Contract module that provides a basic access control mechanism, where there is an 7 | /// account (an admin) that can be granted exclusive access to specific functions. 8 | /// 9 | /// By default, the admin account will be the one that deploys the contract. This can later be 10 | /// changed with {transferAdmin}. 11 | /// 12 | /// This module is used through inheritance. It will make available the modifier `onlyAdmin`, 13 | /// which can be applied to your functions to restrict their use to the admin. 14 | /// 15 | /// @dev Forked from OpenZeppelin 16 | /// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.0/contracts/access/Ownable.sol 17 | interface IAdminable { 18 | /*////////////////////////////////////////////////////////////////////////// 19 | ERRORS 20 | //////////////////////////////////////////////////////////////////////////*/ 21 | 22 | /// @notice Thrown when setting the admin to the zero address. 23 | error Adminable_AdminZeroAddress(); 24 | 25 | /// @notice Thrown when the caller is not the admin. 26 | error Adminable_CallerNotAdmin(address admin, address caller); 27 | 28 | /*////////////////////////////////////////////////////////////////////////// 29 | EVENTS 30 | //////////////////////////////////////////////////////////////////////////*/ 31 | 32 | /// @notice Emitted when the admin is transferred. 33 | /// @param oldAdmin The address of the old admin. 34 | /// @param newAdmin The address of the new admin. 35 | event TransferAdmin(address indexed oldAdmin, address indexed newAdmin); 36 | 37 | /*////////////////////////////////////////////////////////////////////////// 38 | CONSTANT FUNCTIONS 39 | //////////////////////////////////////////////////////////////////////////*/ 40 | 41 | /// @notice The address of the admin account or contract. 42 | /// @return The address of the admin. 43 | function admin() external view returns (address); 44 | 45 | /*////////////////////////////////////////////////////////////////////////// 46 | NON-CONSTANT FUNCTIONS 47 | //////////////////////////////////////////////////////////////////////////*/ 48 | 49 | /// @notice Leaves the contract without admin, so it will not be possible to call `onlyAdmin` 50 | /// functions anymore. 51 | /// 52 | /// WARNING: Renouncing the admin will leave the contract without an admin, thereby removing any 53 | /// functionality that is only available to the admin. 54 | /// 55 | /// Requirements: 56 | /// 57 | /// - The caller must be the admin. 58 | function renounceAdmin() external; 59 | 60 | /// @notice Transfers the admin of the contract to a new account (`newAdmin`). Can only be 61 | /// called by the current admin. 62 | /// @param newAdmin The address of the new admin. 63 | function transferAdmin(address newAdmin) external; 64 | } 65 | -------------------------------------------------------------------------------- /src/access/IOrchestratable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | import { IAdminable } from "./IAdminable.sol"; 5 | 6 | /// @title IOrchestratable 7 | /// @author Paul Razvan Berg 8 | /// @notice Orchestrated static access control between multiple contracts. 9 | /// 10 | /// This should be used as a parent contract of any contract that needs to restrict access to some methods, which 11 | /// should be marked with the `onlyOrchestrated` modifier. 12 | /// 13 | /// During deployment, the contract deployer (`conductor`) can register any contracts that have privileged access 14 | /// by calling `orchestrate`. 15 | /// 16 | /// Once deployment is completed, `conductor` should call `transferConductor(address(0))` to avoid any more 17 | /// contracts ever gaining privileged access. 18 | /// 19 | /// @dev Forked from Alberto Cuesta Cañada 20 | /// https://github.com/albertocuestacanada/Orchestrated/blob/b0adb21/contracts/Orchestrated.sol 21 | interface IOrchestratable is IAdminable { 22 | /*////////////////////////////////////////////////////////////////////////// 23 | ERRORS 24 | //////////////////////////////////////////////////////////////////////////*/ 25 | 26 | /// @notice Thrown when the caller is not an orchestrated address. 27 | error Orchestratable_NotOrchestrated(address caller, bytes4 signature); 28 | 29 | /*////////////////////////////////////////////////////////////////////////// 30 | EVENTS 31 | //////////////////////////////////////////////////////////////////////////*/ 32 | 33 | /// @notice Emitted when access is granted to a new address. 34 | /// @param access The new granted address. 35 | event GrantAccess(address access); 36 | 37 | /// @notice Emitted when the conductor is transferred. 38 | /// @param oldConductor The address of the old conductor. 39 | /// @param newConductor The address of the new conductor. 40 | event TransferConductor(address indexed oldConductor, address indexed newConductor); 41 | 42 | /*////////////////////////////////////////////////////////////////////////// 43 | NON-CONSTANT FUNCTIONS 44 | //////////////////////////////////////////////////////////////////////////*/ 45 | 46 | /// @notice Adds new orchestrated address. 47 | /// @param account Address of EOA or contract to give access to this contract. 48 | /// @param signature bytes4 signature of the function to be given orchestrated access to. 49 | function orchestrate(address account, bytes4 signature) external; 50 | 51 | /// CONSTANT FUNCTIONS /// 52 | 53 | /// @notice The address of the conductor account or contract. 54 | /// @return The address of the conductor. 55 | function conductor() external view returns (address); 56 | 57 | /// @notice Checks the access of an account to a function. 58 | /// @param account Address of EOA or contract to check. 59 | /// @param signature The signature of the function to check. 60 | /// @return True if the account has access. 61 | function orchestration(address account, bytes4 signature) external view returns (bool); 62 | } 63 | -------------------------------------------------------------------------------- /src/utils/ReentrancyGuard.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | /// @title ReentrancyGuard 5 | /// @author Paul Razvan Berg 6 | /// @notice Contract module that helps prevent reentrant calls to a function. 7 | /// 8 | /// Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier available, which can be applied 9 | /// to functions to make sure there are no nested (reentrant) calls to them. 10 | /// 11 | /// Note that because there is a single `nonReentrant` guard, functions marked as `nonReentrant` may not 12 | /// call one another. This can be worked around by making those functions `private`, and then adding 13 | /// `external` `nonReentrant` entry points to them. 14 | /// 15 | /// @dev Forked from OpenZeppelin 16 | /// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.4.0/contracts/utils/ReentrancyGuard.sol 17 | abstract contract ReentrancyGuard { 18 | /*////////////////////////////////////////////////////////////////////////// 19 | ERRORS 20 | //////////////////////////////////////////////////////////////////////////*/ 21 | 22 | /// @notice Thrown when there is a reentrancy call. 23 | error ReentrantCall(); 24 | 25 | /*////////////////////////////////////////////////////////////////////////// 26 | PRIVATE STORAGE 27 | //////////////////////////////////////////////////////////////////////////*/ 28 | 29 | bool private notEntered; 30 | 31 | /*////////////////////////////////////////////////////////////////////////// 32 | CONSTRUCTOR 33 | //////////////////////////////////////////////////////////////////////////*/ 34 | 35 | /// Storing an initial non-zero value makes deployment a bit more expensive but in exchange the 36 | /// refund on every call to nonReentrant will be lower in amount. Since refunds are capped to a 37 | /// percentage of the total transaction's gas, it is best to keep them low in cases like this one, 38 | /// to increase the likelihood of the full refund coming into effect. 39 | constructor() { 40 | notEntered = true; 41 | } 42 | 43 | /*////////////////////////////////////////////////////////////////////////// 44 | MODIFIERS 45 | //////////////////////////////////////////////////////////////////////////*/ 46 | 47 | /// @notice Prevents a contract from calling itself, directly or indirectly. 48 | /// @dev Calling a `nonReentrant` function from another `nonReentrant` function 49 | /// is not supported. It is possible to prevent this from happening by making 50 | /// the `nonReentrant` function external, and make it call a `private` 51 | /// function that does the actual work. 52 | modifier nonReentrant() { 53 | // On the first call to nonReentrant, notEntered will be true. 54 | if (!notEntered) { 55 | revert ReentrantCall(); 56 | } 57 | 58 | // Any calls to nonReentrant after this point will fail. 59 | notEntered = false; 60 | 61 | _; 62 | 63 | // By storing the original value once again, a refund is triggered (https://eips.ethereum.org/EIPS/eip-2200). 64 | notEntered = true; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/token/erc20-permit/ERC20Permit.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { ERC20Permit } from "src/token/erc20/ERC20Permit.sol"; 5 | 6 | import { Base_Test } from "../../Base.t.sol"; 7 | 8 | /// @notice Common logic needed by all {ERC20Permit} unit tests. 9 | abstract contract ERC20Permit_Test is Base_Test { 10 | /*////////////////////////////////////////////////////////////////////////// 11 | EVENTS 12 | //////////////////////////////////////////////////////////////////////////*/ 13 | 14 | event Approval(address indexed owner, address indexed spender, uint256 amount); 15 | 16 | /*////////////////////////////////////////////////////////////////////////// 17 | CONSTANTS 18 | //////////////////////////////////////////////////////////////////////////*/ 19 | 20 | /// @dev December 31, 2099 at 16:00 UTC 21 | uint256 internal constant DECEMBER_2099 = 4_102_416_000; 22 | bytes32 internal immutable DOMAIN_SEPARATOR; 23 | uint8 internal constant DUMMY_V = 27; 24 | bytes32 internal constant DUMMY_R = bytes32(uint256(0x01)); 25 | bytes32 internal constant DUMMY_S = bytes32(uint256(0x02)); 26 | bytes32 internal constant PERMIT_TYPEHASH = 27 | keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 28 | string internal constant version = "1"; 29 | 30 | /*////////////////////////////////////////////////////////////////////////// 31 | TEST CONTRACTS 32 | //////////////////////////////////////////////////////////////////////////*/ 33 | 34 | ERC20Permit internal erc20Permit = new ERC20Permit("EIP-2612 Permit Token", "PERMIT", 18); 35 | 36 | /*////////////////////////////////////////////////////////////////////////// 37 | CONSTRUCTOR 38 | //////////////////////////////////////////////////////////////////////////*/ 39 | 40 | constructor() { 41 | DOMAIN_SEPARATOR = keccak256( 42 | abi.encode( 43 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 44 | keccak256(bytes(erc20Permit.name())), 45 | keccak256(bytes(version)), 46 | block.chainid, 47 | address(erc20Permit) 48 | ) 49 | ); 50 | } 51 | 52 | /*////////////////////////////////////////////////////////////////////////// 53 | INTERNAL NON-CONSTANT FUNCTIONS 54 | //////////////////////////////////////////////////////////////////////////*/ 55 | 56 | /// @dev Helper function to generate an EIP-712 digest, and then sign it. 57 | function getSignature( 58 | uint256 privateKey, 59 | address owner, 60 | address spender, 61 | uint256 value, 62 | uint256 deadline 63 | ) 64 | internal 65 | view 66 | returns (uint8 v, bytes32 r, bytes32 s) 67 | { 68 | privateKey = boundPrivateKey(privateKey); 69 | uint256 nonce = 0; 70 | bytes32 hashStruct = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonce, deadline)); 71 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, hashStruct)); 72 | (v, r, s) = vm.sign(privateKey, digest); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/token/erc20/IERC20Permit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // solhint-disable func-name-mixedcase 3 | pragma solidity >=0.8.4; 4 | 5 | import { IERC20 } from "./IERC20.sol"; 6 | 7 | /// @title IERC20Permit 8 | /// @author Paul Razvan Berg 9 | /// @notice Extension of ERC-20 that allows token holders to use their tokens without sending any 10 | /// transactions by setting the allowance with a signature using the `permit` method, and then spend 11 | /// them via `transferFrom`. 12 | /// @dev See https://eips.ethereum.org/EIPS/eip-2612. 13 | interface IERC20Permit is IERC20 { 14 | /*////////////////////////////////////////////////////////////////////////// 15 | ERRORS 16 | //////////////////////////////////////////////////////////////////////////*/ 17 | 18 | /// @notice Thrown when the recovered owner does not match the actual owner. 19 | error ERC20Permit_InvalidSignature(address owner, uint8 v, bytes32 r, bytes32 s); 20 | 21 | /// @notice Thrown when the owner is the zero address. 22 | error ERC20Permit_OwnerZeroAddress(); 23 | 24 | /// @notice Thrown when the permit expired. 25 | error ERC20Permit_PermitExpired(uint256 currentTime, uint256 deadline); 26 | 27 | /// @notice Thrown when the recovered owner is the zero address. 28 | error ERC20Permit_RecoveredOwnerZeroAddress(); 29 | 30 | /// @notice Thrown when attempting to permit the zero address as the spender. 31 | error ERC20Permit_SpenderZeroAddress(); 32 | 33 | /*////////////////////////////////////////////////////////////////////////// 34 | CONSTANT FUNCTIONS 35 | //////////////////////////////////////////////////////////////////////////*/ 36 | 37 | /// @notice The Eip712 domain's keccak256 hash. 38 | function DOMAIN_SEPARATOR() external view returns (bytes32); 39 | 40 | /// @notice keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 41 | function PERMIT_TYPEHASH() external view returns (bytes32); 42 | 43 | /// @notice Provides replay protection. 44 | function nonces(address account) external view returns (uint256); 45 | 46 | /// @notice Eip712 version of this implementation. 47 | function version() external view returns (string memory); 48 | 49 | /*////////////////////////////////////////////////////////////////////////// 50 | NON-CONSTANT FUNCTIONS 51 | //////////////////////////////////////////////////////////////////////////*/ 52 | 53 | /// @notice Sets `value` as the allowance of `spender` over `owner`'s tokens, assuming the latter's 54 | /// signed approval. 55 | /// 56 | /// @dev Emits an {Approval} event. 57 | /// 58 | /// IMPORTANT: The same issues ERC-20 `approve` has related to transaction 59 | /// ordering also apply here. 60 | /// 61 | /// Requirements: 62 | /// 63 | /// - `owner` cannot be the zero address. 64 | /// - `spender` cannot be the zero address. 65 | /// - `deadline` must be a timestamp in the future. 66 | /// - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` over the Eip712-formatted 67 | /// function arguments. 68 | /// - The signature must use `owner`'s current nonce. 69 | function permit( 70 | address owner, 71 | address spender, 72 | uint256 value, 73 | uint256 deadline, 74 | uint8 v, 75 | bytes32 r, 76 | bytes32 s 77 | ) 78 | external; 79 | } 80 | -------------------------------------------------------------------------------- /src/token/erc20/ERC20Normalizer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | import { IERC20 } from "./IERC20.sol"; 5 | import { IERC20Normalizer } from "./IERC20Normalizer.sol"; 6 | 7 | /// @title ERC20Normalizer 8 | /// @author Paul Razvan Berg 9 | abstract contract ERC20Normalizer is IERC20Normalizer { 10 | /*////////////////////////////////////////////////////////////////////////// 11 | INTERNAL STORAGE 12 | //////////////////////////////////////////////////////////////////////////*/ 13 | 14 | /// @dev Mapping between ERC-20 tokens and their associated scalars $10^(18 - decimals)$. 15 | mapping(IERC20 => uint256) internal _scalars; 16 | 17 | /*////////////////////////////////////////////////////////////////////////// 18 | USER-FACING CONSTANT FUNCTIONS 19 | //////////////////////////////////////////////////////////////////////////*/ 20 | 21 | /// @inheritdoc IERC20Normalizer 22 | function getScalar(IERC20 token) public view override returns (uint256 scalar) { 23 | // Check if we already have a cached scalar for the given token. 24 | scalar = _scalars[token]; 25 | } 26 | 27 | /*////////////////////////////////////////////////////////////////////////// 28 | USER-FACING NON-CONSTANT FUNCTIONS 29 | //////////////////////////////////////////////////////////////////////////*/ 30 | 31 | /// @inheritdoc IERC20Normalizer 32 | function computeScalar(IERC20 token) public returns (uint256 scalar) { 33 | // Query the ERC-20 contract to obtain the decimals. 34 | uint256 decimals = uint256(token.decimals()); 35 | 36 | // Revert if the token's decimals are zero. 37 | if (decimals == 0) { 38 | revert IERC20Normalizer_TokenDecimalsZero(token); 39 | } 40 | 41 | // Revert if the token's decimals are greater than 18. 42 | if (decimals > 18) { 43 | revert IERC20Normalizer_TokenDecimalsGreaterThan18(token, decimals); 44 | } 45 | 46 | // Calculate the scalar. 47 | unchecked { 48 | scalar = 10 ** (18 - decimals); 49 | } 50 | 51 | // Save the scalar in storage. 52 | _scalars[token] = scalar; 53 | } 54 | 55 | /// @inheritdoc IERC20Normalizer 56 | function denormalize(IERC20 token, uint256 amount) external returns (uint256 denormalizedAmount) { 57 | uint256 scalar = getScalar(token); 58 | 59 | // If the scalar is zero, it means that this is the first time we encounter this ERC-20 token. We compute 60 | // its precision scalar and cache it. 61 | if (scalar == 0) { 62 | scalar = computeScalar(token); 63 | } 64 | 65 | // Denormalize the amount. It is safe to use unchecked arithmetic because we do not allow tokens with decimals 66 | // greater than 18. 67 | unchecked { 68 | denormalizedAmount = scalar != 1 ? amount / scalar : amount; 69 | } 70 | } 71 | 72 | /// @inheritdoc IERC20Normalizer 73 | function normalize(IERC20 token, uint256 amount) external returns (uint256 normalizedAmount) { 74 | uint256 scalar = getScalar(token); 75 | 76 | // If the scalar is zero, it means that this is the first time we encounter this ERC-20 token. We need 77 | // to compute its precision scalar and cache it. 78 | if (scalar == 0) { 79 | scalar = computeScalar(token); 80 | } 81 | 82 | // Normalize the amount. We have to use checked arithmetic because the calculation can overflow uint256. 83 | normalizedAmount = scalar != 1 ? amount * scalar : amount; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/token/erc20/IERC20Recover.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | import { IERC20 } from "./IERC20.sol"; 5 | import { IAdminable } from "../../access/IAdminable.sol"; 6 | 7 | /// @title IERC20Recover 8 | /// @author Paul Razvan Berg 9 | /// @notice Contract that gives the owner the ability to recover the ERC-20 tokens that were sent 10 | /// (accidentally, or not) to the contract. 11 | interface IERC20Recover is IAdminable { 12 | /*////////////////////////////////////////////////////////////////////////// 13 | ERRORS 14 | //////////////////////////////////////////////////////////////////////////*/ 15 | 16 | /// @notice Thrown when attempting to recover a token marked as non-recoverable. 17 | error ERC20Recover_RecoverNonRecoverableToken(address token); 18 | 19 | /// @notice Thrown when attempting to recover a zero amount of tokens. 20 | error ERC20Recover_RecoverAmountZero(); 21 | 22 | /// @notice Thrown when the attempting to set the token denylist twice. 23 | error ERC20Recover_TokenDenylistAlreadySet(); 24 | 25 | /// @notice Thrown when the attempting to recover a token without having set the token denylist. 26 | error ERC20Recover_TokenDenylistNotSet(); 27 | 28 | /*////////////////////////////////////////////////////////////////////////// 29 | EVENTS 30 | //////////////////////////////////////////////////////////////////////////*/ 31 | 32 | /// @notice Emitted when tokens are recovered. 33 | /// @param owner The address of the owner recovering the tokens. 34 | /// @param token The address of the recovered token. 35 | /// @param amount The amount of recovered tokens. 36 | event Recover(address indexed owner, IERC20 indexed token, uint256 amount); 37 | 38 | /// @notice Emitted when the token denylist is set. 39 | /// @param owner The address of the owner of the contract. 40 | /// @param tokenDenylist The array of tokens that will not be recoverable. 41 | event SetTokenDenylist(address indexed owner, IERC20[] tokenDenylist); 42 | 43 | /*////////////////////////////////////////////////////////////////////////// 44 | CONSTANT FUNCTIONS 45 | //////////////////////////////////////////////////////////////////////////*/ 46 | 47 | /// @notice Getter for the token denylist. 48 | function getTokenDenylist() external view returns (IERC20[] memory); 49 | 50 | /// @notice A flag that indicates whether the token denylist is set or not. We need this because 51 | /// the token denylist can be set to an empty array. 52 | function isTokenDenylistSet() external view returns (bool); 53 | 54 | /*////////////////////////////////////////////////////////////////////////// 55 | NON-CONSTANT FUNCTIONS 56 | //////////////////////////////////////////////////////////////////////////*/ 57 | 58 | /// @notice Initializes the contract by setting the tokens that this contract cannot recover. 59 | /// 60 | /// @dev Emits an {Initialize} event. 61 | /// 62 | /// Requirements: 63 | /// 64 | /// - The caller must be the owner. 65 | /// - The token denylist must not be already set. 66 | /// 67 | /// @param tokenDenylist_ The array of tokens that will not be recoverable. 68 | function setTokenDenylist(IERC20[] calldata tokenDenylist_) external; 69 | 70 | /// @notice Recovers ERC-20 tokens sent to this contract (by accident or otherwise). 71 | /// @dev Emits a {RecoverToken} event. 72 | /// 73 | /// Requirements: 74 | /// 75 | /// - The caller must be the owner. 76 | /// - The token denylist must be set. 77 | /// - The amount to recover must not be zero. 78 | /// - The token to recover must not be in the token denylist. 79 | /// 80 | /// @param token The token to make the recover for. 81 | /// @param amount The uint256 amount to recover, specified in the token's decimal system. 82 | function recover(IERC20 token, uint256 amount) external; 83 | } 84 | -------------------------------------------------------------------------------- /src/token/erc20/ERC20Permit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | import { ERC20 } from "./ERC20.sol"; 5 | import { IERC20Permit } from "./IERC20Permit.sol"; 6 | 7 | /// @title ERC20Permit 8 | /// @author Paul Razvan Berg 9 | contract ERC20Permit is 10 | IERC20Permit, // 1 inherited component 11 | ERC20 // 1 inherited component 12 | { 13 | /*////////////////////////////////////////////////////////////////////////// 14 | USER-FACING STORAGE 15 | //////////////////////////////////////////////////////////////////////////*/ 16 | 17 | /// @inheritdoc IERC20Permit 18 | bytes32 public immutable override DOMAIN_SEPARATOR; 19 | 20 | /// @inheritdoc IERC20Permit 21 | bytes32 public constant override PERMIT_TYPEHASH = 22 | keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 23 | 24 | /// @inheritdoc IERC20Permit 25 | mapping(address => uint256) public override nonces; 26 | 27 | /// @inheritdoc IERC20Permit 28 | string public constant override version = "1"; 29 | 30 | /*////////////////////////////////////////////////////////////////////////// 31 | CONSTRUCTOR 32 | //////////////////////////////////////////////////////////////////////////*/ 33 | 34 | constructor(string memory _name, string memory _symbol, uint8 _decimals) ERC20(_name, _symbol, _decimals) { 35 | uint256 chainId = block.chainid; 36 | DOMAIN_SEPARATOR = keccak256( 37 | abi.encode( 38 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 39 | keccak256(bytes(name)), 40 | keccak256(bytes(version)), 41 | chainId, 42 | address(this) 43 | ) 44 | ); 45 | } 46 | 47 | /*////////////////////////////////////////////////////////////////////////// 48 | USER-FACING NON-CONSTANT FUNCTIONS 49 | //////////////////////////////////////////////////////////////////////////*/ 50 | 51 | /// @inheritdoc IERC20Permit 52 | function permit( 53 | address owner, 54 | address spender, 55 | uint256 value, 56 | uint256 deadline, 57 | uint8 v, 58 | bytes32 r, 59 | bytes32 s 60 | ) 61 | public 62 | override 63 | { 64 | // Checks: `owner` is not the zero address. 65 | if (owner == address(0)) { 66 | revert ERC20Permit_OwnerZeroAddress(); 67 | } 68 | 69 | // Checks: `spender` is not the zero address. 70 | if (spender == address(0)) { 71 | revert ERC20Permit_SpenderZeroAddress(); 72 | } 73 | 74 | // Checks: the deadline is in the future (or at least at present). 75 | if (deadline < block.timestamp) { 76 | revert ERC20Permit_PermitExpired(block.timestamp, deadline); 77 | } 78 | 79 | // It's safe to use unchecked here because the nonce cannot realistically overflow, ever. 80 | bytes32 hashStruct; 81 | unchecked { 82 | hashStruct = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)); 83 | } 84 | 85 | // EIP-712 messages always start with "\x19\x01". 86 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, hashStruct)); 87 | address recoveredOwner = ecrecover(digest, v, r, s); 88 | 89 | // Checks: `recoveredOwner` is not the zero address. 90 | if (recoveredOwner == address(0)) { 91 | revert ERC20Permit_RecoveredOwnerZeroAddress(); 92 | } 93 | 94 | // Checks: `recoveredOwner` is not the same as `owner`. 95 | if (recoveredOwner != owner) { 96 | revert ERC20Permit_InvalidSignature(owner, v, r, s); 97 | } 98 | 99 | // Effects: update the allowance. 100 | _allowances[owner][spender] = value; 101 | 102 | // Emit an event. 103 | emit Approval(owner, spender, value); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/token/erc20/burn/burn.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { stdError } from "forge-std/StdError.sol"; 5 | 6 | import { IERC20 } from "src/token/erc20/IERC20.sol"; 7 | 8 | import { ERC20_Test } from "../ERC20.t.sol"; 9 | 10 | contract Burn_Test is ERC20_Test { 11 | function test_RevertWhen_HolderZeroAddress() external { 12 | vm.expectRevert(IERC20.ERC20_BurnHolderZeroAddress.selector); 13 | dai.burn({ holder: address(0), amount: 1 }); 14 | } 15 | 16 | modifier whenHolderNotZeroAddress() { 17 | _; 18 | } 19 | 20 | function testFuzz_RevertWhen_HolderBalanceCalculationUnderflowsUint256( 21 | address holder, 22 | uint256 amount 23 | ) 24 | external 25 | whenHolderNotZeroAddress 26 | { 27 | vm.assume(holder != address(0)); 28 | vm.assume(amount > 0); 29 | 30 | // Expect an arithmetic error. 31 | vm.expectRevert(stdError.arithmeticError); 32 | 33 | // Burn the tokens. 34 | dai.burn(holder, amount); 35 | } 36 | 37 | modifier whenHolderBalanceCalculationDoesNotUnderflowUint256() { 38 | _; 39 | } 40 | 41 | /// @dev Checks common assumptions for the tests below. 42 | function checkAssumptions(address holder, uint256 burnAmount) internal pure { 43 | vm.assume(holder != address(0)); 44 | vm.assume(burnAmount > 0 && burnAmount < MAX_UINT256); 45 | } 46 | 47 | function testFuzz_Burn_DecreaseHolderBalance( 48 | address holder, 49 | uint256 mintAmount, 50 | uint256 burnAmount 51 | ) 52 | external 53 | whenHolderNotZeroAddress 54 | whenHolderBalanceCalculationDoesNotUnderflowUint256 55 | { 56 | checkAssumptions(holder, burnAmount); 57 | mintAmount = _bound(mintAmount, burnAmount + 1, MAX_UINT256); 58 | 59 | // Mint `mintAmount` tokens to `holder` so that we have what to burn below. 60 | dai.mint(holder, mintAmount); 61 | 62 | // Burn the tokens. 63 | dai.burn(holder, burnAmount); 64 | 65 | // Assert that the balance of the holder has decreased. 66 | uint256 actualBalance = dai.balanceOf(holder); 67 | uint256 expectedBalance = mintAmount - burnAmount; 68 | assertEq(actualBalance, expectedBalance, "balance"); 69 | } 70 | 71 | function testFuzz_Burn_DecreaseTotalSupply( 72 | address holder, 73 | uint256 mintAmount, 74 | uint256 burnAmount 75 | ) 76 | external 77 | whenHolderNotZeroAddress 78 | whenHolderBalanceCalculationDoesNotUnderflowUint256 79 | { 80 | checkAssumptions(holder, burnAmount); 81 | mintAmount = _bound(mintAmount, burnAmount + 1, MAX_UINT256); 82 | 83 | // Mint `mintAmount` tokens to `holder` so that we have what to burn below. 84 | dai.mint(holder, mintAmount); 85 | 86 | // Load the initial total supply. 87 | uint256 initialTotalSupply = dai.totalSupply(); 88 | 89 | // Burn the tokens. 90 | dai.burn(holder, burnAmount); 91 | 92 | // Assert that the total supply has decreased. 93 | uint256 actualTotalSupply = dai.totalSupply(); 94 | uint256 expectedTotalSupply = initialTotalSupply - burnAmount; 95 | assertEq(actualTotalSupply, expectedTotalSupply, "totalSupply"); 96 | } 97 | 98 | function testFuzz_Burn_Event( 99 | address holder, 100 | uint256 mintAmount, 101 | uint256 burnAmount 102 | ) 103 | external 104 | whenHolderNotZeroAddress 105 | whenHolderBalanceCalculationDoesNotUnderflowUint256 106 | { 107 | checkAssumptions(holder, burnAmount); 108 | mintAmount = _bound(mintAmount, burnAmount + 1, MAX_UINT256); 109 | 110 | // Mint `mintAmount` tokens to `holder` so that we have what to burn below. 111 | dai.mint(holder, mintAmount); 112 | 113 | // Expect a {Transfer} event to be emitted. 114 | vm.expectEmit({ emitter: address(dai) }); 115 | emit Transfer({ from: holder, to: address(0), amount: burnAmount }); 116 | 117 | // Burn the tokens. 118 | dai.burn(holder, burnAmount); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/token/erc20/ERC20Recover.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | import { IERC20 } from "./IERC20.sol"; 5 | import { IERC20Recover } from "./IERC20Recover.sol"; 6 | import { SafeERC20 } from "./SafeERC20.sol"; 7 | import { Adminable } from "../../access/Adminable.sol"; 8 | 9 | /// @title ERC20Recover 10 | /// @author Paul Razvan Berg 11 | abstract contract ERC20Recover is 12 | Adminable, // 1 inherited component 13 | IERC20Recover // 2 inherited components 14 | { 15 | using SafeERC20 for IERC20; 16 | 17 | /*////////////////////////////////////////////////////////////////////////// 18 | USER-FACING STORAGE 19 | //////////////////////////////////////////////////////////////////////////*/ 20 | 21 | /// @inheritdoc IERC20Recover 22 | bool public override isTokenDenylistSet; 23 | 24 | /*////////////////////////////////////////////////////////////////////////// 25 | INTERNAL STORAGE 26 | //////////////////////////////////////////////////////////////////////////*/ 27 | 28 | /// @dev Mapping between unsigned integers and non-recoverable tokens. 29 | IERC20[] internal tokenDenylist; 30 | 31 | /*////////////////////////////////////////////////////////////////////////// 32 | USER-FACING CONSTANT FUNCTIONS 33 | //////////////////////////////////////////////////////////////////////////*/ 34 | 35 | /// @inheritdoc IERC20Recover 36 | function getTokenDenylist() external view returns (IERC20[] memory) { 37 | return tokenDenylist; 38 | } 39 | 40 | /*////////////////////////////////////////////////////////////////////////// 41 | USER-FACING NON-CONSTANT FUNCTIONS 42 | //////////////////////////////////////////////////////////////////////////*/ 43 | 44 | /// @inheritdoc IERC20Recover 45 | function recover(IERC20 token, uint256 amount) public override onlyAdmin { 46 | // Checks: the token denylist is set. 47 | if (!isTokenDenylistSet) { 48 | revert ERC20Recover_TokenDenylistNotSet(); 49 | } 50 | 51 | // Checks: the amount to recover is not zero. 52 | if (amount == 0) { 53 | revert ERC20Recover_RecoverAmountZero(); 54 | } 55 | 56 | // Iterate over the non-recoverable token array. 57 | uint256 length = tokenDenylist.length; 58 | IERC20 nonRecoverableToken; 59 | 60 | for (uint256 i = 0; i < length;) { 61 | // Check that the addresses of the tokens are not the same. 62 | nonRecoverableToken = tokenDenylist[i]; 63 | if (token == nonRecoverableToken) { 64 | revert ERC20Recover_RecoverNonRecoverableToken(address(token)); 65 | } 66 | 67 | // Increment the for loop iterator. 68 | unchecked { 69 | i += 1; 70 | } 71 | } 72 | 73 | // Interactions: recover the tokens by transferring them to the admin. 74 | token.safeTransfer(admin, amount); 75 | 76 | // Emit an event. 77 | emit Recover(admin, token, amount); 78 | } 79 | 80 | /// @inheritdoc IERC20Recover 81 | function setTokenDenylist(IERC20[] memory tokenDenylist_) public override onlyAdmin { 82 | // Checks: the token denylist is not already set. 83 | if (isTokenDenylistSet) { 84 | revert ERC20Recover_TokenDenylistAlreadySet(); 85 | } 86 | 87 | // Iterate over the token list. 88 | uint256 length = tokenDenylist_.length; 89 | IERC20 token; 90 | for (uint256 i = 0; i < length;) { 91 | token = tokenDenylist_[i]; 92 | 93 | // Sanity check each token contract by calling the `symbol` method. 94 | token.symbol(); 95 | 96 | // Update the mapping. 97 | tokenDenylist.push(token); 98 | 99 | // Increment the for loop iterator. 100 | unchecked { 101 | i += 1; 102 | } 103 | } 104 | 105 | // Effects: prevent this function from ever being called again. 106 | isTokenDenylistSet = true; 107 | 108 | // Emit an event. 109 | emit SetTokenDenylist(admin, tokenDenylist_); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/token/erc20/SafeERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | import { IERC20 } from "./IERC20.sol"; 5 | import { Address } from "../../utils/Address.sol"; 6 | 7 | /// @title SafeERC20.sol 8 | /// @author Paul Razvan Berg 9 | /// @notice Wraps around ERC-20 operations that throw on failure (when the token contract 10 | /// returns false). Tokens that return no value (and instead revert or throw 11 | /// on failure) are also supported, non-reverting calls are assumed to be successful. 12 | /// 13 | /// To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, 14 | /// which allows you to call the safe operations as `token.safeTransfer(...)`, etc. 15 | /// 16 | /// @dev Forked from OpenZeppelin 17 | /// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.4.0/contracts/token/ERC20/SafeERC20.sol 18 | library SafeERC20 { 19 | using Address for address; 20 | 21 | /*////////////////////////////////////////////////////////////////////////// 22 | ERRORS 23 | //////////////////////////////////////////////////////////////////////////*/ 24 | 25 | /// @notice Thrown when the call is made to a non-contract. 26 | error SafeERC20_CallToNonContract(address target); 27 | 28 | /// @notice Thrown when there is no return data. 29 | error SafeERC20_NoReturnData(); 30 | 31 | /*////////////////////////////////////////////////////////////////////////// 32 | INTERNAL FUNCTIONS 33 | //////////////////////////////////////////////////////////////////////////*/ 34 | 35 | function safeTransfer(IERC20 token, address to, uint256 amount) internal { 36 | callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, amount)); 37 | } 38 | 39 | function safeTransferFrom(IERC20 token, address from, address to, uint256 amount) internal { 40 | callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, amount)); 41 | } 42 | 43 | /*////////////////////////////////////////////////////////////////////////// 44 | PRIVATE FUNCTIONS 45 | //////////////////////////////////////////////////////////////////////////*/ 46 | 47 | /// @dev Imitates a Solidity high-level call (a regular function call to a contract), relaxing the requirement 48 | /// on the return value: the return value is optional (but if data is returned, it cannot be false). 49 | /// @param token The token targeted by the call. 50 | /// @param data The call data (encoded using abi.encode or one of its variants). 51 | function callOptionalReturn(IERC20 token, bytes memory data) private { 52 | // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since 53 | // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that 54 | // the target address contains contract code and also asserts for success in the low-level call. 55 | bytes memory returndata = functionCall(address(token), data, "SafeERC20LowLevelCall"); 56 | if (returndata.length > 0) { 57 | // Return data is optional. 58 | if (!abi.decode(returndata, (bool))) { 59 | revert SafeERC20_NoReturnData(); 60 | } 61 | } 62 | } 63 | 64 | function functionCall( 65 | address target, 66 | bytes memory data, 67 | string memory errorMessage 68 | ) 69 | private 70 | returns (bytes memory) 71 | { 72 | if (!target.isContract()) { 73 | revert SafeERC20_CallToNonContract(target); 74 | } 75 | 76 | // solhint-disable-next-line avoid-low-level-calls 77 | (bool success, bytes memory returndata) = target.call(data); 78 | if (success) { 79 | return returndata; 80 | } else { 81 | // Look for revert reason and bubble it up if present. 82 | if (returndata.length > 0) { 83 | // The easiest way to bubble the revert reason is using memory via assembly. 84 | // solhint-disable-next-line no-inline-assembly 85 | assembly { 86 | let returndata_size := mload(returndata) 87 | revert(add(32, returndata), returndata_size) 88 | } 89 | } else { 90 | revert(errorMessage); 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PRBContracts [![GitHub Actions][gha-badge]][gha] [![Foundry][foundry-badge]][foundry] [![License: MIT][license-badge]][license] 2 | 3 | [gha]: https://github.com/PaulRBerg/prb-contracts/actions 4 | [gha-badge]: https://github.com/PaulRBerg/prb-contracts/actions/workflows/ci.yml/badge.svg 5 | [foundry]: https://getfoundry.sh/ 6 | [foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg 7 | [license]: https://opensource.org/licenses/MIT 8 | [license-badge]: https://img.shields.io/badge/License-MIT-blue.svg 9 | 10 | Off-the-shelf Solidity smart contracts. 11 | 12 | - Designed for Solidity >=0.8.4 13 | - Uses custom errors instead of revert reason strings 14 | - Complementary to [OpenZeppelin's library](https://github.com/OpenZeppelin/openzeppelin-contracts) 15 | - Well-documented via NatSpec comments 16 | - Thoroughly tested with Foundry 17 | 18 | I initially created this library to streamline my personal workflow, as I was tired of having to maintain identical 19 | contracts across multiple repositories. However, if you find this library beneficial to your own projects, that's a 20 | win-win situation for both of us. 21 | 22 | ## Install 23 | 24 | ### Foundry 25 | 26 | First, run the install step: 27 | 28 | ```sh 29 | forge install --no-commit PaulRBerg/prb-contracts@v5 30 | ``` 31 | 32 | Your `.gitmodules` file should now contain the following entry: 33 | 34 | ```toml 35 | [submodule "lib/prb-contracts"] 36 | branch = "v5" 37 | path = "lib/prb-contracts" 38 | url = "https://github.com/PaulRBerg/prb-contracts" 39 | ``` 40 | 41 | Finally, add this to your `remappings.txt` file: 42 | 43 | ```text 44 | @prb/contracts/=lib/prb-contracts/src/ 45 | ``` 46 | 47 | ### Hardhat 48 | 49 | ```sh 50 | pnpm add @prb/contracts 51 | ``` 52 | 53 | ## Usage 54 | 55 | Once installed, you can use the contracts like this: 56 | 57 | ```solidity 58 | // SPDX-License-Identifier: MIT 59 | pragma solidity >=0.8.4; 60 | 61 | import { ERC20 } from "@prb/contracts/token/erc20/ERC20.sol"; 62 | import { ERC20Permit } from "@prb/contracts/token/erc20/ERC20Permit.sol"; 63 | 64 | contract MyToken is ERC20, ERC20Permit { 65 | constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20Permit(name_, symbol_, decimals_) {} 66 | } 67 | ``` 68 | 69 | ## Contributing 70 | 71 | Feel free to dive in! [Open](https://github.com/PaulRBerg/prb-proxy/issues/new) an issue, 72 | [start](https://github.com/PaulRBerg/prb-proxy/discussions/new) a discussion or submit a PR. 73 | 74 | ### Pre Requisites 75 | 76 | You will need the following software on your machine: 77 | 78 | - [Git](https://git-scm.com/downloads) 79 | - [Foundry](https://github.com/foundry-rs/foundry) 80 | - [Node.Js](https://nodejs.org/en/download/) 81 | - [Pnpm](https://pnpm.io) 82 | 83 | In addition, familiarity with [Solidity](https://soliditylang.org/) is requisite. 84 | 85 | ### Set Up 86 | 87 | Clone this repository: 88 | 89 | ```sh 90 | $ git clone git@github.com:PaulRBerg/prb-contracts.git 91 | ``` 92 | 93 | Then, inside the project's directory, run this to install the Node.js dependencies: 94 | 95 | ```sh 96 | $ pnpm install 97 | ``` 98 | 99 | Now you can start making changes. 100 | 101 | ### Syntax Highlighting 102 | 103 | You will need the following VSCode extensions: 104 | 105 | - [hardhat-solidity](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity) 106 | - [vscode-tree-language](https://marketplace.visualstudio.com/items?itemName=CTC.vscode-tree-extension) 107 | 108 | ## Security 109 | 110 | While I have strict standards for code quality and test coverage, it's important to note that this project may not be 111 | entirely risk-free. Although I have taken measures to ensure the security of the contracts, they have not yet been 112 | audited by a third-party security researcher. 113 | 114 | ### Caveat Emptor 115 | 116 | Please be aware that this software is experimental and is provided on an "as is" and "as available" basis. I do not 117 | offer any warranties, and I cannot be held responsible for any direct or indirect loss resulting from the continued use 118 | of this codebase. 119 | 120 | ### Contact 121 | 122 | If you discover any bugs or security issues, please report them via [Telegram](https://t.me/PaulRBerg). 123 | 124 | ## Related Efforts 125 | 126 | - [openzeppelin-contracts](https://github.com/OpenZeppelin/openzeppelin-contracts) 127 | - Alberto Cuesta Cañada's [ERC20Permit](https://github.com/alcueca/ERC20Permit) and 128 | [Orchestrated](https://github.com/alcueca/Orchestrated) 129 | 130 | ## License 131 | 132 | This project is licensed under MIT. 133 | -------------------------------------------------------------------------------- /test/Base.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { PRBTest } from "@prb/test/PRBTest.sol"; 5 | import { StdCheats } from "forge-std/StdCheats.sol"; 6 | import { StdUtils } from "forge-std/StdUtils.sol"; 7 | 8 | import { ERC20GodMode } from "src/token/erc20/ERC20GodMode.sol"; 9 | import { IERC20 } from "src/token/erc20/IERC20.sol"; 10 | 11 | /// @notice Common contract members needed across test contracts. 12 | abstract contract Base_Test is PRBTest, StdCheats, StdUtils { 13 | /*////////////////////////////////////////////////////////////////////////// 14 | EVENTS 15 | //////////////////////////////////////////////////////////////////////////*/ 16 | 17 | event LogNamedArray(string key, IERC20[] value); 18 | 19 | /*////////////////////////////////////////////////////////////////////////// 20 | STRUCTS 21 | //////////////////////////////////////////////////////////////////////////*/ 22 | 23 | struct Users { 24 | address payable alice; 25 | address payable admin; 26 | address payable bob; 27 | address payable eve; 28 | } 29 | 30 | /*////////////////////////////////////////////////////////////////////////// 31 | CONSTANTS 32 | //////////////////////////////////////////////////////////////////////////*/ 33 | 34 | uint256 internal constant ONE_MILLION_DAI = 1_000_000e18; 35 | uint256 internal constant ONE_MILLION_USDC = 1_000_000e6; 36 | 37 | /*////////////////////////////////////////////////////////////////////////// 38 | TESTING CONTRACTS 39 | //////////////////////////////////////////////////////////////////////////*/ 40 | 41 | ERC20GodMode internal dai = new ERC20GodMode("Dai Stablecoin", "DAI", 18); 42 | ERC20GodMode internal tkn0 = new ERC20GodMode("Token 0", "TKN0", 0); 43 | ERC20GodMode internal usdc = new ERC20GodMode("USD Coin", "USDC", 6); 44 | Users internal users; 45 | 46 | /*////////////////////////////////////////////////////////////////////////// 47 | SETUP FUNCTION 48 | //////////////////////////////////////////////////////////////////////////*/ 49 | 50 | /// @dev A setup function invoked before each test case. 51 | function setUp() public virtual { 52 | // Create users for testing. 53 | users = Users({ 54 | alice: createUser("Alice"), 55 | admin: createUser("Admin"), 56 | bob: createUser("Bob"), 57 | eve: createUser("Eve") 58 | }); 59 | 60 | // Make the admin the default caller in all subsequent tests. 61 | vm.startPrank({ msgSender: users.admin }); 62 | } 63 | 64 | /*////////////////////////////////////////////////////////////////////////// 65 | INTERNAL CONSTANT FUNCTIONS 66 | //////////////////////////////////////////////////////////////////////////*/ 67 | 68 | /// @dev Helper function that multiplies the `amount` by `10^decimals` and returns a `uint256.` 69 | function bn(uint256 amount, uint256 decimals) internal pure returns (uint256 result) { 70 | result = amount * 10 ** decimals; 71 | } 72 | 73 | /*////////////////////////////////////////////////////////////////////////// 74 | INTERNAL NON-CONSTANT FUNCTIONS 75 | //////////////////////////////////////////////////////////////////////////*/ 76 | 77 | /// @dev Helper function to compare two `IERC20` arrays. 78 | function assertEq(IERC20[] memory a, IERC20[] memory b) internal { 79 | if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { 80 | emit Log("Error: a == b not satisfied [IERC20[]]"); 81 | emit LogNamedArray(" Expected", b); 82 | emit LogNamedArray(" Actual", a); 83 | fail(); 84 | } 85 | } 86 | 87 | /// @dev Helper function to compare two `IERC20` arrays. 88 | function assertEq(IERC20[] memory a, IERC20[] memory b, string memory err) internal { 89 | if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { 90 | emit LogNamedString("Error", err); 91 | assertEq(a, b); 92 | } 93 | } 94 | 95 | /// @dev Generates an address by hashing the name, labels the address and funds it with 100 ETH, 1 million DAI, 96 | /// and 1 million non-compliant tokens. 97 | function createUser(string memory name) internal returns (address payable addr) { 98 | addr = payable(makeAddr(name)); 99 | vm.deal({ account: addr, newBalance: 100 ether }); 100 | dai.mint({ beneficiary: addr, amount: ONE_MILLION_DAI }); 101 | usdc.mint({ beneficiary: addr, amount: ONE_MILLION_USDC }); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/token/erc20/transfer-from/transferFrom.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { IERC20 } from "src/token/erc20/IERC20.sol"; 5 | 6 | import { ERC20_Test } from "../ERC20.t.sol"; 7 | 8 | contract TransferFrom_Test is ERC20_Test { 9 | function test_RevertWhen_SpenderAllowanceNotEnough(address owner, uint256 amount) external { 10 | vm.assume(owner != address(0)); 11 | vm.assume(amount > 0); 12 | 13 | address spender = users.alice; 14 | uint256 currentAllowance = 0; 15 | vm.expectRevert( 16 | abi.encodeWithSelector( 17 | IERC20.ERC20_InsufficientAllowance.selector, owner, spender, currentAllowance, amount 18 | ) 19 | ); 20 | dai.transferFrom({ from: owner, to: spender, amount: amount }); 21 | } 22 | 23 | modifier whenSpenderAllowanceEnough() { 24 | _; 25 | } 26 | 27 | function checkAssumptions(address owner, address to, uint256 amount0) internal pure { 28 | vm.assume(owner != address(0) && to != address(0)); 29 | vm.assume(owner != to); 30 | vm.assume(amount0 > 0); 31 | } 32 | 33 | function testFuzz_TransferFrom_DecreaseOwnerBalance( 34 | address owner, 35 | address to, 36 | uint256 amount0, 37 | uint256 amount1 38 | ) 39 | external 40 | whenSpenderAllowanceEnough 41 | { 42 | checkAssumptions(owner, to, amount0); 43 | amount1 = _bound(amount1, 1, amount0); 44 | 45 | // Mint `amount0` tokens to the owner. 46 | dai.mint(owner, amount0); 47 | 48 | // Approve Alice to spend tokens from the owner. 49 | changePrank(owner); 50 | dai.approve({ spender: users.alice, value: amount0 }); 51 | 52 | // Make Alice the caller in this test. 53 | changePrank(users.alice); 54 | 55 | // Load the initial owner's balance. 56 | uint256 initialOwnerBalance = dai.balanceOf(owner); 57 | 58 | // Transfer the tokens. 59 | dai.transferFrom({ from: owner, to: to, amount: amount1 }); 60 | 61 | // Assert that the owner's balance has decreased. 62 | uint256 actualOwnerBalance = dai.balanceOf(owner); 63 | uint256 expectedOwnerBalance = initialOwnerBalance - amount1; 64 | assertEq(actualOwnerBalance, expectedOwnerBalance, "owner balance"); 65 | } 66 | 67 | function testFuzz_TransferFrom_IncreaseReceiverBalance( 68 | address owner, 69 | address to, 70 | uint256 amount0, 71 | uint256 amount1 72 | ) 73 | external 74 | whenSpenderAllowanceEnough 75 | { 76 | checkAssumptions(owner, to, amount0); 77 | amount1 = _bound(amount1, 1, amount0); 78 | 79 | // Mint `amount0` tokens to the owner. 80 | dai.mint(owner, amount0); 81 | 82 | // Approve Alice to spend tokens from the owner. 83 | changePrank(owner); 84 | dai.approve({ spender: users.alice, value: amount0 }); 85 | 86 | // Make Alice the caller in this test. 87 | changePrank(users.alice); 88 | 89 | // Load the initial receiver's balance. 90 | uint256 initialToBalance = dai.balanceOf(to); 91 | 92 | // Transfer the tokens. 93 | dai.transferFrom({ from: owner, to: to, amount: amount1 }); 94 | 95 | // Assert that the receiver's balance has increased. 96 | uint256 actualToBalance = dai.balanceOf(to); 97 | uint256 expectedToBalance = initialToBalance + amount1; 98 | assertEq(actualToBalance, expectedToBalance, "to balance"); 99 | } 100 | 101 | function testFuzz_TransferFrom_Event( 102 | address owner, 103 | address to, 104 | uint256 amount0, 105 | uint256 amount1 106 | ) 107 | external 108 | whenSpenderAllowanceEnough 109 | { 110 | checkAssumptions(owner, to, amount0); 111 | amount1 = _bound(amount1, 1, amount0); 112 | 113 | // Mint `amount0` tokens to the owner. 114 | dai.mint(owner, amount0); 115 | 116 | // Approve Alice to spend tokens from the owner. 117 | changePrank(owner); 118 | dai.approve({ spender: users.alice, value: amount0 }); 119 | 120 | // Make the spender the caller in this test. 121 | changePrank(users.alice); 122 | 123 | // Expect an {Approval} and a {Transfer} event to be emitted. 124 | vm.expectEmit({ emitter: address(dai) }); 125 | emit Approval({ owner: owner, spender: users.alice, amount: amount0 - amount1 }); 126 | vm.expectEmit({ emitter: address(dai) }); 127 | emit Transfer({ from: owner, to: to, amount: amount1 }); 128 | 129 | // Transfer the tokens. 130 | dai.transferFrom({ from: owner, to: to, amount: amount1 }); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /test/token/erc20/mint/mint.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { stdError } from "forge-std/StdError.sol"; 5 | 6 | import { IERC20 } from "src/token/erc20/IERC20.sol"; 7 | 8 | import { ERC20_Test } from "../ERC20.t.sol"; 9 | 10 | contract Mint_Test is ERC20_Test { 11 | function test_RevertWhen_BeneficiaryZeroAddress() external { 12 | vm.expectRevert(IERC20.ERC20_MintBeneficiaryZeroAddress.selector); 13 | dai.mint({ beneficiary: address(0), amount: 1 }); 14 | } 15 | 16 | modifier whenBeneficiaryNotZeroAddress() { 17 | _; 18 | } 19 | 20 | /// @dev Checks common assumptions for the tests below. 21 | function checkAssumptions(address beneficiary, uint256 amount0) internal pure { 22 | vm.assume(beneficiary != address(0)); 23 | vm.assume(amount0 > 0); 24 | } 25 | 26 | function testFuzz_RevertWhen_BeneficiaryBalanceCalculationOverflowsUint256( 27 | address beneficiary, 28 | uint256 amount0, 29 | uint256 amount1 30 | ) 31 | external 32 | whenBeneficiaryNotZeroAddress 33 | { 34 | checkAssumptions(beneficiary, amount0); 35 | amount1 = _bound(amount1, MAX_UINT256 - amount0 + 1, MAX_UINT256); 36 | 37 | // Mint `amount0` tokens to `beneficiary`. 38 | dai.mint(beneficiary, amount0); 39 | 40 | // Expect an arithmetic error. 41 | vm.expectRevert(stdError.arithmeticError); 42 | 43 | // Mint the tokens. 44 | dai.mint(beneficiary, amount1); 45 | } 46 | 47 | modifier whenBeneficiaryBalanceCalculationDoesNotOverflowUint256() { 48 | _; 49 | } 50 | 51 | function testFuzz_RevertWhen_TotalSupplyCalculationOverflowsUint256( 52 | address beneficiary, 53 | uint256 amount0, 54 | uint256 amount1 55 | ) 56 | external 57 | whenBeneficiaryNotZeroAddress 58 | whenBeneficiaryBalanceCalculationDoesNotOverflowUint256 59 | { 60 | checkAssumptions(beneficiary, amount0); 61 | amount1 = _bound(amount1, MAX_UINT256 - amount0 + 1, MAX_UINT256); 62 | 63 | // Mint `amount0` tokens to Alice. 64 | dai.mint(users.alice, amount0); 65 | 66 | // Expect an arithmetic panic. 67 | vm.expectRevert(stdError.arithmeticError); 68 | 69 | // Mint the tokens. 70 | dai.mint(beneficiary, amount1); 71 | } 72 | 73 | modifier whenTotalSupplyCalculationDoesNotOverflowUint256() { 74 | _; 75 | } 76 | 77 | function testFuzz_Mint_IncreaseBeneficiaryBalance( 78 | address beneficiary, 79 | uint256 amount 80 | ) 81 | external 82 | whenBeneficiaryNotZeroAddress 83 | whenBeneficiaryBalanceCalculationDoesNotOverflowUint256 84 | whenTotalSupplyCalculationDoesNotOverflowUint256 85 | { 86 | checkAssumptions(beneficiary, amount); 87 | 88 | // Load the initial balance. 89 | uint256 initialBalance = dai.balanceOf(beneficiary); 90 | 91 | // Mint the tokens. 92 | dai.mint(beneficiary, amount); 93 | 94 | // Assert that the balance has increased. 95 | uint256 actualBalance = dai.balanceOf(beneficiary); 96 | uint256 expectedBalance = initialBalance + amount; 97 | assertEq(actualBalance, expectedBalance, "balance"); 98 | } 99 | 100 | function testFuzz_Mint_IncreaseTotalSupply( 101 | address beneficiary, 102 | uint256 amount 103 | ) 104 | external 105 | whenBeneficiaryNotZeroAddress 106 | whenBeneficiaryBalanceCalculationDoesNotOverflowUint256 107 | whenTotalSupplyCalculationDoesNotOverflowUint256 108 | { 109 | checkAssumptions(beneficiary, amount); 110 | 111 | // Load the initial total supply. 112 | uint256 initialTotalSupply = dai.totalSupply(); 113 | 114 | // Mint the tokens. 115 | dai.mint(beneficiary, amount); 116 | 117 | // Assert that the total supply has increased. 118 | uint256 actualTotalSupply = dai.totalSupply(); 119 | uint256 expectedTotalSupply = initialTotalSupply + amount; 120 | assertEq(actualTotalSupply, expectedTotalSupply, "totalSupply"); 121 | } 122 | 123 | function testFuzz_Mint_Event( 124 | address beneficiary, 125 | uint256 amount 126 | ) 127 | external 128 | whenBeneficiaryNotZeroAddress 129 | whenBeneficiaryBalanceCalculationDoesNotOverflowUint256 130 | whenTotalSupplyCalculationDoesNotOverflowUint256 131 | { 132 | checkAssumptions(beneficiary, amount); 133 | 134 | // Expect a {Transfer} event to be emitted. 135 | vm.expectEmit({ emitter: address(dai) }); 136 | emit Transfer({ from: address(0), to: beneficiary, amount: amount }); 137 | 138 | // Mint the tokens. 139 | dai.mint(beneficiary, amount); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Common Changelog](https://common-changelog.org/), and this project adheres to 6 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | [5.0.6]: https://github.com/PaulRBerg/prb-contracts/compare/v5.0.5...v5.0.6 9 | [5.0.5]: https://github.com/PaulRBerg/prb-contracts/compare/v5.0.4...v5.0.5 10 | [5.0.4]: https://github.com/PaulRBerg/prb-contracts/compare/v5.0.3...v5.0.4 11 | [5.0.3]: https://github.com/PaulRBerg/prb-contracts/compare/v5.0.2...v5.0.3 12 | [5.0.2]: https://github.com/PaulRBerg/prb-contracts/compare/v5.0.1...v5.0.2 13 | [5.0.1]: https://github.com/PaulRBerg/prb-contracts/compare/v5.0.0...v5.0.1 14 | [5.0.0]: https://github.com/PaulRBerg/prb-contracts/compare/v4.1.1...v5.0.0 15 | [4.1.1]: https://github.com/PaulRBerg/prb-contracts/compare/v4.1.0...v4.1.1 16 | [4.1.0]: https://github.com/PaulRBerg/prb-contracts/compare/v4.0.0...v4.1.0 17 | [4.0.0]: https://github.com/PaulRBerg/prb-contracts/compare/v3.9.0...v4.0.0 18 | [3.9.0]: https://github.com/PaulRBerg/prb-contracts/compare/v3.8.1...v3.9.0 19 | [3.8.1]: https://github.com/PaulRBerg/prb-contracts/compare/v3.8.0...v3.8.1 20 | [3.8.0]: https://github.com/PaulRBerg/prb-contracts/releases/tag/v3.8.0 21 | 22 | ## [5.0.6] - 2023-04-13 23 | 24 | ### Changed 25 | 26 | - Bump submodules (@PaulRBerg) 27 | - Bump Node.js dependencies (@PaulRBerg) 28 | - Improve writing in comments (@PaulRBerg) 29 | 30 | ### Removed 31 | 32 | - Remove `rimraf` Node.js dependency (@PaulRBerg) 33 | 34 | ## [5.0.5] - 2023-03-17 35 | 36 | ### Removed 37 | 38 | - Remove problematic `src/=src/` remapping ([#41](https://github.com/PaulRBerg/prb-contracts/pull/41)) (@PaulRBerg) 39 | 40 | ## [5.0.4] - 2023-03-17 41 | 42 | ### Changed 43 | 44 | - Bump submodules (@PaulRBerg) 45 | 46 | ## [5.0.3] - 2023-03-03 47 | 48 | ### Changed 49 | 50 | - Bump submodules (@PaulRBerg) 51 | - Format contracts with `forge fmt` (@PaulRBerg) 52 | - Improve documentation (@PaulRBerg) 53 | 54 | ## [5.0.2] - 2023-02-07 55 | 56 | ### Fixed 57 | 58 | - Delete stale `prb-math` submodule (@PaulRBerg) 59 | 60 | ## [5.0.1] - 2023-02-06 61 | 62 | ### Fixed 63 | 64 | - Fix installation in Node.js projects with `pinst` (@PaulRBerg) 65 | 66 | ## [5.0.0] - 2023-02-06 67 | 68 | ### Changed 69 | 70 | - Change license to MIT (@PaulRBerg) 71 | - Delete the "\_" prefix from admin functions (@PaulRBerg) 72 | - Improve custom error and function parameter names (@PaulRBerg) 73 | - Improve documentation (@PaulRBerg) 74 | - Improve formatting by running the latest Prettier plugin (@PaulRBerg) 75 | - Improve wording and grammar in NatSpec comments (@PaulRBerg) 76 | - Move contracts to `src` directory (@PaulRBerg) 77 | - Perform approval before transfer in `transferFrom` (@PaulRBerg) 78 | - Refactor `amount` to `value` in the `approve` function of the `ERC20` contract (@PaulRBerg) 79 | - Refactor the `NonStandardERC20` contract to `NonCompliantERC20` (@PaulRBerg) 80 | - Refactor `nonRecoverableTokens` to `tokenDenylist` in `ERC20Recover` (@PaulRBerg) 81 | - Optimize calculations in the `approve`, `burn`, and `mint` functions of the `ERC20` contract (@PaulRBerg) 82 | - Use named arguments in function calls (@PaulRBerg) 83 | 84 | ### Added 85 | 86 | - Add new contract `Adminable`, which supersedes `Ownable` (@PaulRBerg) 87 | - Add new contract `ERC20Normalizer` (@PaulRBerg) 88 | 89 | ### Removed 90 | 91 | - Remove PRBMath re-exports (@PaulRBerg) 92 | - Remove `Ownable` contract (@PaulRBerg) 93 | - Remove `GodModeERC20` contract (@PaulRBerg) 94 | 95 | ## [4.1.1] - 2022-04-06 96 | 97 | ### Fixed 98 | 99 | - Implement the constructor in the `NonStandardERC20` contract (@PaulRBerg) 100 | 101 | ## [4.1.0] - 2022-04-06 102 | 103 | ### Added 104 | 105 | - Add `burn` and `mint` methods in the `NonStandardERC20` contract (@PaulRBerg) 106 | - Add a new contract `ERC20GodMode` that replicates `GodModeERC20` (@PaulRBerg) 107 | 108 | ## [4.0.0] - 2022-04-04 109 | 110 | ### Changed 111 | 112 | - Refactor the `Erc` prefix into `ERC` in all `ERC-20` references 113 | ([#25](https://github.com/PaulRBerg/prb-contracts/issues/25)) (@PaulRBerg) 114 | 115 | ### Removed 116 | 117 | - The `Admin` and `IAdmin` contracts and their related bindings (@PaulRBerg) 118 | 119 | ## [3.9.0] - 2022-04-03 120 | 121 | ### Changed 122 | 123 | - Define the custom errors in the smart contract interface files (@PaulRBerg) 124 | 125 | ## [3.8.1] - 2022-03-11 126 | 127 | ### Fixed 128 | 129 | - Include `CHANGELOG`, `LICENSE` and `README` in the package shipped to npm (@PaulRBerg) 130 | 131 | ## [3.8.0] - 2022-03-08 132 | 133 | ### Changed 134 | 135 | - Change the package name from `@PaulRBerg/contracts` to `@prb/contracts` (@PaulRBerg) 136 | - Switch from `prb-math` package to `@prb/math` (@PaulRBerg) 137 | - Update links in README and `package.json` files (@PaulRBerg) 138 | 139 | ### Fixed 140 | 141 | - Fix the EIP-2612 permit typehash ([#24](https://github.com/PaulRBerg/prb-contracts/pull/24)) (@surbhiaudichya) 142 | -------------------------------------------------------------------------------- /test/token/erc20/transfer/transfer.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { IERC20 } from "src/token/erc20/IERC20.sol"; 5 | 6 | import { ERC20_Test } from "../ERC20.t.sol"; 7 | 8 | contract Transfer_Test is ERC20_Test { 9 | function test_RevertWhen_SenderZeroAddress() external { 10 | // Make the zero address the caller in this test. 11 | changePrank(address(0)); 12 | 13 | // Run the test. 14 | vm.expectRevert(IERC20.ERC20_TransferFromZeroAddress.selector); 15 | dai.transfer({ to: users.alice, amount: ONE_MILLION_DAI }); 16 | } 17 | 18 | modifier whenSenderNotZeroAddress() { 19 | _; 20 | } 21 | 22 | function test_RevertWhen_ReceiverZeroAddress() external whenSenderNotZeroAddress { 23 | vm.expectRevert(IERC20.ERC20_TransferToZeroAddress.selector); 24 | dai.transfer({ to: address(0), amount: ONE_MILLION_DAI }); 25 | } 26 | 27 | modifier whenRecipientNotZeroAddress() { 28 | _; 29 | } 30 | 31 | function testFuzz_RevertWhen_SenderNotEnoughBalance(uint256 amount) 32 | external 33 | whenSenderNotZeroAddress 34 | whenRecipientNotZeroAddress 35 | { 36 | vm.assume(amount > 0); 37 | 38 | uint256 senderBalance = 0; 39 | vm.expectRevert(abi.encodeWithSelector(IERC20.ERC20_FromInsufficientBalance.selector, senderBalance, amount)); 40 | dai.transfer(users.alice, amount); 41 | } 42 | 43 | modifier whenSenderEnoughBalance() { 44 | _; 45 | } 46 | 47 | function testFuzz_Transfer_ReceiverSender(uint256 amount) 48 | external 49 | whenSenderNotZeroAddress 50 | whenRecipientNotZeroAddress 51 | whenSenderEnoughBalance 52 | { 53 | vm.assume(amount > 0); 54 | 55 | // Mint `amount` tokens to Alice so that we have something to transfer below. 56 | dai.mint({ beneficiary: users.alice, amount: amount }); 57 | 58 | // Load the initial balance. 59 | uint256 initialBalance = dai.balanceOf(users.alice); 60 | 61 | // Transfer the tokens. 62 | dai.transfer(users.alice, amount); 63 | 64 | // Assert that the user's balance has remained unchanged. 65 | uint256 actualBalance = dai.balanceOf(users.alice); 66 | uint256 expectedBalance = initialBalance; 67 | assertEq(actualBalance, expectedBalance, "balance"); 68 | } 69 | 70 | function checkAssumptions(address to, uint256 amount) internal view { 71 | vm.assume(to != address(0)); 72 | vm.assume(to != users.alice); 73 | vm.assume(amount > 0); 74 | } 75 | 76 | function testFuzz_Transfer_ReceiverNotSender_DecreaseSenderBalance( 77 | address to, 78 | uint256 amount 79 | ) 80 | external 81 | whenSenderNotZeroAddress 82 | whenRecipientNotZeroAddress 83 | whenSenderEnoughBalance 84 | { 85 | checkAssumptions(to, amount); 86 | 87 | // Mint `amount` tokens to Alice so that we have something to transfer below. 88 | dai.mint(users.alice, amount); 89 | 90 | // Run the test. 91 | uint256 initialBalance = dai.balanceOf(users.alice); 92 | 93 | // Transfer the tokens. 94 | dai.transfer(to, amount); 95 | 96 | // Assert that the sender's balance has decreased. 97 | uint256 actualBalance = dai.balanceOf(users.alice); 98 | uint256 expectedBalance = initialBalance - amount; 99 | assertEq(actualBalance, expectedBalance, "balance"); 100 | } 101 | 102 | function testFuzz_Transfer_ReceiverNotSender_IncreaseReceiverBalance( 103 | address to, 104 | uint256 amount 105 | ) 106 | external 107 | whenSenderNotZeroAddress 108 | whenRecipientNotZeroAddress 109 | whenSenderEnoughBalance 110 | { 111 | checkAssumptions(to, amount); 112 | 113 | // Mint `amount` tokens to Alice so that we have something to transfer below. 114 | dai.mint(users.alice, amount); 115 | 116 | // Run the test. 117 | uint256 initialBalance = dai.balanceOf(to); 118 | 119 | // Transfer the tokens. 120 | dai.transfer(to, amount); 121 | 122 | // Assert that the receiver's balance has increased. 123 | uint256 actualBalance = dai.balanceOf(to); 124 | uint256 expectedBalance = initialBalance + amount; 125 | assertEq(actualBalance, expectedBalance, "balance"); 126 | } 127 | 128 | function testFuzz_Transfer_ReceiverNotSender_Event( 129 | address to, 130 | uint256 amount 131 | ) 132 | external 133 | whenSenderNotZeroAddress 134 | whenRecipientNotZeroAddress 135 | whenSenderEnoughBalance 136 | { 137 | checkAssumptions(to, amount); 138 | 139 | // Mint `amount` tokens to Alice so that we have something to transfer below. 140 | dai.mint(users.alice, amount); 141 | 142 | // Expect a {Transfer} event to be emitted. 143 | vm.expectEmit({ emitter: address(dai) }); 144 | emit Transfer(users.alice, to, amount); 145 | 146 | // Transfer the tokens. 147 | dai.transfer(to, amount); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /test/token/erc20-permit/permit/permit.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19 <0.9.0; 3 | 4 | import { IERC20Permit } from "src/token/erc20/IERC20Permit.sol"; 5 | 6 | import { ERC20Permit_Test } from "../ERC20Permit.t.sol"; 7 | 8 | contract Permit_Test is ERC20Permit_Test { 9 | function test_RevertWhen_OwnerZeroAddress() external { 10 | vm.expectRevert(IERC20Permit.ERC20Permit_OwnerZeroAddress.selector); 11 | erc20Permit.permit({ 12 | owner: address(0), 13 | spender: users.alice, 14 | value: 1, 15 | deadline: DECEMBER_2099, 16 | v: DUMMY_V, 17 | r: DUMMY_R, 18 | s: DUMMY_S 19 | }); 20 | } 21 | 22 | modifier whenOwnerNotZeroAddress() { 23 | _; 24 | } 25 | 26 | function test_RevertWhen_SpenderZeroAddress() external whenOwnerNotZeroAddress { 27 | vm.expectRevert(IERC20Permit.ERC20Permit_SpenderZeroAddress.selector); 28 | erc20Permit.permit({ 29 | owner: users.alice, 30 | spender: address(0), 31 | value: 1, 32 | deadline: DECEMBER_2099, 33 | v: DUMMY_V, 34 | r: DUMMY_R, 35 | s: DUMMY_S 36 | }); 37 | } 38 | 39 | modifier whenSpenderNotZeroAddress() { 40 | _; 41 | } 42 | 43 | function test_RevertWhen_DeadlineInThePast(uint256 deadline) 44 | external 45 | whenOwnerNotZeroAddress 46 | whenSpenderNotZeroAddress 47 | { 48 | deadline = _bound(deadline, 0, block.timestamp - 1 seconds); 49 | 50 | vm.expectRevert( 51 | abi.encodeWithSelector(IERC20Permit.ERC20Permit_PermitExpired.selector, block.timestamp, deadline) 52 | ); 53 | erc20Permit.permit({ 54 | owner: users.alice, 55 | spender: users.bob, 56 | value: 1, 57 | deadline: deadline, 58 | v: DUMMY_V, 59 | r: DUMMY_R, 60 | s: DUMMY_S 61 | }); 62 | } 63 | 64 | modifier whenDeadlineNotInThePast() { 65 | _; 66 | } 67 | 68 | /// @dev Setting `v` to any number other than 27 or 28 makes the `ecrecover` precompile return the zero address. 69 | /// https://ethereum.stackexchange.com/questions/69328/how-to-get-the-zero-address-from-ecrecover 70 | function test_RevertWhen_RecoveredOwnerZeroAddress( 71 | uint256 deadline, 72 | uint8 v 73 | ) 74 | external 75 | whenOwnerNotZeroAddress 76 | whenSpenderNotZeroAddress 77 | whenDeadlineNotInThePast 78 | { 79 | vm.assume(v != 27 && v != 28); 80 | deadline = _bound(deadline, block.timestamp, DECEMBER_2099); 81 | 82 | vm.expectRevert(IERC20Permit.ERC20Permit_RecoveredOwnerZeroAddress.selector); 83 | erc20Permit.permit({ 84 | owner: users.alice, 85 | spender: users.bob, 86 | value: 1, 87 | deadline: deadline, 88 | v: v, 89 | r: DUMMY_R, 90 | s: DUMMY_S 91 | }); 92 | } 93 | 94 | modifier whenRecoveredOwnerNotZeroAddress() { 95 | _; 96 | } 97 | 98 | function test_RevertWhen_SignatureNotValid(uint256 deadline) 99 | external 100 | whenOwnerNotZeroAddress 101 | whenSpenderNotZeroAddress 102 | whenDeadlineNotInThePast 103 | whenRecoveredOwnerNotZeroAddress 104 | { 105 | deadline = _bound(deadline, block.timestamp, DECEMBER_2099); 106 | 107 | address owner = users.alice; 108 | vm.expectRevert( 109 | abi.encodeWithSelector(IERC20Permit.ERC20Permit_InvalidSignature.selector, owner, DUMMY_V, DUMMY_R, DUMMY_S) 110 | ); 111 | erc20Permit.permit({ 112 | owner: owner, 113 | spender: users.bob, 114 | value: 1, 115 | deadline: deadline, 116 | v: DUMMY_V, 117 | r: DUMMY_R, 118 | s: DUMMY_S 119 | }); 120 | } 121 | 122 | modifier whenSignatureValid() { 123 | _; 124 | } 125 | 126 | function testFuzz_Permit( 127 | uint256 privateKey, 128 | address spender, 129 | uint256 value, 130 | uint256 deadline 131 | ) 132 | external 133 | whenOwnerNotZeroAddress 134 | whenSpenderNotZeroAddress 135 | whenDeadlineNotInThePast 136 | whenRecoveredOwnerNotZeroAddress 137 | whenSignatureValid 138 | { 139 | vm.assume(spender != address(0)); 140 | privateKey = boundPrivateKey(privateKey); 141 | deadline = _bound(deadline, block.timestamp, DECEMBER_2099); 142 | 143 | address owner = vm.addr(privateKey); 144 | (uint8 v, bytes32 r, bytes32 s) = getSignature(privateKey, owner, spender, value, deadline); 145 | erc20Permit.permit(owner, spender, value, deadline, v, r, s); 146 | uint256 actualAllowance = erc20Permit.allowance(owner, spender); 147 | uint256 expectedAllowance = value; 148 | assertEq(actualAllowance, expectedAllowance, "allowance"); 149 | } 150 | 151 | function testFuzz_Permit_IncreaseNonce( 152 | uint256 privateKey, 153 | address spender, 154 | uint256 value, 155 | uint256 deadline 156 | ) 157 | external 158 | whenOwnerNotZeroAddress 159 | whenSpenderNotZeroAddress 160 | whenDeadlineNotInThePast 161 | whenRecoveredOwnerNotZeroAddress 162 | whenSignatureValid 163 | { 164 | vm.assume(spender != address(0)); 165 | privateKey = boundPrivateKey(privateKey); 166 | deadline = _bound(deadline, block.timestamp, DECEMBER_2099); 167 | 168 | address owner = vm.addr(privateKey); 169 | (uint8 v, bytes32 r, bytes32 s) = getSignature(privateKey, owner, spender, value, deadline); 170 | erc20Permit.permit(owner, spender, value, deadline, v, r, s); 171 | uint256 actualNonce = erc20Permit.nonces(owner); 172 | uint256 expectedNonce = 1; 173 | assertEq(actualNonce, expectedNonce, "nonce"); 174 | } 175 | 176 | function testFuzz_Permit_Approval( 177 | uint256 privateKey, 178 | address spender, 179 | uint256 value, 180 | uint256 deadline 181 | ) 182 | external 183 | whenOwnerNotZeroAddress 184 | whenSpenderNotZeroAddress 185 | whenDeadlineNotInThePast 186 | whenRecoveredOwnerNotZeroAddress 187 | whenSignatureValid 188 | { 189 | vm.assume(spender != address(0)); 190 | privateKey = boundPrivateKey(privateKey); 191 | deadline = _bound(deadline, block.timestamp, DECEMBER_2099); 192 | 193 | address owner = vm.addr(privateKey); 194 | vm.expectEmit({ emitter: address(erc20Permit) }); 195 | emit Approval(owner, spender, value); 196 | (uint8 v, bytes32 r, bytes32 s) = getSignature(privateKey, owner, spender, value, deadline); 197 | erc20Permit.permit(owner, spender, value, deadline, v, r, s); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/token/erc20/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | /// @title IERC20 5 | /// @author Paul Razvan Berg 6 | /// @notice Implementation for the ERC-20 standard. 7 | /// 8 | /// We have followed general OpenZeppelin guidelines: functions revert instead of returning 9 | /// `false` on failure. This behavior is nonetheless conventional and does not conflict with 10 | /// the with the expectations of ERC-20 applications. 11 | /// 12 | /// Additionally, an {Approval} event is emitted on calls to {transferFrom}. This allows 13 | /// applications to reconstruct the allowance for all accounts just by listening to said 14 | /// events. Other implementations of the ERC may not emit these events, as it isn't 15 | /// required by the specification. 16 | /// 17 | /// Finally, the non-standard {decreaseAllowance} and {increaseAllowance} functions have been 18 | /// added to mitigate the well-known issues around setting allowances. 19 | /// 20 | /// @dev Forked from OpenZeppelin 21 | /// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.4.0/contracts/token/ERC20/ERC20.sol 22 | interface IERC20 { 23 | /*////////////////////////////////////////////////////////////////////////// 24 | ERRORS 25 | //////////////////////////////////////////////////////////////////////////*/ 26 | 27 | /// @notice Thrown when attempting to approve with the zero address as the owner. 28 | error ERC20_ApproveOwnerZeroAddress(); 29 | 30 | /// @notice Thrown when attempting to approve the zero address as the spender. 31 | error ERC20_ApproveSpenderZeroAddress(); 32 | 33 | /// @notice Thrown when attempting to burn tokens from the zero address. 34 | error ERC20_BurnHolderZeroAddress(); 35 | 36 | /// @notice Thrown when attempting to transfer more tokens than there are in the from account. 37 | error ERC20_FromInsufficientBalance(uint256 senderBalance, uint256 transferAmount); 38 | 39 | /// @notice Thrown when spender attempts to transfer more tokens than the owner had given them allowance for. 40 | error ERC20_InsufficientAllowance(address owner, address spender, uint256 allowance, uint256 transferAmount); 41 | 42 | /// @notice Thrown when attempting to mint tokens to the zero address. 43 | error ERC20_MintBeneficiaryZeroAddress(); 44 | 45 | /// @notice Thrown when attempting to transfer tokens from the zero address. 46 | error ERC20_TransferFromZeroAddress(); 47 | 48 | /// @notice Thrown when the attempting to transfer tokens to the zero address. 49 | error ERC20_TransferToZeroAddress(); 50 | 51 | /*////////////////////////////////////////////////////////////////////////// 52 | EVENTS 53 | //////////////////////////////////////////////////////////////////////////*/ 54 | 55 | /// @notice Emitted when an approval occurs. 56 | /// @param owner The address of the owner of the tokens. 57 | /// @param spender The address of the spender. 58 | /// @param value The maximum value that can be spent. 59 | event Approval(address indexed owner, address indexed spender, uint256 value); 60 | 61 | /// @notice Emitted when a transfer occurs. 62 | /// @param from The account sending the tokens. 63 | /// @param to The account receiving the tokens. 64 | /// @param amount The amount of tokens transferred. 65 | event Transfer(address indexed from, address indexed to, uint256 amount); 66 | 67 | /*////////////////////////////////////////////////////////////////////////// 68 | CONSTANT FUNCTIONS 69 | //////////////////////////////////////////////////////////////////////////*/ 70 | 71 | /// @notice Returns the remaining number of tokens that `spender` will be allowed to spend 72 | /// on behalf of `owner` through {transferFrom}. This is zero by default. 73 | /// 74 | /// @dev This value changes when {approve} or {transferFrom} are called. 75 | function allowance(address owner, address spender) external view returns (uint256); 76 | 77 | /// @notice Returns the amount of tokens owned by `account`. 78 | function balanceOf(address account) external view returns (uint256); 79 | 80 | /// @notice Returns the number of decimals used to get its user representation. 81 | function decimals() external view returns (uint8); 82 | 83 | /// @notice Returns the name of the token. 84 | function name() external view returns (string memory); 85 | 86 | /// @notice Returns the symbol of the token, usually a shorter version of the name. 87 | function symbol() external view returns (string memory); 88 | 89 | /// @notice Returns the amount of tokens in existence. 90 | function totalSupply() external view returns (uint256); 91 | 92 | /*////////////////////////////////////////////////////////////////////////// 93 | NON-CONSTANT FUNCTIONS 94 | //////////////////////////////////////////////////////////////////////////*/ 95 | 96 | /// @notice Sets `value` as the allowance of `spender` over the caller's tokens. 97 | /// 98 | /// @dev Emits an {Approval} event. 99 | /// 100 | /// IMPORTANT: Beware that changing an allowance with this method brings the risk that someone may 101 | /// use both the old and the new allowance by unfortunate transaction ordering. One possible solution 102 | /// to mitigate this race condition is to first reduce the spender's allowance to 0 and set the desired 103 | /// value afterwards: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 104 | /// 105 | /// Requirements: 106 | /// 107 | /// - `spender` cannot be the zero address. 108 | /// 109 | /// @return a boolean value indicating whether the operation succeeded. 110 | function approve(address spender, uint256 value) external returns (bool); 111 | 112 | /// @notice Atomically decreases the allowance granted to `spender` by the caller. 113 | /// 114 | /// @dev Emits an {Approval} event indicating the updated allowance. 115 | /// 116 | /// This is an alternative to {approve} that can be used as a mitigation for problems described 117 | /// in {IERC20-approve}. 118 | /// 119 | /// Requirements: 120 | /// 121 | /// - `spender` cannot be the zero address. 122 | /// - `spender` must have allowance for the caller of at least `value`. 123 | function decreaseAllowance(address spender, uint256 value) external returns (bool); 124 | 125 | /// @notice Atomically increases the allowance granted to `spender` by the caller. 126 | /// 127 | /// @dev Emits an {Approval} event indicating the updated allowance. 128 | /// 129 | /// This is an alternative to {approve} that can be used as a mitigation for the problems described above. 130 | /// 131 | /// Requirements: 132 | /// 133 | /// - `spender` must not be the zero address. 134 | function increaseAllowance(address spender, uint256 value) external returns (bool); 135 | 136 | /// @notice Moves `amount` tokens from the caller's account to `to`. 137 | /// 138 | /// @dev Emits a {Transfer} event. 139 | /// 140 | /// Requirements: 141 | /// 142 | /// - `to` must not be the zero address. 143 | /// - The caller must have a balance of at least `amount`. 144 | /// 145 | /// @return a boolean value indicating whether the operation succeeded. 146 | function transfer(address to, uint256 amount) external returns (bool); 147 | 148 | /// @notice Moves `amount` tokens from `from` to `to` using the allowance mechanism. `amount` 149 | /// `is then deducted from the caller's allowance. 150 | /// 151 | /// @dev Emits a {Transfer} event and an {Approval} event indicating the updated allowance. This is 152 | /// not required by the ERC. See the note at the beginning of {ERC-20}. 153 | /// 154 | /// Requirements: 155 | /// 156 | /// - `from` and `to` must not be the zero address. 157 | /// - `from` must have a balance of at least `amount`. 158 | /// - The caller must have approved `from` to spent at least `amount` tokens. 159 | /// 160 | /// @return a boolean value indicating whether the operation succeeded. 161 | function transferFrom(address from, address to, uint256 amount) external returns (bool); 162 | } 163 | -------------------------------------------------------------------------------- /src/token/erc20/ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.4; 3 | 4 | import { IERC20 } from "./IERC20.sol"; 5 | 6 | /// @title ERC20 7 | /// @author Paul Razvan Berg 8 | contract ERC20 is IERC20 { 9 | /*////////////////////////////////////////////////////////////////////////// 10 | USER-FACING STORAGE 11 | //////////////////////////////////////////////////////////////////////////*/ 12 | 13 | /// @inheritdoc IERC20 14 | string public override name; 15 | 16 | /// @inheritdoc IERC20 17 | string public override symbol; 18 | 19 | /// @inheritdoc IERC20 20 | uint8 public immutable override decimals; 21 | 22 | /// @inheritdoc IERC20 23 | uint256 public override totalSupply; 24 | 25 | /*////////////////////////////////////////////////////////////////////////// 26 | INTERNAL STORAGE 27 | //////////////////////////////////////////////////////////////////////////*/ 28 | 29 | /// @dev Internal mapping of allowances. 30 | mapping(address => mapping(address => uint256)) internal _allowances; 31 | 32 | /// @dev Internal mapping of balances. 33 | mapping(address => uint256) internal _balances; 34 | 35 | /*////////////////////////////////////////////////////////////////////////// 36 | CONSTRUCTOR 37 | //////////////////////////////////////////////////////////////////////////*/ 38 | 39 | /// @notice All three of these arguments are immutable: they can only be set once during construction. 40 | /// @param name_ ERC-20 name of this token. 41 | /// @param symbol_ ERC-20 symbol of this token. 42 | /// @param decimals_ ERC-20 decimal precision of this token. 43 | constructor(string memory name_, string memory symbol_, uint8 decimals_) { 44 | name = name_; 45 | symbol = symbol_; 46 | decimals = decimals_; 47 | } 48 | 49 | /*////////////////////////////////////////////////////////////////////////// 50 | USER-FACING CONSTANT FUNCTIONS 51 | //////////////////////////////////////////////////////////////////////////*/ 52 | 53 | /// @inheritdoc IERC20 54 | function allowance(address owner, address spender) public view override returns (uint256) { 55 | return _allowances[owner][spender]; 56 | } 57 | 58 | function balanceOf(address account) public view virtual override returns (uint256) { 59 | return _balances[account]; 60 | } 61 | 62 | /*////////////////////////////////////////////////////////////////////////// 63 | USER-FACING NON-CONSTANT FUNCTIONS 64 | //////////////////////////////////////////////////////////////////////////*/ 65 | 66 | /// @inheritdoc IERC20 67 | function approve(address spender, uint256 value) public virtual override returns (bool) { 68 | _approve(msg.sender, spender, value); 69 | return true; 70 | } 71 | 72 | /// @inheritdoc IERC20 73 | function decreaseAllowance(address spender, uint256 value) public virtual override returns (bool) { 74 | // Calculate the new allowance. 75 | uint256 newAllowance = _allowances[msg.sender][spender] - value; 76 | 77 | // Make the approval. 78 | _approve(msg.sender, spender, newAllowance); 79 | return true; 80 | } 81 | 82 | /// @inheritdoc IERC20 83 | function increaseAllowance(address spender, uint256 value) public virtual override returns (bool) { 84 | // Calculate the new allowance. 85 | uint256 newAllowance = _allowances[msg.sender][spender] + value; 86 | 87 | // Make the approval. 88 | _approve(msg.sender, spender, newAllowance); 89 | return true; 90 | } 91 | 92 | /// @inheritdoc IERC20 93 | function transfer(address to, uint256 amount) public virtual override returns (bool) { 94 | _transfer(msg.sender, to, amount); 95 | return true; 96 | } 97 | 98 | /// @inheritdoc IERC20 99 | function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { 100 | // Checks: the spender's allowance is sufficient. 101 | address spender = msg.sender; 102 | uint256 currentAllowance = _allowances[from][spender]; 103 | if (currentAllowance < amount) { 104 | revert ERC20_InsufficientAllowance(from, spender, currentAllowance, amount); 105 | } 106 | 107 | // Effects: update the allowance. 108 | unchecked { 109 | _approve(from, spender, currentAllowance - amount); 110 | } 111 | 112 | // Checks, Effects and Interactions: make the transfer. 113 | _transfer(from, to, amount); 114 | 115 | return true; 116 | } 117 | 118 | /*////////////////////////////////////////////////////////////////////////// 119 | INTERNAL NON-CONSTANT FUNCTIONS 120 | //////////////////////////////////////////////////////////////////////////*/ 121 | 122 | /// @notice Sets `value` as the allowance of `spender` over the `owner`s tokens. 123 | /// 124 | /// @dev Emits an {Approval} event. 125 | /// 126 | /// Requirements: 127 | /// 128 | /// - `owner` must not be the zero address. 129 | /// - `spender` must not be the zero address. 130 | function _approve(address owner, address spender, uint256 value) internal virtual { 131 | // Checks: `owner` is not the zero address. 132 | if (owner == address(0)) { 133 | revert ERC20_ApproveOwnerZeroAddress(); 134 | } 135 | 136 | // Checks: `spender` is not the zero address. 137 | if (spender == address(0)) { 138 | revert ERC20_ApproveSpenderZeroAddress(); 139 | } 140 | 141 | // Effects: update the allowance. 142 | _allowances[owner][spender] = value; 143 | 144 | // Emit an event. 145 | emit Approval(owner, spender, value); 146 | } 147 | 148 | /// @notice Destroys `amount` tokens from `holder`, decreasing the token supply. 149 | /// 150 | /// @dev Emits a {Transfer} event. 151 | /// 152 | /// Requirements: 153 | /// 154 | /// - `holder` must have at least `amount` tokens. 155 | function _burn(address holder, uint256 amount) internal { 156 | // Checks: `holder` is not the zero address. 157 | if (holder == address(0)) { 158 | revert ERC20_BurnHolderZeroAddress(); 159 | } 160 | 161 | // Effects: burn the tokens. 162 | _balances[holder] -= amount; 163 | 164 | // Effects: reduce the total supply. 165 | unchecked { 166 | // Underflow not possible: amount <= account balance <= total supply. 167 | totalSupply -= amount; 168 | } 169 | 170 | // Emit an event. 171 | emit Transfer(holder, address(0), amount); 172 | } 173 | 174 | /// @notice Prints new `amount` tokens into existence and assigns them to `beneficiary`, increasing the 175 | /// total supply. 176 | /// 177 | /// @dev Emits a {Transfer} event. 178 | /// 179 | /// Requirements: 180 | /// 181 | /// - The beneficiary's balance and the total supply must not overflow. 182 | function _mint(address beneficiary, uint256 amount) internal { 183 | // Checks: `beneficiary` is not the zero address. 184 | if (beneficiary == address(0)) { 185 | revert ERC20_MintBeneficiaryZeroAddress(); 186 | } 187 | 188 | /// Effects: increase the total supply. 189 | totalSupply += amount; 190 | 191 | /// Effects: mint the new tokens. 192 | unchecked { 193 | // Overflow not possible: `balance + amount` is at most `totalSupply + amount`, which is checked above. 194 | _balances[beneficiary] += amount; 195 | } 196 | 197 | // Emit an event. 198 | emit Transfer(address(0), beneficiary, amount); 199 | } 200 | 201 | /// @notice Moves `amount` tokens from `from` to `to`. 202 | /// 203 | /// @dev Emits a {Transfer} event. 204 | /// 205 | /// Requirements: 206 | /// 207 | /// - `from` must not be the zero address. 208 | /// - `to` must not be the zero address. 209 | /// - `from` must have a balance of at least `amount`. 210 | function _transfer(address from, address to, uint256 amount) internal virtual { 211 | // Checks: `from` is not the zero address. 212 | if (from == address(0)) { 213 | revert ERC20_TransferFromZeroAddress(); 214 | } 215 | 216 | // Checks: `to` is not the zero address. 217 | if (to == address(0)) { 218 | revert ERC20_TransferToZeroAddress(); 219 | } 220 | 221 | // Checks: `from` has enough balance. 222 | uint256 fromBalance = _balances[from]; 223 | if (fromBalance < amount) { 224 | revert ERC20_FromInsufficientBalance(fromBalance, amount); 225 | } 226 | 227 | // Effects: update the balance of `from` and `to`.. 228 | unchecked { 229 | _balances[from] = fromBalance - amount; 230 | // Overflow not possible: the sum of all balances is capped by the total supply, and the sum is preserved by 231 | // decrementing then incrementing. 232 | _balances[to] += amount; 233 | } 234 | 235 | // Emit an event. 236 | emit Transfer(from, to, amount); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.1' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | devDependencies: 8 | prettier: 9 | specifier: ^2.8.7 10 | version: 2.8.7 11 | solhint-community: 12 | specifier: ^3.5.2 13 | version: 3.5.2 14 | 15 | packages: 16 | 17 | /@babel/code-frame@7.21.4: 18 | resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} 19 | engines: {node: '>=6.9.0'} 20 | dependencies: 21 | '@babel/highlight': 7.18.6 22 | dev: true 23 | 24 | /@babel/helper-validator-identifier@7.19.1: 25 | resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} 26 | engines: {node: '>=6.9.0'} 27 | dev: true 28 | 29 | /@babel/highlight@7.18.6: 30 | resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} 31 | engines: {node: '>=6.9.0'} 32 | dependencies: 33 | '@babel/helper-validator-identifier': 7.19.1 34 | chalk: 2.4.2 35 | js-tokens: 4.0.0 36 | dev: true 37 | 38 | /@solidity-parser/parser@0.16.0: 39 | resolution: {integrity: sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==} 40 | dependencies: 41 | antlr4ts: 0.5.0-alpha.4 42 | dev: true 43 | 44 | /ajv@6.12.6: 45 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 46 | dependencies: 47 | fast-deep-equal: 3.1.3 48 | fast-json-stable-stringify: 2.1.0 49 | json-schema-traverse: 0.4.1 50 | uri-js: 4.4.1 51 | dev: true 52 | 53 | /ajv@8.12.0: 54 | resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} 55 | dependencies: 56 | fast-deep-equal: 3.1.3 57 | json-schema-traverse: 1.0.0 58 | require-from-string: 2.0.2 59 | uri-js: 4.4.1 60 | dev: true 61 | 62 | /ansi-regex@5.0.1: 63 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 64 | engines: {node: '>=8'} 65 | dev: true 66 | 67 | /ansi-styles@3.2.1: 68 | resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 69 | engines: {node: '>=4'} 70 | dependencies: 71 | color-convert: 1.9.3 72 | dev: true 73 | 74 | /ansi-styles@4.3.0: 75 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 76 | engines: {node: '>=8'} 77 | dependencies: 78 | color-convert: 2.0.1 79 | dev: true 80 | 81 | /antlr4@4.12.0: 82 | resolution: {integrity: sha512-23iB5IzXJZRZeK9TigzUyrNc9pSmNqAerJRBcNq1ETrmttMWRgaYZzC561IgEO3ygKsDJTYDTozABXa4b/fTQQ==} 83 | engines: {node: '>=16'} 84 | dev: true 85 | 86 | /antlr4ts@0.5.0-alpha.4: 87 | resolution: {integrity: sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==} 88 | dev: true 89 | 90 | /argparse@2.0.1: 91 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 92 | dev: true 93 | 94 | /ast-parents@0.0.1: 95 | resolution: {integrity: sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==} 96 | dev: true 97 | 98 | /astral-regex@2.0.0: 99 | resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} 100 | engines: {node: '>=8'} 101 | dev: true 102 | 103 | /balanced-match@1.0.2: 104 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 105 | dev: true 106 | 107 | /brace-expansion@2.0.1: 108 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 109 | dependencies: 110 | balanced-match: 1.0.2 111 | dev: true 112 | 113 | /callsites@3.1.0: 114 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 115 | engines: {node: '>=6'} 116 | dev: true 117 | 118 | /chalk@2.4.2: 119 | resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 120 | engines: {node: '>=4'} 121 | dependencies: 122 | ansi-styles: 3.2.1 123 | escape-string-regexp: 1.0.5 124 | supports-color: 5.5.0 125 | dev: true 126 | 127 | /chalk@4.1.2: 128 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 129 | engines: {node: '>=10'} 130 | dependencies: 131 | ansi-styles: 4.3.0 132 | supports-color: 7.2.0 133 | dev: true 134 | 135 | /color-convert@1.9.3: 136 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 137 | dependencies: 138 | color-name: 1.1.3 139 | dev: true 140 | 141 | /color-convert@2.0.1: 142 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 143 | engines: {node: '>=7.0.0'} 144 | dependencies: 145 | color-name: 1.1.4 146 | dev: true 147 | 148 | /color-name@1.1.3: 149 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 150 | dev: true 151 | 152 | /color-name@1.1.4: 153 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 154 | dev: true 155 | 156 | /commander@10.0.0: 157 | resolution: {integrity: sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==} 158 | engines: {node: '>=14'} 159 | dev: true 160 | 161 | /cosmiconfig@8.1.3: 162 | resolution: {integrity: sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==} 163 | engines: {node: '>=14'} 164 | dependencies: 165 | import-fresh: 3.3.0 166 | js-yaml: 4.1.0 167 | parse-json: 5.2.0 168 | path-type: 4.0.0 169 | dev: true 170 | 171 | /emoji-regex@8.0.0: 172 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 173 | dev: true 174 | 175 | /error-ex@1.3.2: 176 | resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} 177 | dependencies: 178 | is-arrayish: 0.2.1 179 | dev: true 180 | 181 | /escape-string-regexp@1.0.5: 182 | resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 183 | engines: {node: '>=0.8.0'} 184 | dev: true 185 | 186 | /fast-deep-equal@3.1.3: 187 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 188 | dev: true 189 | 190 | /fast-diff@1.2.0: 191 | resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} 192 | dev: true 193 | 194 | /fast-json-stable-stringify@2.1.0: 195 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 196 | dev: true 197 | 198 | /fs.realpath@1.0.0: 199 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 200 | dev: true 201 | 202 | /glob@8.1.0: 203 | resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} 204 | engines: {node: '>=12'} 205 | dependencies: 206 | fs.realpath: 1.0.0 207 | inflight: 1.0.6 208 | inherits: 2.0.4 209 | minimatch: 5.1.6 210 | once: 1.4.0 211 | dev: true 212 | 213 | /has-flag@3.0.0: 214 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 215 | engines: {node: '>=4'} 216 | dev: true 217 | 218 | /has-flag@4.0.0: 219 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 220 | engines: {node: '>=8'} 221 | dev: true 222 | 223 | /ignore@5.2.4: 224 | resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} 225 | engines: {node: '>= 4'} 226 | dev: true 227 | 228 | /import-fresh@3.3.0: 229 | resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} 230 | engines: {node: '>=6'} 231 | dependencies: 232 | parent-module: 1.0.1 233 | resolve-from: 4.0.0 234 | dev: true 235 | 236 | /inflight@1.0.6: 237 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 238 | dependencies: 239 | once: 1.4.0 240 | wrappy: 1.0.2 241 | dev: true 242 | 243 | /inherits@2.0.4: 244 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 245 | dev: true 246 | 247 | /is-arrayish@0.2.1: 248 | resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} 249 | dev: true 250 | 251 | /is-fullwidth-code-point@3.0.0: 252 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 253 | engines: {node: '>=8'} 254 | dev: true 255 | 256 | /js-tokens@4.0.0: 257 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 258 | dev: true 259 | 260 | /js-yaml@4.1.0: 261 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 262 | hasBin: true 263 | dependencies: 264 | argparse: 2.0.1 265 | dev: true 266 | 267 | /json-parse-even-better-errors@2.3.1: 268 | resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} 269 | dev: true 270 | 271 | /json-schema-traverse@0.4.1: 272 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 273 | dev: true 274 | 275 | /json-schema-traverse@1.0.0: 276 | resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} 277 | dev: true 278 | 279 | /lines-and-columns@1.2.4: 280 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 281 | dev: true 282 | 283 | /lodash.truncate@4.4.2: 284 | resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} 285 | dev: true 286 | 287 | /lodash@4.17.21: 288 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 289 | dev: true 290 | 291 | /minimatch@5.1.6: 292 | resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} 293 | engines: {node: '>=10'} 294 | dependencies: 295 | brace-expansion: 2.0.1 296 | dev: true 297 | 298 | /once@1.4.0: 299 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 300 | dependencies: 301 | wrappy: 1.0.2 302 | dev: true 303 | 304 | /parent-module@1.0.1: 305 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 306 | engines: {node: '>=6'} 307 | dependencies: 308 | callsites: 3.1.0 309 | dev: true 310 | 311 | /parse-json@5.2.0: 312 | resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} 313 | engines: {node: '>=8'} 314 | dependencies: 315 | '@babel/code-frame': 7.21.4 316 | error-ex: 1.3.2 317 | json-parse-even-better-errors: 2.3.1 318 | lines-and-columns: 1.2.4 319 | dev: true 320 | 321 | /path-type@4.0.0: 322 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 323 | engines: {node: '>=8'} 324 | dev: true 325 | 326 | /pluralize@8.0.0: 327 | resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} 328 | engines: {node: '>=4'} 329 | dev: true 330 | 331 | /prettier@2.8.7: 332 | resolution: {integrity: sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==} 333 | engines: {node: '>=10.13.0'} 334 | hasBin: true 335 | dev: true 336 | 337 | /punycode@2.3.0: 338 | resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} 339 | engines: {node: '>=6'} 340 | dev: true 341 | 342 | /require-from-string@2.0.2: 343 | resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 344 | engines: {node: '>=0.10.0'} 345 | dev: true 346 | 347 | /resolve-from@4.0.0: 348 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 349 | engines: {node: '>=4'} 350 | dev: true 351 | 352 | /semver@6.3.0: 353 | resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} 354 | hasBin: true 355 | dev: true 356 | 357 | /slice-ansi@4.0.0: 358 | resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} 359 | engines: {node: '>=10'} 360 | dependencies: 361 | ansi-styles: 4.3.0 362 | astral-regex: 2.0.0 363 | is-fullwidth-code-point: 3.0.0 364 | dev: true 365 | 366 | /solhint-community@3.5.2: 367 | resolution: {integrity: sha512-l3lF2n8mF33p266u5atCSqjT9SyyOBD1qaWrQBAXHNk2xAxmi+pEynIVuTIn6FVD3JiuHRgutjKJcngs8Iolbg==} 368 | hasBin: true 369 | dependencies: 370 | '@solidity-parser/parser': 0.16.0 371 | ajv: 6.12.6 372 | antlr4: 4.12.0 373 | ast-parents: 0.0.1 374 | chalk: 4.1.2 375 | commander: 10.0.0 376 | cosmiconfig: 8.1.3 377 | fast-diff: 1.2.0 378 | glob: 8.1.0 379 | ignore: 5.2.4 380 | js-yaml: 4.1.0 381 | lodash: 4.17.21 382 | pluralize: 8.0.0 383 | semver: 6.3.0 384 | strip-ansi: 6.0.1 385 | table: 6.8.1 386 | text-table: 0.2.0 387 | optionalDependencies: 388 | prettier: 2.8.7 389 | dev: true 390 | 391 | /string-width@4.2.3: 392 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 393 | engines: {node: '>=8'} 394 | dependencies: 395 | emoji-regex: 8.0.0 396 | is-fullwidth-code-point: 3.0.0 397 | strip-ansi: 6.0.1 398 | dev: true 399 | 400 | /strip-ansi@6.0.1: 401 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 402 | engines: {node: '>=8'} 403 | dependencies: 404 | ansi-regex: 5.0.1 405 | dev: true 406 | 407 | /supports-color@5.5.0: 408 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 409 | engines: {node: '>=4'} 410 | dependencies: 411 | has-flag: 3.0.0 412 | dev: true 413 | 414 | /supports-color@7.2.0: 415 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 416 | engines: {node: '>=8'} 417 | dependencies: 418 | has-flag: 4.0.0 419 | dev: true 420 | 421 | /table@6.8.1: 422 | resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} 423 | engines: {node: '>=10.0.0'} 424 | dependencies: 425 | ajv: 8.12.0 426 | lodash.truncate: 4.4.2 427 | slice-ansi: 4.0.0 428 | string-width: 4.2.3 429 | strip-ansi: 6.0.1 430 | dev: true 431 | 432 | /text-table@0.2.0: 433 | resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} 434 | dev: true 435 | 436 | /uri-js@4.4.1: 437 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 438 | dependencies: 439 | punycode: 2.3.0 440 | dev: true 441 | 442 | /wrappy@1.0.2: 443 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 444 | dev: true 445 | --------------------------------------------------------------------------------