diff --git a/foundry.toml b/foundry.toml index d81f4dc7..c82ec44e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -18,6 +18,7 @@ fs_permissions = [ { access = "read", path = "./test/mechanisms/huff-vrgda/mocks/LinearVRGDAWrappers.huff" }, { access = "read", path = "./test/mechanisms/huff-vrgda/mocks/LogisticVRGDAWrappers.huff" }, + { access = "read", path = "./test/mechanisms/huff-vrgda/mocks/SignedWadMathWrappers.huff" }, { access = "read", path = "./test/proxies/mocks/ClonesWrappers.huff" }, { access = "read", path = "./test/proxies/mocks/ProxyWrappers.huff" }, diff --git a/src/mechanisms/huff-vrgda/LogisticVRGDA.huff b/src/mechanisms/huff-vrgda/LogisticVRGDA.huff index abf5c807..5d4ca763 100644 --- a/src/mechanisms/huff-vrgda/LogisticVRGDA.huff +++ b/src/mechanisms/huff-vrgda/LogisticVRGDA.huff @@ -26,7 +26,7 @@ [LOGISTIC_LIMIT] // [logisticLimit, sold, 1e18, timeScale] add // [sold + logisticLimit, 1e18, timeScale] [LOGISTIC_LIMIT_DOUBLED] // [logisticLimitDoubled, (sold+logisticLimit), 1e18, timeScale] - UNSAFE_WAD_DIV() // [(logisticLimitDoubled / (sold + logisticLimit)), 1e18, timeScale] + UNSAFE_DIV() // [(logisticLimitDoubled / (sold + logisticLimit)), 1e18, timeScale] sub // [((logisticLimitDoubled / (sold + logisticLimit)) - 1e18), timeScale] LN_WAD(fail) // [wadLn((logisticLimitDoubled / (sold + logisticLimit)) - 1e18), timeScale] diff --git a/src/mechanisms/huff-vrgda/SignedWadMath.huff b/src/mechanisms/huff-vrgda/SignedWadMath.huff index 685f54e1..53e8f162 100644 --- a/src/mechanisms/huff-vrgda/SignedWadMath.huff +++ b/src/mechanisms/huff-vrgda/SignedWadMath.huff @@ -13,18 +13,18 @@ [WAD] mul } -#define macro toDaysWadUnsafe() = takes (1) returns (1) { - [WAD] mul [DAY] div +#define macro TO_DAYS_WAD_UNSAFE() = takes (1) returns (1) { + [WAD] mul [DAY] swap1 div } #define macro FROM_DAYS_WAD_UNSAFE() = takes (1) returns (1) { - [DAY] mul [WAD] div + [DAY] mul [WAD] swap1 div } #define macro UNSAFE_WAD_MUL() = takes (2) returns (1) { - mul [WAD] sdiv + mul [WAD] swap1 sdiv } @@ -129,7 +129,7 @@ dup2 // [x, 0xb17217f7d1cf79abc9e3b398, x] 0x60 shl // [x << 96, 0xb17217f7d1cf79abc9e3b398, x] sdiv // [x << 96 / 0xb17217f7d1cf79abc9e3b398, x] - 0x7ffffff20f9306d2eea00000 // [2**95, x << 96 / 0xb17217f7d1cf79abc9e3b398, x] + 0x800000000000000000000000 // [2**95, x << 96 / 0xb17217f7d1cf79abc9e3b398, x] add // [2**95 + x << 96 / 0xb17217f7d1cf79abc9e3b398, x] 0x60 sar // [(2**95 + x << 96 / 0xb17217f7d1cf79abc9e3b398) >> 96, x] @@ -314,7 +314,7 @@ add // [q, p, k, x] dup4 mul // [q * x, p, k, x] - 0x60 shr // [(q * x) >> 0x60, p, k, x] + 0x60 sar // [(q * x) >> 0x60, p, k, x] 0x0a0f742023def783a307a986912e add // [q, p, k, x] diff --git a/test/mechanisms/huff-vrgda/LinearVRGDA.t.sol b/test/mechanisms/huff-vrgda/LinearVRGDA.t.sol index eca307f9..5d1ef94d 100644 --- a/test/mechanisms/huff-vrgda/LinearVRGDA.t.sol +++ b/test/mechanisms/huff-vrgda/LinearVRGDA.t.sol @@ -71,7 +71,7 @@ contract LinearVRGDATest is Test { vm.warp(block.timestamp + fromDaysWadUnsafe(vrgda.getTargetSaleTime(1e18))); uint256 cost = vrgda.getVRGDAPrice(toDaysWadUnsafe(block.timestamp), 0); - assertEq(cost / 0.00001e18, uint256(vrgda.targetPrice()) / 0.00001e18); + assertApproxEqRel(cost, uint256(vrgda.targetPrice()), 0.0001e18); // ~lossy day-conversion warp } function testPricingBasic() public { @@ -82,7 +82,7 @@ contract LinearVRGDATest is Test { vm.warp(block.timestamp + timeDelta); uint256 cost = vrgda.getVRGDAPrice(toDaysWadUnsafe(block.timestamp), numMint); - assertEq(cost / 0.00001e18, uint256(vrgda.targetPrice()) / 0.00001e18); + assertApproxEqRel(cost, uint256(vrgda.targetPrice()), 0.0001e18); // ~lossy day-conversion warp } function testAlwaysTargetPriceInRightConditions(uint256 sold) public { diff --git a/test/mechanisms/huff-vrgda/LogisticVRGDA.t.sol b/test/mechanisms/huff-vrgda/LogisticVRGDA.t.sol index 5918e037..44afdcc6 100644 --- a/test/mechanisms/huff-vrgda/LogisticVRGDA.t.sol +++ b/test/mechanisms/huff-vrgda/LogisticVRGDA.t.sol @@ -45,7 +45,7 @@ contract LogisticVRGDATest is Test { int256 wadMaxSellable = toWadUnsafe(MAX_SELLABLE); targetPrice = 69.42e18; logisticLimit = int256(wadMaxSellable + 1e18); - logisticLimitDoubled = int256(wadMaxSellable * 2e18); + logisticLimitDoubled = int256(logisticLimit * 2e18); timeScale = 0.0023e18; // calculate the decay constant @@ -79,7 +79,8 @@ contract LogisticVRGDATest is Test { vm.warp(block.timestamp + fromDaysWadUnsafe(vrgda.getTargetSaleTime(1e18))); uint256 cost = vrgda.getVRGDAPrice(toDaysWadUnsafe(block.timestamp), 0); - assertEq(cost / 0.0000001e18, uint256(vrgda.targetPrice()) / 0.0000001e18); + // Approximate because the warp round-trips through integer seconds (lossy day-conversion). + assertApproxEqRel(cost, uint256(vrgda.targetPrice()), 0.0001e18); } function testPricingBasic() public { @@ -92,7 +93,7 @@ contract LogisticVRGDATest is Test { uint256 cost = vrgda.getVRGDAPrice(toDaysWadUnsafe(block.timestamp), numMint); // Equal within 2 percent since num mint is rounded from true decimal amount. - assertEq(cost / 0.02e18, uint256(vrgda.targetPrice()) / 0.02e18); + assertApproxEqRel(cost, uint256(vrgda.targetPrice()), 0.02e18); } function testGetTargetSaleTimeDoesNotRevertEarly() public view { diff --git a/test/mechanisms/huff-vrgda/SignedWadMath.t.sol b/test/mechanisms/huff-vrgda/SignedWadMath.t.sol new file mode 100644 index 00000000..d8b1818a --- /dev/null +++ b/test/mechanisms/huff-vrgda/SignedWadMath.t.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import "forge-std/Test.sol"; +import "foundry-huff/HuffDeployer.sol"; + +import { + toWadUnsafe, + toDaysWadUnsafe, + fromDaysWadUnsafe, + unsafeWadMul, + unsafeWadDiv, + unsafeDiv, + wadMul, + wadDiv, + wadExp, + wadLn +} from "./utils/SignedWadMath.sol"; + +/// @notice The Huff SignedWadMath surface under test. +interface ISignedWadMath { + function toWadUnsafe(uint256) external returns (int256); + function toDaysWadUnsafe(uint256) external returns (int256); + function fromDaysWadUnsafe(int256) external returns (uint256); + function unsafeWadMul(int256, int256) external returns (int256); + function unsafeWadDiv(int256, int256) external returns (int256); + function unsafeDiv(int256, int256) external returns (int256); + function wadMul(int256, int256) external returns (int256); + function wadDiv(int256, int256) external returns (int256); + function wadExp(int256) external returns (int256); + function wadLn(int256) external returns (int256); + function powWad(int256, int256) external returns (int256); + function log2(uint256) external returns (uint256); +} + +/// @notice Wraps the reference free functions as *external* functions so the +/// reverting ones can be probed with try/catch for revert-parity. +contract Ref { + function toWadUnsafe(uint256 x) external pure returns (int256) { return toWadUnsafe(x); } + function toDaysWadUnsafe(uint256 x) external pure returns (int256) { return toDaysWadUnsafe(x); } + function fromDaysWadUnsafe(int256 x) external pure returns (uint256) { return fromDaysWadUnsafe(x); } + function unsafeWadMul(int256 x, int256 y) external pure returns (int256) { return unsafeWadMul(x, y); } + function unsafeWadDiv(int256 x, int256 y) external pure returns (int256) { return unsafeWadDiv(x, y); } + function unsafeDiv(int256 x, int256 y) external pure returns (int256) { return unsafeDiv(x, y); } + function wadMul(int256 x, int256 y) external pure returns (int256) { return wadMul(x, y); } + function wadDiv(int256 x, int256 y) external pure returns (int256) { return wadDiv(x, y); } + function wadExp(int256 x) external pure returns (int256) { return wadExp(x); } + function wadLn(int256 x) external pure returns (int256) { return wadLn(x); } + + /// @dev Mirrors solmate's powWad and the Huff POW_WAD exactly: + /// expWad( sdiv(mul(lnWad(x), y), 1e18) ) + function powWad(int256 x, int256 y) external pure returns (int256) { + int256 l = wadLn(x); // reverts if x <= 0 + int256 z; + assembly { z := sdiv(mul(l, y), 1000000000000000000) } + return wadExp(z); // reverts on overflow + } + + /// @dev floor(log2(x)); reverts on x == 0, matching the Huff LOG_2. + function log2(uint256 x) external pure returns (uint256 r) { + require(x != 0, "UNDEFINED"); + while (x > 1) { x >>= 1; r++; } + } +} + +contract SignedWadMathTest is Test { + ISignedWadMath huff; + Ref ref; + + int256 constant WAD = 1e18; + + function setUp() public { + ref = new Ref(); + string memory wrapper = vm.readFile("test/mechanisms/huff-vrgda/mocks/SignedWadMathWrappers.huff"); + huff = ISignedWadMath( + HuffDeployer.deploy_with_code("mechanisms/huff-vrgda/SignedWadMath", wrapper) + ); + } + + /* ------------------------------------------------------------------ */ + /* NON-REVERTING OPS: EXACT MATCH */ + /* ------------------------------------------------------------------ */ + + function testDiff_toWadUnsafe(uint256 x) public { + assertEq(huff.toWadUnsafe(x), ref.toWadUnsafe(x)); + } + + function testDiff_toDaysWadUnsafe(uint256 x) public { + assertEq(huff.toDaysWadUnsafe(x), ref.toDaysWadUnsafe(x)); + } + + function testDiff_fromDaysWadUnsafe(int256 x) public { + assertEq(huff.fromDaysWadUnsafe(x), ref.fromDaysWadUnsafe(x)); + } + + function testDiff_unsafeWadMul(int256 x, int256 y) public { + assertEq(huff.unsafeWadMul(x, y), ref.unsafeWadMul(x, y)); + } + + function testDiff_unsafeWadDiv(int256 x, int256 y) public { + assertEq(huff.unsafeWadDiv(x, y), ref.unsafeWadDiv(x, y)); + } + + function testDiff_unsafeDiv(int256 x, int256 y) public { + assertEq(huff.unsafeDiv(x, y), ref.unsafeDiv(x, y)); + } + + /* ------------------------------------------------------------------ */ + /* REVERTING OPS: VALUE + REVERT PARITY */ + /* ------------------------------------------------------------------ */ + + function testDiff_wadMul(int256 x, int256 y) public { + try ref.wadMul(x, y) returns (int256 e) { + assertEq(huff.wadMul(x, y), e); + } catch { + vm.expectRevert(); + huff.wadMul(x, y); + } + } + + function testDiff_wadDiv(int256 x, int256 y) public { + try ref.wadDiv(x, y) returns (int256 e) { + assertEq(huff.wadDiv(x, y), e); + } catch { + vm.expectRevert(); + huff.wadDiv(x, y); + } + } + + function testDiff_wadExp(int256 x) public { + try ref.wadExp(x) returns (int256 e) { + assertEq(huff.wadExp(x), e); + } catch { + vm.expectRevert(); + huff.wadExp(x); + } + } + + function testDiff_wadLn(int256 x) public { + try ref.wadLn(x) returns (int256 e) { + assertEq(huff.wadLn(x), e); + } catch { + vm.expectRevert(); + huff.wadLn(x); + } + } + + function testDiff_powWad(int256 x, int256 y) public { + // Keep the base positive and exponents in a sane band so a healthy + // fraction of runs exercise the non-reverting path; try/catch covers the rest. + x = bound(x, 1, int256(1e30)); + y = bound(y, -20e18, 20e18); + try ref.powWad(x, y) returns (int256 e) { + assertEq(huff.powWad(x, y), e); + } catch { + vm.expectRevert(); + huff.powWad(x, y); + } + } + + function testDiff_log2(uint256 x) public { + try ref.log2(x) returns (uint256 e) { + assertEq(huff.log2(x), e); + } catch { + vm.expectRevert(); + huff.log2(x); + } + } + + /* ------------------------------------------------------------------ */ + /* TARGETED DOMAIN-BOUNDARY CASES */ + /* ------------------------------------------------------------------ */ + + function testExp_boundaries() public { + // Underflow boundary: x <= -42139678854452767551 returns 0. + assertEq(huff.wadExp(-42139678854452767551), 0); + assertEq(huff.wadExp(type(int256).min), 0); + assertEq(huff.wadExp(-42139678854452767551 + 1), ref.wadExp(-42139678854452767551 + 1)); + + // Overflow boundary: x >= 135305999368893231589 reverts. + vm.expectRevert(); + huff.wadExp(135305999368893231589); + vm.expectRevert(); + huff.wadExp(type(int256).max); + + // Just below the overflow boundary must succeed and match. + assertEq(huff.wadExp(135305999368893231589 - 1), ref.wadExp(135305999368893231589 - 1)); + } + + function testLn_boundaries() public { + // Undefined for x <= 0. + vm.expectRevert(); + huff.wadLn(0); + vm.expectRevert(); + huff.wadLn(-1); + vm.expectRevert(); + huff.wadLn(type(int256).min); + + // ln(1e18) == 0, ln(1) is very negative — both must match the reference. + assertEq(huff.wadLn(WAD), ref.wadLn(WAD)); + assertEq(huff.wadLn(1), ref.wadLn(1)); + assertEq(huff.wadLn(type(int256).max), ref.wadLn(type(int256).max)); + } + + /* ------------------------------------------------------------------ */ + /* KNOWN-ANSWER ANCHORS (vs real math) */ + /* ------------------------------------------------------------------ */ + + function testExp_knownAnswers() public { + assertEq(huff.wadExp(0), WAD); // e^0 = 1 + // e^1 ~= 2.718281828e18 + assertApproxEqRel(huff.wadExp(WAD), 2718281828459045235, 1e12); + // e^-1 ~= 0.367879441e18 + assertApproxEqRel(huff.wadExp(-WAD), 367879441171442321, 1e12); + } + + function testLn_knownAnswers() public { + assertEq(huff.wadLn(WAD), 0); // ln(1) = 0 + // ln(e) = 1e18 where e ~= 2.718281828e18 + assertApproxEqRel(huff.wadLn(2718281828459045235), WAD, 1e12); + } + + function testWadMul_knownAnswers() public { + assertEq(huff.wadMul(3 * WAD, 2 * WAD), 6 * WAD); + assertEq(huff.wadMul(-3 * WAD, 2 * WAD), -6 * WAD); + assertEq(huff.unsafeWadMul(3 * WAD, 2 * WAD), 6 * WAD); + } +} diff --git a/test/mechanisms/huff-vrgda/mocks/SignedWadMathWrappers.huff b/test/mechanisms/huff-vrgda/mocks/SignedWadMathWrappers.huff new file mode 100644 index 00000000..cf8ab222 --- /dev/null +++ b/test/mechanisms/huff-vrgda/mocks/SignedWadMathWrappers.huff @@ -0,0 +1,63 @@ +/// @title SignedWadMath Test Wrappers +/// @notice Exposes every SignedWadMath macro as an external function for +/// differential testing against the reference Solidity implementation. +/// @dev The body of src/mechanisms/huff-vrgda/SignedWadMath.huff is appended +/// automatically by HuffDeployer.deploy_with_code("mechanisms/huff-vrgda/SignedWadMath", ...). + +#define function toWadUnsafe(uint256) nonpayable returns (int256) +#define function toDaysWadUnsafe(uint256) nonpayable returns (int256) +#define function fromDaysWadUnsafe(int256) nonpayable returns (uint256) +#define function unsafeWadMul(int256, int256) nonpayable returns (int256) +#define function unsafeWadDiv(int256, int256) nonpayable returns (int256) +#define function unsafeDiv(int256, int256) nonpayable returns (int256) +#define function wadMul(int256, int256) nonpayable returns (int256) +#define function wadDiv(int256, int256) nonpayable returns (int256) +#define function wadExp(int256) nonpayable returns (int256) +#define function wadLn(int256) nonpayable returns (int256) +#define function powWad(int256, int256) nonpayable returns (int256) +#define function log2(uint256) nonpayable returns (uint256) + +#define macro MAIN() = takes (0) returns (0) { + 0x00 calldataload 0xE0 shr // [selector] + + dup1 __FUNC_SIG(toWadUnsafe) eq toWad jumpi + dup1 __FUNC_SIG(toDaysWadUnsafe) eq toDays jumpi + dup1 __FUNC_SIG(fromDaysWadUnsafe) eq fromDays jumpi + dup1 __FUNC_SIG(unsafeWadMul) eq uWadMul jumpi + dup1 __FUNC_SIG(unsafeWadDiv) eq uWadDiv jumpi + dup1 __FUNC_SIG(unsafeDiv) eq uDiv jumpi + dup1 __FUNC_SIG(wadMul) eq wMul jumpi + dup1 __FUNC_SIG(wadDiv) eq wDiv jumpi + dup1 __FUNC_SIG(wadExp) eq wExp jumpi + dup1 __FUNC_SIG(wadLn) eq wLn jumpi + dup1 __FUNC_SIG(powWad) eq wPow jumpi + dup1 __FUNC_SIG(log2) eq wLog2 jumpi + + fail: + 0x00 0x00 revert + + toWad: + 0x04 calldataload TO_WAD_UNSAFE() 0x00 mstore 0x20 0x00 return + toDays: + 0x04 calldataload TO_DAYS_WAD_UNSAFE() 0x00 mstore 0x20 0x00 return + fromDays: + 0x04 calldataload FROM_DAYS_WAD_UNSAFE() 0x00 mstore 0x20 0x00 return + uWadMul: + 0x24 calldataload 0x04 calldataload UNSAFE_WAD_MUL() 0x00 mstore 0x20 0x00 return + uWadDiv: + 0x24 calldataload 0x04 calldataload UNSAFE_WAD_DIV() 0x00 mstore 0x20 0x00 return + uDiv: + 0x24 calldataload 0x04 calldataload UNSAFE_DIV() 0x00 mstore 0x20 0x00 return + wMul: + 0x24 calldataload 0x04 calldataload WAD_MUL(fail) 0x00 mstore 0x20 0x00 return + wDiv: + 0x24 calldataload 0x04 calldataload WAD_DIV(fail) 0x00 mstore 0x20 0x00 return + wExp: + 0x04 calldataload EXP_WAD(fail) 0x00 mstore 0x20 0x00 return + wLn: + 0x04 calldataload LN_WAD(fail) 0x00 mstore 0x20 0x00 return + wPow: + 0x24 calldataload 0x04 calldataload POW_WAD(fail) 0x00 mstore 0x20 0x00 return + wLog2: + 0x04 calldataload LOG_2(fail) 0x00 mstore 0x20 0x00 return +}