diff --git a/test/forge/BaseTest.sol b/test/forge/BaseTest.sol index efca0a363..fdbce4bf0 100644 --- a/test/forge/BaseTest.sol +++ b/test/forge/BaseTest.sol @@ -8,8 +8,9 @@ import "src/Blue.sol"; import {ERC20Mock as ERC20} from "src/mocks/ERC20Mock.sol"; import {OracleMock as Oracle} from "src/mocks/OracleMock.sol"; import {IrmMock as Irm} from "src/mocks/IrmMock.sol"; +import {MathUtils} from "./helpers/MathUtils.sol"; -contract BaseTest is Test { +contract BaseTest is Test, MathUtils { using FixedPointMathLib for uint256; using MarketLib for Market; @@ -148,12 +149,4 @@ contract BaseTest is Test { function neq(Market memory a, Market memory b) internal pure returns (bool) { return (Id.unwrap(a.id()) != Id.unwrap(b.id())); } - - function max(uint256 a, uint256 b) internal pure returns (uint256) { - return a > b ? a : b; - } - - function min(uint256 a, uint256 b) internal pure returns (uint256) { - return a < b ? a : b; - } } diff --git a/test/forge/helpers/MathUtils.sol b/test/forge/helpers/MathUtils.sol new file mode 100644 index 000000000..8d5e655dc --- /dev/null +++ b/test/forge/helpers/MathUtils.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +abstract contract MathUtils { + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? a : b; + } + + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } +} diff --git a/test/forge/unit/TestFixedPointMathLib.t.sol b/test/forge/unit/TestFixedPointMathLib.t.sol new file mode 100644 index 000000000..ee418e5fa --- /dev/null +++ b/test/forge/unit/TestFixedPointMathLib.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import {WAD, FixedPointMathLib} from "src/libraries/FixedPointMathLib.sol"; + +contract UnitFixedPointMathLib is Test { + using FixedPointMathLib for uint256; + + function testWTaylorCompounded(uint256 rate, uint256 timeElapsed) public { + // Assume rate is less than a ~500% APY. (~180% APR) + vm.assume(rate < (WAD / 20_000_000) && timeElapsed < 365 days); + uint256 result = rate.wTaylorCompounded(timeElapsed) + WAD; + uint256 toCompare = wPow(WAD + rate, timeElapsed); + assertLe(result, toCompare, "rate should be less than the compounded rate"); + assertGe(result, WAD + timeElapsed * rate, "rate should be greater than the simple interest rate"); + assertLe((toCompare - result) * 100_00 / toCompare, 8_00, "The error should be less than or equal to 8%"); + } + + function wPow(uint256 x, uint256 n) private pure returns (uint256 z) { + z = WAD; + for (; n != 0; n /= 2) { + z = n % 2 != 0 ? z.wMulUp(x) : z; + x = x.wMulUp(x); + } + } +} diff --git a/test/forge/unit/TestMarketLib.t.sol b/test/forge/unit/TestMarketLib.t.sol new file mode 100644 index 000000000..5a7dc4762 --- /dev/null +++ b/test/forge/unit/TestMarketLib.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import {Market, Id} from "src/interfaces/IBlue.sol"; +import {MarketLib} from "src/libraries/MarketLib.sol"; + +contract UnitMarketLibTest is Test { + using MarketLib for Market; + + function testMarketIdWithDifferentBorrowableAsset(Market memory market, address newBorrowableAsset) public { + vm.assume(market.borrowableAsset != newBorrowableAsset); + Id oldId = market.id(); + market.borrowableAsset = newBorrowableAsset; + Id newId = market.id(); + assertNotEq(Id.unwrap(oldId), Id.unwrap(newId)); + } + + function testMarketIdWithDifferentCollateralAsset(Market memory market, address newCollateralAsset) public { + vm.assume(market.collateralAsset != newCollateralAsset); + Id oldId = market.id(); + market.collateralAsset = newCollateralAsset; + Id newId = market.id(); + assertNotEq(Id.unwrap(oldId), Id.unwrap(newId)); + } + + function testMarketIdWithDifferentOracle(Market memory market, address newOracle) public { + vm.assume(market.oracle != newOracle); + Id oldId = market.id(); + market.oracle = newOracle; + Id newId = market.id(); + assertNotEq(Id.unwrap(oldId), Id.unwrap(newId)); + } + + function testMarketIdWithDifferentIrm(Market memory market, address newIrm) public { + vm.assume(market.irm != newIrm); + Id oldId = market.id(); + market.irm = newIrm; + Id newId = market.id(); + assertNotEq(Id.unwrap(oldId), Id.unwrap(newId)); + } + + function testMarketIdWithDifferentLltv(Market memory market, uint256 newLltv) public { + vm.assume(market.lltv != newLltv); + Id oldId = market.id(); + market.lltv = newLltv; + Id newId = market.id(); + assertNotEq(Id.unwrap(oldId), Id.unwrap(newId)); + } +} diff --git a/test/forge/unit/TestUtilsLib.t.sol b/test/forge/unit/TestUtilsLib.t.sol new file mode 100644 index 000000000..5a9dd0bcd --- /dev/null +++ b/test/forge/unit/TestUtilsLib.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import {UtilsLib} from "src/libraries/UtilsLib.sol"; + +contract UnitUtilsLibTest is Test { + function testZeroFloorSub(uint256 x, uint256 y) public { + assertEq(UtilsLib.zeroFloorSub(x, y), x > y ? x - y : 0); + } + + function testExactlyOneZero(uint256 x, uint256 y) public { + assertEq(UtilsLib.exactlyOneZero(x, y), (x != 0 && y == 0) || (x == 0 && y != 0)); + } + + function testExactlyOneZeroBothZero() public { + assertFalse(UtilsLib.exactlyOneZero(0, 0)); + } + + function testExactlyOneZeroBothNonZero(uint256 x, uint256 y) public { + x = bound(x, 1, type(uint256).max); + y = bound(y, 1, type(uint256).max); + assertFalse(UtilsLib.exactlyOneZero(x, y)); + } + + function testExactlyOneZeroFirstIsZero(uint256 y) public { + y = bound(y, 1, type(uint256).max); + assertTrue(UtilsLib.exactlyOneZero(0, y)); + } + + function testExactlyOneZeroSecondIsZero(uint256 x) public { + x = bound(x, 1, type(uint256).max); + assertTrue(UtilsLib.exactlyOneZero(x, 0)); + } +}