├── .gitignore ├── tests ├── submission.sol ├── SolmateBugTest.sol ├── wrapper │ ├── ERC20BurnWrapper.sol │ ├── SignedWadMathWrapper.sol │ └── FixedPointMathLibWrapper.sol ├── helper │ ├── AddressSet.sol │ ├── IVM.sol │ ├── TestHelper.sol │ └── helper.sol ├── ERC20Test.sol ├── FixedPointMathLibTest.sol └── SignedWadMathTest.sol ├── contracts ├── ERC20Burn.sol ├── ERC20.sol ├── FixedPointMathLib.sol └── SignedWadMath.sol ├── medusa.json ├── README.md └── secureum-workshop.md /.gitignore: -------------------------------------------------------------------------------- 1 | corpus/* 2 | crytic-export/* 3 | medusa -------------------------------------------------------------------------------- /tests/submission.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.19; 3 | 4 | import {FixedPointMathLibTest} from "./FixedPointMathLibTest.sol"; 5 | import {SignedWadMathTest} from "./SignedWadMathTest.sol"; 6 | import {ERC20Test} from "./ERC20Test.sol"; -------------------------------------------------------------------------------- /contracts/ERC20Burn.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.19; 2 | import "./ERC20.sol"; 3 | 4 | // ERC20 token with a burn function 5 | contract ERC20Burn is ERC20("MyToken", "MT", 18) { 6 | 7 | constructor(){ 8 | _mint(msg.sender, 10**18); 9 | } 10 | 11 | function burn(uint amount) public{ 12 | _burn(msg.sender, amount); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /tests/SolmateBugTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.19; 3 | 4 | import {TestHelper} from "./helper/TestHelper.sol"; 5 | import {SignedWadMathWrapper} from "./wrapper/SignedWadMathWrapper.sol"; 6 | 7 | contract SolmateBugTest is TestHelper { 8 | SignedWadMathWrapper signedWadMathWrapper; 9 | 10 | constructor() { 11 | signedWadMathWrapper = new SignedWadMathWrapper(); 12 | } 13 | 14 | // Check that wadMul() reverts if x * y > type(int256).max 15 | function wadMulBothNegativeOverflow(int256 x, int256 y) public { 16 | /* 17 | Precondition: 18 | - x < 0 and y < 0 19 | - x * y > int256 max 20 | */ 21 | x = clampLt(x, 0); 22 | y = clampLt(y, type(int256).max / x); 23 | 24 | // Action: Call wadMul() 25 | bytes memory reason = expectRevert( 26 | signedWadMathWrapper.wadMul, 27 | x, 28 | y, 29 | "wadMulBothNegativeOverflow() did not revert" 30 | ); 31 | 32 | // Check: Reverted with empty reason 33 | assertEq( 34 | reason.length, 35 | 0, 36 | "wadMulBothNegativeOverflow() reverted with message" 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/wrapper/ERC20BurnWrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.19; 3 | 4 | import {ERC20Burn} from "contracts/ERC20Burn.sol"; 5 | import {AddressSet, LibAddressSet} from "tests/helper/AddressSet.sol"; 6 | 7 | contract ERC20BurnWrapper is ERC20Burn { 8 | using LibAddressSet for AddressSet; 9 | 10 | // User tracking 11 | AddressSet internal _actors; 12 | 13 | function getActor(uint256 actorSeed) public view returns (address) { 14 | return _actors.rand(actorSeed); 15 | } 16 | 17 | function actorCount() public view returns (uint256) { 18 | return _actors.count(); 19 | } 20 | 21 | // ERC20Burn functions 22 | constructor() ERC20Burn() { 23 | _actors.add(msg.sender); 24 | } 25 | 26 | function approve( 27 | address spender, 28 | uint256 amount 29 | ) public override returns (bool) { 30 | _actors.add(msg.sender); 31 | _actors.add(spender); 32 | 33 | return super.approve(spender, amount); 34 | } 35 | 36 | function transfer( 37 | address to, 38 | uint256 amount 39 | ) public override returns (bool) { 40 | _actors.add(msg.sender); 41 | _actors.add(to); 42 | 43 | return super.transfer(to, amount); 44 | } 45 | 46 | function transferFrom( 47 | address from, 48 | address to, 49 | uint256 amount 50 | ) public override returns (bool) { 51 | _actors.add(msg.sender); 52 | _actors.add(from); 53 | _actors.add(to); 54 | 55 | return super.transferFrom(from, to, amount); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/helper/AddressSet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.13; 3 | 4 | // Taken from https://github.com/horsefacts/weth-invariant-testing/blob/main/test/helpers/AddressSet.sol 5 | struct AddressSet { 6 | address[] addrs; 7 | mapping(address => bool) saved; 8 | } 9 | 10 | library LibAddressSet { 11 | function add(AddressSet storage s, address addr) internal { 12 | if (!s.saved[addr]) { 13 | s.addrs.push(addr); 14 | s.saved[addr] = true; 15 | } 16 | } 17 | 18 | function contains( 19 | AddressSet storage s, 20 | address addr 21 | ) internal view returns (bool) { 22 | return s.saved[addr]; 23 | } 24 | 25 | function count(AddressSet storage s) internal view returns (uint256) { 26 | return s.addrs.length; 27 | } 28 | 29 | function rand( 30 | AddressSet storage s, 31 | uint256 seed 32 | ) internal view returns (address) { 33 | if (s.addrs.length > 0) { 34 | return s.addrs[seed % s.addrs.length]; 35 | } else { 36 | return address(0); 37 | } 38 | } 39 | 40 | function forEach( 41 | AddressSet storage s, 42 | function(address) external func 43 | ) internal { 44 | for (uint256 i; i < s.addrs.length; ++i) { 45 | func(s.addrs[i]); 46 | } 47 | } 48 | 49 | function reduce( 50 | AddressSet storage s, 51 | uint256 acc, 52 | function(uint256, address) external returns (uint256) func 53 | ) internal returns (uint256) { 54 | for (uint256 i; i < s.addrs.length; ++i) { 55 | acc = func(acc, s.addrs[i]); 56 | } 57 | return acc; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/wrapper/SignedWadMathWrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.19; 3 | 4 | import {SignedWadMath} from "contracts/SignedWadMath.sol"; 5 | 6 | contract SignedWadMathWrapper { 7 | function toWadUnsafe(uint256 x) external pure returns (int256) { 8 | return SignedWadMath.toWadUnsafe(x); 9 | } 10 | 11 | function toDaysWadUnsafe(uint256 x) external pure returns (int256) { 12 | return SignedWadMath.toDaysWadUnsafe(x); 13 | } 14 | 15 | function fromDaysWadUnsafe(int256 x) external pure returns (uint256 r) { 16 | return SignedWadMath.fromDaysWadUnsafe(x); 17 | } 18 | 19 | function unsafeWadMul(int256 x, int256 y) external pure returns (int256 r) { 20 | return SignedWadMath.unsafeWadMul(x, y); 21 | } 22 | 23 | function unsafeWadDiv(int256 x, int256 y) external pure returns (int256) { 24 | return SignedWadMath.unsafeWadDiv(x, y); 25 | } 26 | 27 | function wadMul(int256 x, int256 y) external pure returns (int256) { 28 | return SignedWadMath.wadMul(x, y); 29 | } 30 | 31 | function wadDiv(int256 x, int256 y) external pure returns (int256) { 32 | return SignedWadMath.wadDiv(x, y); 33 | } 34 | 35 | function wadPow(int256 x, int256 y) external pure returns (int256) { 36 | return SignedWadMath.wadPow(x, y); 37 | } 38 | 39 | function wadExp(int256 x) external pure returns (int256) { 40 | return SignedWadMath.wadExp(x); 41 | } 42 | 43 | function wadLn(int256 x) external pure returns (int256) { 44 | return SignedWadMath.wadLn(x); 45 | } 46 | 47 | function unsafeDiv(int256 x, int256 y) external pure returns (int256) { 48 | return SignedWadMath.unsafeDiv(x, y); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/helper/IVM.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.19; 3 | 4 | interface IVM { 5 | function addr(uint256) external returns (address); 6 | function ffi(string[] calldata) external returns (bytes memory); 7 | function parseBytes(string calldata) external returns (bytes memory); 8 | function parseBytes32(string calldata) external returns (bytes32); 9 | function parseAddress(string calldata) external returns (address); 10 | function parseUint(string calldata) external returns (uint256); 11 | function parseInt(string calldata) external returns (int256); 12 | function parseBool(string calldata) external returns (bool); 13 | function sign(uint256, bytes32) external returns (uint8, bytes32, bytes32); 14 | function toString(address) external returns (string memory); 15 | function toString(bool) external returns (string memory); 16 | function toString(uint256) external returns (string memory); 17 | function toString(int256) external returns (string memory); 18 | function toString(bytes32) external returns (string memory); 19 | function toString(bytes memory) external returns (string memory); 20 | function warp(uint64) external; 21 | function load(address, bytes32) external returns (bytes32); 22 | function store(address, bytes32, bytes32) external; 23 | function roll(uint256) external; 24 | function prank(address) external; 25 | function prankHere(address) external; 26 | function getNonce(address) external returns (uint64); 27 | function setNonce(address, uint64) external; 28 | function fee(uint256) external; 29 | function etch(address, bytes calldata) external; 30 | function difficulty(uint256) external; 31 | function deal(address, uint256) external; 32 | function coinbase(address) external; 33 | function chainId(uint256) external; 34 | } 35 | -------------------------------------------------------------------------------- /medusa.json: -------------------------------------------------------------------------------- 1 | { 2 | "fuzzing": { 3 | "workers": 10, 4 | "workerResetLimit": 50, 5 | "timeout": 0, 6 | "testLimit": 0, 7 | "callSequenceLength": 100, 8 | "corpusDirectory": "corpus", 9 | "coverageEnabled": true, 10 | "deploymentOrder": [], 11 | "constructorArgs": {}, 12 | "deployerAddress": "0x30000", 13 | "senderAddresses": [ 14 | "0x10000", 15 | "0x20000", 16 | "0x30000" 17 | ], 18 | "blockNumberDelayMax": 60480, 19 | "blockTimestampDelayMax": 604800, 20 | "blockGasLimit": 125000000, 21 | "transactionGasLimit": 12500000, 22 | "testing": { 23 | "stopOnFailedTest": true, 24 | "stopOnFailedContractMatching": true, 25 | "stopOnNoTests": true, 26 | "testAllContracts": false, 27 | "traceAll": false, 28 | "assertionTesting": { 29 | "enabled": true, 30 | "testViewMethods": false, 31 | "assertionModes": { 32 | "failOnCompilerInsertedPanic": false, 33 | "failOnAssertion": true, 34 | "failOnArithmeticUnderflow": false, 35 | "failOnDivideByZero": false, 36 | "failOnEnumTypeConversionOutOfBounds": false, 37 | "failOnIncorrectStorageAccess": false, 38 | "failOnPopEmptyArray": false, 39 | "failOnOutOfBoundsArrayAccess": false, 40 | "failOnAllocateTooMuchMemory": false, 41 | "failOnCallUninitializedVariable": false 42 | } 43 | }, 44 | "propertyTesting": { 45 | "enabled": true, 46 | "testPrefixes": [ 47 | "fuzz_" 48 | ] 49 | }, 50 | "optimizationTesting": { 51 | "enabled": false, 52 | "testPrefixes": [ 53 | "optimize_" 54 | ] 55 | } 56 | }, 57 | "chainConfig": { 58 | "codeSizeCheckDisabled": true, 59 | "cheatCodes": { 60 | "cheatCodesEnabled": true, 61 | "enableFFI": false 62 | } 63 | } 64 | }, 65 | "compilation": { 66 | "platform": "crytic-compile", 67 | "platformConfig": { 68 | "target": ".", 69 | "solcVersion": "", 70 | "exportDirectory": "", 71 | "args": [] 72 | } 73 | }, 74 | "logging": { 75 | "level": "info", 76 | "logDirectory": "" 77 | } 78 | } -------------------------------------------------------------------------------- /tests/wrapper/FixedPointMathLibWrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.19; 3 | 4 | import {FixedPointMathLib} from "contracts/FixedPointMathLib.sol"; 5 | 6 | contract FixedPointMathLibWrapper { 7 | function mulWadDown(uint256 x, uint256 y) external pure returns (uint256) { 8 | return FixedPointMathLib.mulWadDown(x, y); 9 | } 10 | 11 | function mulWadUp(uint256 x, uint256 y) external pure returns (uint256) { 12 | return FixedPointMathLib.mulWadUp(x, y); 13 | } 14 | 15 | function divWadDown(uint256 x, uint256 y) external pure returns (uint256) { 16 | return FixedPointMathLib.divWadDown(x, y); 17 | } 18 | 19 | function divWadUp(uint256 x, uint256 y) external pure returns (uint256) { 20 | return FixedPointMathLib.divWadUp(x, y); 21 | } 22 | 23 | function mulDivDown( 24 | uint256 x, 25 | uint256 y, 26 | uint256 denominator 27 | ) external pure returns (uint256) { 28 | return FixedPointMathLib.mulDivDown(x, y, denominator); 29 | } 30 | 31 | function mulDivUp( 32 | uint256 x, 33 | uint256 y, 34 | uint256 denominator 35 | ) external pure returns (uint256) { 36 | return FixedPointMathLib.mulDivUp(x, y, denominator); 37 | } 38 | 39 | function rpow( 40 | uint256 x, 41 | uint256 n, 42 | uint256 scalar 43 | ) external pure returns (uint256) { 44 | return FixedPointMathLib.rpow(x, n, scalar); 45 | } 46 | 47 | function sqrt(uint256 x) external pure returns (uint256) { 48 | return FixedPointMathLib.sqrt(x); 49 | } 50 | 51 | function unsafeMod(uint256 x, uint256 y) external pure returns (uint256) { 52 | return FixedPointMathLib.unsafeMod(x, y); 53 | } 54 | 55 | function unsafeDiv(uint256 x, uint256 y) external pure returns (uint256) { 56 | return FixedPointMathLib.unsafeDiv(x, y); 57 | } 58 | 59 | function unsafeDivUp(uint256 x, uint256 y) external pure returns (uint256) { 60 | return FixedPointMathLib.unsafeDivUp(x, y); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solmate-invariants 2 | 3 | This repository contains over 100 invariants implemented in [`medusa`](https://github.com/crytic/medusa) during a workshop held by [TrailOfBits](https://www.trailofbits.com/). More information about the workshop can be found [here](/secureum-workshop.md). 4 | 5 | The goal was to find injected bugs in following three contracts, which are adapted directly from [solmate](https://github.com/transmissions11/solmate): 6 | 7 | - [`ERC20Burn`](/contracts/ERC20Burn.sol) - A standard ERC-20 token with burn functionality. 8 | - [`FixedPointMathLib`](/contracts/FixedPointMathLib.sol) - Arithmetic library with operations for fixed-point numbers. 9 | - [`SignedWadMath`](/contracts/SignedWadMath.sol) - Signed integer 18 decimal fixed point arithmetic library. 10 | 11 | The invariants for each contract are defined in separate test files: 12 | 13 | - [`ERC20Test.sol`](/tests/ERC20Test.sol) 14 | - [`FixedPointMathLibTest.sol`](/tests/FixedPointMathLibTest.sol) 15 | - [`SignedWadMathTest.sol`](/tests/SignedWadMathTest.sol) 16 | 17 | ## Running `medusa` 18 | 19 | First, follow the instructions stated [here](/secureum-workshop.md#before-starting) to setup `medusa` and `solc`. 20 | 21 | To start fuzzing, run the following command: 22 | 23 | ```bash 24 | medusa fuzz --target tests/submission.sol --deployment-order ERC20Test,FixedPointMathLibTest,SignedWadMathTest 25 | ``` 26 | 27 | ## Finding a bug in solmate 28 | 29 | During the workshop, an edge case was discovered in the [`wadMul()`](https://github.com/transmissions11/solmate/blob/bfc9c25865a274a7827fea5abf6e4fb64fc64e6c/src/utils/SignedWadMath.sol#L58-L72) function of solmate's `SignedWadMath` library. Specific details of the bug can be found [here](https://github.com/transmissions11/solmate/pull/380). 30 | 31 | The invariant that found the bug is in [`SolmateBugTest.sol`](/tests/SolmateBugTest.sol). To run the test, use the following command: 32 | 33 | ```bash 34 | medusa fuzz --target tests/SolmateBugTest.sol --deployment-order SolmateBugTest 35 | ``` 36 | 37 | `medusa` will instantly flag out an example where the invariant is violated. 38 | 39 | ## References 40 | 41 | Some useful references while writing invariants: 42 | - [Pre-built properties](https://github.com/crytic/properties) by [TrailOfBits](https://www.trailofbits.com/) 43 | - [WETH invariant testing](https://github.com/horsefacts/weth-invariant-testing/tree/main) by [@horsefacts](https://twitter.com/eth_call) 44 | -------------------------------------------------------------------------------- /contracts/ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | 3 | // SECUREUM 4 | // The following file contains ERC20/WETH/SafeTransferLib from solmate 5 | // permit-like functions were removed (EIP-2612) 6 | 7 | pragma solidity 0.8.19; 8 | 9 | /// @notice Modern and gas efficient ERC20 10 | /// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol) 11 | /// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) 12 | /// @author [Secureum] EIP-2612 implementation was removed 13 | /// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. 14 | abstract contract ERC20 { 15 | /*////////////////////////////////////////////////////////////// 16 | EVENTS 17 | //////////////////////////////////////////////////////////////*/ 18 | 19 | event Transfer(address indexed from, address indexed to, uint256 amount); 20 | 21 | event Approval(address indexed owner, address indexed spender, uint256 amount); 22 | 23 | /*////////////////////////////////////////////////////////////// 24 | METADATA STORAGE 25 | //////////////////////////////////////////////////////////////*/ 26 | 27 | string public name; 28 | 29 | string public symbol; 30 | 31 | uint8 public immutable decimals; 32 | 33 | /*////////////////////////////////////////////////////////////// 34 | ERC20 STORAGE 35 | //////////////////////////////////////////////////////////////*/ 36 | 37 | uint256 public totalSupply; 38 | 39 | mapping(address => uint256) public balanceOf; 40 | 41 | mapping(address => mapping(address => uint256)) public allowance; 42 | 43 | 44 | /*////////////////////////////////////////////////////////////// 45 | CONSTRUCTOR 46 | //////////////////////////////////////////////////////////////*/ 47 | 48 | constructor( 49 | string memory _name, 50 | string memory _symbol, 51 | uint8 _decimals 52 | ) { 53 | name = _name; 54 | symbol = _symbol; 55 | decimals = _decimals; 56 | } 57 | 58 | /*////////////////////////////////////////////////////////////// 59 | ERC20 LOGIC 60 | //////////////////////////////////////////////////////////////*/ 61 | 62 | function approve(address spender, uint256 amount) public virtual returns (bool) { 63 | allowance[msg.sender][spender] = amount; 64 | 65 | emit Approval(msg.sender, spender, amount); 66 | 67 | return true; 68 | } 69 | 70 | function transfer(address to, uint256 amount) public virtual returns (bool) { 71 | balanceOf[msg.sender] -= amount; 72 | 73 | // Cannot overflow because the sum of all user 74 | // balances can't exceed the max uint256 value. 75 | unchecked { 76 | balanceOf[to] += amount; 77 | } 78 | 79 | emit Transfer(msg.sender, to, amount); 80 | 81 | return true; 82 | } 83 | 84 | function transferFrom( 85 | address from, 86 | address to, 87 | uint256 amount 88 | ) public virtual returns (bool) { 89 | uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. 90 | 91 | if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; 92 | 93 | balanceOf[from] -= amount; 94 | 95 | // Cannot overflow because the sum of all user 96 | // balances can't exceed the max uint256 value. 97 | unchecked { 98 | balanceOf[to] += amount; 99 | } 100 | 101 | emit Transfer(from, to, amount); 102 | 103 | return true; 104 | } 105 | 106 | 107 | 108 | /*////////////////////////////////////////////////////////////// 109 | INTERNAL MINT/BURN LOGIC 110 | //////////////////////////////////////////////////////////////*/ 111 | 112 | function _mint(address to, uint256 amount) internal virtual { 113 | totalSupply += amount; 114 | 115 | // Cannot overflow because the sum of all user 116 | // balances can't exceed the max uint256 value. 117 | unchecked { 118 | balanceOf[to] += amount; 119 | } 120 | 121 | emit Transfer(address(0), to, amount); 122 | } 123 | 124 | function _burn(address from, uint256 amount) internal virtual { 125 | balanceOf[from] -= amount; 126 | 127 | // Cannot underflow because a user's balance 128 | // will never be larger than the total supply. 129 | unchecked { 130 | totalSupply -= amount; 131 | } 132 | 133 | emit Transfer(from, address(0), amount); 134 | } 135 | } 136 | 137 | -------------------------------------------------------------------------------- /secureum-workshop.md: -------------------------------------------------------------------------------- 1 | 2 | # Secureum Medusa workshop 3 | 4 | The goals of this workshop are to: 5 | - Learn about invariants development 6 | - Become familar with the medusa fuzzer 7 | 8 | Medusa is a [new experimental fuzzer](https://github.com/crytic/medusa). Do not hesitate to ask questions on secureum's discord, or create [github issues](https://github.com/crytic/medusa/issues/new) if you encounter any issue. 9 | 10 | ## Before starting 11 | 12 | To install medusa, follow [the installation instructions](https://github.com/crytic/medusa/#installation). 13 | 14 | ### Solc 15 | 16 | Solc 0.8.19 is used for this workshop. We recommend [solc-select](https://github.com/crytic/solc-select) to easily switch between solc versions: 17 | ``` 18 | pip3 install solc-select 19 | solc-select install 0.8.19 20 | solc-select use 0.8.19 21 | ``` 22 | 23 | ## The contest 24 | 25 | The goals of the contest is to write invariants for three targets (`SignedWadMath`, `FixedPointMathLib`, `ERC20Burn`). All the contracts are inspired from [solmate](https://github.com/transmissions11/solmate). 26 | 27 | 28 | ### Helper 29 | - [`helper`](./contracts/helper.sol) comes from the [properties](https://github.com/crytic/properties) repo, and contains helpers to ease the creation of invariants. In particular we recommend to use: 30 | - `asssertX` (`Eq`, `Neq`, `Gte`, `Gt`, `Lte`, `Lt`) to test assertion between values 31 | - `clampX` ( `Between`, `Lt`, `Lte`, `Gt`, `Gte` ) to restraint the inputs' values 32 | 33 | ### SignedWadMath 34 | - [`SignedWadMath`](./contracts/SignedWadMath.sol) is a signed 18 decimal fixed point (wad) arithmetic library. 35 | - [`SignedWadMathTest`](./contracts/SignedWadMathTest.sol) is an example of test for `SignedWadMath` 36 | - `testtoWadUnsafe` is an example of invariant to help you 37 | 38 | ### FixedPointMathLib 39 | - [`FixedPointMathLib`](./contracts/FixedPointMathLib.sol) is an arithmetic library with operations for fixed-point numbers. 40 | - [`FixedPointMathLibTest`](./contracts/FixedPointMathLibTest.sol) is an example of test for `SignedWadMath` 41 | - `testmulDivDown` is an example of invariant to help you 42 | 43 | ### ERC20Burn 44 | - [`ERC20`](./contracts/ERC20.sol) is a standard ERC20 token. 45 | - [`ERC20Burn`](./contracts/ERC20Burn.sol) extends `ERC20` with a burn function 46 | - [`ERC20Test`](./contracts/ERC20Test.sol) is an example of test for `ERC20Burn` 47 | - `fuzz_Supply` is an example of invariant to help you 48 | - [`ERC20TestAdvanced`](./contracts/ERC20TestAdvanced.sol) is an example of an advanced test for `ERC20Burn` 49 | - `ERC20TestAdvanced` uses the [external testing approach](https://secure-contracts.com/program-analysis/echidna/basic/common-testing-approaches.html#external-testing) and uses a proxy contract to simulate a user. This approach is more complex to use, but allows to test for more complex scenario 50 | - `testTransferFrom` is an example of invariant to help you 51 | 52 | ### ERC20Burn 53 | 54 | 55 | ## How to start 56 | 57 | A few pointers to start: 58 | 59 | - Read the documentation 60 | - Start small, and create simple invariants first 61 | - Start with `SignedWadMath` 62 | - Consider when operation should or it should not revert 63 | - Some properties could require to use certain tolerance 64 | - `ERC20TestAdvanced` is recommended only for users that have already explored the other contracts 65 | - Do not hesitate to introduce bugs in your code to verify that your invariants can catch them 66 | 67 | 68 | To start a fuzzing campagn 69 | ```bash 70 | medusa fuzz --target contracts/NAME.sol --deployment-order CONTRACT_NAME 71 | ``` 72 | Replace `NAME.sol` and `CONTRACT_NAME`. 73 | 74 | ## Expected Results and Evaluation 75 | 76 | User should be able to fully test the contracts. It is worth mentioning that the code is unmodified and there are no known issues. If you find some security or correctness issue in the code do NOT post it in this repository nor upstream, since these are public messages. Instead, [contact us](mailto:josselin@trailofits.com) to confirm the issue and discuss how to proceed. 77 | 78 | For Secureum, the resulting properties will be evaluated introducing an artificial bug in the code and running a short fuzzing campaign. 79 | 80 | We encourage you to try different approaches and invariants. Invariants based development is a powerful tool for developer and auditors that require practices and experience to master it. 81 | 82 | ## Configuration 83 | [medusa.json](./medusa.json) was generated with `medusa init`. The following changes were applied: 84 | - `testAllContracts` was set to true 85 | - `corpusDirectory` was set to "corpus" 86 | - `assertionTesting/enabled` was set to true 87 | 88 | ## Documentation 89 | - [Medusa configuration](https://github.com/crytic/medusa/wiki/Project-Configuration) 90 | - [Fuzzing workshop](https://www.youtube.com/watch?v=QofNQxW_K08&list=PLciHOL_J7Iwqdja9UH4ZzE8dP1IxtsBXI) 91 | - [Fuzzing training](https://secure-contracts.com/program-analysis/echidna/index.html) 92 | -------------------------------------------------------------------------------- /tests/helper/TestHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.19; 3 | 4 | import {PropertiesAsserts} from "./helper.sol"; 5 | 6 | contract TestHelper is PropertiesAsserts { 7 | function failWithMsg(string memory reason) internal { 8 | emit AssertFail(reason); 9 | assert(false); 10 | } 11 | 12 | // A hacky way to check for panics 13 | function assertPanic( 14 | bytes memory errorMsg, 15 | uint8 panicCode, 16 | string memory reason 17 | ) internal { 18 | if ( 19 | bytes4(errorMsg) != bytes4("NH{q") || 20 | errorMsg[errorMsg.length - 1] != bytes1(panicCode) 21 | ) { 22 | failWithMsg(reason); 23 | } 24 | } 25 | 26 | // Ensure the difference between expected and result is within errorMargin 27 | function assertErrorWithin( 28 | int256 expected, 29 | int256 result, 30 | int256 errorMargin, 31 | string memory reason 32 | ) internal { 33 | int256 err = expected > result ? expected - result : result - expected; 34 | if (expected < 0) { 35 | expected = -expected; 36 | } 37 | assertLte(err, (expected * errorMargin) / 1e18, reason); 38 | } 39 | 40 | // Call function and ensure it did not revert 41 | function callWithoutRevert( 42 | function(uint256) external returns (uint256) f, 43 | uint256 a, 44 | string memory reason 45 | ) internal returns (uint256 r) { 46 | try f(a) returns (uint256 result) { 47 | r = result; 48 | } catch { 49 | failWithMsg(reason); 50 | } 51 | } 52 | 53 | function callWithoutRevert( 54 | function(uint256, uint256) external returns (uint256) f, 55 | uint256 a, 56 | uint256 b, 57 | string memory reason 58 | ) internal returns (uint256 r) { 59 | try f(a, b) returns (uint256 result) { 60 | r = result; 61 | } catch { 62 | failWithMsg(reason); 63 | } 64 | } 65 | 66 | function callWithoutRevert( 67 | function(uint256, uint256, uint256) external returns (uint256) f, 68 | uint256 a, 69 | uint256 b, 70 | uint256 c, 71 | string memory reason 72 | ) internal returns (uint256 r) { 73 | try f(a, b, c) returns (uint256 result) { 74 | r = result; 75 | } catch { 76 | failWithMsg(reason); 77 | } 78 | } 79 | 80 | function callWithoutRevert( 81 | function(int256) external returns (int256) f, 82 | int256 a, 83 | string memory reason 84 | ) internal returns (int256 r) { 85 | try f(a) returns (int256 result) { 86 | r = result; 87 | } catch { 88 | failWithMsg(reason); 89 | } 90 | } 91 | 92 | function callWithoutRevert( 93 | function(int256, int256) external returns (int256) f, 94 | int256 a, 95 | int256 b, 96 | string memory reason 97 | ) internal returns (int256 r) { 98 | try f(a, b) returns (int256 result) { 99 | r = result; 100 | } catch { 101 | failWithMsg(reason); 102 | } 103 | } 104 | 105 | // Call function and ensure that it reverted 106 | function expectRevert( 107 | function(uint256) external returns (uint256) f, 108 | uint256 a, 109 | string memory reason 110 | ) internal returns (bytes memory r) { 111 | try f(a) { 112 | failWithMsg(reason); 113 | } catch (bytes memory errorMsg) { 114 | r = errorMsg; 115 | } 116 | } 117 | 118 | function expectRevert( 119 | function(uint256, uint256) external returns (uint256) f, 120 | uint256 a, 121 | uint256 b, 122 | string memory reason 123 | ) internal returns (bytes memory r) { 124 | try f(a, b) { 125 | failWithMsg(reason); 126 | } catch (bytes memory errorMsg) { 127 | r = errorMsg; 128 | } 129 | } 130 | 131 | function expectRevert( 132 | function(uint256, uint256, uint256) external returns (uint256) f, 133 | uint256 a, 134 | uint256 b, 135 | uint256 c, 136 | string memory reason 137 | ) internal returns (bytes memory r) { 138 | try f(a, b, c) { 139 | failWithMsg(reason); 140 | } catch (bytes memory errorMsg) { 141 | r = errorMsg; 142 | } 143 | } 144 | 145 | function expectRevert( 146 | function(int256) external returns (int256) f, 147 | int256 a, 148 | string memory reason 149 | ) internal returns (bytes memory r) { 150 | try f(a) { 151 | failWithMsg(reason); 152 | } catch (bytes memory errorMsg) { 153 | r = errorMsg; 154 | } 155 | } 156 | 157 | function expectRevert( 158 | function(int256, int256) external returns (int256) f, 159 | int256 a, 160 | int256 b, 161 | string memory reason 162 | ) internal returns (bytes memory r) { 163 | try f(a, b) { 164 | failWithMsg(reason); 165 | } catch (bytes memory errorMsg) { 166 | r = errorMsg; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /contracts/FixedPointMathLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity 0.8.19; 3 | 4 | /// @notice Arithmetic library with operations for fixed-point numbers. 5 | /// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) 6 | /// @author Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) 7 | library FixedPointMathLib { 8 | /*////////////////////////////////////////////////////////////// 9 | SIMPLIFIED FIXED POINT OPERATIONS 10 | //////////////////////////////////////////////////////////////*/ 11 | 12 | uint256 internal constant MAX_UINT256 = 2**256 - 1; 13 | 14 | uint256 internal constant WAD = 1e18; // The scalar of ETH and most ERC20s. 15 | 16 | function mulWadDown(uint256 x, uint256 y) internal pure returns (uint256) { 17 | return mulDivDown(x, y, WAD); // Equivalent to (x * y) / WAD rounded down. 18 | } 19 | 20 | function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256) { 21 | return mulDivUp(x, y, WAD); // Equivalent to (x * y) / WAD rounded up. 22 | } 23 | 24 | function divWadDown(uint256 x, uint256 y) internal pure returns (uint256) { 25 | return mulDivDown(x, WAD, y); // Equivalent to (x * WAD) / y rounded down. 26 | } 27 | 28 | function divWadUp(uint256 x, uint256 y) internal pure returns (uint256) { 29 | return mulDivUp(x, WAD, y); // Equivalent to (x * WAD) / y rounded up. 30 | } 31 | 32 | /*////////////////////////////////////////////////////////////// 33 | LOW LEVEL FIXED POINT OPERATIONS 34 | //////////////////////////////////////////////////////////////*/ 35 | 36 | function mulDivDown( 37 | uint256 x, 38 | uint256 y, 39 | uint256 denominator 40 | ) internal pure returns (uint256 z) { 41 | /// @solidity memory-safe-assembly 42 | assembly { 43 | // Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y)) 44 | if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) { 45 | revert(0, 0) 46 | } 47 | 48 | // Divide x * y by the denominator. 49 | z := div(mul(x, y), denominator) 50 | } 51 | } 52 | 53 | function mulDivUp( 54 | uint256 x, 55 | uint256 y, 56 | uint256 denominator 57 | ) internal pure returns (uint256 z) { 58 | /// @solidity memory-safe-assembly 59 | assembly { 60 | // Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y)) 61 | if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) { 62 | revert(0, 0) 63 | } 64 | 65 | // If x * y modulo the denominator is strictly greater than 0, 66 | // 1 is added to round up the division of x * y by the denominator. 67 | z := add(gt(mod(mul(x, y), denominator), 0), div(mul(x, y), denominator)) 68 | } 69 | } 70 | 71 | function rpow( 72 | uint256 x, 73 | uint256 n, 74 | uint256 scalar 75 | ) internal pure returns (uint256 z) { 76 | /// @solidity memory-safe-assembly 77 | assembly { 78 | switch x 79 | case 0 { 80 | switch n 81 | case 0 { 82 | // 0 ** 0 = 1 83 | z := scalar 84 | } 85 | default { 86 | // 0 ** n = 0 87 | z := 0 88 | } 89 | } 90 | default { 91 | switch mod(n, 2) 92 | case 0 { 93 | // If n is even, store scalar in z for now. 94 | z := scalar 95 | } 96 | default { 97 | // If n is odd, store x in z for now. 98 | z := x 99 | } 100 | 101 | // Shifting right by 1 is like dividing by 2. 102 | let half := shr(1, scalar) 103 | 104 | for { 105 | // Shift n right by 1 before looping to halve it. 106 | n := shr(1, n) 107 | } n { 108 | // Shift n right by 1 each iteration to halve it. 109 | n := shr(1, n) 110 | } { 111 | // Revert immediately if x ** 2 would overflow. 112 | // Equivalent to iszero(eq(div(xx, x), x)) here. 113 | if shr(128, x) { 114 | revert(0, 0) 115 | } 116 | 117 | // Store x squared. 118 | let xx := mul(x, x) 119 | 120 | // Round to the nearest number. 121 | let xxRound := add(xx, half) 122 | 123 | // Revert if xx + half overflowed. 124 | if lt(xxRound, xx) { 125 | revert(0, 0) 126 | } 127 | 128 | // Set x to scaled xxRound. 129 | x := div(xxRound, scalar) 130 | 131 | // If n is even: 132 | if mod(n, 2) { 133 | // Compute z * x. 134 | let zx := mul(z, x) 135 | 136 | // If z * x overflowed: 137 | if iszero(eq(div(zx, x), z)) { 138 | // Revert if x is non-zero. 139 | if iszero(iszero(x)) { 140 | revert(0, 0) 141 | } 142 | } 143 | 144 | // Round to the nearest number. 145 | let zxRound := add(zx, half) 146 | 147 | // Revert if zx + half overflowed. 148 | if lt(zxRound, zx) { 149 | revert(0, 0) 150 | } 151 | 152 | // Return properly scaled zxRound. 153 | z := div(zxRound, scalar) 154 | } 155 | } 156 | } 157 | } 158 | } 159 | 160 | /*////////////////////////////////////////////////////////////// 161 | GENERAL NUMBER UTILITIES 162 | //////////////////////////////////////////////////////////////*/ 163 | 164 | function sqrt(uint256 x) internal pure returns (uint256 z) { 165 | /// @solidity memory-safe-assembly 166 | assembly { 167 | let y := x // We start y at x, which will help us make our initial estimate. 168 | 169 | z := 181 // The "correct" value is 1, but this saves a multiplication later. 170 | 171 | // This segment is to get a reasonable initial estimate for the Babylonian method. With a bad 172 | // start, the correct # of bits increases ~linearly each iteration instead of ~quadratically. 173 | 174 | // We check y >= 2^(k + 8) but shift right by k bits 175 | // each branch to ensure that if x >= 256, then y >= 256. 176 | if iszero(lt(y, 0x10000000000000000000000000000000000)) { 177 | y := shr(128, y) 178 | z := shl(64, z) 179 | } 180 | if iszero(lt(y, 0x1000000000000000000)) { 181 | y := shr(64, y) 182 | z := shl(32, z) 183 | } 184 | if iszero(lt(y, 0x10000000000)) { 185 | y := shr(32, y) 186 | z := shl(16, z) 187 | } 188 | if iszero(lt(y, 0x1000000)) { 189 | y := shr(16, y) 190 | z := shl(8, z) 191 | } 192 | 193 | // Goal was to get z*z*y within a small factor of x. More iterations could 194 | // get y in a tighter range. Currently, we will have y in [256, 256*2^16). 195 | // We ensured y >= 256 so that the relative difference between y and y+1 is small. 196 | // That's not possible if x < 256 but we can just verify those cases exhaustively. 197 | 198 | // Now, z*z*y <= x < z*z*(y+1), and y <= 2^(16+8), and either y >= 256, or x < 256. 199 | // Correctness can be checked exhaustively for x < 256, so we assume y >= 256. 200 | // Then z*sqrt(y) is within sqrt(257)/sqrt(256) of sqrt(x), or about 20bps. 201 | 202 | // For s in the range [1/256, 256], the estimate f(s) = (181/1024) * (s+1) is in the range 203 | // (1/2.84 * sqrt(s), 2.84 * sqrt(s)), with largest error when s = 1 and when s = 256 or 1/256. 204 | 205 | // Since y is in [256, 256*2^16), let a = y/65536, so that a is in [1/256, 256). Then we can estimate 206 | // sqrt(y) using sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2^18. 207 | 208 | // There is no overflow risk here since y < 2^136 after the first branch above. 209 | z := shr(18, mul(z, add(y, 65536))) // A mul() is saved from starting z at 181. 210 | 211 | // Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough. 212 | z := shr(1, add(z, div(x, z))) 213 | z := shr(1, add(z, div(x, z))) 214 | z := shr(1, add(z, div(x, z))) 215 | z := shr(1, add(z, div(x, z))) 216 | z := shr(1, add(z, div(x, z))) 217 | z := shr(1, add(z, div(x, z))) 218 | z := shr(1, add(z, div(x, z))) 219 | 220 | // If x+1 is a perfect square, the Babylonian method cycles between 221 | // floor(sqrt(x)) and ceil(sqrt(x)). This statement ensures we return floor. 222 | // See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division 223 | // Since the ceil is rare, we save gas on the assignment and repeat division in the rare case. 224 | // If you don't care whether the floor or ceil square root is returned, you can remove this statement. 225 | z := sub(z, lt(div(x, z), z)) 226 | } 227 | } 228 | 229 | function unsafeMod(uint256 x, uint256 y) internal pure returns (uint256 z) { 230 | /// @solidity memory-safe-assembly 231 | assembly { 232 | // Mod x by y. Note this will return 233 | // 0 instead of reverting if y is zero. 234 | z := mod(x, y) 235 | } 236 | } 237 | 238 | function unsafeDiv(uint256 x, uint256 y) internal pure returns (uint256 r) { 239 | /// @solidity memory-safe-assembly 240 | assembly { 241 | // Divide x by y. Note this will return 242 | // 0 instead of reverting if y is zero. 243 | r := div(x, y) 244 | } 245 | } 246 | 247 | function unsafeDivUp(uint256 x, uint256 y) internal pure returns (uint256 z) { 248 | /// @solidity memory-safe-assembly 249 | assembly { 250 | // Add 1 to x * y if x % y > 0. Note this will 251 | // return 0 instead of reverting if y is zero. 252 | z := add(gt(mod(x, y), 0), div(x, y)) 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /contracts/SignedWadMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.19; 3 | 4 | /// @notice Signed 18 decimal fixed point (wad) arithmetic library. 5 | /// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SignedWadMath.sol) 6 | /// @author Modified from Remco Bloemen (https://xn--2-umb.com/22/exp-ln/index.html) 7 | 8 | library SignedWadMath { 9 | /// @dev Will not revert on overflow, only use where overflow is not possible. 10 | function toWadUnsafe(uint256 x) pure internal returns (int256 r) { 11 | /// @solidity memory-safe-assembly 12 | assembly { 13 | // Multiply x by 1e18. 14 | r := mul(x, 1000000000000000000) 15 | } 16 | } 17 | 18 | /// @dev Takes an integer amount of seconds and converts it to a wad amount of days. 19 | /// @dev Will not revert on overflow, only use where overflow is not possible. 20 | /// @dev Not meant for negative second amounts, it assumes x is positive. 21 | function toDaysWadUnsafe(uint256 x) pure internal returns (int256 r) { 22 | /// @solidity memory-safe-assembly 23 | assembly { 24 | // Multiply x by 1e18 and then divide it by 86400. 25 | r := div(mul(x, 1000000000000000000), 86400) 26 | } 27 | } 28 | 29 | /// @dev Takes a wad amount of days and converts it to an integer amount of seconds. 30 | /// @dev Will not revert on overflow, only use where overflow is not possible. 31 | /// @dev Not meant for negative day amounts, it assumes x is positive. 32 | function fromDaysWadUnsafe(int256 x) pure internal returns (uint256 r) { 33 | /// @solidity memory-safe-assembly 34 | assembly { 35 | // Multiply x by 86400 and then divide it by 1e18. 36 | r := div(mul(x, 86400), 1000000000000000000) 37 | } 38 | } 39 | 40 | /// @dev Will not revert on overflow, only use where overflow is not possible. 41 | function unsafeWadMul(int256 x, int256 y) pure internal returns (int256 r) { 42 | /// @solidity memory-safe-assembly 43 | assembly { 44 | // Multiply x by y and divide by 1e18. 45 | r := sdiv(mul(x, y), 1000000000000000000) 46 | } 47 | } 48 | 49 | /// @dev Will return 0 instead of reverting if y is zero and will 50 | /// not revert on overflow, only use where overflow is not possible. 51 | function unsafeWadDiv(int256 x, int256 y) pure internal returns (int256 r) { 52 | /// @solidity memory-safe-assembly 53 | assembly { 54 | // Multiply x by 1e18 and divide it by y. 55 | r := sdiv(mul(x, 1000000000000000000), y) 56 | } 57 | } 58 | 59 | function wadMul(int256 x, int256 y) pure internal returns (int256 r) { 60 | /// @solidity memory-safe-assembly 61 | assembly { 62 | // Store x * y in r for now. 63 | r := mul(x, y) 64 | 65 | // Equivalent to require(x == 0 || (x * y) / x == y) 66 | if iszero(or(iszero(x), eq(sdiv(r, x), y))) { 67 | revert(0, 0) 68 | } 69 | 70 | // Scale the result down by 1e18. 71 | r := sdiv(r, 1000000000000000000) 72 | } 73 | } 74 | 75 | function wadDiv(int256 x, int256 y) pure internal returns (int256 r) { 76 | /// @solidity memory-safe-assembly 77 | assembly { 78 | // Store x * 1e18 in r for now. 79 | r := mul(x, 1000000000000000000) 80 | 81 | // Equivalent to require(y != 0 && ((x * 1e18) / 1e18 == x)) 82 | if iszero(and(iszero(iszero(y)), eq(sdiv(r, 1000000000000000000), x))) { 83 | revert(0, 0) 84 | } 85 | 86 | // Divide r by y. 87 | r := sdiv(r, y) 88 | } 89 | } 90 | 91 | /// @dev Will not work with negative bases, only use when x is positive. 92 | function wadPow(int256 x, int256 y) pure internal returns (int256) { 93 | // Equivalent to x to the power of y because x ** y = (e ** ln(x)) ** y = e ** (ln(x) * y) 94 | return wadExp((wadLn(x) * y) / 1e18); // Using ln(x) means x must be greater than 0. 95 | } 96 | 97 | function wadExp(int256 x) pure internal returns (int256 r) { 98 | unchecked { 99 | // When the result is < 0.5 we return zero. This happens when 100 | // x <= floor(log(0.5e18) * 1e18) ~ -42e18 101 | if (x <= -42139678854452767551) return 0; 102 | 103 | // When the result is > (2**255 - 1) / 1e18 we can not represent it as an 104 | // int. This happens when x >= floor(log((2**255 - 1) / 1e18) * 1e18) ~ 135. 105 | if (x >= 135305999368893231589) revert("EXP_OVERFLOW"); 106 | 107 | // x is now in the range (-42, 136) * 1e18. Convert to (-42, 136) * 2**96 108 | // for more intermediate precision and a binary basis. This base conversion 109 | // is a multiplication by 1e18 / 2**96 = 5**18 / 2**78. 110 | x = (x << 78) / 5**18; 111 | 112 | // Reduce range of x to (-½ ln 2, ½ ln 2) * 2**96 by factoring out powers 113 | // of two such that exp(x) = exp(x') * 2**k, where k is an integer. 114 | // Solving this gives k = round(x / log(2)) and x' = x - k * log(2). 115 | int256 k = ((x << 96) / 54916777467707473351141471128 + 2**95) >> 96; 116 | x = x - k * 54916777467707473351141471128; 117 | 118 | // k is in the range [-61, 195]. 119 | 120 | // Evaluate using a (6, 7)-term rational approximation. 121 | // p is made monic, we'll multiply by a scale factor later. 122 | int256 y = x + 1346386616545796478920950773328; 123 | y = ((y * x) >> 96) + 57155421227552351082224309758442; 124 | int256 p = y + x - 94201549194550492254356042504812; 125 | p = ((p * y) >> 96) + 28719021644029726153956944680412240; 126 | p = p * x + (4385272521454847904659076985693276 << 96); 127 | 128 | // We leave p in 2**192 basis so we don't need to scale it back up for the division. 129 | int256 q = x - 2855989394907223263936484059900; 130 | q = ((q * x) >> 96) + 50020603652535783019961831881945; 131 | q = ((q * x) >> 96) - 533845033583426703283633433725380; 132 | q = ((q * x) >> 96) + 3604857256930695427073651918091429; 133 | q = ((q * x) >> 96) - 14423608567350463180887372962807573; 134 | q = ((q * x) >> 96) + 26449188498355588339934803723976023; 135 | 136 | /// @solidity memory-safe-assembly 137 | assembly { 138 | // Div in assembly because solidity adds a zero check despite the unchecked. 139 | // The q polynomial won't have zeros in the domain as all its roots are complex. 140 | // No scaling is necessary because p is already 2**96 too large. 141 | r := sdiv(p, q) 142 | } 143 | 144 | // r should be in the range (0.09, 0.25) * 2**96. 145 | 146 | // We now need to multiply r by: 147 | // * the scale factor s = ~6.031367120. 148 | // * the 2**k factor from the range reduction. 149 | // * the 1e18 / 2**96 factor for base conversion. 150 | // We do this all at once, with an intermediate result in 2**213 151 | // basis, so the final right shift is always by a positive amount. 152 | r = int256((uint256(r) * 3822833074963236453042738258902158003155416615667) >> uint256(195 - k)); 153 | } 154 | } 155 | 156 | function wadLn(int256 x) pure internal returns (int256 r) { 157 | unchecked { 158 | require(x > 0, "UNDEFINED"); 159 | 160 | // We want to convert x from 10**18 fixed point to 2**96 fixed point. 161 | // We do this by multiplying by 2**96 / 10**18. But since 162 | // ln(x * C) = ln(x) + ln(C), we can simply do nothing here 163 | // and add ln(2**96 / 10**18) at the end. 164 | 165 | /// @solidity memory-safe-assembly 166 | assembly { 167 | r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) 168 | r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) 169 | r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) 170 | r := or(r, shl(4, lt(0xffff, shr(r, x)))) 171 | r := or(r, shl(3, lt(0xff, shr(r, x)))) 172 | r := or(r, shl(2, lt(0xf, shr(r, x)))) 173 | r := or(r, shl(1, lt(0x3, shr(r, x)))) 174 | r := or(r, lt(0x1, shr(r, x))) 175 | } 176 | 177 | // Reduce range of x to (1, 2) * 2**96 178 | // ln(2^k * x) = k * ln(2) + ln(x) 179 | int256 k = r - 96; 180 | x <<= uint256(159 - k); 181 | x = int256(uint256(x) >> 159); 182 | 183 | // Evaluate using a (8, 8)-term rational approximation. 184 | // p is made monic, we will multiply by a scale factor later. 185 | int256 p = x + 3273285459638523848632254066296; 186 | p = ((p * x) >> 96) + 24828157081833163892658089445524; 187 | p = ((p * x) >> 96) + 43456485725739037958740375743393; 188 | p = ((p * x) >> 96) - 11111509109440967052023855526967; 189 | p = ((p * x) >> 96) - 45023709667254063763336534515857; 190 | p = ((p * x) >> 96) - 14706773417378608786704636184526; 191 | p = p * x - (795164235651350426258249787498 << 96); 192 | 193 | // We leave p in 2**192 basis so we don't need to scale it back up for the division. 194 | // q is monic by convention. 195 | int256 q = x + 5573035233440673466300451813936; 196 | q = ((q * x) >> 96) + 71694874799317883764090561454958; 197 | q = ((q * x) >> 96) + 283447036172924575727196451306956; 198 | q = ((q * x) >> 96) + 401686690394027663651624208769553; 199 | q = ((q * x) >> 96) + 204048457590392012362485061816622; 200 | q = ((q * x) >> 96) + 31853899698501571402653359427138; 201 | q = ((q * x) >> 96) + 909429971244387300277376558375; 202 | /// @solidity memory-safe-assembly 203 | assembly { 204 | // Div in assembly because solidity adds a zero check despite the unchecked. 205 | // The q polynomial is known not to have zeros in the domain. 206 | // No scaling required because p is already 2**96 too large. 207 | r := sdiv(p, q) 208 | } 209 | 210 | // r is in the range (0, 0.125) * 2**96 211 | 212 | // Finalization, we need to: 213 | // * multiply by the scale factor s = 5.549… 214 | // * add ln(2**96 / 10**18) 215 | // * add k * ln(2) 216 | // * multiply by 10**18 / 2**96 = 5**18 >> 78 217 | 218 | // mul s * 5e18 * 2**96, base is now 5**18 * 2**192 219 | r *= 1677202110996718588342820967067443963516166; 220 | // add ln(2) * k * 5e18 * 2**192 221 | r += 16597577552685614221487285958193947469193820559219878177908093499208371 * k; 222 | // add ln(2**96 / 10**18) * 5e18 * 2**192 223 | r += 600920179829731861736702779321621459595472258049074101567377883020018308; 224 | // base conversion: mul 2**18 / 2**192 225 | r >>= 174; 226 | } 227 | } 228 | 229 | /// @dev Will return 0 instead of reverting if y is zero. 230 | function unsafeDiv(int256 x, int256 y) pure internal returns (int256 r) { 231 | /// @solidity memory-safe-assembly 232 | assembly { 233 | // Divide x by y. 234 | r := sdiv(x, y) 235 | } 236 | } 237 | } -------------------------------------------------------------------------------- /tests/ERC20Test.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.19; 3 | 4 | import {IVM} from "./helper/IVM.sol"; 5 | import {TestHelper} from "./helper/TestHelper.sol"; 6 | import {ERC20BurnWrapper} from "./wrapper/ERC20BurnWrapper.sol"; 7 | 8 | contract ERC20Test is TestHelper { 9 | // Cheatcodes 10 | IVM vm = IVM(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); 11 | 12 | // ERC20Burn related 13 | uint256 constant INITIAL_TOTAL_SUPPLY = 1e18; 14 | string startingName; 15 | string startingSymbol; 16 | uint8 startingDecimals; 17 | ERC20BurnWrapper token; 18 | 19 | // State tracking 20 | uint256 previousTotalSupply = INITIAL_TOTAL_SUPPLY; 21 | 22 | constructor() { 23 | // Deploy the token 24 | token = new ERC20BurnWrapper(); 25 | 26 | // Ensure deployment works as intended 27 | assert(token.balanceOf(address(this)) == INITIAL_TOTAL_SUPPLY); 28 | assert(keccak256(bytes(token.name())) == keccak256(bytes("MyToken"))); 29 | assert(keccak256(bytes(token.symbol())) == keccak256(bytes("MT"))); 30 | assert(token.decimals() == 18); 31 | 32 | // Store name, symbol and decimals 33 | startingName = token.name(); 34 | startingSymbol = token.symbol(); 35 | startingDecimals = token.decimals(); 36 | } 37 | 38 | // ASSERTIONS 39 | 40 | function testApprove( 41 | uint256 callerSeed, 42 | uint256 spenderSeed, 43 | uint256 amount 44 | ) public { 45 | address caller = token.getActor(callerSeed); 46 | address spender = token.getActor(spenderSeed); 47 | 48 | // Action: Caller approves amount for spender 49 | vm.prank(caller); 50 | bool r = token.approve(spender, amount); 51 | 52 | /* 53 | Check: 54 | - approve() returned true 55 | - spender's allowance == amount 56 | */ 57 | assertEq(r, true, "approve() returned false"); 58 | 59 | assertEq( 60 | token.allowance(caller, spender), 61 | amount, 62 | "Allowance not set correctly" 63 | ); 64 | } 65 | 66 | function testTransfer( 67 | uint256 fromSeed, 68 | uint256 toSeed, 69 | uint256 amount 70 | ) public { 71 | address sender = token.getActor(fromSeed); 72 | address recipient = token.getActor(toSeed); 73 | 74 | // Precondition: amount <= sender's balance 75 | amount = clampLte(amount, token.balanceOf(sender)); 76 | 77 | // Action: Transfer amount from sender to recipient 78 | uint256 senderBalanceBefore = token.balanceOf(sender); 79 | uint256 recipientBalanceBefore = token.balanceOf(recipient); 80 | 81 | vm.prank(sender); 82 | bool r = token.transfer(recipient, amount); 83 | 84 | /* 85 | Check: 86 | - transfer() returned true 87 | - If from == to, balance remained the same 88 | - If from != to: 89 | - sender's balance decreased by amount 90 | - recipient's balance increased by amount 91 | */ 92 | assertEq(r, true, "transfer() returned false"); 93 | 94 | if (sender == recipient) { 95 | assertEq( 96 | recipientBalanceBefore, 97 | token.balanceOf(recipient), 98 | "Incorrect balance after transfer() to self" 99 | ); 100 | } else { 101 | assertEq( 102 | senderBalanceBefore - amount, 103 | token.balanceOf(sender), 104 | "Incorrect sender balance after transfer()" 105 | ); 106 | 107 | assertEq( 108 | recipientBalanceBefore + amount, 109 | token.balanceOf(recipient), 110 | "Incorrect recipient balance after transfer()" 111 | ); 112 | } 113 | } 114 | 115 | function testTransferFrom( 116 | uint256 callerSeed, 117 | uint256 fromSeed, 118 | uint256 toSeed, 119 | bool giveApproval, 120 | uint256 amount 121 | ) public { 122 | address caller = token.getActor(callerSeed); 123 | address sender = token.getActor(fromSeed); 124 | address recipient = token.getActor(toSeed); 125 | 126 | /* 127 | Precondition: 128 | - amount <= sender's balance 129 | - amount <= caller's allowance 130 | */ 131 | amount = clampLte(amount, token.balanceOf(sender)); 132 | 133 | if (giveApproval) { 134 | vm.prank(sender); 135 | token.approve(caller, amount); 136 | } else { 137 | amount = clampLte(amount, token.allowance(sender, caller)); 138 | } 139 | 140 | // Action: Transfer amount from sender to recipient 141 | uint256 senderBalanceBefore = token.balanceOf(sender); 142 | uint256 recipientBalanceBefore = token.balanceOf(recipient); 143 | uint256 allowanceBefore = token.allowance(sender, caller); 144 | 145 | vm.prank(caller); 146 | bool r = token.transferFrom(sender, recipient, amount); 147 | 148 | /* 149 | Check: 150 | - transferFrom() returned true 151 | - If sender == recipient, balance remained the same 152 | - If sender != recipient: 153 | - sender's balance decreased by amount 154 | - recipient's balance increased by amount 155 | - If caller's allowance == uint256 max, allowance remained the same 156 | - If caller's allowance < uint256 max, allowance decreased by amount 157 | */ 158 | assertEq(r, true, "transfer() returned false"); 159 | 160 | if (sender == recipient) { 161 | assertEq( 162 | recipientBalanceBefore, 163 | token.balanceOf(recipient), 164 | "Incorrect balance after transferFrom() to self" 165 | ); 166 | } else { 167 | assertEq( 168 | senderBalanceBefore - amount, 169 | token.balanceOf(sender), 170 | "Incorrect sender balance after transferFrom()" 171 | ); 172 | 173 | assertEq( 174 | recipientBalanceBefore + amount, 175 | token.balanceOf(recipient), 176 | "Incorrect recipient balance after transferFrom()" 177 | ); 178 | } 179 | 180 | if (allowanceBefore == type(uint256).max) { 181 | assertEq( 182 | allowanceBefore, 183 | token.allowance(sender, caller), 184 | "Incorrect allowance after transferFrom() with infinite allowance" 185 | ); 186 | } else { 187 | assertEq( 188 | allowanceBefore - amount, 189 | token.allowance(sender, caller), 190 | "Incorrect allowance after transferFrom()" 191 | ); 192 | } 193 | } 194 | 195 | function testBurn(uint256 callerSeed, uint256 amount) public { 196 | address caller = token.getActor(callerSeed); 197 | 198 | // Precondition: amount <= caller's balance 199 | amount = clampLte(amount, token.balanceOf(caller)); 200 | 201 | // Action: Burn amount from caller's balance 202 | uint256 balanceBefore = token.balanceOf(caller); 203 | uint256 totalSupplyBefore = token.totalSupply(); 204 | 205 | vm.prank(caller); 206 | token.burn(amount); 207 | 208 | /* 209 | Check: 210 | - Caller's balance decreased by amount 211 | - totalSupply decreased by amount 212 | */ 213 | assertEq( 214 | balanceBefore - amount, 215 | token.balanceOf(caller), 216 | "Incorrect balance after burn()" 217 | ); 218 | 219 | assertEq( 220 | totalSupplyBefore - amount, 221 | token.totalSupply(), 222 | "Incorrect totalSupply after burn()" 223 | ); 224 | } 225 | 226 | function testApproveTwice( 227 | uint256 callerSeed, 228 | uint256 spenderSeed, 229 | uint256 amount 230 | ) public { 231 | address caller = token.getActor(callerSeed); 232 | address spender = token.getActor(spenderSeed); 233 | 234 | /* 235 | Action: 236 | 1. Caller approves amount for spender 237 | 2. Caller approves amount / 2 for spender 238 | */ 239 | vm.prank(caller); 240 | bool r = token.approve(spender, amount); 241 | 242 | uint256 allowanceAfterFirstApprove = token.allowance(caller, spender); 243 | 244 | vm.prank(caller); 245 | bool r2 = token.approve(spender, amount / 2); 246 | 247 | /* 248 | Check: 249 | - First approve() returned true 250 | - Second approve() returned true 251 | - First approve() set spender's allowance to amount 252 | - Second approve() set spender's allowance to amount / 2 253 | */ 254 | assertEq(r, true, "First approve() returned false"); 255 | assertEq(r2, true, "Second approve() returned false"); 256 | 257 | assertEq( 258 | allowanceAfterFirstApprove, 259 | amount, 260 | "Allowance not correct after first approve" 261 | ); 262 | 263 | assertEq( 264 | token.allowance(caller, spender), 265 | amount / 2, 266 | "Allowance not correct after second approve" 267 | ); 268 | } 269 | 270 | function testTransferMoreThanSenderBalance( 271 | uint256 fromSeed, 272 | uint256 toSeed, 273 | uint256 amount 274 | ) public { 275 | address sender = token.getActor(fromSeed); 276 | address recipient = token.getActor(toSeed); 277 | 278 | // Precondition: amount > sender's balance 279 | amount = clampGt(amount, token.balanceOf(sender)); 280 | 281 | // Action: Transfer amount from sender to recipient 282 | vm.prank(sender); 283 | try token.transfer(recipient, amount) { 284 | failWithMsg( 285 | "transfer() with more than sender's balance did not revert" 286 | ); 287 | } catch (bytes memory reason) { 288 | // Check: Reverted due to arithmetic underflow 289 | assertPanic( 290 | reason, 291 | 0x11, 292 | "transfer() with more than sender's balance reverted with wrong reason" 293 | ); 294 | } 295 | } 296 | 297 | function testTransferFromMoreThanSenderBalance( 298 | uint256 callerSeed, 299 | uint256 fromSeed, 300 | uint256 toSeed, 301 | uint256 amount 302 | ) public { 303 | address caller = token.getActor(callerSeed); 304 | address sender = token.getActor(fromSeed); 305 | address recipient = token.getActor(toSeed); 306 | 307 | // Precondition: amount > sender's balance 308 | amount = clampGt(amount, token.balanceOf(sender)); 309 | 310 | // Give caller the required allowance 311 | vm.prank(sender); 312 | token.approve(caller, amount); 313 | 314 | // Action: Transfer amount from sender to recipient 315 | vm.prank(caller); 316 | try token.transferFrom(sender, recipient, amount) { 317 | failWithMsg( 318 | "transferFrom() with more than sender's balance did not revert" 319 | ); 320 | } catch (bytes memory reason) { 321 | // Check: Reverted due to arithmetic underflow 322 | assertPanic( 323 | reason, 324 | 0x11, 325 | "transferFrom() more than sender's balance reverted with wrong reason" 326 | ); 327 | } 328 | } 329 | 330 | function testTransferFromMoreThanCallerAllowance( 331 | uint256 callerSeed, 332 | uint256 fromSeed, 333 | uint256 toSeed, 334 | uint256 amount 335 | ) public { 336 | address caller = token.getActor(callerSeed); 337 | address sender = token.getActor(fromSeed); 338 | address recipient = token.getActor(toSeed); 339 | 340 | // Avoid this test if caller's allowance is uint256 max 341 | if (token.allowance(sender, caller) == type(uint256).max) return; 342 | 343 | // Precondition: amount > caller's allowance 344 | amount = clampGt(amount, token.allowance(sender, caller)); 345 | 346 | // Action: Transfer amount from sender to recipient 347 | vm.prank(caller); 348 | try token.transferFrom(sender, recipient, amount) { 349 | failWithMsg( 350 | "transferFrom() with more than caller's allowance did not revert" 351 | ); 352 | } catch (bytes memory reason) { 353 | // Check: Reverted due to arithmetic underflow 354 | assertPanic( 355 | reason, 356 | 0x11, 357 | "transferFrom() more than caller's allowance reverted with wrong reason" 358 | ); 359 | } 360 | } 361 | 362 | function testBurnMoreThanCallerBalance( 363 | uint256 callerSeed, 364 | uint256 amount 365 | ) public { 366 | address caller = token.getActor(callerSeed); 367 | 368 | // Precondition: amount > caller's balance 369 | amount = clampGt(amount, token.balanceOf(caller)); 370 | 371 | // Action: Burn amount from caller's balance 372 | vm.prank(caller); 373 | try token.burn(amount) { 374 | failWithMsg( 375 | "burn() with more than caller's balance did not revert" 376 | ); 377 | } catch (bytes memory reason) { 378 | // Check: Reverted due to arithmetic underflow 379 | assertPanic( 380 | reason, 381 | 0x11, 382 | "burn() more than caller's allowance reverted with wrong reason" 383 | ); 384 | } 385 | } 386 | 387 | function testTotalSupplyRemainsUnchanged( 388 | uint256 callerSeed, 389 | uint256 fromSeed, 390 | uint256 toSeed, 391 | uint256 amount, 392 | bool giveApproval, 393 | uint256 funcSeed 394 | ) public { 395 | // Store totalSupply before 396 | uint256 totalSupplyBefore = token.totalSupply(); 397 | 398 | // Call one out of 3 functions 399 | funcSeed = funcSeed % 3; 400 | if (funcSeed == 0) testApprove(callerSeed, fromSeed, amount); 401 | else if (funcSeed == 1) testTransfer(fromSeed, toSeed, amount); 402 | else 403 | testTransferFrom( 404 | callerSeed, 405 | fromSeed, 406 | toSeed, 407 | giveApproval, 408 | amount 409 | ); 410 | 411 | // Check that totalSupply didn't change 412 | assertEq( 413 | token.totalSupply(), 414 | totalSupplyBefore, 415 | "totalSupply() changed" 416 | ); 417 | } 418 | 419 | function testOtherUsersBalancesRemainUnchanged( 420 | uint256 callerSeed, 421 | uint256 fromSeed, 422 | uint256 toSeed, 423 | uint256 amount, 424 | bool giveApproval, 425 | uint256 funcSeed 426 | ) public { 427 | // Store all user balances for future comparison 428 | uint256[] memory userBalances = new uint256[](token.actorCount()); 429 | for (uint256 i; i < token.actorCount(); i++) { 430 | userBalances[i] = token.balanceOf(token.getActor(i)); 431 | } 432 | 433 | // Choose one out of 4 functions 434 | funcSeed = funcSeed % 4; 435 | if (funcSeed == 0) { 436 | // Action: Call approve() 437 | testApprove(callerSeed, fromSeed, amount); 438 | 439 | // Check: All user balances remain unchanged 440 | for (uint256 i; i < token.actorCount(); i++) { 441 | assertEq( 442 | userBalances[i], 443 | token.balanceOf(token.getActor(i)), 444 | "Wrong user balance changed after approve()" 445 | ); 446 | } 447 | } else if (funcSeed == 1 || funcSeed == 2) { 448 | // Action: Call transfer() or transferFrom() 449 | if (funcSeed == 1) { 450 | testTransfer(fromSeed, toSeed, amount); 451 | } else { 452 | testTransferFrom( 453 | callerSeed, 454 | fromSeed, 455 | toSeed, 456 | giveApproval, 457 | amount 458 | ); 459 | } 460 | 461 | // Check: All user balances except from and to remain unchanged 462 | for (uint256 i; i < token.actorCount(); i++) { 463 | if ( 464 | token.getActor(i) == token.getActor(fromSeed) || 465 | token.getActor(i) == token.getActor(toSeed) 466 | ) continue; 467 | 468 | assertEq( 469 | userBalances[i], 470 | token.balanceOf(token.getActor(i)), 471 | "Wrong user balance changed after transfer()/transferFrom()" 472 | ); 473 | } 474 | } else { 475 | // Action: Call burn() 476 | testBurn(callerSeed, amount); 477 | 478 | // Check: All user balances except caller remain unchanged 479 | for (uint256 i; i < token.actorCount(); i++) { 480 | if (token.getActor(i) == token.getActor(callerSeed)) continue; 481 | 482 | assertEq( 483 | userBalances[i], 484 | token.balanceOf(token.getActor(i)), 485 | "Wrong user balance changed after burn()" 486 | ); 487 | } 488 | } 489 | } 490 | 491 | // PROPERTIES 492 | 493 | // name never changes 494 | function fuzz_nameNeverChanges() public returns (bool) { 495 | return keccak256(bytes(token.name())) == keccak256(bytes(startingName)); 496 | } 497 | 498 | // symbol never changes 499 | function fuzz_symbolNeverChanges() public returns (bool) { 500 | return 501 | keccak256(bytes(token.symbol())) == 502 | keccak256(bytes(startingSymbol)); 503 | } 504 | 505 | // decimal never changes 506 | function fuzz_decimalNeverChanges() public returns (bool) { 507 | return token.decimals() == startingDecimals; 508 | } 509 | 510 | // totalSupply never exceeds initial minted amount 511 | function fuzz_totalSupplyNeverExceedsInitialAmount() public returns (bool) { 512 | return token.totalSupply() <= INITIAL_TOTAL_SUPPLY; 513 | } 514 | 515 | // totalSupply never increases 516 | function fuzz_totalSupplyNeverIncreases() public returns (bool) { 517 | if (token.totalSupply() > previousTotalSupply) { 518 | return false; 519 | } 520 | 521 | // Store totalSupply for future use 522 | previousTotalSupply = token.totalSupply(); 523 | 524 | return true; 525 | } 526 | 527 | /* 528 | 1. No user balance exceeds totalSupply 529 | 2. Sum of all balances == totalSupply 530 | 531 | Note: We group these invariants together to avoid looping over users twice 532 | */ 533 | function fuzz_userProperties() public returns (bool) { 534 | uint256 sumOfBalances; 535 | for (uint256 i; i < token.actorCount(); i++) { 536 | uint256 userBalance = token.balanceOf(token.getActor(i)); 537 | 538 | // Invariant 1 539 | if (userBalance > token.totalSupply()) { 540 | return false; 541 | } 542 | 543 | sumOfBalances += userBalance; 544 | } 545 | 546 | // Invariant 2 547 | return sumOfBalances == token.totalSupply(); 548 | } 549 | } 550 | -------------------------------------------------------------------------------- /tests/helper/helper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.19; 3 | // From https://github.com/crytic/properties/blob/main/contracts/util/PropertiesHelper.sol 4 | 5 | abstract contract PropertiesAsserts { 6 | event LogUint256(string, uint256); 7 | event LogAddress(string, address); 8 | event LogString(string); 9 | 10 | event AssertFail(string); 11 | event AssertEqFail(string); 12 | event AssertNeqFail(string); 13 | event AssertGteFail(string); 14 | event AssertGtFail(string); 15 | event AssertLteFail(string); 16 | event AssertLtFail(string); 17 | 18 | function assertWithMsg(bool b, string memory reason) internal { 19 | if (!b) { 20 | emit AssertFail(reason); 21 | assert(false); 22 | } 23 | } 24 | 25 | /// @notice asserts that a is equal to b. Violations are logged using reason. 26 | function assertEq(uint256 a, uint256 b, string memory reason) internal { 27 | if (a != b) { 28 | string memory aStr = PropertiesLibString.toString(a); 29 | string memory bStr = PropertiesLibString.toString(b); 30 | bytes memory assertMsg = abi.encodePacked( 31 | "Invalid: ", 32 | aStr, 33 | "!=", 34 | bStr, 35 | ", reason: ", 36 | reason 37 | ); 38 | emit AssertEqFail(string(assertMsg)); 39 | assert(false); 40 | } 41 | } 42 | 43 | /// @notice int256 version of assertEq 44 | function assertEq(int256 a, int256 b, string memory reason) internal { 45 | if (a != b) { 46 | string memory aStr = PropertiesLibString.toString(a); 47 | string memory bStr = PropertiesLibString.toString(b); 48 | bytes memory assertMsg = abi.encodePacked( 49 | "Invalid: ", 50 | aStr, 51 | "!=", 52 | bStr, 53 | ", reason: ", 54 | reason 55 | ); 56 | emit AssertEqFail(string(assertMsg)); 57 | assert(false); 58 | } 59 | } 60 | 61 | /// @notice bool version of assertEq 62 | function assertEq(bool a, bool b, string memory reason) internal { 63 | if (a != b) { 64 | string memory aStr = PropertiesLibString.toString(a); 65 | string memory bStr = PropertiesLibString.toString(b); 66 | bytes memory assertMsg = abi.encodePacked( 67 | "Invalid: ", 68 | aStr, 69 | "!=", 70 | bStr, 71 | ", reason: ", 72 | reason 73 | ); 74 | emit AssertEqFail(string(assertMsg)); 75 | assert(false); 76 | } 77 | } 78 | 79 | /// @notice asserts that a is not equal to b. Violations are logged using reason. 80 | function assertNeq(uint256 a, uint256 b, string memory reason) internal { 81 | if (a == b) { 82 | string memory aStr = PropertiesLibString.toString(a); 83 | string memory bStr = PropertiesLibString.toString(b); 84 | bytes memory assertMsg = abi.encodePacked( 85 | "Invalid: ", 86 | aStr, 87 | "==", 88 | bStr, 89 | ", reason: ", 90 | reason 91 | ); 92 | emit AssertNeqFail(string(assertMsg)); 93 | assert(false); 94 | } 95 | } 96 | 97 | /// @notice int256 version of assertNeq 98 | function assertNeq(int256 a, int256 b, string memory reason) internal { 99 | if (a == b) { 100 | string memory aStr = PropertiesLibString.toString(a); 101 | string memory bStr = PropertiesLibString.toString(b); 102 | bytes memory assertMsg = abi.encodePacked( 103 | "Invalid: ", 104 | aStr, 105 | "==", 106 | bStr, 107 | ", reason: ", 108 | reason 109 | ); 110 | emit AssertNeqFail(string(assertMsg)); 111 | assert(false); 112 | } 113 | } 114 | 115 | /// @notice bool version of assertNEq 116 | function assertNeq(bool a, bool b, string memory reason) internal { 117 | if (a == b) { 118 | string memory aStr = PropertiesLibString.toString(a); 119 | string memory bStr = PropertiesLibString.toString(b); 120 | bytes memory assertMsg = abi.encodePacked( 121 | "Invalid: ", 122 | aStr, 123 | "==", 124 | bStr, 125 | ", reason: ", 126 | reason 127 | ); 128 | emit AssertEqFail(string(assertMsg)); 129 | assert(false); 130 | } 131 | } 132 | 133 | /// @notice asserts that a is greater than or equal to b. Violations are logged using reason. 134 | function assertGte(uint256 a, uint256 b, string memory reason) internal { 135 | if (!(a >= b)) { 136 | string memory aStr = PropertiesLibString.toString(a); 137 | string memory bStr = PropertiesLibString.toString(b); 138 | bytes memory assertMsg = abi.encodePacked( 139 | "Invalid: ", 140 | aStr, 141 | "<", 142 | bStr, 143 | " failed, reason: ", 144 | reason 145 | ); 146 | emit AssertGteFail(string(assertMsg)); 147 | assert(false); 148 | } 149 | } 150 | 151 | /// @notice int256 version of assertGte 152 | function assertGte(int256 a, int256 b, string memory reason) internal { 153 | if (!(a >= b)) { 154 | string memory aStr = PropertiesLibString.toString(a); 155 | string memory bStr = PropertiesLibString.toString(b); 156 | bytes memory assertMsg = abi.encodePacked( 157 | "Invalid: ", 158 | aStr, 159 | "<", 160 | bStr, 161 | " failed, reason: ", 162 | reason 163 | ); 164 | emit AssertGteFail(string(assertMsg)); 165 | assert(false); 166 | } 167 | } 168 | 169 | /// @notice asserts that a is greater than b. Violations are logged using reason. 170 | function assertGt(uint256 a, uint256 b, string memory reason) internal { 171 | if (!(a > b)) { 172 | string memory aStr = PropertiesLibString.toString(a); 173 | string memory bStr = PropertiesLibString.toString(b); 174 | bytes memory assertMsg = abi.encodePacked( 175 | "Invalid: ", 176 | aStr, 177 | "<=", 178 | bStr, 179 | " failed, reason: ", 180 | reason 181 | ); 182 | emit AssertGtFail(string(assertMsg)); 183 | assert(false); 184 | } 185 | } 186 | 187 | /// @notice int256 version of assertGt 188 | function assertGt(int256 a, int256 b, string memory reason) internal { 189 | if (!(a > b)) { 190 | string memory aStr = PropertiesLibString.toString(a); 191 | string memory bStr = PropertiesLibString.toString(b); 192 | bytes memory assertMsg = abi.encodePacked( 193 | "Invalid: ", 194 | aStr, 195 | "<=", 196 | bStr, 197 | " failed, reason: ", 198 | reason 199 | ); 200 | emit AssertGtFail(string(assertMsg)); 201 | assert(false); 202 | } 203 | } 204 | 205 | /// @notice asserts that a is less than or equal to b. Violations are logged using reason. 206 | function assertLte(uint256 a, uint256 b, string memory reason) internal { 207 | if (!(a <= b)) { 208 | string memory aStr = PropertiesLibString.toString(a); 209 | string memory bStr = PropertiesLibString.toString(b); 210 | bytes memory assertMsg = abi.encodePacked( 211 | "Invalid: ", 212 | aStr, 213 | ">", 214 | bStr, 215 | " failed, reason: ", 216 | reason 217 | ); 218 | emit AssertLteFail(string(assertMsg)); 219 | assert(false); 220 | } 221 | } 222 | 223 | /// @notice int256 version of assertLte 224 | function assertLte(int256 a, int256 b, string memory reason) internal { 225 | if (!(a <= b)) { 226 | string memory aStr = PropertiesLibString.toString(a); 227 | string memory bStr = PropertiesLibString.toString(b); 228 | bytes memory assertMsg = abi.encodePacked( 229 | "Invalid: ", 230 | aStr, 231 | ">", 232 | bStr, 233 | " failed, reason: ", 234 | reason 235 | ); 236 | emit AssertLteFail(string(assertMsg)); 237 | assert(false); 238 | } 239 | } 240 | 241 | /// @notice asserts that a is less than b. Violations are logged using reason. 242 | function assertLt(uint256 a, uint256 b, string memory reason) internal { 243 | if (!(a < b)) { 244 | string memory aStr = PropertiesLibString.toString(a); 245 | string memory bStr = PropertiesLibString.toString(b); 246 | bytes memory assertMsg = abi.encodePacked( 247 | "Invalid: ", 248 | aStr, 249 | ">=", 250 | bStr, 251 | " failed, reason: ", 252 | reason 253 | ); 254 | emit AssertLtFail(string(assertMsg)); 255 | assert(false); 256 | } 257 | } 258 | 259 | /// @notice int256 version of assertLt 260 | function assertLt(int256 a, int256 b, string memory reason) internal { 261 | if (!(a < b)) { 262 | string memory aStr = PropertiesLibString.toString(a); 263 | string memory bStr = PropertiesLibString.toString(b); 264 | bytes memory assertMsg = abi.encodePacked( 265 | "Invalid: ", 266 | aStr, 267 | ">=", 268 | bStr, 269 | " failed, reason: ", 270 | reason 271 | ); 272 | emit AssertLtFail(string(assertMsg)); 273 | assert(false); 274 | } 275 | } 276 | 277 | /// @notice Clamps value to be between low and high, both inclusive 278 | function clampBetween( 279 | uint256 value, 280 | uint256 low, 281 | uint256 high 282 | ) internal returns (uint256) { 283 | if (value < low || value > high) { 284 | uint ans = low + (value % (high - low + 1)); 285 | string memory valueStr = PropertiesLibString.toString(value); 286 | string memory ansStr = PropertiesLibString.toString(ans); 287 | bytes memory message = abi.encodePacked( 288 | "Clamping value ", 289 | valueStr, 290 | " to ", 291 | ansStr 292 | ); 293 | emit LogString(string(message)); 294 | return ans; 295 | } 296 | return value; 297 | } 298 | 299 | /// @notice int256 version of clampBetween 300 | function clampBetween( 301 | int256 value, 302 | int256 low, 303 | int256 high 304 | ) internal returns (int256) { 305 | if (value < low || value > high) { 306 | int range = high - low + 1; 307 | int clamped = (value - low) % (range); 308 | if (clamped < 0) clamped += range; 309 | int ans = low + clamped; 310 | string memory valueStr = PropertiesLibString.toString(value); 311 | string memory ansStr = PropertiesLibString.toString(ans); 312 | bytes memory message = abi.encodePacked( 313 | "Clamping value ", 314 | valueStr, 315 | " to ", 316 | ansStr 317 | ); 318 | emit LogString(string(message)); 319 | return ans; 320 | } 321 | return value; 322 | } 323 | 324 | /// @notice clamps a to be less than b 325 | function clampLt(uint256 a, uint256 b) internal returns (uint256) { 326 | if (!(a < b)) { 327 | assertNeq( 328 | b, 329 | 0, 330 | "clampLt cannot clamp value a to be less than zero. Check your inputs/assumptions." 331 | ); 332 | uint256 value = a % b; 333 | string memory aStr = PropertiesLibString.toString(a); 334 | string memory valueStr = PropertiesLibString.toString(value); 335 | bytes memory message = abi.encodePacked( 336 | "Clamping value ", 337 | aStr, 338 | " to ", 339 | valueStr 340 | ); 341 | emit LogString(string(message)); 342 | return value; 343 | } 344 | return a; 345 | } 346 | 347 | /// @notice int256 version of clampLt 348 | function clampLt(int256 a, int256 b) internal returns (int256) { 349 | if (!(a < b)) { 350 | int256 value = b - 1; 351 | string memory aStr = PropertiesLibString.toString(a); 352 | string memory valueStr = PropertiesLibString.toString(value); 353 | bytes memory message = abi.encodePacked( 354 | "Clamping value ", 355 | aStr, 356 | " to ", 357 | valueStr 358 | ); 359 | emit LogString(string(message)); 360 | return value; 361 | } 362 | return a; 363 | } 364 | 365 | /// @notice clamps a to be less than or equal to b 366 | function clampLte(uint256 a, uint256 b) internal returns (uint256) { 367 | if (!(a <= b)) { 368 | uint256 value = a % (b + 1); 369 | string memory aStr = PropertiesLibString.toString(a); 370 | string memory valueStr = PropertiesLibString.toString(value); 371 | bytes memory message = abi.encodePacked( 372 | "Clamping value ", 373 | aStr, 374 | " to ", 375 | valueStr 376 | ); 377 | emit LogString(string(message)); 378 | return value; 379 | } 380 | return a; 381 | } 382 | 383 | /// @notice int256 version of clampLte 384 | function clampLte(int256 a, int256 b) internal returns (int256) { 385 | if (!(a <= b)) { 386 | int256 value = b; 387 | string memory aStr = PropertiesLibString.toString(a); 388 | string memory valueStr = PropertiesLibString.toString(value); 389 | bytes memory message = abi.encodePacked( 390 | "Clamping value ", 391 | aStr, 392 | " to ", 393 | valueStr 394 | ); 395 | emit LogString(string(message)); 396 | return value; 397 | } 398 | return a; 399 | } 400 | 401 | /// @notice clamps a to be greater than b 402 | function clampGt(uint256 a, uint256 b) internal returns (uint256) { 403 | if (!(a > b)) { 404 | assertNeq( 405 | b, 406 | type(uint256).max, 407 | "clampGt cannot clamp value a to be larger than uint256.max. Check your inputs/assumptions." 408 | ); 409 | uint256 value = b + 1; 410 | string memory aStr = PropertiesLibString.toString(a); 411 | string memory valueStr = PropertiesLibString.toString(value); 412 | bytes memory message = abi.encodePacked( 413 | "Clamping value ", 414 | aStr, 415 | " to ", 416 | valueStr 417 | ); 418 | emit LogString(string(message)); 419 | return value; 420 | } else { 421 | return a; 422 | } 423 | } 424 | 425 | /// @notice int256 version of clampGt 426 | function clampGt(int256 a, int256 b) internal returns (int256) { 427 | if (!(a > b)) { 428 | int256 value = b + 1; 429 | string memory aStr = PropertiesLibString.toString(a); 430 | string memory valueStr = PropertiesLibString.toString(value); 431 | bytes memory message = abi.encodePacked( 432 | "Clamping value ", 433 | aStr, 434 | " to ", 435 | valueStr 436 | ); 437 | emit LogString(string(message)); 438 | return value; 439 | } else { 440 | return a; 441 | } 442 | } 443 | 444 | /// @notice clamps a to be greater than or equal to b 445 | function clampGte(uint256 a, uint256 b) internal returns (uint256) { 446 | if (!(a > b)) { 447 | uint256 value = b; 448 | string memory aStr = PropertiesLibString.toString(a); 449 | string memory valueStr = PropertiesLibString.toString(value); 450 | bytes memory message = abi.encodePacked( 451 | "Clamping value ", 452 | aStr, 453 | " to ", 454 | valueStr 455 | ); 456 | emit LogString(string(message)); 457 | return value; 458 | } 459 | return a; 460 | } 461 | 462 | /// @notice int256 version of clampGte 463 | function clampGte(int256 a, int256 b) internal returns (int256) { 464 | if (!(a > b)) { 465 | int256 value = b; 466 | string memory aStr = PropertiesLibString.toString(a); 467 | string memory valueStr = PropertiesLibString.toString(value); 468 | bytes memory message = abi.encodePacked( 469 | "Clamping value ", 470 | aStr, 471 | " to ", 472 | valueStr 473 | ); 474 | emit LogString(string(message)); 475 | return value; 476 | } 477 | return a; 478 | } 479 | } 480 | 481 | /// @notice Efficient library for creating string representations of integers. 482 | /// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/LibString.sol) 483 | /// @author Modified from Solady (https://github.com/Vectorized/solady/blob/main/src/utils/LibString.sol) 484 | /// @dev Name of the library is modified to prevent collisions with contract-under-test uses of LibString 485 | library PropertiesLibString { 486 | function toString(int256 value) internal pure returns (string memory str) { 487 | uint256 absValue = value >= 0 ? uint256(value) : uint256(-value); 488 | str = toString(absValue); 489 | 490 | if (value < 0) { 491 | str = string(abi.encodePacked("-", str)); 492 | } 493 | } 494 | 495 | function toString(uint256 value) internal pure returns (string memory str) { 496 | /// @solidity memory-safe-assembly 497 | assembly { 498 | // The maximum value of a uint256 contains 78 digits (1 byte per digit), but we allocate 160 bytes 499 | // to keep the free memory pointer word aligned. We'll need 1 word for the length, 1 word for the 500 | // trailing zeros padding, and 3 other words for a max of 78 digits. In total: 5 * 32 = 160 bytes. 501 | let newFreeMemoryPointer := add(mload(0x40), 160) 502 | 503 | // Update the free memory pointer to avoid overriding our string. 504 | mstore(0x40, newFreeMemoryPointer) 505 | 506 | // Assign str to the end of the zone of newly allocated memory. 507 | str := sub(newFreeMemoryPointer, 32) 508 | 509 | // Clean the last word of memory it may not be overwritten. 510 | mstore(str, 0) 511 | 512 | // Cache the end of the memory to calculate the length later. 513 | let end := str 514 | 515 | // We write the string from rightmost digit to leftmost digit. 516 | // The following is essentially a do-while loop that also handles the zero case. 517 | // prettier-ignore 518 | for { let temp := value } 1 {} { 519 | // Move the pointer 1 byte to the left. 520 | str := sub(str, 1) 521 | 522 | // Write the character to the pointer. 523 | // The ASCII index of the '0' character is 48. 524 | mstore8(str, add(48, mod(temp, 10))) 525 | 526 | // Keep dividing temp until zero. 527 | temp := div(temp, 10) 528 | 529 | // prettier-ignore 530 | if iszero(temp) { break } 531 | } 532 | 533 | // Compute and cache the final total length of the string. 534 | let length := sub(end, str) 535 | 536 | // Move the pointer 32 bytes leftwards to make room for the length. 537 | str := sub(str, 32) 538 | 539 | // Store the string's length at the start of memory allocated for our string. 540 | mstore(str, length) 541 | } 542 | } 543 | 544 | function toString(address value) internal pure returns (string memory str) { 545 | bytes memory s = new bytes(40); 546 | for (uint i = 0; i < 20; i++) { 547 | bytes1 b = bytes1( 548 | uint8(uint(uint160(value)) / (2 ** (8 * (19 - i)))) 549 | ); 550 | bytes1 hi = bytes1(uint8(b) / 16); 551 | bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); 552 | s[2 * i] = char(hi); 553 | s[2 * i + 1] = char(lo); 554 | } 555 | return string(s); 556 | } 557 | 558 | function toString(bool b) internal pure returns (string memory str) { 559 | if (b) { 560 | return "true"; 561 | } 562 | 563 | return "false"; 564 | } 565 | 566 | function char(bytes1 b) internal pure returns (bytes1 c) { 567 | if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); 568 | else return bytes1(uint8(b) + 0x57); 569 | } 570 | } -------------------------------------------------------------------------------- /tests/FixedPointMathLibTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.19; 3 | 4 | import {FixedPointMathLib} from "contracts/FixedPointMathLib.sol"; 5 | import {TestHelper} from "./helper/TestHelper.sol"; 6 | import {FixedPointMathLibWrapper} from "./wrapper/FixedPointMathLibWrapper.sol"; 7 | 8 | contract FixedPointMathLibTest is TestHelper { 9 | // FixedPointMathLib wrapper contract 10 | FixedPointMathLibWrapper fixedPointMathLibWrapper; 11 | 12 | // Constants 13 | uint256 constant SQRT_UINT256_MAX = 340282366920938463463374607431768211455; 14 | 15 | constructor() { 16 | fixedPointMathLibWrapper = new FixedPointMathLibWrapper(); 17 | } 18 | 19 | // FixedPointMathLib constants 20 | 21 | function maxUint256() public { 22 | assertEq( 23 | FixedPointMathLib.MAX_UINT256, 24 | type(uint256).max, 25 | "MAX_UINT256 is wrong" 26 | ); 27 | } 28 | 29 | function wad() public { 30 | assertEq(FixedPointMathLib.WAD, 1e18, "WAD is wrong"); 31 | } 32 | 33 | // ASSERTIONS 34 | 35 | function mulWadDown(uint256 x, uint256 y) public { 36 | // Precondition: x * y <= uint256 max 37 | if (x != 0) y = clampLte(y, type(uint256).max / x); 38 | 39 | // Action: Call mulWadDown() 40 | uint256 result = callWithoutRevert( 41 | fixedPointMathLibWrapper.mulWadDown, 42 | x, 43 | y, 44 | "mulWadDown() reverted" 45 | ); 46 | 47 | // Check: result == x * y / WAD 48 | assertEq( 49 | result, 50 | (x * y) / FixedPointMathLib.WAD, 51 | "Wrong result for mulWadDown()" 52 | ); 53 | } 54 | 55 | function mulWadDownOverflow(uint256 x, uint256 y) public { 56 | // Precondition: x * y > uint256 max 57 | x = clampGt(x, 1); 58 | y = clampGt(y, type(uint256).max / x); 59 | 60 | // Action: Call mulDivDown() 61 | bytes memory reason = expectRevert( 62 | fixedPointMathLibWrapper.mulWadDown, 63 | x, 64 | y, 65 | "mulWadDownOverflow() did not revert" 66 | ); 67 | 68 | // Check: reverted without error message 69 | assertEq( 70 | reason.length, 71 | 0, 72 | "mulWadDownOverflow() reverted with error message" 73 | ); 74 | } 75 | 76 | function mulWadUp(uint256 x, uint256 y) public { 77 | // Precondition: x * y <= uint256 max 78 | if (x != 0) y = clampLte(y, type(uint256).max / x); 79 | 80 | // Action: Call mulWadUp() 81 | uint256 result = callWithoutRevert( 82 | fixedPointMathLibWrapper.mulWadUp, 83 | x, 84 | y, 85 | "mulWadUp() reverted" 86 | ); 87 | 88 | /* 89 | Check: 90 | - If x * y % WAD > 0, result == x * y / WAD + 1 91 | - Otherwise, result == x * y / WAD 92 | */ 93 | if ((x * y) % FixedPointMathLib.WAD > 0) { 94 | assertEq( 95 | result, 96 | (x * y) / FixedPointMathLib.WAD + 1, 97 | "Wrong result for mulDivUp()" 98 | ); 99 | } else { 100 | assertEq( 101 | result, 102 | (x * y) / FixedPointMathLib.WAD, 103 | "Wrong result for mulDivUp()" 104 | ); 105 | } 106 | } 107 | 108 | function mulWadUpOverflow(uint256 x, uint256 y) public { 109 | // Precondition: x * y > uint256 max 110 | x = clampGt(x, 1); 111 | y = clampGt(y, type(uint256).max / x); 112 | 113 | // Action: Call mulWadUp() 114 | bytes memory reason = expectRevert( 115 | fixedPointMathLibWrapper.mulWadUp, 116 | x, 117 | y, 118 | "mulWadUpOverflow() did not revert" 119 | ); 120 | 121 | // Check: reverted without error message 122 | assertEq( 123 | reason.length, 124 | 0, 125 | "mulWadUpOverflow() reverted with error message" 126 | ); 127 | } 128 | 129 | function divWadDown(uint256 x, uint256 y) public { 130 | /* 131 | Precondition: 132 | - x * WAD <= uint256 max 133 | - y != 0 134 | */ 135 | x = clampLte(x, type(uint256).max / FixedPointMathLib.WAD); 136 | if (y == 0) return; 137 | 138 | // Action: Call divWadDown() 139 | uint256 result = callWithoutRevert( 140 | fixedPointMathLibWrapper.divWadDown, 141 | x, 142 | y, 143 | "divWadDown() reverted" 144 | ); 145 | 146 | // Check: result == x * WAD / y 147 | assertEq( 148 | result, 149 | (x * FixedPointMathLib.WAD) / y, 150 | "Wrong result for divWadDown()" 151 | ); 152 | } 153 | 154 | function divWadDownZero(uint256 x) public { 155 | // Precondition: x * WAD <= uint256 max 156 | x = clampLte(x, type(uint256).max / FixedPointMathLib.WAD); 157 | 158 | // Action: Call divWadDown(x, 0) 159 | bytes memory reason = expectRevert( 160 | fixedPointMathLibWrapper.divWadDown, 161 | x, 162 | 0, 163 | "divWadDownZero() did not revert" 164 | ); 165 | 166 | // Check: reverted without error message 167 | assertEq( 168 | reason.length, 169 | 0, 170 | "divWadDownZero() reverted with error message" 171 | ); 172 | } 173 | 174 | function divWadDownOverflow(uint256 x, uint256 y) public { 175 | /* 176 | Precondition: 177 | - x * WAD > uint256 max 178 | - y != 0 179 | */ 180 | x = clampGt(x, type(uint256).max / FixedPointMathLib.WAD); 181 | if (y == 0) return; 182 | 183 | // Action: Call divWadDown() 184 | bytes memory reason = expectRevert( 185 | fixedPointMathLibWrapper.divWadDown, 186 | x, 187 | y, 188 | "divWadDownOverflow() did not revert" 189 | ); 190 | 191 | // Check: reverted without error message 192 | assertEq( 193 | reason.length, 194 | 0, 195 | "divWadDownOverflow() reverted with error message" 196 | ); 197 | } 198 | 199 | function divWadUp(uint256 x, uint256 y) public { 200 | /* 201 | Precondition: 202 | - x * WAD <= uint256 max 203 | - y != 0 204 | */ 205 | x = clampLte(x, type(uint256).max / FixedPointMathLib.WAD); 206 | if (y == 0) return; 207 | 208 | // Action: Call divWadUp() 209 | uint256 result = callWithoutRevert( 210 | fixedPointMathLibWrapper.divWadUp, 211 | x, 212 | y, 213 | "divWadUp() reverted" 214 | ); 215 | 216 | /* 217 | Check: 218 | - If x * WAD % y > 0, result == x * WAD / y + 1 219 | - Otherwise, result == x * WAD / y 220 | */ 221 | if ((x * FixedPointMathLib.WAD) % y > 0) { 222 | assertEq( 223 | result, 224 | (x * FixedPointMathLib.WAD) / y + 1, 225 | "Wrong result for divWadUp()" 226 | ); 227 | } else { 228 | assertEq( 229 | result, 230 | (x * FixedPointMathLib.WAD) / y, 231 | "Wrong result for divWadUp()" 232 | ); 233 | } 234 | } 235 | 236 | function divWadUpZero(uint256 x) public { 237 | // Precondition: x * WAD <= uint256 max 238 | x = clampLte(x, type(uint256).max / FixedPointMathLib.WAD); 239 | 240 | // Action: Call divWadUp(x, 0) 241 | bytes memory reason = expectRevert( 242 | fixedPointMathLibWrapper.divWadUp, 243 | x, 244 | 0, 245 | "divWadUpZero() did not revert" 246 | ); 247 | 248 | // Check: reverted without error message 249 | assertEq( 250 | reason.length, 251 | 0, 252 | "divWadUpZero() reverted with error message" 253 | ); 254 | } 255 | 256 | function divWadUpOverflow(uint256 x, uint256 y) public { 257 | /* 258 | Precondition: 259 | - x * WAD > uint256 max 260 | - y != 0 261 | */ 262 | x = clampGt(x, type(uint256).max / FixedPointMathLib.WAD); 263 | if (y == 0) return; 264 | 265 | // Action: Call divWadUp() 266 | bytes memory reason = expectRevert( 267 | fixedPointMathLibWrapper.divWadUp, 268 | x, 269 | y, 270 | "divWadDownOverflow() did not revert" 271 | ); 272 | 273 | // Check: reverted without error message 274 | assertEq( 275 | reason.length, 276 | 0, 277 | "divWadDownOverflow() reverted with error message" 278 | ); 279 | } 280 | 281 | function mulDivDown(uint256 x, uint256 y, uint256 denominator) public { 282 | /* 283 | Precondition: 284 | - x * y <= uint256 max 285 | - denominator != 0 286 | */ 287 | if (x != 0) y = clampLte(y, type(uint256).max / x); 288 | if (denominator == 0) return; 289 | 290 | // Action: Call mulDivDown() 291 | uint256 result = callWithoutRevert( 292 | fixedPointMathLibWrapper.mulDivDown, 293 | x, 294 | y, 295 | denominator, 296 | "mulDivDown() reverted" 297 | ); 298 | 299 | // Check: result == x * y / denominator 300 | assertEq( 301 | result, 302 | (x * y) / denominator, 303 | "Wrong result for mulDivDown()" 304 | ); 305 | } 306 | 307 | function mulDivDownDenominatorZero(uint256 x, uint256 y) public { 308 | // Precondition: x * y <= uint256 max 309 | if (x != 0) y = clampLte(y, type(uint256).max / x); 310 | 311 | // Action: Call mulDivDown() with denominator == 0 312 | bytes memory reason = expectRevert( 313 | fixedPointMathLibWrapper.mulDivDown, 314 | x, 315 | y, 316 | 0, 317 | "mulDivDownDenominatorZero() did not revert" 318 | ); 319 | 320 | // Check: reverted without error message 321 | assertEq( 322 | reason.length, 323 | 0, 324 | "mulDivDownDenominatorZero() reverted with error message" 325 | ); 326 | } 327 | 328 | function mulDivDownOverflow( 329 | uint256 x, 330 | uint256 y, 331 | uint256 denominator 332 | ) public { 333 | /* 334 | Precondition: 335 | - x * y > uint256 max 336 | - denominator != 0 337 | */ 338 | x = clampGt(x, 1); 339 | y = clampGt(y, type(uint256).max / x); 340 | if (denominator == 0) return; 341 | 342 | // Action: Call mulDivDown() 343 | bytes memory reason = expectRevert( 344 | fixedPointMathLibWrapper.mulDivDown, 345 | x, 346 | y, 347 | denominator, 348 | "mulDivDownOverflow() did not revert" 349 | ); 350 | 351 | // Check: reverted without error message 352 | assertEq( 353 | reason.length, 354 | 0, 355 | "mulDivDownOverflow() reverted with error message" 356 | ); 357 | } 358 | 359 | function mulDivUp(uint256 x, uint256 y, uint256 denominator) public { 360 | /* 361 | Precondition: 362 | - x * y <= uint256 max 363 | - denominator != 0 364 | */ 365 | if (x != 0) y = clampLte(y, type(uint256).max / x); 366 | if (denominator == 0) return; 367 | 368 | // Action: Call mulDivUp() 369 | uint256 result = callWithoutRevert( 370 | fixedPointMathLibWrapper.mulDivUp, 371 | x, 372 | y, 373 | denominator, 374 | "mulDivUp() reverted" 375 | ); 376 | 377 | /* 378 | Check: 379 | - If x * y % denominator > 0, result == x * y / denominator + 1 380 | - Otherwise, result == x * y / denominator 381 | */ 382 | if ((x * y) % denominator > 0) { 383 | assertEq( 384 | result, 385 | (x * y) / denominator + 1, 386 | "Wrong result for mulDivUp()" 387 | ); 388 | } else { 389 | assertEq( 390 | result, 391 | (x * y) / denominator, 392 | "Wrong result for mulDivUp()" 393 | ); 394 | } 395 | } 396 | 397 | function mulDivUpDenominatorZero(uint256 x, uint256 y) public { 398 | // Precondition: x * y <= uint256 max 399 | if (x != 0) y = clampLte(y, type(uint256).max / x); 400 | 401 | // Action: Call mulDivDown() with denominator == 0 402 | bytes memory reason = expectRevert( 403 | fixedPointMathLibWrapper.mulDivUp, 404 | x, 405 | y, 406 | 0, 407 | "mulDivUpDenominatorZero() did not revert" 408 | ); 409 | 410 | // Check: reverted without error message 411 | assertEq( 412 | reason.length, 413 | 0, 414 | "mulDivUpDenominatorZero() reverted with error message" 415 | ); 416 | } 417 | 418 | function mulDivUpOverflow( 419 | uint256 x, 420 | uint256 y, 421 | uint256 denominator 422 | ) public { 423 | /* 424 | Precondition: 425 | - x * y > uint256 max 426 | - denominator != 0 427 | */ 428 | x = clampGt(x, 1); 429 | y = clampGt(y, type(uint256).max / x); 430 | if (denominator == 0) return; 431 | 432 | // Action: Call mulDivDown() 433 | bytes memory reason = expectRevert( 434 | fixedPointMathLibWrapper.mulDivUp, 435 | x, 436 | y, 437 | denominator, 438 | "mulDivUpOverflow() did not revert" 439 | ); 440 | 441 | // Check: reverted without error message 442 | assertEq( 443 | reason.length, 444 | 0, 445 | "mulDivUpOverflow() reverted with error message" 446 | ); 447 | } 448 | 449 | // Helper function for checking correctness of rpow() 450 | function simulateRpow( 451 | uint256 x, 452 | uint256 n, 453 | uint256 scalar 454 | ) public pure returns (uint256 z) { 455 | z = n % 2 == 0 ? scalar : x; 456 | 457 | uint256 half = scalar / 2; 458 | 459 | for (n /= 2; n > 0; n /= 2) { 460 | uint256 xxRound = x * x + half; 461 | x = scalar == 0 ? 0 : xxRound / scalar; 462 | 463 | if (n % 2 != 0) { 464 | uint256 zxRound = z * x + half; 465 | z = scalar == 0 ? 0 : zxRound / scalar; 466 | } 467 | } 468 | } 469 | 470 | function rpow(uint256 x, uint256 n, uint256 scalar) public { 471 | // Precondition: x > 0 472 | x = clampGt(n, 0); 473 | 474 | // Call helper function and check if it reverted 475 | (bool success, bytes memory data) = address(this).call( 476 | abi.encodeWithSelector(this.simulateRpow.selector, x, n, scalar) 477 | ); 478 | 479 | if (success) { 480 | // If simulateRpow() didn't overflow, get the result 481 | uint256 expectedResult = abi.decode(data, (uint256)); 482 | 483 | // Action: Call rpow() 484 | uint256 result = callWithoutRevert( 485 | fixedPointMathLibWrapper.rpow, 486 | x, 487 | n, 488 | scalar, 489 | "rpow() reverted unexpectedly" 490 | ); 491 | 492 | // Check: result == expectedResult 493 | assertEq(result, expectedResult, "Wrong result for rpow()"); 494 | } else { 495 | // If simulateRpow() overflows, call rpow() and check that it reverts 496 | bytes memory reason = expectRevert( 497 | fixedPointMathLibWrapper.rpow, 498 | x, 499 | n, 500 | scalar, 501 | "rpow() didn't revert" 502 | ); 503 | 504 | // Check: Call reverted with no error message 505 | assertEq(reason.length, 0, "rpow() reverted with error message"); 506 | } 507 | } 508 | 509 | function rpowXZero(uint256 n, uint256 scalar) public { 510 | // Action: Call rpow(0, n, scalar) 511 | uint256 result = callWithoutRevert( 512 | fixedPointMathLibWrapper.rpow, 513 | 0, 514 | n, 515 | scalar, 516 | "rpowXZero() reverted unexpectedly" 517 | ); 518 | 519 | /* 520 | Check: 521 | - If n == 0, result == scalar 522 | - Otherwise, result == 0 523 | */ 524 | if (n == 0) { 525 | assertEq(result, scalar, "Wrong result for rpow(0, 0, scalar)"); 526 | } else { 527 | assertEq(result, 0, "Wrong result for rpow(0, n, scalar)"); 528 | } 529 | } 530 | 531 | function sqrt(uint256 x) public { 532 | // Action: Call sqrt() 533 | uint256 result = callWithoutRevert( 534 | fixedPointMathLibWrapper.sqrt, 535 | x, 536 | "sqrt() reverted" 537 | ); 538 | 539 | /* 540 | Check: 541 | - If x == 0 or 1, result == x 542 | - Otherwise: 543 | - result < x 544 | - result <= sqrt(uint256 max) 545 | - result ** 2 <= x 546 | - x > (result - 1) ** 2 547 | - If result < sqrt(uint256 max), x < (result + 1) ** 2 548 | */ 549 | if (x == 0 || x == 1) { 550 | assertEq(result, x, "Wrong result for sqrt()"); 551 | return; 552 | } 553 | 554 | assertLt(result, x, "sqrt(): result is larger than x"); 555 | assertLte( 556 | result, 557 | SQRT_UINT256_MAX, 558 | "sqrt(): result greater than sqrt(uint256 max)" 559 | ); 560 | assertLte(result ** 2, x, "sqrt(): result ** 2 is larger than x"); 561 | 562 | assertGt( 563 | x, 564 | (result - 1) ** 2, 565 | "sqrt(): (result - 1) ** 2 is smaller than x" 566 | ); 567 | if (result < SQRT_UINT256_MAX) { 568 | assertLt( 569 | x, 570 | (result + 1) ** 2, 571 | "sqrt(): (result + 1) ** 2 is smaller than x" 572 | ); 573 | } 574 | } 575 | 576 | function sqrtPerfectSquare(uint256 x) public { 577 | // Precondition: x < sqrt(uint256 max) 578 | x = clampLte(x, SQRT_UINT256_MAX); 579 | 580 | // Action: Call sqrt(x ** 2) 581 | uint256 result = callWithoutRevert( 582 | fixedPointMathLibWrapper.sqrt, 583 | x ** 2, 584 | "sqrtPerfectSquare() reverted" 585 | ); 586 | 587 | // Check: result == x 588 | assertEq(result, x, "Wrong result for sqrtPerfectSquare()"); 589 | } 590 | 591 | function sqrtRpow(uint256 x) public { 592 | // Precondition: x < sqrt(uint256 max) 593 | x = clampLte(x, SQRT_UINT256_MAX); 594 | 595 | // Action: Call sqrt(rpow(x, 2, 1)) 596 | uint256 result = callWithoutRevert( 597 | fixedPointMathLibWrapper.rpow, 598 | x, 599 | 2, 600 | 1, 601 | "sqrtRpow(): rpow() reverted" 602 | ); 603 | 604 | result = callWithoutRevert( 605 | fixedPointMathLibWrapper.sqrt, 606 | result, 607 | "sqrtRpow(): sqrt() reverted" 608 | ); 609 | 610 | // Check: result == x 611 | assertEq(result, x, "Wrong result for sqrtRpow()"); 612 | } 613 | 614 | function unsafeMod(uint256 x, uint256 y) public { 615 | // Precondition: y != 0 616 | if (y == 0) return; 617 | 618 | // Action: Call unsafeMod() 619 | uint256 result = callWithoutRevert( 620 | fixedPointMathLibWrapper.unsafeMod, 621 | x, 622 | y, 623 | "unsafeMod() reverted" 624 | ); 625 | 626 | // Check: result == x % y 627 | assertEq(result, x % y, "Wrong result for unsafeMod()"); 628 | } 629 | 630 | function unsafeModDenominatorZero(uint256 x) public { 631 | // Action: Call unsafeMod(x, 0) 632 | uint256 result = callWithoutRevert( 633 | fixedPointMathLibWrapper.unsafeMod, 634 | x, 635 | 0, 636 | "unsafeMod() reverted" 637 | ); 638 | 639 | // Check: result == 0 640 | assertEq(result, 0, "Wrong result for unsafeMod()"); 641 | } 642 | 643 | function unsafeDiv(uint256 x, uint256 y) public { 644 | // Precondition: y != 0 645 | if (y == 0) return; 646 | 647 | // Action: Call unsafeDiv() 648 | uint256 result = callWithoutRevert( 649 | fixedPointMathLibWrapper.unsafeDiv, 650 | x, 651 | y, 652 | "unsafeDiv() reverted" 653 | ); 654 | 655 | // Check: result == x / y 656 | assertEq(result, x / y, "Wrong result for unsafeDiv()"); 657 | } 658 | 659 | function unsafeDivDenominatorZero(uint256 x) public { 660 | // Action: Call unsafeDiv(x, 0) 661 | uint256 result = callWithoutRevert( 662 | fixedPointMathLibWrapper.unsafeDiv, 663 | x, 664 | 0, 665 | "unsafeDiv() reverted" 666 | ); 667 | 668 | // Check: result == 0 669 | assertEq(result, 0, "Wrong result for unsafeDiv()"); 670 | } 671 | 672 | function unsafeDivUp(uint256 x, uint256 y) public { 673 | // Precondition: y != 0 674 | if (y == 0) return; 675 | 676 | // Action: Call unsafeDivUp() 677 | uint256 result = callWithoutRevert( 678 | fixedPointMathLibWrapper.unsafeDivUp, 679 | x, 680 | y, 681 | "unsafeDivUp() reverted" 682 | ); 683 | 684 | /* 685 | Check: 686 | - If x % y > 0, result == x / y + 1 687 | - Otherwise, result == x / y 688 | */ 689 | if (x % y > 0) { 690 | assertEq(result, x / y + 1, "Wrong result for unsafeDivUp()"); 691 | } else { 692 | assertEq(result, x / y, "Wrong result for unsafeDivUp()"); 693 | } 694 | } 695 | 696 | function unsafeDivUpDenominatorZero(uint256 x) public { 697 | // Action: Call unsafeDivUp(x, 0) 698 | uint256 result = callWithoutRevert( 699 | fixedPointMathLibWrapper.unsafeDivUp, 700 | x, 701 | 0, 702 | "unsafeDivUp() reverted" 703 | ); 704 | 705 | // Check: result == 0 706 | assertEq(result, 0, "Wrong result for unsafeDivUp()"); 707 | } 708 | 709 | function functionsNeverRevert( 710 | uint256 x, 711 | uint256 y, 712 | uint256 funcSeed 713 | ) public { 714 | // Cache all unsafe functions 715 | bytes4[] memory unsafeFunctionSelectors = new bytes4[](3); 716 | unsafeFunctionSelectors[0] = fixedPointMathLibWrapper 717 | .unsafeMod 718 | .selector; 719 | unsafeFunctionSelectors[1] = fixedPointMathLibWrapper 720 | .unsafeDiv 721 | .selector; 722 | unsafeFunctionSelectors[2] = fixedPointMathLibWrapper 723 | .unsafeDivUp 724 | .selector; 725 | 726 | // Choose a function to call 727 | funcSeed = funcSeed % 4; 728 | bytes memory data; 729 | if (funcSeed < 3) { 730 | data = abi.encodeWithSelector( 731 | unsafeFunctionSelectors[funcSeed], 732 | x, 733 | y 734 | ); 735 | } else { 736 | data = abi.encodeWithSelector( 737 | fixedPointMathLibWrapper.sqrt.selector, 738 | x 739 | ); 740 | } 741 | 742 | // Action: Call unsafe function 743 | (bool success, ) = address(fixedPointMathLibWrapper).call(data); 744 | 745 | // Check: Function call didn't revert 746 | assertEq(success, true, "Function reverted when it shouldn't"); 747 | } 748 | } 749 | -------------------------------------------------------------------------------- /tests/SignedWadMathTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.8.19; 3 | 4 | import {SignedWadMath} from "contracts/SignedWadMath.sol"; 5 | import {TestHelper} from "./helper/TestHelper.sol"; 6 | import {SignedWadMathWrapper} from "./wrapper/SignedWadMathWrapper.sol"; 7 | 8 | contract SignedWadMathTest is TestHelper { 9 | // SignedWadMath wrapper contract 10 | SignedWadMathWrapper signedWadMathWrapper; 11 | 12 | constructor() { 13 | signedWadMathWrapper = new SignedWadMathWrapper(); 14 | } 15 | 16 | // ASSERTIONS 17 | 18 | function toWadUnsafe(uint256 x) public { 19 | // Precondition: x * 1e18 <= int256 max 20 | x = clampBetween(x, 1, uint256(type(int256).max) / 1e18); 21 | 22 | // Action: Call toWadUnsafe() 23 | int256 result = SignedWadMath.toWadUnsafe(x); 24 | 25 | /* 26 | Check: 27 | - result > 0 28 | - result > x 29 | - result == x * 1e18 30 | */ 31 | assertGt(result, 0, "testToWadUnsafe() result overflowed"); 32 | assertGt(uint256(result), x, "testToWadUnsafe() result overflowed"); 33 | assertEq( 34 | x * 1e18, 35 | uint256(result), 36 | "Wrong result for testToWadUnsafe()" 37 | ); 38 | } 39 | 40 | function toWadUnsafeZero() public { 41 | // Action: Call toWadUnsafe(0) 42 | int256 result = SignedWadMath.toWadUnsafe(0); 43 | 44 | // Check: result == 0 45 | assertEq(result, 0, "Wrong result for testToWadUnsafeZero()"); 46 | } 47 | 48 | function toWadUnsafeOverflow(uint256 x) public { 49 | // Precondition: x * 1e18 > int256 max 50 | x = clampGt(x, uint256(type(int256).max) / 1e18); 51 | 52 | // Action: Call toWadUnsafe() 53 | int256 result = SignedWadMath.toWadUnsafe(x); 54 | 55 | /* 56 | Check: 57 | - result / 1e18 != x 58 | - result == x * 1e18, but unchecked 59 | */ 60 | 61 | // If result is negative, it can't be equal to x * 1e18 as x is positive 62 | if (result >= 0) { 63 | assertNeq( 64 | uint256(result) / 1e18, 65 | x, 66 | "testToWadUnsafeOverflow() didn't overflow" 67 | ); 68 | } 69 | unchecked { 70 | assertEq( 71 | int256(x * 1e18), 72 | result, 73 | "Wrong result for testToWadUnsafeOverflow()" 74 | ); 75 | } 76 | } 77 | 78 | function toDaysWadUnsafe(uint256 x) public { 79 | // Precondition: x * 1e18 <= uint256 max 80 | x = clampBetween(x, 1, type(uint256).max / 1e18); 81 | 82 | // Action: Call toDaysWadUnsafe() 83 | int256 result = SignedWadMath.toDaysWadUnsafe(x); 84 | 85 | /* 86 | Check: 87 | - result > 0 88 | - result > x 89 | - result == x * 1e18 / 86400 90 | */ 91 | assertGt(result, 0, "testToWadUnsafe() result overflowed"); 92 | assertGt(uint256(result), x, "testToWadUnsafe() result overflowed"); 93 | assertEq( 94 | (x * 1e18) / 86400, 95 | uint256(result), 96 | "Wrong result for testToWadUnsafe()" 97 | ); 98 | } 99 | 100 | function toDaysWadUnsafeZero() public { 101 | // Action: Call toDaysWadUnsafe(0) 102 | int256 result = SignedWadMath.toDaysWadUnsafe(0); 103 | 104 | // Check: result == 0 105 | assertEq(result, 0, "Wrong result for toDaysWadUnsafeZero()"); 106 | } 107 | 108 | function toDaysWadUnsafeOverflow(uint256 x) public { 109 | // Precondition: x * 1e18 > uint256 max 110 | x = clampGt(x, type(uint256).max / 1e18); 111 | 112 | // Action: Call toDaysWadUnsafe() 113 | int256 result = SignedWadMath.toDaysWadUnsafe(x); 114 | 115 | /* 116 | Check: 117 | - result * 86400 / 1e18 != x 118 | - result == x * 1e18 / 86400, but unchecked 119 | */ 120 | 121 | // If result is negative, it can't be equal to x * 1e18 as x is positive 122 | if (result >= 0) { 123 | assertNeq( 124 | (uint256(result) * 86400) / 1e18, 125 | x, 126 | "toDaysWadUnsafeOverflow() didn't overflow" 127 | ); 128 | } 129 | unchecked { 130 | assertEq( 131 | int256((x * 1e18) / 86400), 132 | result, 133 | "Wrong result for toDaysWadUnsafeOverflow()" 134 | ); 135 | } 136 | } 137 | 138 | function fromDaysWadUnsafe(int256 x) public { 139 | // Precondition: x * 86400 <= uint256 max 140 | x = clampBetween(x, 1, int256(type(uint256).max / 86400)); 141 | 142 | // Action: Call fromDaysWadUnsafe() 143 | uint256 result = SignedWadMath.fromDaysWadUnsafe(x); 144 | 145 | /* 146 | Check: 147 | - result < x 148 | - result == x * 86400 / 1e18 149 | */ 150 | assertLt(result, uint256(x), "fromDaysWadUnsafe() result overflowed"); 151 | assertEq( 152 | (uint256(x) * 86400) / 1e18, 153 | result, 154 | "Wrong result for fromDaysWadUnsafe()" 155 | ); 156 | } 157 | 158 | function fromDaysWadUnsafeZero() public { 159 | // Action: Call fromDaysWadUnsafe(0) 160 | uint256 result = SignedWadMath.fromDaysWadUnsafe(0); 161 | 162 | // Check: result == 0 163 | assertEq(result, 0, "Wrong result for fromDaysWadUnsafeZero()"); 164 | } 165 | 166 | function fromDaysWadUnsafeOverflow(int256 x) public { 167 | // Precondition: x * 86400 > uint256 max 168 | x = clampGt(x, int256(type(uint256).max / 86400)); 169 | 170 | // Action: Call fromDaysWadUnsafe() 171 | uint256 result = SignedWadMath.fromDaysWadUnsafe(x); 172 | 173 | /* 174 | Check: 175 | - result * 1e18 / 86400 != x 176 | - result == x * 86400 / 1e18, but unchecked 177 | */ 178 | assertNeq( 179 | result * 1e18 / 86400, 180 | uint256(x), 181 | "fromDaysWadUnsafeOverflow() didn't overflow" 182 | ); 183 | unchecked { 184 | assertEq( 185 | uint256(x) * 86400 / 1e18, 186 | result, 187 | "Wrong result for fromDaysWadUnsafeOverflow()" 188 | ); 189 | } 190 | } 191 | 192 | function fromDaysWadUnsafeNegative(int256 x) public { 193 | // Precondition: x * 86400 > uint256 max 194 | x = clampLt(x, 0); 195 | 196 | // Action: Call fromDaysWadUnsafe() 197 | uint256 result = SignedWadMath.fromDaysWadUnsafe(x); 198 | 199 | /* 200 | Check: 201 | - result == x * 86400 / 1e18, but unchecked 202 | */ 203 | unchecked { 204 | assertEq( 205 | (uint256(x) * 86400) / 1e18, 206 | result, 207 | "Wrong result for fromDaysWadUnsafeNegative()" 208 | ); 209 | } 210 | } 211 | 212 | function unsafeWadMulBothPositive(int256 x, int256 y) public { 213 | /* 214 | Precondition: 215 | - x > 0 and y > 0 216 | - x * y <= int256 max 217 | */ 218 | x = clampGt(x, 0); 219 | y = clampBetween(y, 1, type(int256).max / x); 220 | 221 | // Action: Call unsafeWadMul() 222 | int256 result = SignedWadMath.unsafeWadMul(x, y); 223 | 224 | /* 225 | Check: 226 | - result >= 0 227 | - result == x * y / 1e18 228 | */ 229 | assertGte( 230 | result, 231 | 0, 232 | "Result not positive for unsafeWadMulBothPositive()" 233 | ); 234 | assertEq( 235 | result, 236 | (x * y) / 1e18, 237 | "Wrong result for unsafeWadMulBothPositive()" 238 | ); 239 | } 240 | 241 | function unsafeWadMulBothNegative(int256 x, int256 y) public { 242 | /* 243 | Precondition: 244 | - x < 0 and y < 0 245 | - x * y <= int256 max 246 | */ 247 | x = clampLt(x, 0); 248 | y = clampBetween(y, type(int256).max / x, -1); 249 | 250 | // Action: Call unsafeWadMul() 251 | int256 result = SignedWadMath.unsafeWadMul(x, y); 252 | 253 | /* 254 | Check: 255 | - result >= 0 256 | - result == x * y / 1e18 257 | */ 258 | assertGte( 259 | result, 260 | 0, 261 | "Result not positive for unsafeWadMulBothNegative()" 262 | ); 263 | assertEq( 264 | result, 265 | (x * y) / 1e18, 266 | "Wrong result for unsafeWadMulBothNegative()" 267 | ); 268 | } 269 | 270 | function unsafeWadMulSingleNegative(int256 x, int256 y) public { 271 | /* 272 | Precondition: 273 | - x < 0 and y > 0 274 | - x * y >= int256 min 275 | */ 276 | x = clampLt(x, 0); 277 | y = clampBetween(y, 1, type(int256).min / x); 278 | 279 | // Action: Call unsafeWadMul(x, y) and unsafeWadMul(y, x) 280 | int256 result = SignedWadMath.unsafeWadMul(x, y); 281 | int256 result2 = SignedWadMath.unsafeWadMul(y, x); 282 | 283 | /* 284 | Check: 285 | - For result and result2: 286 | - r <= 0 287 | - r == x * y / 1e18 288 | */ 289 | assertLte( 290 | result, 291 | 0, 292 | "Result not <= 0 for unsafeWadMulSingleNegative()" 293 | ); 294 | assertEq( 295 | result, 296 | (x * y) / 1e18, 297 | "Wrong result for unsafeWadMulBothNegative()" 298 | ); 299 | 300 | assertLte( 301 | result2, 302 | 0, 303 | "Result not <= 0 for unsafeWadMulSingleNegative()" 304 | ); 305 | assertEq( 306 | result2, 307 | (x * y) / 1e18, 308 | "Wrong result for unsafeWadMulBothNegative()" 309 | ); 310 | } 311 | 312 | function unsafeWadMulZero(int256 x) public { 313 | // Action: Call unsafeWadMul(x, 0) and unsafeWadMul(0, x) 314 | int256 result = SignedWadMath.unsafeWadMul(x, 0); 315 | int256 result2 = SignedWadMath.unsafeWadMul(0, x); 316 | 317 | // Check: result == 0 and result2 == 0 318 | assertEq(result, 0, "Wrong result for unsafeWadMul(x, 0)"); 319 | assertEq(result2, 0, "Wrong result for unsafeWadMul(0, x)"); 320 | } 321 | 322 | function unsafeWadMulBothPositiveOverflow(int256 x, int256 y) public { 323 | /* 324 | Precondition: 325 | - x > 0 and y > 0 326 | - x * y > int256 max 327 | */ 328 | x = clampGt(x, 0); 329 | y = clampGt(y, type(int256).max / x); 330 | 331 | // Action: Call unsafeWadMul() 332 | int256 result = SignedWadMath.unsafeWadMul(x, y); 333 | 334 | // Check: result == x * y / 1e18, when unchecked 335 | unchecked { 336 | assertEq( 337 | result, 338 | (x * y) / 1e18, 339 | "Wrong result for unsafeWadMulBothPositiveOverflow()" 340 | ); 341 | } 342 | } 343 | 344 | function unsafeWadMulBothNegativeOverflow(int256 x, int256 y) public { 345 | /* 346 | Precondition: 347 | - x < 0 and y < 0 348 | - x * y > int256 max 349 | */ 350 | x = clampLt(x, 0); 351 | y = clampLt(y, type(int256).max / x); 352 | 353 | // Action: Call unsafeWadMul() 354 | int256 result = SignedWadMath.unsafeWadMul(x, y); 355 | 356 | // Check: result == x * y / 1e18, when unchecked 357 | unchecked { 358 | assertEq( 359 | result, 360 | (x * y) / 1e18, 361 | "Wrong result for unsafeWadMulBothNegativeOverflow()" 362 | ); 363 | } 364 | } 365 | 366 | function unsafeWadMulSingleNegativeOverflow(int256 x, int256 y) public { 367 | /* 368 | Precondition: 369 | - x < 0 and y > 0 370 | - x * y < int256 min 371 | */ 372 | x = clampLt(x, 0); 373 | y = clampGt(y, type(int256).min / x); 374 | 375 | // Action: Call unsafeWadMul(x, y) and unsafeWadMul(y, x) 376 | int256 result = SignedWadMath.unsafeWadMul(x, y); 377 | int256 result2 = SignedWadMath.unsafeWadMul(y, x); 378 | 379 | /* 380 | Check: 381 | - For result and result2: 382 | - result == x * y / 1e18, when unchecked 383 | */ 384 | unchecked { 385 | assertEq( 386 | result, 387 | (x * y) / 1e18, 388 | "Wrong result for unsafeWadMulSingleNegative()" 389 | ); 390 | assertEq( 391 | result2, 392 | (x * y) / 1e18, 393 | "Wrong result for unsafeWadMulSingleNegative()" 394 | ); 395 | } 396 | } 397 | 398 | function unsafeWadDivBothPositive(int256 x, int256 y) public { 399 | /* 400 | Precondition: 401 | - x > 0 and y > 0 402 | - x * 1e18 <= int256 max 403 | */ 404 | x = clampBetween(x, 1, type(int256).max / 1e18); 405 | y = clampGt(y, 0); 406 | 407 | // Action: Call unsafeWadDiv() 408 | int256 result = SignedWadMath.unsafeWadDiv(x, y); 409 | 410 | /* 411 | Check: 412 | - result >= 0 413 | - result == x * 1e18 / y 414 | */ 415 | assertGte( 416 | result, 417 | 0, 418 | "Result not positive for unsafeWadDivBothPositive()" 419 | ); 420 | assertEq( 421 | result, 422 | (x * 1e18) / y, 423 | "Wrong result for unsafeWadDivBothPositive()" 424 | ); 425 | } 426 | 427 | function unsafeWadDivBothNegative(int256 x, int256 y) public { 428 | /* 429 | Precondition: 430 | - x < 0 and y < 0 431 | - x * 1e18 >= int256 min 432 | */ 433 | x = clampBetween(x, type(int256).min / 1e18, -1); 434 | y = clampLt(y, 0); 435 | 436 | // Action: Call unsafeWadDiv() 437 | int256 result = SignedWadMath.unsafeWadDiv(x, y); 438 | 439 | /* 440 | Check: 441 | - result >= 0 442 | - result == x * 1e18 / y 443 | */ 444 | assertGte( 445 | result, 446 | 0, 447 | "Result not positive for unsafeWadDivBothNegative()" 448 | ); 449 | assertEq( 450 | result, 451 | (x * 1e18) / y, 452 | "Wrong result for unsafeWadDivBothNegative()" 453 | ); 454 | } 455 | 456 | function unsafeWadDivSingleNegative(int256 x, int256 y) public { 457 | /* 458 | Precondition: 459 | - x < 0 and y > 0 OR x > 0 and y < 0 460 | - int256 min <= x * 1e18 <= int256 max 461 | */ 462 | if (x < 0) { 463 | x = clampBetween(x, type(int256).min / 1e18, -1); 464 | y = clampGt(y, 0); 465 | } else { 466 | x = clampBetween(x, 1, type(int256).max / 1e18); 467 | y = clampLt(y, 0); 468 | } 469 | 470 | // Action: Call unsafeWadDiv() 471 | int256 result = SignedWadMath.unsafeWadDiv(x, y); 472 | 473 | /* 474 | Check: 475 | - result <= 0 476 | - result == x * 1e18 / y 477 | */ 478 | assertLte( 479 | result, 480 | 0, 481 | "Result not <= 0 for unsafeWadDivSingleNegative()" 482 | ); 483 | assertEq( 484 | result, 485 | (x * 1e18) / y, 486 | "Wrong result for unsafeWadDivSingleNegative()" 487 | ); 488 | } 489 | 490 | function unsafeWadDivNumeratorZero(int256 y) public { 491 | // Precondition: y != 0 492 | if (y == 0) return; 493 | 494 | // Action: Call unsafeWadDiv(0, y) 495 | int256 result = SignedWadMath.unsafeWadDiv(0, y); 496 | 497 | // Check: result == 0 498 | assertEq(result, 0, "Wrong result for unsafeWadDivNumeratorZero()"); 499 | } 500 | 501 | function unsafeWadDivDenominatorZero(int256 x) public { 502 | // Action: Call unsafeWadDiv(x, 0) 503 | int256 result = callWithoutRevert( 504 | signedWadMathWrapper.unsafeWadDiv, 505 | x, 506 | 0, 507 | "unsafeWadDivDenominatorZero() reverted" 508 | ); 509 | 510 | // Check: result == 0 511 | assertEq(result, 0, "Wrong result for unsafeWadDivDenominatorZero()"); 512 | } 513 | 514 | function unsafeWadDivPositiveOverflow(int256 x, int256 y) public { 515 | /* 516 | Precondition: 517 | - x * 1e18 > int256 max 518 | - y != 0 519 | */ 520 | x = clampGt(x, type(int256).max / 1e18); 521 | if (y == 0) return; 522 | 523 | // Action: Call unsafeWadDiv() 524 | int256 result = SignedWadMath.unsafeWadDiv(x, y); 525 | 526 | // Check: result == x * 1e18 / y, when unchecked 527 | unchecked { 528 | assertEq( 529 | result, 530 | (x * 1e18) / y, 531 | "Wrong result for unsafeWadDivBothPositiveOverflow()" 532 | ); 533 | } 534 | } 535 | 536 | function unsafeWadDivNegativeOverflow(int256 x, int256 y) public { 537 | /* 538 | Precondition: 539 | - x * 1e18 < int256 min 540 | - y != 0 541 | */ 542 | x = clampLt(x, type(int256).min / 1e18); 543 | if (y == 0) return; 544 | 545 | // Action: Call unsafeWadDiv() 546 | int256 result = SignedWadMath.unsafeWadDiv(x, y); 547 | 548 | // Check: result == x * 1e18 / y, when unchecked 549 | unchecked { 550 | assertEq( 551 | result, 552 | (x * 1e18) / y, 553 | "Wrong result for unsafeWadDivBothPositiveOverflow()" 554 | ); 555 | } 556 | } 557 | 558 | function wadMulBothPositive(int256 x, int256 y) public { 559 | /* 560 | Precondition: 561 | - x > 0 and y > 0 562 | - x * y <= int256 max 563 | */ 564 | x = clampGt(x, 0); 565 | y = clampBetween(y, 1, type(int256).max / x); 566 | 567 | // Action: Call wadMul() 568 | int256 result = callWithoutRevert( 569 | signedWadMathWrapper.wadMul, 570 | x, 571 | y, 572 | "wadMulBothPositive() reverted" 573 | ); 574 | 575 | /* 576 | Check: 577 | - result >= 0 578 | - result == x * y / 1e18 579 | */ 580 | assertGte(result, 0, "Result not positive for wadMulBothPositive()"); 581 | assertEq( 582 | result, 583 | (x * y) / 1e18, 584 | "Wrong result for wadMulBothPositive()" 585 | ); 586 | } 587 | 588 | function wadMulBothNegative(int256 x, int256 y) public { 589 | /* 590 | Precondition: 591 | - x < 0 and y < 0 592 | - x * y <= int256 max 593 | */ 594 | x = clampLt(x, 0); 595 | y = clampBetween(y, type(int256).max / x, -1); 596 | 597 | // Action: Call wadMul() 598 | int256 result = callWithoutRevert( 599 | signedWadMathWrapper.wadMul, 600 | x, 601 | y, 602 | "wadMulBothNegative() reverted" 603 | ); 604 | 605 | /* 606 | Check: 607 | - result >= 0 608 | - result == x * y / 1e18 609 | */ 610 | assertGte(result, 0, "Result not positive for wadMulBothNegative()"); 611 | assertEq( 612 | result, 613 | (x * y) / 1e18, 614 | "Wrong result for wadMulBothNegative()" 615 | ); 616 | } 617 | 618 | function wadMulSingleNegative(int256 x, int256 y) public { 619 | /* 620 | Precondition: 621 | - x < 0 and y > 0 622 | - x * y >= int256 min 623 | */ 624 | x = clampLt(x, 0); 625 | y = clampBetween(y, 1, type(int256).min / x); 626 | 627 | // Action: Call wadMul(x, y) 628 | int256 result = callWithoutRevert( 629 | signedWadMathWrapper.wadMul, 630 | x, 631 | y, 632 | "wadMulSingleNegative() reverted" 633 | ); 634 | 635 | /* 636 | Check: 637 | - r <= 0 638 | - r == x * y / 1e18 639 | */ 640 | assertLte(result, 0, "Result not <= 0 for wadMulSingleNegative()"); 641 | assertEq( 642 | result, 643 | (x * y) / 1e18, 644 | "Wrong result for wadMulSingleNegative()" 645 | ); 646 | 647 | // Action: Call wadMul(y, x) 648 | result = callWithoutRevert( 649 | signedWadMathWrapper.wadMul, 650 | y, 651 | x, 652 | "wadMulSingleNegative() reverted" 653 | ); 654 | 655 | /* 656 | Check: 657 | - r <= 0 658 | - r == x * y / 1e18 659 | */ 660 | assertLte(result, 0, "Result not <= 0 for wadMulSingleNegative()"); 661 | assertEq( 662 | result, 663 | (x * y) / 1e18, 664 | "Wrong result for wadMulSingleNegative()" 665 | ); 666 | } 667 | 668 | function wadMulZero(int256 x) public { 669 | // Action: Call wadMul(x, 0) 670 | int256 result = callWithoutRevert( 671 | signedWadMathWrapper.wadMul, 672 | x, 673 | 0, 674 | "wadMulZero() reverted" 675 | ); 676 | 677 | // Check: result == 0 678 | assertEq(result, 0, "Wrong result for wadMulZero(x, 0)"); 679 | 680 | // Action: Call wadMul(0, x) 681 | result = callWithoutRevert( 682 | signedWadMathWrapper.wadMul, 683 | 0, 684 | x, 685 | "wadMulZero() reverted" 686 | ); 687 | 688 | // Check: result == 0 689 | assertEq(result, 0, "Wrong result for wadMulZero(0, x)"); 690 | } 691 | 692 | function wadMulBothPositiveOverflow(int256 x, int256 y) public { 693 | /* 694 | Precondition: 695 | - x > 0 and y > 0 696 | - x * y > int256 max 697 | */ 698 | x = clampGt(x, 0); 699 | y = clampGt(y, type(int256).max / x); 700 | 701 | // Action: Call wadMul() 702 | bytes memory reason = expectRevert( 703 | signedWadMathWrapper.wadMul, 704 | x, 705 | y, 706 | "wadMulBothPositiveOverflow() did not revert" 707 | ); 708 | 709 | // Check: Reverted with empty reason 710 | assertEq( 711 | reason.length, 712 | 0, 713 | "wadMulBothPositiveOverflow() reverted with message" 714 | ); 715 | } 716 | 717 | function wadMulBothNegativeOverflow(int256 x, int256 y) public { 718 | /* 719 | Precondition: 720 | - x < 0 and y < 0 721 | - x * y > int256 max 722 | */ 723 | x = clampLt(x, 0); 724 | y = clampLt(y, type(int256).max / x); 725 | if (x == -1 && y == type(int256).min) return; // Exclude edgecase that passes 726 | 727 | // Action: Call wadMul() 728 | bytes memory reason = expectRevert( 729 | signedWadMathWrapper.wadMul, 730 | x, 731 | y, 732 | "wadMulBothNegativeOverflow() did not revert" 733 | ); 734 | 735 | // Check: Reverted with empty reason 736 | assertEq( 737 | reason.length, 738 | 0, 739 | "wadMulBothNegativeOverflow() reverted with message" 740 | ); 741 | } 742 | 743 | function wadMulSingleNegativeOverflow(int256 x, int256 y) public { 744 | /* 745 | Precondition: 746 | - x < 0 and y > 0 747 | - x * y < int256 min 748 | */ 749 | x = clampLt(x, 0); 750 | y = clampGt(y, type(int256).min / x); 751 | 752 | // Action: Call wadMul(x, y) 753 | bytes memory reason = expectRevert( 754 | signedWadMathWrapper.wadMul, 755 | x, 756 | y, 757 | "wadMulSingleNegativeOverflow() did not revert" 758 | ); 759 | 760 | // Check: Reverted with empty reason 761 | assertEq( 762 | reason.length, 763 | 0, 764 | "wadMulSingleNegativeOverflow() reverted with message" 765 | ); 766 | 767 | // Action: Call wadMul(y, x) 768 | reason = expectRevert( 769 | signedWadMathWrapper.wadMul, 770 | y, 771 | x, 772 | "wadMulSingleNegativeOverflow() did not revert" 773 | ); 774 | 775 | // Check: Reverted with empty reason 776 | assertEq( 777 | reason.length, 778 | 0, 779 | "wadMulSingleNegativeOverflow() reverted with message" 780 | ); 781 | } 782 | 783 | function wadMulEdgeCase() public { 784 | // Precondition: x == -1, y == int256 min 785 | int256 x = -1; 786 | int256 y = type(int256).min; 787 | 788 | // This doesn't revert although x * y > int256 max 789 | int256 result = callWithoutRevert( 790 | signedWadMathWrapper.wadMul, 791 | x, 792 | y, 793 | "wadMulEdgeCase() reverted" 794 | ); 795 | 796 | // Check: result == x * y / 1e18, when unchecked 797 | // Evalutes to -57896044618658097711785492504343953926634992332820282019728 798 | unchecked { 799 | assertEq( 800 | result, 801 | (x * y) / 1e18, 802 | "Wrong result for wadMulEdgeCase()" 803 | ); 804 | } 805 | } 806 | 807 | function wadDivBothPositive(int256 x, int256 y) public { 808 | /* 809 | Precondition: 810 | - x > 0 and y > 0 811 | - x * 1e18 <= int256 max 812 | */ 813 | x = clampBetween(x, 1, type(int256).max / 1e18); 814 | y = clampGt(y, 0); 815 | 816 | // Action: Call wadDiv() 817 | int256 result = callWithoutRevert( 818 | signedWadMathWrapper.wadDiv, 819 | x, 820 | y, 821 | "wadDivBothPositive() reverted" 822 | ); 823 | 824 | /* 825 | Check: 826 | - result >= 0 827 | - result == x * 1e18 / y 828 | */ 829 | assertGte(result, 0, "Result not positive for wadDivBothPositive()"); 830 | assertEq( 831 | result, 832 | (x * 1e18) / y, 833 | "Wrong result for wadDivBothPositive()" 834 | ); 835 | } 836 | 837 | function wadDivBothNegative(int256 x, int256 y) public { 838 | /* 839 | Precondition: 840 | - x < 0 and y < 0 841 | - x * 1e18 >= int256 min 842 | */ 843 | x = clampBetween(x, type(int256).min / 1e18, -1); 844 | y = clampLt(y, 0); 845 | 846 | // Action: Call wadDiv() 847 | int256 result = callWithoutRevert( 848 | signedWadMathWrapper.wadDiv, 849 | x, 850 | y, 851 | "wadDivBothNegative() reverted" 852 | ); 853 | 854 | /* 855 | Check: 856 | - result >= 0 857 | - result == x * 1e18 / y 858 | */ 859 | assertGte(result, 0, "Result not positive for wadDivBothNegative()"); 860 | assertEq( 861 | result, 862 | (x * 1e18) / y, 863 | "Wrong result for wadDivBothNegative()" 864 | ); 865 | } 866 | 867 | function wadDivSingleNegative(int256 x, int256 y) public { 868 | /* 869 | Precondition: 870 | - x < 0 and y > 0 OR x > 0 and y < 0 871 | - int256 min <= x * 1e18 <= int256 max 872 | */ 873 | if (x < 0) { 874 | x = clampBetween(x, type(int256).min / 1e18, -1); 875 | y = clampGt(y, 0); 876 | } else { 877 | x = clampBetween(x, 1, type(int256).max / 1e18); 878 | y = clampLt(y, 0); 879 | } 880 | 881 | // Action: Call wadDiv() 882 | int256 result = callWithoutRevert( 883 | signedWadMathWrapper.wadDiv, 884 | x, 885 | y, 886 | "wadDivSingleNegative() reverted" 887 | ); 888 | 889 | /* 890 | Check: 891 | - result <= 0 892 | - result == x * 1e18 / y 893 | */ 894 | assertLte(result, 0, "Result not <= 0 for wadDivSingleNegative()"); 895 | assertEq( 896 | result, 897 | (x * 1e18) / y, 898 | "Wrong result for wadDivSingleNegative()" 899 | ); 900 | } 901 | 902 | function wadDivNumeratorZero(int256 y) public { 903 | // Precondition: y != 0 904 | if (y == 0) return; 905 | 906 | // Action: Call wadDiv(0, y) 907 | int256 result = callWithoutRevert( 908 | signedWadMathWrapper.wadDiv, 909 | 0, 910 | y, 911 | "wadDivNumeratorZero() reverted" 912 | ); 913 | 914 | // Check: result == 0 915 | assertEq(result, 0, "Wrong result for wadDivNumeratorZero(0, y)"); 916 | } 917 | 918 | function wadDivDenominatorZero(int256 x) public { 919 | // Action: Call wadDiv(x, 0) 920 | bytes memory reason = expectRevert( 921 | signedWadMathWrapper.wadDiv, 922 | x, 923 | 0, 924 | "wadDivDenominatorZero() did not revert" 925 | ); 926 | 927 | // Check: Reverted with empty reason 928 | assertEq( 929 | reason.length, 930 | 0, 931 | "wadDivDenominatorZero() reverted with message" 932 | ); 933 | } 934 | 935 | function wadDivPositiveOverflow(int256 x, int256 y) public { 936 | /* 937 | Precondition: 938 | - x * 1e18 > int256 max 939 | - y != 0 940 | */ 941 | x = clampGt(x, type(int256).max / 1e18); 942 | if (y == 0) return; 943 | 944 | // Action: Call wadDiv() 945 | bytes memory reason = expectRevert( 946 | signedWadMathWrapper.wadDiv, 947 | x, 948 | y, 949 | "wadDivPositiveOverflow() did not revert" 950 | ); 951 | 952 | // Check: Reverted with empty reason 953 | assertEq( 954 | reason.length, 955 | 0, 956 | "wadDivPositiveOverflow() reverted with message" 957 | ); 958 | } 959 | 960 | function wadDivNegativeOverflow(int256 x, int256 y) public { 961 | /* 962 | Precondition: 963 | - x * 1e18 < int256 min 964 | - y != 0 965 | */ 966 | x = clampLt(x, type(int256).min / 1e18); 967 | if (y == 0) return; 968 | 969 | // Action: Call wadDiv() 970 | bytes memory reason = expectRevert( 971 | signedWadMathWrapper.wadDiv, 972 | x, 973 | y, 974 | "wadDivNegativeOverflow() did not revert" 975 | ); 976 | 977 | // Check: Reverted with empty reason 978 | assertEq( 979 | reason.length, 980 | 0, 981 | "wadDivNegativeOverflow() reverted with message" 982 | ); 983 | } 984 | 985 | function wadExpLowerBound(int256 x) public { 986 | // Precondition: x <= -42139678854452767551 987 | x = clampLte(x, -42139678854452767551); 988 | 989 | // Action: Call wadExp() 990 | int256 result = callWithoutRevert( 991 | signedWadMathWrapper.wadExp, 992 | x, 993 | "wadExpLowerBound() reverted" 994 | ); 995 | 996 | // Check: result == 0 997 | assertEq(result, 0, "Wrong result for wadExpLowerBound()"); 998 | } 999 | 1000 | function wadExpUpperBound(int256 x) public { 1001 | // Precondition: x >= 135305999368893231589 1002 | x = clampGte(x, 135305999368893231589); 1003 | 1004 | // Action: Call wadExp() 1005 | expectRevert( 1006 | signedWadMathWrapper.wadExp, 1007 | x, 1008 | "wadExpUpperBound() did not revert" 1009 | ); 1010 | } 1011 | 1012 | function wadExpZero() public { 1013 | // Action: Call wadExp(0) 1014 | int256 result = callWithoutRevert( 1015 | signedWadMathWrapper.wadExp, 1016 | 0, 1017 | "wadExpZero() reverted" 1018 | ); 1019 | 1020 | // Check: result == 1e18 1021 | assertEq(result, 1e18, "Wrong result for wadExpZero()"); 1022 | } 1023 | 1024 | function wadExpResultPositive(int256 x) public { 1025 | /* 1026 | Precondition: 1027 | - -42139678854452767551 < x < 135305999368893231589 1028 | - x != 0 1029 | */ 1030 | x = clampBetween( 1031 | x, 1032 | -42139678854452767551 + 1, 1033 | 135305999368893231589 - 1 1034 | ); 1035 | if (x == 0) return; 1036 | 1037 | // Action: Call wadExp() 1038 | int256 result = callWithoutRevert( 1039 | signedWadMathWrapper.wadExp, 1040 | x, 1041 | "wadExpResultPositive() reverted" 1042 | ); 1043 | 1044 | // Check: result >= 0 1045 | assertGte(result, 0, "Result not positive for wadExpResultPositive()"); 1046 | } 1047 | 1048 | function wadLnLowerBound(int256 x) public { 1049 | // Precondition: x <= 0 1050 | x = clampLte(x, 0); 1051 | 1052 | // Action: Call wadLn() 1053 | expectRevert( 1054 | signedWadMathWrapper.wadLn, 1055 | x, 1056 | "wadLnLowerBound() did not revert" 1057 | ); 1058 | } 1059 | 1060 | function wadLnOne() public { 1061 | // Action: Call wadLn(1e18) 1062 | int256 result = callWithoutRevert( 1063 | signedWadMathWrapper.wadLn, 1064 | 1e18, 1065 | "wadLnOne() reverted" 1066 | ); 1067 | 1068 | // Check: result == 0 1069 | assertEq(result, 0, "Wrong result for wadLnOne()"); 1070 | } 1071 | 1072 | function wadLnResultPositive(int256 x) public { 1073 | // Precondition: x > 1e18 1074 | x = clampGt(x, 1e18); 1075 | 1076 | // Action: Call wadLn() 1077 | int256 result = callWithoutRevert( 1078 | signedWadMathWrapper.wadLn, 1079 | x, 1080 | "wadLnResultPositive() reverted" 1081 | ); 1082 | 1083 | // Check: result > 0 1084 | assertGt(result, 0, "Result not positive for wadLnResultPositive()"); 1085 | } 1086 | 1087 | function wadLnExp(int256 x) public { 1088 | // Precondition: 1e18 <= x < 135305999368893231589 1089 | x = clampBetween(x, 1e18, 135305999368893231589 - 1); 1090 | 1091 | // Action: Call wadLn(wadExp(x)) 1092 | int256 result = callWithoutRevert( 1093 | signedWadMathWrapper.wadExp, 1094 | x, 1095 | "wadLnExp() reverted" 1096 | ); 1097 | if (result <= 0) return; 1098 | 1099 | result = callWithoutRevert( 1100 | signedWadMathWrapper.wadLn, 1101 | result, 1102 | "wadLnExp() reverted" 1103 | ); 1104 | 1105 | // Check: result is within x - 1 to x + 1 1106 | assertLte(result, x + 1, "Wrong result for wadLnExp()"); 1107 | assertGte(result, x - 1, "Wrong result for wadLnExp()"); 1108 | } 1109 | 1110 | function wadExpLn(int256 x) public { 1111 | // Precondition: x >= 1e18 1112 | x = clampGte(x, 1e18); 1113 | 1114 | // Action: Call wadExp(wadLn(x)) 1115 | int256 result = callWithoutRevert( 1116 | signedWadMathWrapper.wadLn, 1117 | x, 1118 | "wadExpLn() reverted" 1119 | ); 1120 | if (result <= -42139678854452767551 || result >= 135305999368893231589) 1121 | return; 1122 | 1123 | result = callWithoutRevert( 1124 | signedWadMathWrapper.wadExp, 1125 | result, 1126 | "wadExpLn() reverted" 1127 | ); 1128 | 1129 | // Check: result is within 1/1e17 of x 1130 | assertErrorWithin(x, result, 10, "Wrong result for wadExpLn()"); 1131 | } 1132 | 1133 | function wadPow(int256 x, int256 y) public { 1134 | /* 1135 | Precondition: 1136 | - x >= 1e18 1137 | - If y is positive, wadLn(x) * y / 1e18 < 135305999368893231589 1138 | - If y is negative, wadLn(x) * y / 1e18 > -42139678854452767551 1139 | */ 1140 | x = clampGte(x, 1e18); 1141 | if (y > 0) { 1142 | y = clampLt( 1143 | y, 1144 | (135305999368893231589 * 1e18) / SignedWadMath.wadLn(x) 1145 | ); 1146 | assert(y > 0); 1147 | } else if (y < 0) { 1148 | y = clampGt( 1149 | y, 1150 | (-42139678854452767551 * 1e18) / SignedWadMath.wadLn(x) 1151 | ); 1152 | assert(y < 0); 1153 | } 1154 | 1155 | // Action: Call wadPow 1156 | int256 result = callWithoutRevert( 1157 | signedWadMathWrapper.wadPow, 1158 | x, 1159 | y, 1160 | "wadPow() reverted" 1161 | ); 1162 | 1163 | // Check: result == wadExp(wadLn(x) * y / 1e18) 1164 | assertEq( 1165 | result, 1166 | SignedWadMath.wadExp((SignedWadMath.wadLn(x) * y) / 1e18), 1167 | "Wrong result for wadPow()" 1168 | ); 1169 | } 1170 | 1171 | function unsafeDivBothPositive(int256 x, int256 y) public { 1172 | // Precondition: x > 0 and y > 0 1173 | x = clampGt(x, 0); 1174 | y = clampGt(y, 0); 1175 | 1176 | // Action: Call unsafeDiv() 1177 | int256 result = SignedWadMath.unsafeDiv(x, y); 1178 | 1179 | /* 1180 | Check: 1181 | - result >= 0 1182 | - result == x / y 1183 | */ 1184 | assertGte(result, 0, "Result not positive for unsafeDivBothPositive()"); 1185 | assertEq(result, x / y, "Wrong result for unsafeDivBothPositive()"); 1186 | } 1187 | 1188 | function unsafeDivBothNegative(int256 x, int256 y) public { 1189 | // Precondition: x < 0 and y < 0 1190 | x = clampLt(x, 0); 1191 | y = clampLt(y, 0); 1192 | 1193 | // Action: Call unsafeDiv() 1194 | int256 result = SignedWadMath.unsafeDiv(x, y); 1195 | 1196 | /* 1197 | Check: 1198 | - result >= 0 1199 | - result == x / y 1200 | */ 1201 | assertGte(result, 0, "Result not positive for unsafeDivBothNegative()"); 1202 | assertEq(result, x / y, "Wrong result for unsafeDivBothNegative()"); 1203 | } 1204 | 1205 | function unsafeDivSingleNegative(int256 x, int256 y) public { 1206 | /* 1207 | Precondition: 1208 | - x < 0 and y > 0 OR x > 0 and y < 0 1209 | */ 1210 | if (x < 0) { 1211 | y = clampGt(y, 0); 1212 | } else { 1213 | y = clampLt(y, 0); 1214 | } 1215 | 1216 | // Action: Call unsafeDiv() 1217 | int256 result = SignedWadMath.unsafeDiv(x, y); 1218 | 1219 | /* 1220 | Check: 1221 | - result <= 0 1222 | - result == x / y 1223 | */ 1224 | assertLte(result, 0, "Result not <= 0 for unsafeDivSingleNegative()"); 1225 | assertEq(result, x / y, "Wrong result for unsafeDivSingleNegative()"); 1226 | } 1227 | 1228 | function unsafeDivNumeratorZero(int256 y) public { 1229 | // Precondition: y != 0 1230 | if (y == 0) return; 1231 | 1232 | // Action: Call unsafeDiv(0, y) 1233 | int256 result = SignedWadMath.unsafeDiv(0, y); 1234 | 1235 | // Check: result == 0 1236 | assertEq(result, 0, "Wrong result for unsafeDivNumeratorZero()"); 1237 | } 1238 | 1239 | function unsafeDivDenominatorZero(int256 x) public { 1240 | // Action: Call unsafeDiv(x, 0) 1241 | int256 result = callWithoutRevert( 1242 | signedWadMathWrapper.unsafeDiv, 1243 | x, 1244 | 0, 1245 | "unsafeDivDenominatorZero() reverted" 1246 | ); 1247 | 1248 | // Check: result == 0 1249 | assertEq(result, 0, "Wrong result for unsafeDivNumeratorZero()"); 1250 | } 1251 | 1252 | function unsafeFunctionsNeverRevert( 1253 | int256 x, 1254 | int256 y, 1255 | uint256 funcSeed 1256 | ) public { 1257 | // Cache all unsafe functions 1258 | bytes4[] memory singleParamSelectors = new bytes4[](3); 1259 | singleParamSelectors[0] = signedWadMathWrapper.toWadUnsafe.selector; 1260 | singleParamSelectors[1] = signedWadMathWrapper.toDaysWadUnsafe.selector; 1261 | singleParamSelectors[2] = signedWadMathWrapper 1262 | .fromDaysWadUnsafe 1263 | .selector; 1264 | 1265 | bytes4[] memory twoParamSelectors = new bytes4[](3); 1266 | twoParamSelectors[0] = signedWadMathWrapper.unsafeWadMul.selector; 1267 | twoParamSelectors[1] = signedWadMathWrapper.unsafeWadDiv.selector; 1268 | twoParamSelectors[2] = signedWadMathWrapper.unsafeDiv.selector; 1269 | 1270 | // Choose a function to call 1271 | funcSeed = funcSeed % 6; 1272 | bytes memory data; 1273 | if (funcSeed < 3) { 1274 | data = abi.encodeWithSelector(singleParamSelectors[funcSeed], x); 1275 | } else { 1276 | data = abi.encodeWithSelector( 1277 | twoParamSelectors[funcSeed - 3], 1278 | x, 1279 | y 1280 | ); 1281 | } 1282 | 1283 | // Action: Call unsafe function 1284 | (bool success, ) = address(signedWadMathWrapper).call(data); 1285 | 1286 | // Check: Function call didn't revert 1287 | assertEq(success, true, "unsafe function reverted"); 1288 | } 1289 | } 1290 | --------------------------------------------------------------------------------