From 93e8a6cca0be6fa0261ab5736f9efa2093bd9d65 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 5 Sep 2022 21:58:31 -0400 Subject: [PATCH 01/29] multivariable VRGDA --- src/LinearMVRGDA.sol | 47 ++++++++++++++++++++++++++++ src/LogisticMVRGDA.sol | 69 ++++++++++++++++++++++++++++++++++++++++++ src/MVRGDA.sol | 64 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 src/LinearMVRGDA.sol create mode 100644 src/LogisticMVRGDA.sol create mode 100644 src/MVRGDA.sol diff --git a/src/LinearMVRGDA.sol b/src/LinearMVRGDA.sol new file mode 100644 index 0000000..a4c93a0 --- /dev/null +++ b/src/LinearMVRGDA.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {unsafeWadDiv} from "./utils/SignedWadMath.sol"; + +import {MVRGDA} from "./MVRGDA.sol"; + +/// @title Linear Variable Rate Gradual Dutch Auction +/// @author transmissions11 +/// @author FrankieIsLost +/// @author saucepoint +/// @notice VRGDA with a linear issuance curve. +abstract contract LinearMVRGDA is MVRGDA { + /*////////////////////////////////////////////////////////////// + PRICING PARAMETERS + //////////////////////////////////////////////////////////////*/ + + /// @dev The total number of tokens to target selling every full unit of time. + /// @dev Represented as an 18 decimal fixed point number. + mapping(uint256 => int256) internal perTimeUnit; + + /// @notice Sets pricing parameters for the VRGDA. + /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. + /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. + /// @param _perTimeUnit The number of tokens to target selling in 1 full unit of time, scaled by 1e18. + function createLinearVRGDA( + int256 _targetPrice, + int256 _priceDecayPercent, + int256 _perTimeUnit + ) public returns (uint256 varId) { + varId = varCounter; + perTimeUnit[varCounter] = _perTimeUnit; + createVRGDA(_targetPrice, _priceDecayPercent); + } + + /*////////////////////////////////////////////////////////////// + PRICING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. + /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. + /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. + function getTargetSaleTime(uint256 varId, int256 sold) public view override returns (int256) { + return unsafeWadDiv(sold, perTimeUnit[varId]); + } +} diff --git a/src/LogisticMVRGDA.sol b/src/LogisticMVRGDA.sol new file mode 100644 index 0000000..b5d98d8 --- /dev/null +++ b/src/LogisticMVRGDA.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {wadLn, unsafeDiv, unsafeWadDiv} from "./utils/SignedWadMath.sol"; + +import {MVRGDA} from "./MVRGDA.sol"; + +/// @title Logistic Variable Rate Gradual Dutch Auction +/// @author transmissions11 +/// @author FrankieIsLost +/// @notice VRGDA with a logistic issuance curve. +abstract contract LogisticMVRGDA is MVRGDA { + /*////////////////////////////////////////////////////////////// + PRICING PARAMETERS + //////////////////////////////////////////////////////////////*/ + + /// @dev The maximum number of tokens of tokens to sell + 1. We add + /// 1 because the logistic function will never fully reach its limit. + /// @dev Represented as an 18 decimal fixed point number. + mapping(uint256 => int256) public logisticLimit; + + /// @dev The maximum number of tokens of tokens to sell + 1 multiplied + /// by 2. We could compute it on the fly each time but this saves gas. + /// @dev Represented as a 36 decimal fixed point number. + mapping(uint256 => int256) public logisticLimitDoubled; + + /// @dev Time scale controls the steepness of the logistic curve, + /// which affects how quickly we will reach the curve's asymptote. + /// @dev Represented as an 18 decimal fixed point number. + mapping(uint256 => int256) internal timeScale; + + /// @notice Sets pricing parameters for the VRGDA. + /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. + /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. + /// @param _maxSellable The maximum number of tokens to sell, scaled by 1e18. + /// @param _timeScale The steepness of the logistic curve, scaled by 1e18. + function createLogisticVRGDA( + int256 _targetPrice, + int256 _priceDecayPercent, + int256 _maxSellable, + int256 _timeScale + ) public returns (uint256 varId) { + varId = varCounter; + + // Add 1 wad to make the limit inclusive of _maxSellable. + logisticLimit[varCounter] = _maxSellable + 1e18; + + // Scale by 2e18 to both double it and give it 36 decimals. + logisticLimitDoubled[varCounter] = logisticLimit[varCounter] * 2e18; + + timeScale[varCounter] = _timeScale; + + createVRGDA(_targetPrice, _priceDecayPercent); + } + + /*////////////////////////////////////////////////////////////// + PRICING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. + /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. + /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. + function getTargetSaleTime(uint256 varId, int256 sold) public view override returns (int256) { + unchecked { + return -unsafeWadDiv(wadLn(unsafeDiv(logisticLimitDoubled[varId], sold + logisticLimit[varId]) - 1e18), timeScale[varId]); + } + } +} diff --git a/src/MVRGDA.sol b/src/MVRGDA.sol new file mode 100644 index 0000000..c2aae87 --- /dev/null +++ b/src/MVRGDA.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "./utils/SignedWadMath.sol"; + +/// @title Variable Rate Gradual Dutch Auction +/// @author transmissions11 +/// @author FrankieIsLost +/// @author saucepoint +/// @notice Sell tokens roughly according to an issuance schedule. +abstract contract MVRGDA { + /*////////////////////////////////////////////////////////////// + VRGDA PARAMETERS + //////////////////////////////////////////////////////////////*/ + uint256 public varCounter; + + /// @notice Target price for a token, to be scaled according to sales pace. + /// @dev Represented as an 18 decimal fixed point number. + // maps variable Id to target price + mapping(uint256 => int256) public targetPrice; + + /// @dev Precomputed constant that allows us to rewrite a pow() as an exp(). + /// @dev Represented as an 18 decimal fixed point number. + // maps variable Id to decay constant + mapping(uint256 => int256) public decayConstant; + + /// @notice Sets target price and per time unit price decay for the VRGDA. + /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. + /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. + function createVRGDA(int256 _targetPrice, int256 _priceDecayPercent) public { + targetPrice[varCounter] = _targetPrice; + int256 _decayConstant = wadLn(1e18 - _priceDecayPercent); + require(_decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); + decayConstant[varCounter] = _decayConstant; + + unchecked { varCounter++; } + } + + /*////////////////////////////////////////////////////////////// + PRICING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Calculate the price of a token according to the VRGDA formula. + /// @param timeSinceStart Time passed since the VRGDA began, scaled by 1e18. + /// @param sold The total number of tokens that have been sold so far. + /// @return The price of a token according to VRGDA, scaled by 1e18. + function getVRGDAPrice(uint256 varId, int256 timeSinceStart, uint256 sold) public view returns (uint256) { + unchecked { + // prettier-ignore + return uint256(wadMul(targetPrice[varId], wadExp(unsafeWadMul(decayConstant[varId], + // Theoretically calling toWadUnsafe with sold can silently overflow but under + // any reasonable circumstance it will never be large enough. We use sold + 1 as + // the VRGDA formula's n param represents the nth token and sold is the n-1th token. + timeSinceStart - getTargetSaleTime(varId, toWadUnsafe(sold + 1)) + )))); + } + } + + /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. + /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. + /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. + function getTargetSaleTime(uint256 varId, int256 sold) public view virtual returns (int256); +} From dfd923ce6e32ea83f523ad183f4aba3ed97a1a2c Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 5 Sep 2022 23:07:04 -0400 Subject: [PATCH 02/29] initial test --- src/LinearMVRGDA.sol | 2 +- test/LinearMVRGDA.t.sol | 55 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 test/LinearMVRGDA.t.sol diff --git a/src/LinearMVRGDA.sol b/src/LinearMVRGDA.sol index a4c93a0..37ac6a2 100644 --- a/src/LinearMVRGDA.sol +++ b/src/LinearMVRGDA.sol @@ -10,7 +10,7 @@ import {MVRGDA} from "./MVRGDA.sol"; /// @author FrankieIsLost /// @author saucepoint /// @notice VRGDA with a linear issuance curve. -abstract contract LinearMVRGDA is MVRGDA { +contract LinearMVRGDA is MVRGDA { /*////////////////////////////////////////////////////////////// PRICING PARAMETERS //////////////////////////////////////////////////////////////*/ diff --git a/test/LinearMVRGDA.t.sol b/test/LinearMVRGDA.t.sol new file mode 100644 index 0000000..bd4460f --- /dev/null +++ b/test/LinearMVRGDA.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; + +import {toWadUnsafe, toDaysWadUnsafe, fromDaysWadUnsafe} from "../src/utils/SignedWadMath.sol"; + +import {LinearMVRGDA} from "../src//LinearMVRGDA.sol"; + +uint256 constant ONE_THOUSAND_YEARS = 356 days * 1000; + +uint256 constant MAX_SELLABLE = 6392; + +contract LinearMVRGDATest is DSTestPlus { + LinearMVRGDA vrgda; + uint256 varId; + + function setUp() public { + vrgda = new LinearMVRGDA(); + vrgda.createLinearVRGDA( + 69.42e18, // Target price. + 0.31e18, // Price decay percent. + 2e18 // Per time unit. + ); + } + + function testTargetPrice() public { + // Warp to the target sale time so that the VRGDA price equals the target price. + hevm.warp(block.timestamp + fromDaysWadUnsafe(vrgda.getTargetSaleTime(varId, 1e18))); + + uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), 0); + assertRelApproxEq(cost, uint256(vrgda.targetPrice(varId)), 0.00001e18); + } + + function testPricingBasic() public { + // Our VRGDA targets this number of mints at given time. + uint256 timeDelta = 120 days; + uint256 numMint = 239; + + hevm.warp(block.timestamp + timeDelta); + + uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), numMint); + assertRelApproxEq(cost, uint256(vrgda.targetPrice(varId)), 0.00001e18); + } + + function testAlwaysTargetPriceInRightConditions(uint256 sold) public { + sold = bound(sold, 0, type(uint128).max); + + assertRelApproxEq( + vrgda.getVRGDAPrice(varId, vrgda.getTargetSaleTime(varId, toWadUnsafe(sold + 1)), sold), + uint256(vrgda.targetPrice(varId)), + 0.00001e18 + ); + } +} From 1708c1d147ac941d172ab9ae1b96b8b322cdc126 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 5 Sep 2022 23:47:20 -0400 Subject: [PATCH 03/29] add logic to original contracts --- src/LinearVRGDA.sol | 16 ++++++++++++++++ src/LogisticVRGDA.sol | 28 ++++++++++++++++++++++++++++ src/VRGDA.sol | 27 +++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/src/LinearVRGDA.sol b/src/LinearVRGDA.sol index 814d060..854e5e4 100644 --- a/src/LinearVRGDA.sol +++ b/src/LinearVRGDA.sol @@ -18,6 +18,8 @@ abstract contract LinearVRGDA is VRGDA { /// @dev Represented as an 18 decimal fixed point number. int256 internal immutable perTimeUnit; + mapping(uint256 => int256) internal perTimeUnits; + /// @notice Sets pricing parameters for the VRGDA. /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. @@ -30,6 +32,16 @@ abstract contract LinearVRGDA is VRGDA { perTimeUnit = _perTimeUnit; } + function createLinearVRGDA( + int256 _targetPrice, + int256 _priceDecayPercent, + int256 _perTimeUnit + ) public returns (uint256 varId) { + varId = varCounter; + perTimeUnits[varCounter] = _perTimeUnit; + createVRGDA(_targetPrice, _priceDecayPercent); + } + /*////////////////////////////////////////////////////////////// PRICING LOGIC //////////////////////////////////////////////////////////////*/ @@ -41,4 +53,8 @@ abstract contract LinearVRGDA is VRGDA { function getTargetSaleTime(int256 sold) public view override returns (int256) { return unsafeWadDiv(sold, perTimeUnit); } + + function getTargetSaleTime(uint256 varId, int256 sold) public view override returns (int256) { + return unsafeWadDiv(sold, perTimeUnits[varId]); + } } diff --git a/src/LogisticVRGDA.sol b/src/LogisticVRGDA.sol index 2afff17..61cfe54 100644 --- a/src/LogisticVRGDA.sol +++ b/src/LogisticVRGDA.sol @@ -18,16 +18,19 @@ abstract contract LogisticVRGDA is VRGDA { /// 1 because the logistic function will never fully reach its limit. /// @dev Represented as an 18 decimal fixed point number. int256 public immutable logisticLimit; + mapping(uint256 => int256) public logisticLimits; /// @dev The maximum number of tokens of tokens to sell + 1 multiplied /// by 2. We could compute it on the fly each time but this saves gas. /// @dev Represented as a 36 decimal fixed point number. int256 public immutable logisticLimitDoubled; + mapping(uint256 => int256) public logisticLimitDoubles; /// @dev Time scale controls the steepness of the logistic curve, /// which affects how quickly we will reach the curve's asymptote. /// @dev Represented as an 18 decimal fixed point number. int256 internal immutable timeScale; + mapping(uint256 => int256) public timeScales; /// @notice Sets pricing parameters for the VRGDA. /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. @@ -49,6 +52,25 @@ abstract contract LogisticVRGDA is VRGDA { timeScale = _timeScale; } + function createLogisticVRGDA( + int256 _targetPrice, + int256 _priceDecayPercent, + int256 _maxSellable, + int256 _timeScale + ) public returns (uint256 varId) { + varId = varCounter; + + // Add 1 wad to make the limit inclusive of _maxSellable. + logisticLimits[varCounter] = _maxSellable + 1e18; + + // Scale by 2e18 to both double it and give it 36 decimals. + logisticLimitDoubles[varCounter] = logisticLimits[varCounter] * 2e18; + + timeScales[varCounter] = _timeScale; + + createVRGDA(_targetPrice, _priceDecayPercent); + } + /*////////////////////////////////////////////////////////////// PRICING LOGIC //////////////////////////////////////////////////////////////*/ @@ -62,4 +84,10 @@ abstract contract LogisticVRGDA is VRGDA { return -unsafeWadDiv(wadLn(unsafeDiv(logisticLimitDoubled, sold + logisticLimit) - 1e18), timeScale); } } + + function getTargetSaleTime(uint256 varId, int256 sold) public view override returns (int256) { + unchecked { + return -unsafeWadDiv(wadLn(unsafeDiv(logisticLimitDoubles[varId], sold + logisticLimits[varId]) - 1e18), timeScales[varId]); + } + } } diff --git a/src/VRGDA.sol b/src/VRGDA.sol index 7a7cc36..b2ecbdd 100644 --- a/src/VRGDA.sol +++ b/src/VRGDA.sol @@ -11,6 +11,7 @@ abstract contract VRGDA { /*////////////////////////////////////////////////////////////// VRGDA PARAMETERS //////////////////////////////////////////////////////////////*/ + uint256 varCounter; /// @notice Target price for a token, to be scaled according to sales pace. /// @dev Represented as an 18 decimal fixed point number. @@ -20,6 +21,9 @@ abstract contract VRGDA { /// @dev Represented as an 18 decimal fixed point number. int256 internal immutable decayConstant; + mapping(uint256 => int256) public targetPrices; + mapping(uint256 => int256) public decayConstants; + /// @notice Sets target price and per time unit price decay for the VRGDA. /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. @@ -32,6 +36,15 @@ abstract contract VRGDA { require(decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); } + function createVRGDA(int256 _targetPrice, int256 _priceDecayPercent) public { + targetPrices[varCounter] = _targetPrice; + int256 _decayConstant = wadLn(1e18 - _priceDecayPercent); + require(_decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); + decayConstants[varCounter] = _decayConstant; + + unchecked { varCounter++; } + } + /*////////////////////////////////////////////////////////////// PRICING LOGIC //////////////////////////////////////////////////////////////*/ @@ -52,9 +65,23 @@ abstract contract VRGDA { } } + function getVRGDAPrice(uint256 varId, int256 timeSinceStart, uint256 sold) public view returns (uint256) { + unchecked { + // prettier-ignore + return uint256(wadMul(targetPrices[varId], wadExp(unsafeWadMul(decayConstants[varId], + // Theoretically calling toWadUnsafe with sold can silently overflow but under + // any reasonable circumstance it will never be large enough. We use sold + 1 as + // the VRGDA formula's n param represents the nth token and sold is the n-1th token. + timeSinceStart - getTargetSaleTime(varId, toWadUnsafe(sold + 1)) + )))); + } + } + /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. function getTargetSaleTime(int256 sold) public view virtual returns (int256); + + function getTargetSaleTime(uint256 varId, int256 sold) public view virtual returns (int256); } From fab64bb56929357ca2e022c9ba7bcd9890e16059 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 6 Sep 2022 00:12:49 -0400 Subject: [PATCH 04/29] tests for created VRGDAs --- test/LinearVRGDA.t.sol | 55 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/LinearVRGDA.t.sol b/test/LinearVRGDA.t.sol index c106032..3f4a9cd 100644 --- a/test/LinearVRGDA.t.sol +++ b/test/LinearVRGDA.t.sol @@ -50,4 +50,59 @@ contract LinearVRGDATest is DSTestPlus { 0.00001e18 ); } + + function testCreateVRGDA() public { + // Warp to the target sale time so that the VRGDA price equals the target price. + hevm.warp(block.timestamp + fromDaysWadUnsafe(vrgda.getTargetSaleTime(1e18))); + + uint256 varId = vrgda.createLinearVRGDA( + 69.42e18, // Target price. + 0.31e18, // Price decay percent. + 2e18 // Per time unit. + ); + // first created VRGDA will have id 0 + assertEq(varId, 0); + + uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), 0); + assertRelApproxEq(cost, uint256(vrgda.targetPrices(varId)), 0.00001e18); + } + + function testPricingBasicCreatedVar() public { + // create an unused VRGDA variable + vrgda.createLinearVRGDA(0, 1, 0); + + uint256 varId = vrgda.createLinearVRGDA( + 69.42e18, // Target price. + 0.31e18, // Price decay percent. + 2e18 // Per time unit. + ); + + assertEq(varId, 1); // second VRGDA variable created + + // Our VRGDA targets this number of mints at given time. + uint256 timeDelta = 120 days; + uint256 numMint = 239; + + hevm.warp(block.timestamp + timeDelta); + + uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), numMint); + assertRelApproxEq(cost, uint256(vrgda.targetPrice()), 0.00001e18); + } + + function testAlwaysTargetPriceInRightConditionsCreated(uint256 sold) public { + uint256 varId = vrgda.createLinearVRGDA( + 69.42e18, // Target price. + 0.31e18, // Price decay percent. + 2e18 // Per time unit. + ); + // first created VRGDA will have id 0 + assertEq(varId, 0); + sold = bound(sold, 0, type(uint128).max); + + assertRelApproxEq( + vrgda.getVRGDAPrice(varId, vrgda.getTargetSaleTime(varId, toWadUnsafe(sold + 1)), sold), + uint256(vrgda.targetPrices(varId)), + 0.00001e18 + ); + } } From fa32f605004368d8253e37687acc135f681bf10c Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 6 Sep 2022 00:43:03 -0400 Subject: [PATCH 05/29] basic tests for created VRGDA --- test/LogisticVRGDA.t.sol | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/LogisticVRGDA.t.sol b/test/LogisticVRGDA.t.sol index 99fd031..9b57a4f 100644 --- a/test/LogisticVRGDA.t.sol +++ b/test/LogisticVRGDA.t.sol @@ -82,4 +82,43 @@ contract LogisticVRGDATest is DSTestPlus { 0.00001e18 ); } + + function testTargetPriceCreated() public { + uint256 varId = vrgda.createLogisticVRGDA( + 69.42e18, // Target price. + 0.31e18, // Price decay percent. + toWadUnsafe(MAX_SELLABLE), // Max sellable. + 0.0023e18 // Time scale. + ); + assertEq(varId, 0); + + // Warp to the target sale time so that the VRGDA price equals the target price. + hevm.warp(block.timestamp + fromDaysWadUnsafe(vrgda.getTargetSaleTime(varId, 1e18))); + + uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), 0); + assertRelApproxEq(cost, uint256(vrgda.targetPrices(varId)), 0.0000001e18); + } + + function testPricingBasicCreated() public { + // create an unused VRGDA so we can test the second one behaves as expected + vrgda.createLogisticVRGDA(0, 1, 0, 0); + uint256 varId = vrgda.createLogisticVRGDA( + 69.42e18, // Target price. + 0.31e18, // Price decay percent. + toWadUnsafe(MAX_SELLABLE), // Max sellable. + 0.0023e18 // Time scale. + ); + assertEq(varId, 1); + + // Our VRGDA targets this number of mints at given time. + uint256 timeDelta = 120 days; + uint256 numMint = 876; + + hevm.warp(block.timestamp + timeDelta); + + uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), numMint); + + // Equal within 2 percent since num mint is rounded from true decimal amount. + assertRelApproxEq(cost, uint256(vrgda.targetPrices(varId)), 0.02e18); + } } From c88e16eef2b9624c53fe22cdd5378f789ecde301 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 6 Sep 2022 09:03:31 -0400 Subject: [PATCH 06/29] remove separate files --- src/LinearMVRGDA.sol | 47 ---------------------------- src/LogisticMVRGDA.sol | 69 ----------------------------------------- src/MVRGDA.sol | 64 -------------------------------------- test/LinearMVRGDA.t.sol | 55 -------------------------------- 4 files changed, 235 deletions(-) delete mode 100644 src/LinearMVRGDA.sol delete mode 100644 src/LogisticMVRGDA.sol delete mode 100644 src/MVRGDA.sol delete mode 100644 test/LinearMVRGDA.t.sol diff --git a/src/LinearMVRGDA.sol b/src/LinearMVRGDA.sol deleted file mode 100644 index 37ac6a2..0000000 --- a/src/LinearMVRGDA.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import {unsafeWadDiv} from "./utils/SignedWadMath.sol"; - -import {MVRGDA} from "./MVRGDA.sol"; - -/// @title Linear Variable Rate Gradual Dutch Auction -/// @author transmissions11 -/// @author FrankieIsLost -/// @author saucepoint -/// @notice VRGDA with a linear issuance curve. -contract LinearMVRGDA is MVRGDA { - /*////////////////////////////////////////////////////////////// - PRICING PARAMETERS - //////////////////////////////////////////////////////////////*/ - - /// @dev The total number of tokens to target selling every full unit of time. - /// @dev Represented as an 18 decimal fixed point number. - mapping(uint256 => int256) internal perTimeUnit; - - /// @notice Sets pricing parameters for the VRGDA. - /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. - /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. - /// @param _perTimeUnit The number of tokens to target selling in 1 full unit of time, scaled by 1e18. - function createLinearVRGDA( - int256 _targetPrice, - int256 _priceDecayPercent, - int256 _perTimeUnit - ) public returns (uint256 varId) { - varId = varCounter; - perTimeUnit[varCounter] = _perTimeUnit; - createVRGDA(_targetPrice, _priceDecayPercent); - } - - /*////////////////////////////////////////////////////////////// - PRICING LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. - /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is - /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. - function getTargetSaleTime(uint256 varId, int256 sold) public view override returns (int256) { - return unsafeWadDiv(sold, perTimeUnit[varId]); - } -} diff --git a/src/LogisticMVRGDA.sol b/src/LogisticMVRGDA.sol deleted file mode 100644 index b5d98d8..0000000 --- a/src/LogisticMVRGDA.sol +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import {wadLn, unsafeDiv, unsafeWadDiv} from "./utils/SignedWadMath.sol"; - -import {MVRGDA} from "./MVRGDA.sol"; - -/// @title Logistic Variable Rate Gradual Dutch Auction -/// @author transmissions11 -/// @author FrankieIsLost -/// @notice VRGDA with a logistic issuance curve. -abstract contract LogisticMVRGDA is MVRGDA { - /*////////////////////////////////////////////////////////////// - PRICING PARAMETERS - //////////////////////////////////////////////////////////////*/ - - /// @dev The maximum number of tokens of tokens to sell + 1. We add - /// 1 because the logistic function will never fully reach its limit. - /// @dev Represented as an 18 decimal fixed point number. - mapping(uint256 => int256) public logisticLimit; - - /// @dev The maximum number of tokens of tokens to sell + 1 multiplied - /// by 2. We could compute it on the fly each time but this saves gas. - /// @dev Represented as a 36 decimal fixed point number. - mapping(uint256 => int256) public logisticLimitDoubled; - - /// @dev Time scale controls the steepness of the logistic curve, - /// which affects how quickly we will reach the curve's asymptote. - /// @dev Represented as an 18 decimal fixed point number. - mapping(uint256 => int256) internal timeScale; - - /// @notice Sets pricing parameters for the VRGDA. - /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. - /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. - /// @param _maxSellable The maximum number of tokens to sell, scaled by 1e18. - /// @param _timeScale The steepness of the logistic curve, scaled by 1e18. - function createLogisticVRGDA( - int256 _targetPrice, - int256 _priceDecayPercent, - int256 _maxSellable, - int256 _timeScale - ) public returns (uint256 varId) { - varId = varCounter; - - // Add 1 wad to make the limit inclusive of _maxSellable. - logisticLimit[varCounter] = _maxSellable + 1e18; - - // Scale by 2e18 to both double it and give it 36 decimals. - logisticLimitDoubled[varCounter] = logisticLimit[varCounter] * 2e18; - - timeScale[varCounter] = _timeScale; - - createVRGDA(_targetPrice, _priceDecayPercent); - } - - /*////////////////////////////////////////////////////////////// - PRICING LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. - /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is - /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. - function getTargetSaleTime(uint256 varId, int256 sold) public view override returns (int256) { - unchecked { - return -unsafeWadDiv(wadLn(unsafeDiv(logisticLimitDoubled[varId], sold + logisticLimit[varId]) - 1e18), timeScale[varId]); - } - } -} diff --git a/src/MVRGDA.sol b/src/MVRGDA.sol deleted file mode 100644 index c2aae87..0000000 --- a/src/MVRGDA.sol +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "./utils/SignedWadMath.sol"; - -/// @title Variable Rate Gradual Dutch Auction -/// @author transmissions11 -/// @author FrankieIsLost -/// @author saucepoint -/// @notice Sell tokens roughly according to an issuance schedule. -abstract contract MVRGDA { - /*////////////////////////////////////////////////////////////// - VRGDA PARAMETERS - //////////////////////////////////////////////////////////////*/ - uint256 public varCounter; - - /// @notice Target price for a token, to be scaled according to sales pace. - /// @dev Represented as an 18 decimal fixed point number. - // maps variable Id to target price - mapping(uint256 => int256) public targetPrice; - - /// @dev Precomputed constant that allows us to rewrite a pow() as an exp(). - /// @dev Represented as an 18 decimal fixed point number. - // maps variable Id to decay constant - mapping(uint256 => int256) public decayConstant; - - /// @notice Sets target price and per time unit price decay for the VRGDA. - /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. - /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. - function createVRGDA(int256 _targetPrice, int256 _priceDecayPercent) public { - targetPrice[varCounter] = _targetPrice; - int256 _decayConstant = wadLn(1e18 - _priceDecayPercent); - require(_decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); - decayConstant[varCounter] = _decayConstant; - - unchecked { varCounter++; } - } - - /*////////////////////////////////////////////////////////////// - PRICING LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @notice Calculate the price of a token according to the VRGDA formula. - /// @param timeSinceStart Time passed since the VRGDA began, scaled by 1e18. - /// @param sold The total number of tokens that have been sold so far. - /// @return The price of a token according to VRGDA, scaled by 1e18. - function getVRGDAPrice(uint256 varId, int256 timeSinceStart, uint256 sold) public view returns (uint256) { - unchecked { - // prettier-ignore - return uint256(wadMul(targetPrice[varId], wadExp(unsafeWadMul(decayConstant[varId], - // Theoretically calling toWadUnsafe with sold can silently overflow but under - // any reasonable circumstance it will never be large enough. We use sold + 1 as - // the VRGDA formula's n param represents the nth token and sold is the n-1th token. - timeSinceStart - getTargetSaleTime(varId, toWadUnsafe(sold + 1)) - )))); - } - } - - /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. - /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is - /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. - function getTargetSaleTime(uint256 varId, int256 sold) public view virtual returns (int256); -} diff --git a/test/LinearMVRGDA.t.sol b/test/LinearMVRGDA.t.sol deleted file mode 100644 index bd4460f..0000000 --- a/test/LinearMVRGDA.t.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; - -import {toWadUnsafe, toDaysWadUnsafe, fromDaysWadUnsafe} from "../src/utils/SignedWadMath.sol"; - -import {LinearMVRGDA} from "../src//LinearMVRGDA.sol"; - -uint256 constant ONE_THOUSAND_YEARS = 356 days * 1000; - -uint256 constant MAX_SELLABLE = 6392; - -contract LinearMVRGDATest is DSTestPlus { - LinearMVRGDA vrgda; - uint256 varId; - - function setUp() public { - vrgda = new LinearMVRGDA(); - vrgda.createLinearVRGDA( - 69.42e18, // Target price. - 0.31e18, // Price decay percent. - 2e18 // Per time unit. - ); - } - - function testTargetPrice() public { - // Warp to the target sale time so that the VRGDA price equals the target price. - hevm.warp(block.timestamp + fromDaysWadUnsafe(vrgda.getTargetSaleTime(varId, 1e18))); - - uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), 0); - assertRelApproxEq(cost, uint256(vrgda.targetPrice(varId)), 0.00001e18); - } - - function testPricingBasic() public { - // Our VRGDA targets this number of mints at given time. - uint256 timeDelta = 120 days; - uint256 numMint = 239; - - hevm.warp(block.timestamp + timeDelta); - - uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), numMint); - assertRelApproxEq(cost, uint256(vrgda.targetPrice(varId)), 0.00001e18); - } - - function testAlwaysTargetPriceInRightConditions(uint256 sold) public { - sold = bound(sold, 0, type(uint128).max); - - assertRelApproxEq( - vrgda.getVRGDAPrice(varId, vrgda.getTargetSaleTime(varId, toWadUnsafe(sold + 1)), sold), - uint256(vrgda.targetPrice(varId)), - 0.00001e18 - ); - } -} From 39b4bf58bda63baf78e0275ca934cb75ff345899 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 6 Sep 2022 17:23:02 -0400 Subject: [PATCH 07/29] move VRGDA func to library; add example for multi VRGDA --- src/MultiVRGDA.sol | 57 ++++++++++++++++++++++ src/VRGDA.sol | 11 +---- src/lib/VRGDALibrary.sol | 20 ++++++++ test/MultiVRGDA.t.sol | 73 +++++++++++++++++++++++++++++ test/mocks/MockMultiLinearVRGDA.sol | 24 ++++++++++ 5 files changed, 176 insertions(+), 9 deletions(-) create mode 100644 src/MultiVRGDA.sol create mode 100644 src/lib/VRGDALibrary.sol create mode 100644 test/MultiVRGDA.t.sol create mode 100644 test/mocks/MockMultiLinearVRGDA.sol diff --git a/src/MultiVRGDA.sol b/src/MultiVRGDA.sol new file mode 100644 index 0000000..44bde8d --- /dev/null +++ b/src/MultiVRGDA.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "./utils/SignedWadMath.sol"; +import {VRGDALibrary} from "./lib/VRGDALibrary.sol"; + +/// @title Variable Rate Gradual Dutch Auction +/// @author transmissions11 +/// @author FrankieIsLost +/// @author saucepoint +/// @notice Sell tokens roughly according to an issuance schedule. +abstract contract MultiVRGDA { + /*////////////////////////////////////////////////////////////// + VRGDA PARAMETERS + //////////////////////////////////////////////////////////////*/ + uint256 public varCounter; + + /// @notice Target price for a token, to be scaled according to sales pace. + /// @dev Represented as an 18 decimal fixed point number. + // maps variable Id to target price + mapping(uint256 => int256) public targetPrice; + + /// @dev Precomputed constant that allows us to rewrite a pow() as an exp(). + /// @dev Represented as an 18 decimal fixed point number. + // maps variable Id to decay constant + mapping(uint256 => int256) public decayConstant; + + /// @notice Sets target price and per time unit price decay for the VRGDA. + /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. + /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. + function createVRGDA(int256 _targetPrice, int256 _priceDecayPercent) public { + targetPrice[varCounter] = _targetPrice; + int256 _decayConstant = wadLn(1e18 - _priceDecayPercent); + require(_decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); + decayConstant[varCounter] = _decayConstant; + + unchecked { varCounter++; } + } + + /*////////////////////////////////////////////////////////////// + PRICING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Calculate the price of a token according to the VRGDA formula. + /// @param timeSinceStart Time passed since the VRGDA began, scaled by 1e18. + /// @param sold The total number of tokens that have been sold so far. + /// @return The price of a token according to VRGDA, scaled by 1e18. + function getVRGDAPrice(uint256 varId, int256 timeSinceStart, uint256 sold) public view returns (uint256) { + return VRGDALibrary.getVRGDAPrice(targetPrice[varId], decayConstant[varId], timeSinceStart - getTargetSaleTime(varId, toWadUnsafe(sold + 1))); + } + + /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. + /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. + /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. + function getTargetSaleTime(uint256 varId, int256 sold) public view virtual returns (int256); +} diff --git a/src/VRGDA.sol b/src/VRGDA.sol index b2ecbdd..e6b3b2f 100644 --- a/src/VRGDA.sol +++ b/src/VRGDA.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.0; import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "./utils/SignedWadMath.sol"; +import {VRGDALibrary} from "./lib/VRGDALibrary.sol"; /// @title Variable Rate Gradual Dutch Auction /// @author transmissions11 @@ -54,15 +55,7 @@ abstract contract VRGDA { /// @param sold The total number of tokens that have been sold so far. /// @return The price of a token according to VRGDA, scaled by 1e18. function getVRGDAPrice(int256 timeSinceStart, uint256 sold) public view returns (uint256) { - unchecked { - // prettier-ignore - return uint256(wadMul(targetPrice, wadExp(unsafeWadMul(decayConstant, - // Theoretically calling toWadUnsafe with sold can silently overflow but under - // any reasonable circumstance it will never be large enough. We use sold + 1 as - // the VRGDA formula's n param represents the nth token and sold is the n-1th token. - timeSinceStart - getTargetSaleTime(toWadUnsafe(sold + 1)) - )))); - } + return VRGDALibrary.getVRGDAPrice(targetPrice, decayConstant, timeSinceStart - getTargetSaleTime(toWadUnsafe(sold + 1))); } function getVRGDAPrice(uint256 varId, int256 timeSinceStart, uint256 sold) public view returns (uint256) { diff --git a/src/lib/VRGDALibrary.sol b/src/lib/VRGDALibrary.sol new file mode 100644 index 0000000..c3c4e93 --- /dev/null +++ b/src/lib/VRGDALibrary.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "../utils/SignedWadMath.sol"; + +library VRGDALibrary { + /// @notice Calculate the price of a token according to the VRGDA formula. + /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. + /// @param _decayConstant The constant price decays per unit of time with no sales, scaled by 1e18. + /// @param _timeDelta Time difference between time-since-VRGDA-genesis and expected time of sale, (assumes both scaled by 1e18). I.e. timeSinceStart - targetSaleTime + /// @return The price of a token according to VRGDA, scaled by 1e18. + function getVRGDAPrice(int256 _targetPrice, int256 _decayConstant, int256 _timeDelta) internal pure returns (uint256) { + unchecked { + // prettier-ignore + return uint256(wadMul(_targetPrice, wadExp(unsafeWadMul(_decayConstant, + _timeDelta + )))); + } + } +} \ No newline at end of file diff --git a/test/MultiVRGDA.t.sol b/test/MultiVRGDA.t.sol new file mode 100644 index 0000000..6676ddf --- /dev/null +++ b/test/MultiVRGDA.t.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; + +import {toWadUnsafe, toDaysWadUnsafe, fromDaysWadUnsafe} from "../src/utils/SignedWadMath.sol"; + +import {MockMultiLinearVRGDA} from "./mocks/MockMultiLinearVRGDA.sol"; + +uint256 constant ONE_THOUSAND_YEARS = 356 days * 1000; + +uint256 constant MAX_SELLABLE = 6392; + +contract MultiVRGDATest is DSTestPlus { + MockMultiLinearVRGDA vrgda; + uint256 varId; + + function setUp() public { + vrgda = new MockMultiLinearVRGDA(); + varId = vrgda.createVRGDA( + 69.42e18, // Target price. + 0.31e18, // Price decay percent. + 2e18 // Per time unit. + ); + assertEq(varId, 0); + } + + function testTargetPrice() public { + // Warp to the target sale time so that the VRGDA price equals the target price. + hevm.warp(block.timestamp + fromDaysWadUnsafe(vrgda.getTargetSaleTime(varId, 1e18))); + + uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), 0); + assertRelApproxEq(cost, uint256(vrgda.targetPrice(varId)), 0.00001e18); + } + + function testPricingBasic() public { + // Our VRGDA targets this number of mints at given time. + uint256 timeDelta = 120 days; + uint256 numMint = 239; + + hevm.warp(block.timestamp + timeDelta); + + uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), numMint); + assertRelApproxEq(cost, uint256(vrgda.targetPrice(varId)), 0.00001e18); + } + + function testAlwaysTargetPriceInRightConditions(uint256 sold) public { + sold = bound(sold, 0, type(uint128).max); + + assertRelApproxEq( + vrgda.getVRGDAPrice(varId, vrgda.getTargetSaleTime(varId, toWadUnsafe(sold + 1)), sold), + uint256(vrgda.targetPrice(varId)), + 0.00001e18 + ); + } + + function testCreateVRGDA() public { + // creates another VRGDA + varId = vrgda.createVRGDA( + 69.42e18, // Target price. + 0.31e18, // Price decay percent. + 2e18 // Per time unit. + ); + // second created VRGDA will have id 1 + assertEq(varId, 1); + + // Warp to the target sale time so that the VRGDA price equals the target price. + hevm.warp(block.timestamp + fromDaysWadUnsafe(vrgda.getTargetSaleTime(varId, 1e18))); + + uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), 0); + assertRelApproxEq(cost, uint256(vrgda.targetPrice(varId)), 0.00001e18); + } +} diff --git a/test/mocks/MockMultiLinearVRGDA.sol b/test/mocks/MockMultiLinearVRGDA.sol new file mode 100644 index 0000000..e5c910b --- /dev/null +++ b/test/mocks/MockMultiLinearVRGDA.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; +import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe, unsafeWadDiv} from "../../src/utils/SignedWadMath.sol"; +import {VRGDALibrary} from "../../src/lib/VRGDALibrary.sol"; +import {MultiVRGDA} from "../../src/MultiVRGDA.sol"; + +contract MockMultiLinearVRGDA is MultiVRGDA { + + mapping(uint256 => int256) internal perTimeUnits; + + function createVRGDA( + int256 _targetPrice, + int256 _priceDecayPercent, + int256 _perTimeUnit + ) public returns (uint256 varId) { + varId = varCounter; + perTimeUnits[varCounter] = _perTimeUnit; + super.createVRGDA(_targetPrice, _priceDecayPercent); + } + + function getTargetSaleTime(uint256 varId, int256 sold) public view override returns (int256) { + return unsafeWadDiv(sold, perTimeUnits[varId]); + } +} From ed7e1a34311e1a5665f7e14b89c08941d71ab1de Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 6 Sep 2022 22:03:55 -0400 Subject: [PATCH 08/29] compiles --- src/VRGDAStruct.sol | 67 +++++++++++++++++++ src/examples/MultiNFT.sol | 135 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 src/VRGDAStruct.sol create mode 100644 src/examples/MultiNFT.sol diff --git a/src/VRGDAStruct.sol b/src/VRGDAStruct.sol new file mode 100644 index 0000000..0030c5b --- /dev/null +++ b/src/VRGDAStruct.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "./utils/SignedWadMath.sol"; +import {VRGDALibrary} from "./lib/VRGDALibrary.sol"; + +struct VRGDAx { + int256 targetPrice; + int256 decayConstant; + function (int256, int256, int256, uint256) view returns (uint256) getPrice; + function (int256) view returns (int256) getTargetSaleTime; +} + +/// @title Variable Rate Gradual Dutch Auction +/// @author transmissions11 +/// @author FrankieIsLost +/// @author saucepoint +/// @notice Sell tokens roughly according to an issuance schedule. +abstract contract MultiVRGDA { + /*////////////////////////////////////////////////////////////// + VRGDA PARAMETERS + //////////////////////////////////////////////////////////////*/ + + + /// @notice Target price for a token, to be scaled according to sales pace. + /// @dev Represented as an 18 decimal fixed point number. + + /// @dev Precomputed constant that allows us to rewrite a pow() as an exp(). + /// @dev Represented as an 18 decimal fixed point number. + + /// @notice Sets target price and per time unit price decay for the VRGDA. + /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. + /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. + function createVRGDA(int256 _targetPrice, int256 _priceDecayPercent) internal pure returns (VRGDAx memory vrgda) { + int256 _decayConstant = wadLn(1e18 - _priceDecayPercent); + require(_decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); + + vrgda = VRGDAx( + _targetPrice, + _decayConstant, + getVRGDAPrice, + getTargetSaleTime + ); + } + + /*////////////////////////////////////////////////////////////// + PRICING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Calculate the price of a token according to the VRGDA formula. + /// @param timeSinceStart Time passed since the VRGDA began, scaled by 1e18. + /// @param sold The total number of tokens that have been sold so far. + /// @return The price of a token according to VRGDA, scaled by 1e18. + function getVRGDAPrice(int256 targetPrice, int256 decayConstant, int256 timeSinceStart, uint256 sold) public view returns (uint256) { + return VRGDALibrary.getVRGDAPrice(targetPrice, decayConstant, timeSinceStart - getTargetSaleTime(toWadUnsafe(sold + 1))); + } + + /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. + /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. + /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. + function getTargetSaleTime(int256 sold) public view virtual returns (int256); + + function getPrice(VRGDAx memory vrgda, int256 timeSinceStart, uint256 sold) internal view returns (uint256) { + return vrgda.getPrice(vrgda.targetPrice, vrgda.decayConstant, timeSinceStart, sold); + } +} diff --git a/src/examples/MultiNFT.sol b/src/examples/MultiNFT.sol new file mode 100644 index 0000000..1360d74 --- /dev/null +++ b/src/examples/MultiNFT.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {ERC721} from "solmate/tokens/ERC721.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; + +import {toDaysWadUnsafe, toWadUnsafe, unsafeWadDiv, wadLn, unsafeDiv} from "../utils/SignedWadMath.sol"; + +import {MultiVRGDA, VRGDAx} from "../VRGDAStruct.sol"; + +/// @title Multi VRGDA NFT +/// @author transmissions11 +/// @author FrankieIsLost +/// @notice Example NFT sold using LinearVRGDA. +/// @dev This is an example. Do not use in production. +contract MultiNFT is ERC721, MultiVRGDA { + /*////////////////////////////////////////////////////////////// + SALES STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 public totalSold; // The total number of tokens sold so far. + + uint256 public immutable startTime = block.timestamp; // When VRGDA sales begun. + + uint256 public immutable publicStartTime = block.timestamp + 30 days; // When VRGDA sales are public. + + // -- VRGDA Objects -- + VRGDAx internal presaleVRGDA; + VRGDAx internal publicVRGDA; + + // -- Linear VRGDA Params -- + int256 public perTimeUnit; // The number of tokens to target selling in 1 full unit of time, scaled by 1e18. + + // -- Logistic VRGDA Params -- + uint256 public constant MAX_MINTABLE = 100; // Max supply. for logistic VRGDA + int256 public immutable logisticLimit; + int256 public immutable logisticLimitDoubled; + int256 internal immutable timeScale; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor() + ERC721( + "Example Multi VRGDA NFT", // Name. + "mVRGDA" // Symbol. + ) + { + // ------------------------- + // the VRGDA used in presale + // ------------------------- + int256 targetPrice = 69.42e18; + int256 priceDecayPercent = 0.31e18; + perTimeUnit = 2e18; + presaleVRGDA = createVRGDA(targetPrice, priceDecayPercent); + + // ----------------------------- + // the VRGDA used in public sale + // ----------------------------- + targetPrice = 69.42e18; // Target price + priceDecayPercent = 0.31e18; // Price decay percent + + // Maximum # mintable/sellable + // Add 1 wad to make the limit inclusive of _maxSellable + logisticLimit = toWadUnsafe(MAX_MINTABLE) + 1e18; + + // Scale by 2e18 to both double it and give it 36 decimals. + logisticLimitDoubled = logisticLimit * 2e18; + + timeScale = 0.1e18; // Time scale + + publicVRGDA = createVRGDA(targetPrice, priceDecayPercent); + // override the target sale time function + publicVRGDA.getTargetSaleTime = getLogisticTargetSaleTime; + + } + + /*////////////////////////////////////////////////////////////// + VRGDA LOGIC + //////////////////////////////////////////////////////////////*/ + /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. + /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. + /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. + function getTargetSaleTime(int256 sold) public view override returns (int256) { + return unsafeWadDiv(sold, perTimeUnit); + } + + /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. + /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. + /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. + function getLogisticTargetSaleTime(int256 sold) public view returns (int256) { + unchecked { + return -unsafeWadDiv(wadLn(unsafeDiv(logisticLimitDoubled, sold + logisticLimit) - 1e18), timeScale); + } + } + + /*////////////////////////////////////////////////////////////// + MINTING LOGIC + //////////////////////////////////////////////////////////////*/ + + function mint() external payable returns (uint256 mintedId) { + unchecked { + // conditionally set based on time / VRGDA + // i.e. if time < publicStartTime use presaleVRGDA else use publicVRGDA + uint256 price; + + // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. + if (block.timestamp < publicStartTime) { + price = getPrice(presaleVRGDA, toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); + } else { + price = getPrice(publicVRGDA, toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); + } + + require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. + + _mint(msg.sender, mintedId); // Mint the NFT using mintedId. + + // Note: We do this at the end to avoid creating a reentrancy vector. + // Refund the user any ETH they spent over the current price of the NFT. + // Unchecked is safe here because we validate msg.value >= price above. + SafeTransferLib.safeTransferETH(msg.sender, msg.value - price); + } + } + + /*////////////////////////////////////////////////////////////// + URI LOGIC + //////////////////////////////////////////////////////////////*/ + + function tokenURI(uint256) public pure override returns (string memory) { + return "https://example.com"; + } +} From 4a89cefe7cb6fb8c065d0f1329688b88142b888c Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 6 Sep 2022 22:13:41 -0400 Subject: [PATCH 09/29] minor organization changes --- src/VRGDAStruct.sol | 4 ++-- src/examples/MultiNFT.sol | 26 ++++++++++++-------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/VRGDAStruct.sol b/src/VRGDAStruct.sol index 0030c5b..1837d70 100644 --- a/src/VRGDAStruct.sol +++ b/src/VRGDAStruct.sol @@ -7,7 +7,7 @@ import {VRGDALibrary} from "./lib/VRGDALibrary.sol"; struct VRGDAx { int256 targetPrice; int256 decayConstant; - function (int256, int256, int256, uint256) view returns (uint256) getPrice; + function (int256, int256, int256, uint256) view returns (uint256) getVRGDAPrice; function (int256) view returns (int256) getTargetSaleTime; } @@ -62,6 +62,6 @@ abstract contract MultiVRGDA { function getTargetSaleTime(int256 sold) public view virtual returns (int256); function getPrice(VRGDAx memory vrgda, int256 timeSinceStart, uint256 sold) internal view returns (uint256) { - return vrgda.getPrice(vrgda.targetPrice, vrgda.decayConstant, timeSinceStart, sold); + return vrgda.getVRGDAPrice(vrgda.targetPrice, vrgda.decayConstant, timeSinceStart, sold); } } diff --git a/src/examples/MultiNFT.sol b/src/examples/MultiNFT.sol index 1360d74..9b8d099 100644 --- a/src/examples/MultiNFT.sol +++ b/src/examples/MultiNFT.sol @@ -48,32 +48,30 @@ contract MultiNFT is ERC721, MultiVRGDA { ) { // ------------------------- - // the VRGDA used in presale + // create a VRGDA to be used in presale // ------------------------- - int256 targetPrice = 69.42e18; - int256 priceDecayPercent = 0.31e18; - perTimeUnit = 2e18; - presaleVRGDA = createVRGDA(targetPrice, priceDecayPercent); + presaleVRGDA = createVRGDA(69.42e18, 0.31e18); + perTimeUnit = 2e18; // additional state variable used for presaleVRGDA.getTargetSaleTime (this.getTargetSaleTime) // ----------------------------- - // the VRGDA used in public sale + // create a VRGDA to used in public sale + // note: we can reuse presaleVRGDA and overwrite the functions since they are used during different times + // however for the sake of example, let's define two independent VRGDAs // ----------------------------- - targetPrice = 69.42e18; // Target price - priceDecayPercent = 0.31e18; // Price decay percent + publicVRGDA = createVRGDA(69.42e18, 0.31e18); - // Maximum # mintable/sellable + // Set additional state variables used for publicVRGDA.getTargetSaleTime (this.getLogisticTargetSaleTime) // Add 1 wad to make the limit inclusive of _maxSellable logisticLimit = toWadUnsafe(MAX_MINTABLE) + 1e18; // Scale by 2e18 to both double it and give it 36 decimals. logisticLimitDoubled = logisticLimit * 2e18; - timeScale = 0.1e18; // Time scale - - publicVRGDA = createVRGDA(targetPrice, priceDecayPercent); - // override the target sale time function - publicVRGDA.getTargetSaleTime = getLogisticTargetSaleTime; + // Time scale + timeScale = 0.1e18; + // override the target sale time function to use a logistic function + publicVRGDA.getTargetSaleTime = getLogisticTargetSaleTime; } /*////////////////////////////////////////////////////////////// From aa60d7447311166fb27c208f719e353ad28c4ccb Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 6 Sep 2022 23:16:17 -0400 Subject: [PATCH 10/29] bug squash, tests, and simplification of code --- src/VRGDAStruct.sol | 19 +++++----- src/examples/MultiNFT.sol | 10 ++--- test/MultiNFT.t.sol | 80 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 test/MultiNFT.t.sol diff --git a/src/VRGDAStruct.sol b/src/VRGDAStruct.sol index 1837d70..e4c6c63 100644 --- a/src/VRGDAStruct.sol +++ b/src/VRGDAStruct.sol @@ -7,7 +7,6 @@ import {VRGDALibrary} from "./lib/VRGDALibrary.sol"; struct VRGDAx { int256 targetPrice; int256 decayConstant; - function (int256, int256, int256, uint256) view returns (uint256) getVRGDAPrice; function (int256) view returns (int256) getTargetSaleTime; } @@ -38,7 +37,6 @@ abstract contract MultiVRGDA { vrgda = VRGDAx( _targetPrice, _decayConstant, - getVRGDAPrice, getTargetSaleTime ); } @@ -48,20 +46,21 @@ abstract contract MultiVRGDA { //////////////////////////////////////////////////////////////*/ /// @notice Calculate the price of a token according to the VRGDA formula. + /// @param vrgda a VRGDAx struct representing a VRGDA instance /// @param timeSinceStart Time passed since the VRGDA began, scaled by 1e18. /// @param sold The total number of tokens that have been sold so far. - /// @return The price of a token according to VRGDA, scaled by 1e18. - function getVRGDAPrice(int256 targetPrice, int256 decayConstant, int256 timeSinceStart, uint256 sold) public view returns (uint256) { - return VRGDALibrary.getVRGDAPrice(targetPrice, decayConstant, timeSinceStart - getTargetSaleTime(toWadUnsafe(sold + 1))); + /// @return uint256 the price of a token according to VRGDA, scaled by 1e18. + function getVRGDAPrice(VRGDAx memory vrgda, int256 timeSinceStart, uint256 sold) internal view returns (uint256) { + return VRGDALibrary.getVRGDAPrice( + vrgda.targetPrice, + vrgda.decayConstant, + timeSinceStart - vrgda.getTargetSaleTime(toWadUnsafe(sold + 1)) + ); } /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// @return int256 The target time the tokens should be sold by, scaled by 1e18, where the time is /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. function getTargetSaleTime(int256 sold) public view virtual returns (int256); - - function getPrice(VRGDAx memory vrgda, int256 timeSinceStart, uint256 sold) internal view returns (uint256) { - return vrgda.getVRGDAPrice(vrgda.targetPrice, vrgda.decayConstant, timeSinceStart, sold); - } } diff --git a/src/examples/MultiNFT.sol b/src/examples/MultiNFT.sol index 9b8d099..6e094a9 100644 --- a/src/examples/MultiNFT.sol +++ b/src/examples/MultiNFT.sol @@ -20,9 +20,9 @@ contract MultiNFT is ERC721, MultiVRGDA { uint256 public totalSold; // The total number of tokens sold so far. - uint256 public immutable startTime = block.timestamp; // When VRGDA sales begun. + uint256 public immutable startTime = block.timestamp; // When Linear VRGDA sales begin. - uint256 public immutable publicStartTime = block.timestamp + 30 days; // When VRGDA sales are public. + uint256 public immutable publicStartTime = block.timestamp + 30 days; // When Logistic VRGDA sales begin. // -- VRGDA Objects -- VRGDAx internal presaleVRGDA; @@ -101,15 +101,15 @@ contract MultiNFT is ERC721, MultiVRGDA { function mint() external payable returns (uint256 mintedId) { unchecked { - // conditionally set based on time / VRGDA + // conditionally set price based on time + VRGDA // i.e. if time < publicStartTime use presaleVRGDA else use publicVRGDA uint256 price; // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. if (block.timestamp < publicStartTime) { - price = getPrice(presaleVRGDA, toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); + price = getVRGDAPrice(presaleVRGDA, toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); } else { - price = getPrice(publicVRGDA, toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); + price = getVRGDAPrice(publicVRGDA, toDaysWadUnsafe(block.timestamp - publicStartTime), mintedId = totalSold++); } require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. diff --git a/test/MultiNFT.t.sol b/test/MultiNFT.t.sol new file mode 100644 index 0000000..4e7c702 --- /dev/null +++ b/test/MultiNFT.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; +import {fromDaysWadUnsafe} from "../src/utils/SignedWadMath.sol"; +import {MultiNFT} from "../src/examples/MultiNFT.sol"; + +contract MultiNFTTest is DSTestPlus { + MultiNFT nft; + + function setUp() public { + nft = new MultiNFT(); + } + + // ------------------------------------------------------------------- + // test the Linear VRGDA that's used during "presale" (first 30 days) + // ------------------------------------------------------------------- + function testMintPresaleNFT() public { + nft.mint{value: 83.571859212140979125e18}(); + + assertEq(nft.balanceOf(address(this)), 1); + assertEq(nft.ownerOf(0), address(this)); + } + + function testCannotUnderpayForPresaleNFTMint() public { + hevm.expectRevert("UNDERPAID"); + nft.mint{value: 83e18}(); + } + + function testMintManyPresaleNFT() public { + for (uint256 i = 0; i < 100; i++) { + nft.mint{value: address(this).balance}(); + } + + assertEq(nft.balanceOf(address(this)), 100); + for (uint256 i = 0; i < 100; i++) { + assertEq(nft.ownerOf(i), address(this)); + } + } + + // ------------------------------------------------------------------------- + // test the Logistic VRGDA that's used during "public sale" (after 30 days) + // ------------------------------------------------------------------------- + function testMintPublicNFT() public { + // Warp to the target sale time so that the VRGDA price equals the target price. + hevm.warp(nft.publicStartTime()); + + nft.mint{value: 74.713094276091397864e18}(); + + assertEq(nft.balanceOf(address(this)), 1); + assertEq(nft.ownerOf(0), address(this)); + } + + function testCannotUnderpayForPublicNFTMint() public { + hevm.warp(nft.publicStartTime()); + hevm.expectRevert("UNDERPAID"); + nft.mint{value: 74e18}(); + } + + function testMintAllPublicNFT() public { + hevm.warp(nft.publicStartTime()); + for (uint256 i = 0; i < nft.MAX_MINTABLE(); i++) { + nft.mint{value: address(this).balance}(); + } + + assertEq(nft.balanceOf(address(this)), nft.MAX_MINTABLE()); + for (uint256 i = 0; i < nft.MAX_MINTABLE(); i++) { + assertEq(nft.ownerOf(i), address(this)); + } + } + + function testCannotPublicMintMoreThanMax() public { + testMintAllPublicNFT(); + + hevm.expectRevert("UNDEFINED"); + nft.mint{value: address(this).balance}(); + } + + receive() external payable {} +} From 946669f4637a11c8111be0a54edf38aec9d31cd5 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 6 Sep 2022 23:32:04 -0400 Subject: [PATCH 11/29] documentation --- src/MultiVRGDA.sol | 16 ++++++++++++++++ src/VRGDAStruct.sol | 24 +++++++++--------------- src/examples/MultiNFT.sol | 19 ++++++++++--------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/MultiVRGDA.sol b/src/MultiVRGDA.sol index 44bde8d..b3ea450 100644 --- a/src/MultiVRGDA.sol +++ b/src/MultiVRGDA.sol @@ -4,6 +4,22 @@ pragma solidity >=0.8.0; import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "./utils/SignedWadMath.sol"; import {VRGDALibrary} from "./lib/VRGDALibrary.sol"; +/// ----------------------------------------------------------------- +/// NOTE: this is what a multi-VRGDA via state would look like +/// Most likely gonna trash this, but leaving it for reference & discussion +/// +/// Pros: +/// * abstract contract with flexibility to create multiple VRGDA instances +/// * identifies a VRGDA via a unique id +/// * single devex for creating VRGDA instances (createVRGDA) +/// * getTargetSaleTime can condition on `varId` to allow for independent logic per VRGDA +/// +/// Cons: +/// * uses state, which i know you didn't like +/// * assume LinearVRGDA and LogisiticVRGDA inherits MultiVRGDA, a contract cannot inherit from both Linear & Logistic +/// * getTargetSaleTime will likely devolve into an ugly switch statement +/// * devs need to keep track of VRGDA ids. i.e. VRGDA id=0 is resource x and id=1 is resource y + /// @title Variable Rate Gradual Dutch Auction /// @author transmissions11 /// @author FrankieIsLost diff --git a/src/VRGDAStruct.sol b/src/VRGDAStruct.sol index e4c6c63..ed8b8a6 100644 --- a/src/VRGDAStruct.sol +++ b/src/VRGDAStruct.sol @@ -4,6 +4,8 @@ pragma solidity >=0.8.0; import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "./utils/SignedWadMath.sol"; import {VRGDALibrary} from "./lib/VRGDALibrary.sol"; +// TODO: rename to something better? +// VRGDAx sounds badass tho struct VRGDAx { int256 targetPrice; int256 decayConstant; @@ -16,27 +18,19 @@ struct VRGDAx { /// @author saucepoint /// @notice Sell tokens roughly according to an issuance schedule. abstract contract MultiVRGDA { - /*////////////////////////////////////////////////////////////// - VRGDA PARAMETERS - //////////////////////////////////////////////////////////////*/ - - - /// @notice Target price for a token, to be scaled according to sales pace. - /// @dev Represented as an 18 decimal fixed point number. - - /// @dev Precomputed constant that allows us to rewrite a pow() as an exp(). - /// @dev Represented as an 18 decimal fixed point number. + /// look bro, no state 😈 - /// @notice Sets target price and per time unit price decay for the VRGDA. - /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. - /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. + /// @notice Sets target price and per time unit price decay for a VRGDA instance. + /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. (18 decimal fixed point number) + /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. (18 decimal fixed point number) function createVRGDA(int256 _targetPrice, int256 _priceDecayPercent) internal pure returns (VRGDAx memory vrgda) { + // Precomputed constant that allows us to rewrite a pow() as an exp(). int256 _decayConstant = wadLn(1e18 - _priceDecayPercent); require(_decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); vrgda = VRGDAx( - _targetPrice, - _decayConstant, + _targetPrice, // Target price for a token, to be scaled according to sales pace. + _decayConstant, // Precomputed constant that allows us to rewrite a pow() as an exp(). getTargetSaleTime ); } diff --git a/src/examples/MultiNFT.sol b/src/examples/MultiNFT.sol index 6e094a9..57a68a5 100644 --- a/src/examples/MultiNFT.sol +++ b/src/examples/MultiNFT.sol @@ -11,7 +11,7 @@ import {MultiVRGDA, VRGDAx} from "../VRGDAStruct.sol"; /// @title Multi VRGDA NFT /// @author transmissions11 /// @author FrankieIsLost -/// @notice Example NFT sold using LinearVRGDA. +/// @notice Example NFT sold using BOTH Linear VRGDA and Logistic VRGDA. /// @dev This is an example. Do not use in production. contract MultiNFT is ERC721, MultiVRGDA { /*////////////////////////////////////////////////////////////// @@ -51,16 +51,16 @@ contract MultiNFT is ERC721, MultiVRGDA { // create a VRGDA to be used in presale // ------------------------- presaleVRGDA = createVRGDA(69.42e18, 0.31e18); - perTimeUnit = 2e18; // additional state variable used for presaleVRGDA.getTargetSaleTime (this.getTargetSaleTime) + perTimeUnit = 2e18; // additional state variable used for presaleVRGDA.getTargetSaleTime which points to this.getTargetSaleTime // ----------------------------- // create a VRGDA to used in public sale - // note: we can reuse presaleVRGDA and overwrite the functions since they are used during different times - // however for the sake of example, let's define two independent VRGDAs + // note: we can reuse presaleVRGDA and overwrite the .getTargetSaleTime since + // they are used during different times. However for the sake of example, let's define two independent VRGDAs // ----------------------------- publicVRGDA = createVRGDA(69.42e18, 0.31e18); - // Set additional state variables used for publicVRGDA.getTargetSaleTime (this.getLogisticTargetSaleTime) + // Set additional state variables used for publicVRGDA.getTargetSaleTime which points to this.getLogisticTargetSaleTime // Add 1 wad to make the limit inclusive of _maxSellable logisticLimit = toWadUnsafe(MAX_MINTABLE) + 1e18; @@ -79,15 +79,16 @@ contract MultiNFT is ERC721, MultiVRGDA { //////////////////////////////////////////////////////////////*/ /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// @return int256 the target time the tokens should be sold by, scaled by 1e18, where the time is /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. function getTargetSaleTime(int256 sold) public view override returns (int256) { return unsafeWadDiv(sold, perTimeUnit); } - /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. + /// @dev Logistic counterpart to the linear getTargetSaleTime + /// will be assigned to a VRGDAx's .getTargetSaleTime attribute /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// @return int256 The target time the tokens should be sold by, scaled by 1e18, where the time is /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. function getLogisticTargetSaleTime(int256 sold) public view returns (int256) { unchecked { @@ -105,8 +106,8 @@ contract MultiNFT is ERC721, MultiVRGDA { // i.e. if time < publicStartTime use presaleVRGDA else use publicVRGDA uint256 price; - // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. if (block.timestamp < publicStartTime) { + // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. price = getVRGDAPrice(presaleVRGDA, toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); } else { price = getVRGDAPrice(publicVRGDA, toDaysWadUnsafe(block.timestamp - publicStartTime), mintedId = totalSold++); From 24b509add9c629ee10f4e4a32ab6e94f22486466 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 6 Sep 2022 23:36:00 -0400 Subject: [PATCH 12/29] revert state based multi vrgda --- src/LinearVRGDA.sol | 16 ------- src/LogisticVRGDA.sol | 28 ----------- src/VRGDA.sol | 27 ----------- test/LinearVRGDA.t.sol | 55 ---------------------- test/LogisticVRGDA.t.sol | 39 --------------- test/MultiVRGDA.t.sol | 73 ----------------------------- test/mocks/MockMultiLinearVRGDA.sol | 4 ++ 7 files changed, 4 insertions(+), 238 deletions(-) delete mode 100644 test/MultiVRGDA.t.sol diff --git a/src/LinearVRGDA.sol b/src/LinearVRGDA.sol index 854e5e4..814d060 100644 --- a/src/LinearVRGDA.sol +++ b/src/LinearVRGDA.sol @@ -18,8 +18,6 @@ abstract contract LinearVRGDA is VRGDA { /// @dev Represented as an 18 decimal fixed point number. int256 internal immutable perTimeUnit; - mapping(uint256 => int256) internal perTimeUnits; - /// @notice Sets pricing parameters for the VRGDA. /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. @@ -32,16 +30,6 @@ abstract contract LinearVRGDA is VRGDA { perTimeUnit = _perTimeUnit; } - function createLinearVRGDA( - int256 _targetPrice, - int256 _priceDecayPercent, - int256 _perTimeUnit - ) public returns (uint256 varId) { - varId = varCounter; - perTimeUnits[varCounter] = _perTimeUnit; - createVRGDA(_targetPrice, _priceDecayPercent); - } - /*////////////////////////////////////////////////////////////// PRICING LOGIC //////////////////////////////////////////////////////////////*/ @@ -53,8 +41,4 @@ abstract contract LinearVRGDA is VRGDA { function getTargetSaleTime(int256 sold) public view override returns (int256) { return unsafeWadDiv(sold, perTimeUnit); } - - function getTargetSaleTime(uint256 varId, int256 sold) public view override returns (int256) { - return unsafeWadDiv(sold, perTimeUnits[varId]); - } } diff --git a/src/LogisticVRGDA.sol b/src/LogisticVRGDA.sol index 61cfe54..2afff17 100644 --- a/src/LogisticVRGDA.sol +++ b/src/LogisticVRGDA.sol @@ -18,19 +18,16 @@ abstract contract LogisticVRGDA is VRGDA { /// 1 because the logistic function will never fully reach its limit. /// @dev Represented as an 18 decimal fixed point number. int256 public immutable logisticLimit; - mapping(uint256 => int256) public logisticLimits; /// @dev The maximum number of tokens of tokens to sell + 1 multiplied /// by 2. We could compute it on the fly each time but this saves gas. /// @dev Represented as a 36 decimal fixed point number. int256 public immutable logisticLimitDoubled; - mapping(uint256 => int256) public logisticLimitDoubles; /// @dev Time scale controls the steepness of the logistic curve, /// which affects how quickly we will reach the curve's asymptote. /// @dev Represented as an 18 decimal fixed point number. int256 internal immutable timeScale; - mapping(uint256 => int256) public timeScales; /// @notice Sets pricing parameters for the VRGDA. /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. @@ -52,25 +49,6 @@ abstract contract LogisticVRGDA is VRGDA { timeScale = _timeScale; } - function createLogisticVRGDA( - int256 _targetPrice, - int256 _priceDecayPercent, - int256 _maxSellable, - int256 _timeScale - ) public returns (uint256 varId) { - varId = varCounter; - - // Add 1 wad to make the limit inclusive of _maxSellable. - logisticLimits[varCounter] = _maxSellable + 1e18; - - // Scale by 2e18 to both double it and give it 36 decimals. - logisticLimitDoubles[varCounter] = logisticLimits[varCounter] * 2e18; - - timeScales[varCounter] = _timeScale; - - createVRGDA(_targetPrice, _priceDecayPercent); - } - /*////////////////////////////////////////////////////////////// PRICING LOGIC //////////////////////////////////////////////////////////////*/ @@ -84,10 +62,4 @@ abstract contract LogisticVRGDA is VRGDA { return -unsafeWadDiv(wadLn(unsafeDiv(logisticLimitDoubled, sold + logisticLimit) - 1e18), timeScale); } } - - function getTargetSaleTime(uint256 varId, int256 sold) public view override returns (int256) { - unchecked { - return -unsafeWadDiv(wadLn(unsafeDiv(logisticLimitDoubles[varId], sold + logisticLimits[varId]) - 1e18), timeScales[varId]); - } - } } diff --git a/src/VRGDA.sol b/src/VRGDA.sol index e6b3b2f..4cd94d3 100644 --- a/src/VRGDA.sol +++ b/src/VRGDA.sol @@ -12,7 +12,6 @@ abstract contract VRGDA { /*////////////////////////////////////////////////////////////// VRGDA PARAMETERS //////////////////////////////////////////////////////////////*/ - uint256 varCounter; /// @notice Target price for a token, to be scaled according to sales pace. /// @dev Represented as an 18 decimal fixed point number. @@ -22,9 +21,6 @@ abstract contract VRGDA { /// @dev Represented as an 18 decimal fixed point number. int256 internal immutable decayConstant; - mapping(uint256 => int256) public targetPrices; - mapping(uint256 => int256) public decayConstants; - /// @notice Sets target price and per time unit price decay for the VRGDA. /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. @@ -37,15 +33,6 @@ abstract contract VRGDA { require(decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); } - function createVRGDA(int256 _targetPrice, int256 _priceDecayPercent) public { - targetPrices[varCounter] = _targetPrice; - int256 _decayConstant = wadLn(1e18 - _priceDecayPercent); - require(_decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); - decayConstants[varCounter] = _decayConstant; - - unchecked { varCounter++; } - } - /*////////////////////////////////////////////////////////////// PRICING LOGIC //////////////////////////////////////////////////////////////*/ @@ -58,23 +45,9 @@ abstract contract VRGDA { return VRGDALibrary.getVRGDAPrice(targetPrice, decayConstant, timeSinceStart - getTargetSaleTime(toWadUnsafe(sold + 1))); } - function getVRGDAPrice(uint256 varId, int256 timeSinceStart, uint256 sold) public view returns (uint256) { - unchecked { - // prettier-ignore - return uint256(wadMul(targetPrices[varId], wadExp(unsafeWadMul(decayConstants[varId], - // Theoretically calling toWadUnsafe with sold can silently overflow but under - // any reasonable circumstance it will never be large enough. We use sold + 1 as - // the VRGDA formula's n param represents the nth token and sold is the n-1th token. - timeSinceStart - getTargetSaleTime(varId, toWadUnsafe(sold + 1)) - )))); - } - } - /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. function getTargetSaleTime(int256 sold) public view virtual returns (int256); - - function getTargetSaleTime(uint256 varId, int256 sold) public view virtual returns (int256); } diff --git a/test/LinearVRGDA.t.sol b/test/LinearVRGDA.t.sol index 3f4a9cd..c106032 100644 --- a/test/LinearVRGDA.t.sol +++ b/test/LinearVRGDA.t.sol @@ -50,59 +50,4 @@ contract LinearVRGDATest is DSTestPlus { 0.00001e18 ); } - - function testCreateVRGDA() public { - // Warp to the target sale time so that the VRGDA price equals the target price. - hevm.warp(block.timestamp + fromDaysWadUnsafe(vrgda.getTargetSaleTime(1e18))); - - uint256 varId = vrgda.createLinearVRGDA( - 69.42e18, // Target price. - 0.31e18, // Price decay percent. - 2e18 // Per time unit. - ); - // first created VRGDA will have id 0 - assertEq(varId, 0); - - uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), 0); - assertRelApproxEq(cost, uint256(vrgda.targetPrices(varId)), 0.00001e18); - } - - function testPricingBasicCreatedVar() public { - // create an unused VRGDA variable - vrgda.createLinearVRGDA(0, 1, 0); - - uint256 varId = vrgda.createLinearVRGDA( - 69.42e18, // Target price. - 0.31e18, // Price decay percent. - 2e18 // Per time unit. - ); - - assertEq(varId, 1); // second VRGDA variable created - - // Our VRGDA targets this number of mints at given time. - uint256 timeDelta = 120 days; - uint256 numMint = 239; - - hevm.warp(block.timestamp + timeDelta); - - uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), numMint); - assertRelApproxEq(cost, uint256(vrgda.targetPrice()), 0.00001e18); - } - - function testAlwaysTargetPriceInRightConditionsCreated(uint256 sold) public { - uint256 varId = vrgda.createLinearVRGDA( - 69.42e18, // Target price. - 0.31e18, // Price decay percent. - 2e18 // Per time unit. - ); - // first created VRGDA will have id 0 - assertEq(varId, 0); - sold = bound(sold, 0, type(uint128).max); - - assertRelApproxEq( - vrgda.getVRGDAPrice(varId, vrgda.getTargetSaleTime(varId, toWadUnsafe(sold + 1)), sold), - uint256(vrgda.targetPrices(varId)), - 0.00001e18 - ); - } } diff --git a/test/LogisticVRGDA.t.sol b/test/LogisticVRGDA.t.sol index 9b57a4f..99fd031 100644 --- a/test/LogisticVRGDA.t.sol +++ b/test/LogisticVRGDA.t.sol @@ -82,43 +82,4 @@ contract LogisticVRGDATest is DSTestPlus { 0.00001e18 ); } - - function testTargetPriceCreated() public { - uint256 varId = vrgda.createLogisticVRGDA( - 69.42e18, // Target price. - 0.31e18, // Price decay percent. - toWadUnsafe(MAX_SELLABLE), // Max sellable. - 0.0023e18 // Time scale. - ); - assertEq(varId, 0); - - // Warp to the target sale time so that the VRGDA price equals the target price. - hevm.warp(block.timestamp + fromDaysWadUnsafe(vrgda.getTargetSaleTime(varId, 1e18))); - - uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), 0); - assertRelApproxEq(cost, uint256(vrgda.targetPrices(varId)), 0.0000001e18); - } - - function testPricingBasicCreated() public { - // create an unused VRGDA so we can test the second one behaves as expected - vrgda.createLogisticVRGDA(0, 1, 0, 0); - uint256 varId = vrgda.createLogisticVRGDA( - 69.42e18, // Target price. - 0.31e18, // Price decay percent. - toWadUnsafe(MAX_SELLABLE), // Max sellable. - 0.0023e18 // Time scale. - ); - assertEq(varId, 1); - - // Our VRGDA targets this number of mints at given time. - uint256 timeDelta = 120 days; - uint256 numMint = 876; - - hevm.warp(block.timestamp + timeDelta); - - uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), numMint); - - // Equal within 2 percent since num mint is rounded from true decimal amount. - assertRelApproxEq(cost, uint256(vrgda.targetPrices(varId)), 0.02e18); - } } diff --git a/test/MultiVRGDA.t.sol b/test/MultiVRGDA.t.sol deleted file mode 100644 index 6676ddf..0000000 --- a/test/MultiVRGDA.t.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; - -import {toWadUnsafe, toDaysWadUnsafe, fromDaysWadUnsafe} from "../src/utils/SignedWadMath.sol"; - -import {MockMultiLinearVRGDA} from "./mocks/MockMultiLinearVRGDA.sol"; - -uint256 constant ONE_THOUSAND_YEARS = 356 days * 1000; - -uint256 constant MAX_SELLABLE = 6392; - -contract MultiVRGDATest is DSTestPlus { - MockMultiLinearVRGDA vrgda; - uint256 varId; - - function setUp() public { - vrgda = new MockMultiLinearVRGDA(); - varId = vrgda.createVRGDA( - 69.42e18, // Target price. - 0.31e18, // Price decay percent. - 2e18 // Per time unit. - ); - assertEq(varId, 0); - } - - function testTargetPrice() public { - // Warp to the target sale time so that the VRGDA price equals the target price. - hevm.warp(block.timestamp + fromDaysWadUnsafe(vrgda.getTargetSaleTime(varId, 1e18))); - - uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), 0); - assertRelApproxEq(cost, uint256(vrgda.targetPrice(varId)), 0.00001e18); - } - - function testPricingBasic() public { - // Our VRGDA targets this number of mints at given time. - uint256 timeDelta = 120 days; - uint256 numMint = 239; - - hevm.warp(block.timestamp + timeDelta); - - uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), numMint); - assertRelApproxEq(cost, uint256(vrgda.targetPrice(varId)), 0.00001e18); - } - - function testAlwaysTargetPriceInRightConditions(uint256 sold) public { - sold = bound(sold, 0, type(uint128).max); - - assertRelApproxEq( - vrgda.getVRGDAPrice(varId, vrgda.getTargetSaleTime(varId, toWadUnsafe(sold + 1)), sold), - uint256(vrgda.targetPrice(varId)), - 0.00001e18 - ); - } - - function testCreateVRGDA() public { - // creates another VRGDA - varId = vrgda.createVRGDA( - 69.42e18, // Target price. - 0.31e18, // Price decay percent. - 2e18 // Per time unit. - ); - // second created VRGDA will have id 1 - assertEq(varId, 1); - - // Warp to the target sale time so that the VRGDA price equals the target price. - hevm.warp(block.timestamp + fromDaysWadUnsafe(vrgda.getTargetSaleTime(varId, 1e18))); - - uint256 cost = vrgda.getVRGDAPrice(varId, toDaysWadUnsafe(block.timestamp), 0); - assertRelApproxEq(cost, uint256(vrgda.targetPrice(varId)), 0.00001e18); - } -} diff --git a/test/mocks/MockMultiLinearVRGDA.sol b/test/mocks/MockMultiLinearVRGDA.sol index e5c910b..a0b4850 100644 --- a/test/mocks/MockMultiLinearVRGDA.sol +++ b/test/mocks/MockMultiLinearVRGDA.sol @@ -1,4 +1,8 @@ // SPDX-License-Identifier: MIT + +/// NOTE: an example contract that inherits MultiVRGDA +/// which will be likely trashed. included for discussion & reference + pragma solidity >=0.8.0; import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe, unsafeWadDiv} from "../../src/utils/SignedWadMath.sol"; import {VRGDALibrary} from "../../src/lib/VRGDALibrary.sol"; From 812e94e45a0316a84704309149d643bc2d1bbb56 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Tue, 6 Sep 2022 23:56:12 -0400 Subject: [PATCH 13/29] remove state based multiVRGDA --- src/MultiVRGDA.sol | 73 ----------------------------- test/mocks/MockMultiLinearVRGDA.sol | 28 ----------- 2 files changed, 101 deletions(-) delete mode 100644 src/MultiVRGDA.sol delete mode 100644 test/mocks/MockMultiLinearVRGDA.sol diff --git a/src/MultiVRGDA.sol b/src/MultiVRGDA.sol deleted file mode 100644 index b3ea450..0000000 --- a/src/MultiVRGDA.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "./utils/SignedWadMath.sol"; -import {VRGDALibrary} from "./lib/VRGDALibrary.sol"; - -/// ----------------------------------------------------------------- -/// NOTE: this is what a multi-VRGDA via state would look like -/// Most likely gonna trash this, but leaving it for reference & discussion -/// -/// Pros: -/// * abstract contract with flexibility to create multiple VRGDA instances -/// * identifies a VRGDA via a unique id -/// * single devex for creating VRGDA instances (createVRGDA) -/// * getTargetSaleTime can condition on `varId` to allow for independent logic per VRGDA -/// -/// Cons: -/// * uses state, which i know you didn't like -/// * assume LinearVRGDA and LogisiticVRGDA inherits MultiVRGDA, a contract cannot inherit from both Linear & Logistic -/// * getTargetSaleTime will likely devolve into an ugly switch statement -/// * devs need to keep track of VRGDA ids. i.e. VRGDA id=0 is resource x and id=1 is resource y - -/// @title Variable Rate Gradual Dutch Auction -/// @author transmissions11 -/// @author FrankieIsLost -/// @author saucepoint -/// @notice Sell tokens roughly according to an issuance schedule. -abstract contract MultiVRGDA { - /*////////////////////////////////////////////////////////////// - VRGDA PARAMETERS - //////////////////////////////////////////////////////////////*/ - uint256 public varCounter; - - /// @notice Target price for a token, to be scaled according to sales pace. - /// @dev Represented as an 18 decimal fixed point number. - // maps variable Id to target price - mapping(uint256 => int256) public targetPrice; - - /// @dev Precomputed constant that allows us to rewrite a pow() as an exp(). - /// @dev Represented as an 18 decimal fixed point number. - // maps variable Id to decay constant - mapping(uint256 => int256) public decayConstant; - - /// @notice Sets target price and per time unit price decay for the VRGDA. - /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. - /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. - function createVRGDA(int256 _targetPrice, int256 _priceDecayPercent) public { - targetPrice[varCounter] = _targetPrice; - int256 _decayConstant = wadLn(1e18 - _priceDecayPercent); - require(_decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); - decayConstant[varCounter] = _decayConstant; - - unchecked { varCounter++; } - } - - /*////////////////////////////////////////////////////////////// - PRICING LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @notice Calculate the price of a token according to the VRGDA formula. - /// @param timeSinceStart Time passed since the VRGDA began, scaled by 1e18. - /// @param sold The total number of tokens that have been sold so far. - /// @return The price of a token according to VRGDA, scaled by 1e18. - function getVRGDAPrice(uint256 varId, int256 timeSinceStart, uint256 sold) public view returns (uint256) { - return VRGDALibrary.getVRGDAPrice(targetPrice[varId], decayConstant[varId], timeSinceStart - getTargetSaleTime(varId, toWadUnsafe(sold + 1))); - } - - /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. - /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is - /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. - function getTargetSaleTime(uint256 varId, int256 sold) public view virtual returns (int256); -} diff --git a/test/mocks/MockMultiLinearVRGDA.sol b/test/mocks/MockMultiLinearVRGDA.sol deleted file mode 100644 index a0b4850..0000000 --- a/test/mocks/MockMultiLinearVRGDA.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT - -/// NOTE: an example contract that inherits MultiVRGDA -/// which will be likely trashed. included for discussion & reference - -pragma solidity >=0.8.0; -import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe, unsafeWadDiv} from "../../src/utils/SignedWadMath.sol"; -import {VRGDALibrary} from "../../src/lib/VRGDALibrary.sol"; -import {MultiVRGDA} from "../../src/MultiVRGDA.sol"; - -contract MockMultiLinearVRGDA is MultiVRGDA { - - mapping(uint256 => int256) internal perTimeUnits; - - function createVRGDA( - int256 _targetPrice, - int256 _priceDecayPercent, - int256 _perTimeUnit - ) public returns (uint256 varId) { - varId = varCounter; - perTimeUnits[varCounter] = _perTimeUnit; - super.createVRGDA(_targetPrice, _priceDecayPercent); - } - - function getTargetSaleTime(uint256 varId, int256 sold) public view override returns (int256) { - return unsafeWadDiv(sold, perTimeUnits[varId]); - } -} From a23296fa7847cf7b5a023f2c2da3b15c8b6b744a Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 7 Sep 2022 00:33:56 -0400 Subject: [PATCH 14/29] gas snapshot --- .gas-snapshot | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 8f2e464..5b256f0 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,18 +1,25 @@ -LinearNFTTest:testCannotUnderpayForNFTMint() (gas: 37841) -LinearNFTTest:testMintManyNFT() (gas: 4177040) -LinearNFTTest:testMintNFT() (gas: 83907) -LinearVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 10109, ~: 10109) -LinearVRGDATest:testPricingBasic() (gas: 9972) -LinearVRGDATest:testTargetPrice() (gas: 10749) -LogisticNFTTest:testCannotMintMoreThanMax() (gas: 4409801) -LogisticNFTTest:testCannotUnderpayForNFTMint() (gas: 38508) -LogisticNFTTest:testMintAllNFT() (gas: 4399038) -LogisticNFTTest:testMintNFT() (gas: 84596) -LogisticVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 11692, ~: 11692) -LogisticVRGDATest:testFailOverflowForBeyondLimitTokens(uint256,uint256) (runs: 256, μ: 10278, ~: 10278) +LinearNFTTest:testCannotUnderpayForNFTMint() (gas: 38074) +LinearNFTTest:testMintManyNFT() (gas: 4200340) +LinearNFTTest:testMintNFT() (gas: 84140) +LinearVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 10392, ~: 10392) +LinearVRGDATest:testPricingBasic() (gas: 10255) +LinearVRGDATest:testTargetPrice() (gas: 11032) +LogisticNFTTest:testCannotMintMoreThanMax() (gas: 4433171) +LogisticNFTTest:testCannotUnderpayForNFTMint() (gas: 38741) +LogisticNFTTest:testMintAllNFT() (gas: 4422338) +LogisticNFTTest:testMintNFT() (gas: 84829) +LogisticVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 11925, ~: 11925) +LogisticVRGDATest:testFailOverflowForBeyondLimitTokens(uint256,uint256) (runs: 256, μ: 10348, ~: 10348) LogisticVRGDATest:testGetTargetSaleTimeDoesNotRevertEarly() (gas: 6147) LogisticVRGDATest:testGetTargetSaleTimeRevertsWhenExpected() (gas: 8533) -LogisticVRGDATest:testNoOverflowForAllTokens(uint256,uint256) (runs: 256, μ: 11229, ~: 11229) -LogisticVRGDATest:testNoOverflowForMostTokens(uint256,uint256) (runs: 256, μ: 11385, ~: 11528) -LogisticVRGDATest:testPricingBasic() (gas: 10762) -LogisticVRGDATest:testTargetPrice() (gas: 12246) +LogisticVRGDATest:testNoOverflowForAllTokens(uint256,uint256) (runs: 256, μ: 11462, ~: 11462) +LogisticVRGDATest:testNoOverflowForMostTokens(uint256,uint256) (runs: 256, μ: 11635, ~: 11761) +LogisticVRGDATest:testPricingBasic() (gas: 10995) +LogisticVRGDATest:testTargetPrice() (gas: 12479) +MultiNFTTest:testCannotPublicMintMoreThanMax() (gas: 4486412) +MultiNFTTest:testCannotUnderpayForPresaleNFTMint() (gas: 46730) +MultiNFTTest:testCannotUnderpayForPublicNFTMint() (gas: 46405) +MultiNFTTest:testMintAllPublicNFT() (gas: 4477594) +MultiNFTTest:testMintManyPresaleNFT() (gas: 4269662) +MultiNFTTest:testMintPresaleNFT() (gas: 92830) +MultiNFTTest:testMintPublicNFT() (gas: 94982) From 6b50ad74b0798d9ef54af42144f17f434738faca Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 7 Sep 2022 15:19:32 -0400 Subject: [PATCH 15/29] exploring struct and lib focused impl --- src/dev/Contract.sol | 41 ++++++++++++++++++++++++++++ src/dev/LinearVRGDALib.sol | 56 ++++++++++++++++++++++++++++++++++++++ src/dev/VRGDALib.sol | 51 ++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 src/dev/Contract.sol create mode 100644 src/dev/LinearVRGDALib.sol create mode 100644 src/dev/VRGDALib.sol diff --git a/src/dev/Contract.sol b/src/dev/Contract.sol new file mode 100644 index 0000000..ca80eed --- /dev/null +++ b/src/dev/Contract.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {LinearVRGDALib, LinearVRGDAx} from "./LinearVRGDALib.sol"; + +contract Contract { + using LinearVRGDALib for LinearVRGDAx; + + int256 resourceA; + int256 resourceB; + + LinearVRGDAx internal vrgdaA; + LinearVRGDAx internal vrgdaB; + + constructor () { + vrgdaA = LinearVRGDALib.createLinearVRGDA(1e18, 0.2e18, 1e18); + vrgdaB = LinearVRGDALib.createLinearVRGDA(0.5e18, 0.2e18, 2e18); + } + + function buyA(uint256 amount) public payable { + uint256 price = vrgdaA.getVRGDAPrice(resourceA); + require(msg.value >= (price * amount), "Not enough ETH"); + + resourceA += int256(amount); + } + + function buyB(uint256 amount) public payable { + uint256 price = vrgdaB.getVRGDAPrice(resourceB); + require(msg.value >= (price * amount), "Not enough ETH"); + + resourceB += int256(amount); + } + + function getPriceA() public view returns (uint256) { + return vrgdaA.getVRGDAPrice(resourceA); + } + + function getPriceB() public view returns (uint256) { + return vrgdaB.getVRGDAPrice(resourceB); + } +} \ No newline at end of file diff --git a/src/dev/LinearVRGDALib.sol b/src/dev/LinearVRGDALib.sol new file mode 100644 index 0000000..82bba56 --- /dev/null +++ b/src/dev/LinearVRGDALib.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {unsafeWadDiv} from "../utils/SignedWadMath.sol"; + +import {VRGDALib, VRGDAx} from "./VRGDALib.sol"; + +struct LinearVRGDAx { + int256 perTimeUnit; + VRGDAx vrgda; +} + +/// @title Linear Variable Rate Gradual Dutch Auction +/// @author transmissions11 +/// @author FrankieIsLost +/// @author saucepoint +/// @notice VRGDA with a linear issuance curve. +library LinearVRGDALib { + /*////////////////////////////////////////////////////////////// + PRICING PARAMETERS + //////////////////////////////////////////////////////////////*/ + + /// @notice Sets pricing parameters for the VRGDA. + /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. + /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. + /// @param _perTimeUnit The number of tokens to target selling in 1 full unit of time, scaled by 1e18. + function createLinearVRGDA( + int256 _targetPrice, + int256 _priceDecayPercent, + int256 _perTimeUnit + ) internal pure returns (LinearVRGDAx memory linearVRGDA) { + VRGDAx memory vrgda = VRGDALib.createVRGDA(_targetPrice, _priceDecayPercent); + linearVRGDA = LinearVRGDAx(_perTimeUnit, vrgda); + } + + /*////////////////////////////////////////////////////////////// + PRICING LOGIC + //////////////////////////////////////////////////////////////*/ + + function getVRGDAPrice(LinearVRGDAx memory self, int256 sold) + internal + pure + returns (uint256) + { + int256 timeDelta = getTargetSaleTime(self, sold); + return VRGDALib.getVRGDAPrice(self.vrgda.targetPrice, self.vrgda.decayConstant, timeDelta); + } + + /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. + /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. + /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. + function getTargetSaleTime(LinearVRGDAx memory vrgda, int256 sold) internal pure returns (int256) { + return unsafeWadDiv(sold, vrgda.perTimeUnit); + } +} diff --git a/src/dev/VRGDALib.sol b/src/dev/VRGDALib.sol new file mode 100644 index 0000000..a5d2787 --- /dev/null +++ b/src/dev/VRGDALib.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "../utils/SignedWadMath.sol"; + +// TODO: rename to something better? +// VRGDAx sounds badass tho +struct VRGDAx { + int256 targetPrice; + int256 decayConstant; +} + +/// @title Variable Rate Gradual Dutch Auction +/// @author transmissions11 +/// @author FrankieIsLost +/// @notice Sell tokens roughly according to an issuance schedule. +library VRGDALib { + + /// @notice Sets target price and per time unit price decay for a VRGDA instance. + /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. (18 decimal fixed point number) + /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. (18 decimal fixed point number) + function createVRGDA(int256 _targetPrice, int256 _priceDecayPercent) internal pure returns (VRGDAx memory vrgda) { + // Precomputed constant that allows us to rewrite a pow() as an exp(). + int256 _decayConstant = wadLn(1e18 - _priceDecayPercent); + require(_decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); + + vrgda = VRGDAx( + _targetPrice, // Target price for a token, to be scaled according to sales pace. + _decayConstant // Precomputed constant that allows us to rewrite a pow() as an exp(). + ); + } + + /*////////////////////////////////////////////////////////////// + PRICING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Calculate the price of a token according to the VRGDA formula. + /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. + /// @param _decayConstant The constant price decays per unit of time with no sales, scaled by 1e18. + /// @param _timeDelta Time difference between time-since-VRGDA-genesis and expected time of sale, (assumes both scaled by 1e18). I.e. timeSinceStart - targetSaleTime + /// @return The price of a token according to VRGDA, scaled by 1e18. + function getVRGDAPrice(int256 _targetPrice, int256 _decayConstant, int256 _timeDelta) internal pure returns (uint256) { + unchecked { + // prettier-ignore + return uint256(wadMul( + _targetPrice, + wadExp(unsafeWadMul(_decayConstant, _timeDelta)) + )); + } + } +} From d7f4c729d2f996de700a96ce3bc5dd5b37d067a2 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 7 Sep 2022 15:40:00 -0400 Subject: [PATCH 16/29] simplification of code --- src/dev/LinearVRGDALib.sol | 10 ++++++---- src/dev/VRGDALib.sol | 4 ++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/dev/LinearVRGDALib.sol b/src/dev/LinearVRGDALib.sol index 82bba56..b46cc34 100644 --- a/src/dev/LinearVRGDALib.sol +++ b/src/dev/LinearVRGDALib.sol @@ -16,6 +16,7 @@ struct LinearVRGDAx { /// @author saucepoint /// @notice VRGDA with a linear issuance curve. library LinearVRGDALib { + using VRGDALib for VRGDAx; /*////////////////////////////////////////////////////////////// PRICING PARAMETERS //////////////////////////////////////////////////////////////*/ @@ -42,15 +43,16 @@ library LinearVRGDALib { pure returns (uint256) { - int256 timeDelta = getTargetSaleTime(self, sold); - return VRGDALib.getVRGDAPrice(self.vrgda.targetPrice, self.vrgda.decayConstant, timeDelta); + int256 timeDelta = getTargetSaleTime(self.perTimeUnit, sold); + // alternative syntax: VRGDALib.getVRGDAPrice(self.vrgda.targetPrice, self.vrgda.decayConstant, timeDelta); + return self.vrgda.getVRGDAPrice(timeDelta); } /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. - function getTargetSaleTime(LinearVRGDAx memory vrgda, int256 sold) internal pure returns (int256) { - return unsafeWadDiv(sold, vrgda.perTimeUnit); + function getTargetSaleTime(int256 perTimeUnit, int256 sold) internal pure returns (int256) { + return unsafeWadDiv(sold, perTimeUnit); } } diff --git a/src/dev/VRGDALib.sol b/src/dev/VRGDALib.sol index a5d2787..e78f711 100644 --- a/src/dev/VRGDALib.sol +++ b/src/dev/VRGDALib.sol @@ -48,4 +48,8 @@ library VRGDALib { )); } } + + function getVRGDAPrice(VRGDAx memory self, int256 timeDelta) internal pure returns (uint256) { + return getVRGDAPrice(self.targetPrice, self.decayConstant, timeDelta); + } } From f7a1a10feb59bd2dec288bb4ce99950c4c7a4b05 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 7 Sep 2022 15:52:31 -0400 Subject: [PATCH 17/29] temp test to measure gas --- src/dev/LinearVRGDALib.sol | 5 ++--- test/Contract.t.sol | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 test/Contract.t.sol diff --git a/src/dev/LinearVRGDALib.sol b/src/dev/LinearVRGDALib.sol index b46cc34..a0b8448 100644 --- a/src/dev/LinearVRGDALib.sol +++ b/src/dev/LinearVRGDALib.sol @@ -16,7 +16,6 @@ struct LinearVRGDAx { /// @author saucepoint /// @notice VRGDA with a linear issuance curve. library LinearVRGDALib { - using VRGDALib for VRGDAx; /*////////////////////////////////////////////////////////////// PRICING PARAMETERS //////////////////////////////////////////////////////////////*/ @@ -44,8 +43,8 @@ library LinearVRGDALib { returns (uint256) { int256 timeDelta = getTargetSaleTime(self.perTimeUnit, sold); - // alternative syntax: VRGDALib.getVRGDAPrice(self.vrgda.targetPrice, self.vrgda.decayConstant, timeDelta); - return self.vrgda.getVRGDAPrice(timeDelta); + // alternative syntax: self.vrgda.getVRGDAPrice(timeDelta); (uses more gas though) + return VRGDALib.getVRGDAPrice(self.vrgda.targetPrice, self.vrgda.decayConstant, timeDelta); } /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. diff --git a/test/Contract.t.sol b/test/Contract.t.sol new file mode 100644 index 0000000..10488b5 --- /dev/null +++ b/test/Contract.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; + +import {Contract} from "../src/dev/Contract.sol"; + +contract LinearNFTTest is DSTestPlus { + Contract c; + + function setUp() public { + c = new Contract(); + } + + function rng(uint256 a, uint256 b) public pure returns (uint256) { + return uint256(keccak256(abi.encodePacked(a, b))) % 100; + } + + function testGas() public { + uint256 price = c.getPriceA(); + c.buyA{value: price}(1); + + price = c.getPriceB(); + c.buyB{value: price}(1); + + uint256 amount; + for (uint256 i = 0; i < 100; i++) { + price = c.getPriceA(); + amount = rng(i, price); + c.buyA{value: (price * amount)}(amount); + hevm.warp(block.timestamp + rng(i, block.timestamp)); + } + + for (uint256 i = 0; i < 100; i++) { + price = c.getPriceB(); + amount = rng(i, price); + c.buyB{value: (price * amount)}(amount); + hevm.warp(block.timestamp + rng(i, block.timestamp)); + } + } +} From c5dbac2c8d8033a76168bf36fdb57965e57a2fa1 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 7 Sep 2022 15:56:25 -0400 Subject: [PATCH 18/29] remove gas intense version --- src/dev/LinearVRGDALib.sol | 2 +- src/dev/VRGDALib.sol | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/dev/LinearVRGDALib.sol b/src/dev/LinearVRGDALib.sol index a0b8448..e546df4 100644 --- a/src/dev/LinearVRGDALib.sol +++ b/src/dev/LinearVRGDALib.sol @@ -43,7 +43,7 @@ library LinearVRGDALib { returns (uint256) { int256 timeDelta = getTargetSaleTime(self.perTimeUnit, sold); - // alternative syntax: self.vrgda.getVRGDAPrice(timeDelta); (uses more gas though) + return VRGDALib.getVRGDAPrice(self.vrgda.targetPrice, self.vrgda.decayConstant, timeDelta); } diff --git a/src/dev/VRGDALib.sol b/src/dev/VRGDALib.sol index e78f711..a5d2787 100644 --- a/src/dev/VRGDALib.sol +++ b/src/dev/VRGDALib.sol @@ -48,8 +48,4 @@ library VRGDALib { )); } } - - function getVRGDAPrice(VRGDAx memory self, int256 timeDelta) internal pure returns (uint256) { - return getVRGDAPrice(self.targetPrice, self.decayConstant, timeDelta); - } } From d1f1291d8df152b1a726647a289e356c58fcae18 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 7 Sep 2022 16:49:02 -0400 Subject: [PATCH 19/29] reduce mem usage --- src/dev/LinearVRGDALib.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/dev/LinearVRGDALib.sol b/src/dev/LinearVRGDALib.sol index e546df4..a549416 100644 --- a/src/dev/LinearVRGDALib.sol +++ b/src/dev/LinearVRGDALib.sol @@ -29,8 +29,10 @@ library LinearVRGDALib { int256 _priceDecayPercent, int256 _perTimeUnit ) internal pure returns (LinearVRGDAx memory linearVRGDA) { - VRGDAx memory vrgda = VRGDALib.createVRGDA(_targetPrice, _priceDecayPercent); - linearVRGDA = LinearVRGDAx(_perTimeUnit, vrgda); + linearVRGDA = LinearVRGDAx( + _perTimeUnit, + VRGDALib.createVRGDA(_targetPrice, _priceDecayPercent) + ); } /*////////////////////////////////////////////////////////////// From 99346d1966cc70c4b14fc185207c50e07cfff1e9 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 7 Sep 2022 17:03:58 -0400 Subject: [PATCH 20/29] add logisticVRGDAx; cleanup demo for clarity --- src/dev/Contract.sol | 26 ++++++++------- src/dev/LogisticVRGDALib.sol | 65 ++++++++++++++++++++++++++++++++++++ test/Contract.t.sol | 16 ++++----- 3 files changed, 87 insertions(+), 20 deletions(-) create mode 100644 src/dev/LogisticVRGDALib.sol diff --git a/src/dev/Contract.sol b/src/dev/Contract.sol index ca80eed..0f7a515 100644 --- a/src/dev/Contract.sol +++ b/src/dev/Contract.sol @@ -2,40 +2,42 @@ pragma solidity >=0.8.0; import {LinearVRGDALib, LinearVRGDAx} from "./LinearVRGDALib.sol"; +import {LogisticVRGDALib, LogisticVRGDAx} from "./LogisticVRGDALib.sol"; contract Contract { using LinearVRGDALib for LinearVRGDAx; + using LogisticVRGDALib for LogisticVRGDAx; int256 resourceA; int256 resourceB; - LinearVRGDAx internal vrgdaA; - LinearVRGDAx internal vrgdaB; + LinearVRGDAx internal linearAuction; + LogisticVRGDAx internal logAuction; constructor () { - vrgdaA = LinearVRGDALib.createLinearVRGDA(1e18, 0.2e18, 1e18); - vrgdaB = LinearVRGDALib.createLinearVRGDA(0.5e18, 0.2e18, 2e18); + linearAuction = LinearVRGDALib.createLinearVRGDA(1e18, 0.2e18, 1e18); + logAuction = LogisticVRGDALib.createLogisticVRGDA(1e18, 0.2e18, 100e18, 100e18); } - function buyA(uint256 amount) public payable { - uint256 price = vrgdaA.getVRGDAPrice(resourceA); + function buyLinear(uint256 amount) public payable { + uint256 price = linearAuction.getVRGDAPrice(resourceA); require(msg.value >= (price * amount), "Not enough ETH"); resourceA += int256(amount); } - function buyB(uint256 amount) public payable { - uint256 price = vrgdaB.getVRGDAPrice(resourceB); + function buyLogistic(uint256 amount) public payable { + uint256 price = logAuction.getVRGDAPrice(resourceB); require(msg.value >= (price * amount), "Not enough ETH"); resourceB += int256(amount); } - function getPriceA() public view returns (uint256) { - return vrgdaA.getVRGDAPrice(resourceA); + function getPriceLinear() public view returns (uint256) { + return linearAuction.getVRGDAPrice(resourceA); } - function getPriceB() public view returns (uint256) { - return vrgdaB.getVRGDAPrice(resourceB); + function getPriceLogistic() public view returns (uint256) { + return logAuction.getVRGDAPrice(resourceB); } } \ No newline at end of file diff --git a/src/dev/LogisticVRGDALib.sol b/src/dev/LogisticVRGDALib.sol new file mode 100644 index 0000000..bb4ecf5 --- /dev/null +++ b/src/dev/LogisticVRGDALib.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {wadLn, unsafeDiv, unsafeWadDiv} from "../utils/SignedWadMath.sol"; + +import {VRGDALib, VRGDAx} from "./VRGDALib.sol"; + +struct LogisticVRGDAx { + int256 logisticLimit; + int256 timeScale; + VRGDAx vrgda; +} + +/// @title Logistic Variable Rate Gradual Dutch Auction +/// @author transmissions11 +/// @author FrankieIsLost +/// @author saucepoint +/// @notice VRGDA with a linear issuance curve. +library LogisticVRGDALib { + /*////////////////////////////////////////////////////////////// + PRICING PARAMETERS + //////////////////////////////////////////////////////////////*/ + + /// @notice Sets pricing parameters for the VRGDA. + /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. + /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. + /// @param _maxSellable The maximum number of tokens to sell, scaled by 1e18. + /// @param _timeScale The steepness of the logistic curve, scaled by 1e18. + function createLogisticVRGDA( + int256 _targetPrice, + int256 _priceDecayPercent, + int256 _maxSellable, + int256 _timeScale + ) internal pure returns (LogisticVRGDAx memory logisticVRGDAx) { + logisticVRGDAx = LogisticVRGDAx( + _maxSellable + 1e18, // add 1 wad to make the limit inclusive of _maxSellable + _timeScale, + VRGDALib.createVRGDA(_targetPrice, _priceDecayPercent) + ); + } + + /*////////////////////////////////////////////////////////////// + PRICING LOGIC + //////////////////////////////////////////////////////////////*/ + + function getVRGDAPrice(LogisticVRGDAx memory self, int256 sold) + internal + pure + returns (uint256) + { + int256 timeDelta = getTargetSaleTime(self.logisticLimit, self.timeScale, sold); + + return VRGDALib.getVRGDAPrice(self.vrgda.targetPrice, self.vrgda.decayConstant, timeDelta); + } + + /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. + /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. + /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. + function getTargetSaleTime(int256 logisticLimit, int256 timeScale, int256 sold) internal pure returns (int256) { + unchecked { + return -unsafeWadDiv(wadLn(unsafeDiv(logisticLimit * 2e18, sold + logisticLimit) - 1e18), timeScale); + } + } +} diff --git a/test/Contract.t.sol b/test/Contract.t.sol index 10488b5..8bd57fb 100644 --- a/test/Contract.t.sol +++ b/test/Contract.t.sol @@ -17,24 +17,24 @@ contract LinearNFTTest is DSTestPlus { } function testGas() public { - uint256 price = c.getPriceA(); - c.buyA{value: price}(1); + uint256 price = c.getPriceLinear(); + c.buyLinear{value: price}(1); - price = c.getPriceB(); - c.buyB{value: price}(1); + price = c.getPriceLogistic(); + c.buyLogistic{value: price}(1); uint256 amount; for (uint256 i = 0; i < 100; i++) { - price = c.getPriceA(); + price = c.getPriceLinear(); amount = rng(i, price); - c.buyA{value: (price * amount)}(amount); + c.buyLinear{value: (price * amount)}(amount); hevm.warp(block.timestamp + rng(i, block.timestamp)); } for (uint256 i = 0; i < 100; i++) { - price = c.getPriceB(); + price = c.getPriceLogistic(); amount = rng(i, price); - c.buyB{value: (price * amount)}(amount); + c.buyLogistic{value: (price * amount)}(amount); hevm.warp(block.timestamp + rng(i, block.timestamp)); } } From d89d20c55e8a9e2200ebba22bdf4b8ca15596c9e Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 7 Sep 2022 18:19:51 -0400 Subject: [PATCH 21/29] documentation and correct timeDelta logic --- src/dev/Contract.sol | 26 ++++++++++++++++++++------ src/dev/LinearVRGDALib.sol | 23 ++++++++++++++--------- src/dev/LogisticVRGDALib.sol | 24 +++++++++++++++--------- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/dev/Contract.sol b/src/dev/Contract.sol index 0f7a515..e2b9beb 100644 --- a/src/dev/Contract.sol +++ b/src/dev/Contract.sol @@ -3,41 +3,55 @@ pragma solidity >=0.8.0; import {LinearVRGDALib, LinearVRGDAx} from "./LinearVRGDALib.sol"; import {LogisticVRGDALib, LogisticVRGDAx} from "./LogisticVRGDALib.sol"; +import {toWadUnsafe} from "../utils/SignedWadMath.sol"; contract Contract { using LinearVRGDALib for LinearVRGDAx; using LogisticVRGDALib for LogisticVRGDAx; - int256 resourceA; - int256 resourceB; + uint256 startTime = block.timestamp; + // dummy counters to represent "purchases" + int256 resourceA; // priced via Linear VRGDA + int256 resourceB; // priced via Logistic VRGDA + + // define 2 VRGDAs to price the resources LinearVRGDAx internal linearAuction; LogisticVRGDAx internal logAuction; constructor () { + // initialize the VRGDAs linearAuction = LinearVRGDALib.createLinearVRGDA(1e18, 0.2e18, 1e18); logAuction = LogisticVRGDALib.createLogisticVRGDA(1e18, 0.2e18, 100e18, 100e18); } + // purchase resourceA, according to the linear VRGDA function buyLinear(uint256 amount) public payable { - uint256 price = linearAuction.getVRGDAPrice(resourceA); + int256 timeSinceStart = toWadUnsafe(block.timestamp - startTime); + uint256 price = linearAuction.getVRGDAPrice(timeSinceStart, resourceA); require(msg.value >= (price * amount), "Not enough ETH"); resourceA += int256(amount); } + // purchase resourceB, according to the logistic VRGDA function buyLogistic(uint256 amount) public payable { - uint256 price = logAuction.getVRGDAPrice(resourceB); + int256 timeSinceStart = toWadUnsafe(block.timestamp - startTime); + uint256 price = logAuction.getVRGDAPrice(timeSinceStart, resourceB); require(msg.value >= (price * amount), "Not enough ETH"); resourceB += int256(amount); } + // view function for getting the price of resourceA function getPriceLinear() public view returns (uint256) { - return linearAuction.getVRGDAPrice(resourceA); + int256 timeSinceStart = toWadUnsafe(block.timestamp - startTime); + return linearAuction.getVRGDAPrice(timeSinceStart, resourceA); } + // view function for getting the price of resourceB function getPriceLogistic() public view returns (uint256) { - return logAuction.getVRGDAPrice(resourceB); + int256 timeSinceStart = toWadUnsafe(block.timestamp - startTime); + return logAuction.getVRGDAPrice(timeSinceStart, resourceB); } } \ No newline at end of file diff --git a/src/dev/LinearVRGDALib.sol b/src/dev/LinearVRGDALib.sol index a549416..681a2d5 100644 --- a/src/dev/LinearVRGDALib.sol +++ b/src/dev/LinearVRGDALib.sol @@ -16,14 +16,12 @@ struct LinearVRGDAx { /// @author saucepoint /// @notice VRGDA with a linear issuance curve. library LinearVRGDALib { - /*////////////////////////////////////////////////////////////// - PRICING PARAMETERS - //////////////////////////////////////////////////////////////*/ - /// @notice Sets pricing parameters for the VRGDA. + /// @notice Create a Linear VRGDA using specified parameters. /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. - /// @param _perTimeUnit The number of tokens to target selling in 1 full unit of time, scaled by 1e18. + /// @param _perTimeUnit The number of tokens to target sell in 1 full unit of time, scaled by 1e18. + /// @return linearVRGDA The created Linear VRGDA (of type struct LinearVRGDAx). function createLinearVRGDA( int256 _targetPrice, int256 _priceDecayPercent, @@ -39,19 +37,26 @@ library LinearVRGDALib { PRICING LOGIC //////////////////////////////////////////////////////////////*/ - function getVRGDAPrice(LinearVRGDAx memory self, int256 sold) + /// @notice Calculate the price of a token according to the VRGDA formula. + /// @param self a VRGDA represented as the LinearVRGDAx struct + /// @param timeSinceStart Units of time passed since the VRGDA began, scaled by 1e18. + /// @param sold The number of tokens sold so far, scaled by 1e18. + /// @return uint256 The price of a token according to VRGDA, scaled by 1e18. + function getVRGDAPrice(LinearVRGDAx memory self, int256 timeSinceStart, int256 sold) internal pure returns (uint256) { - int256 timeDelta = getTargetSaleTime(self.perTimeUnit, sold); - + int256 timeDelta; + unchecked { + timeDelta = timeSinceStart - getTargetSaleTime(self.perTimeUnit, sold); + } return VRGDALib.getVRGDAPrice(self.vrgda.targetPrice, self.vrgda.decayConstant, timeDelta); } /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// @return int256 The target time the tokens should be sold by, scaled by 1e18, where the time is /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. function getTargetSaleTime(int256 perTimeUnit, int256 sold) internal pure returns (int256) { return unsafeWadDiv(sold, perTimeUnit); diff --git a/src/dev/LogisticVRGDALib.sol b/src/dev/LogisticVRGDALib.sol index bb4ecf5..c116d10 100644 --- a/src/dev/LogisticVRGDALib.sol +++ b/src/dev/LogisticVRGDALib.sol @@ -15,13 +15,10 @@ struct LogisticVRGDAx { /// @author transmissions11 /// @author FrankieIsLost /// @author saucepoint -/// @notice VRGDA with a linear issuance curve. +/// @notice VRGDA with a logistic issuance curve. library LogisticVRGDALib { - /*////////////////////////////////////////////////////////////// - PRICING PARAMETERS - //////////////////////////////////////////////////////////////*/ - /// @notice Sets pricing parameters for the VRGDA. + /// @notice Create a Logistic VRGDA using specified parameters. /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. /// @param _maxSellable The maximum number of tokens to sell, scaled by 1e18. @@ -43,19 +40,28 @@ library LogisticVRGDALib { PRICING LOGIC //////////////////////////////////////////////////////////////*/ - function getVRGDAPrice(LogisticVRGDAx memory self, int256 sold) + /// @notice Calculate the price of a token according to the VRGDA formula. + /// @param self a VRGDA represented as the LinearVRGDAx struct + /// @param timeSinceStart Units of time passed since the VRGDA began, scaled by 1e18. + /// @param sold The number of tokens sold so far, scaled by 1e18. + /// @return uint256 The price of a token according to VRGDA, scaled by 1e18. + function getVRGDAPrice(LogisticVRGDAx memory self, int256 timeSinceStart, int256 sold) internal pure returns (uint256) { - int256 timeDelta = getTargetSaleTime(self.logisticLimit, self.timeScale, sold); - + int256 timeDelta; + unchecked { + timeDelta = timeSinceStart - getTargetSaleTime(self.logisticLimit, self.timeScale, sold); + } return VRGDALib.getVRGDAPrice(self.vrgda.targetPrice, self.vrgda.decayConstant, timeDelta); } /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. + /// @param logisticLimit The logistic limit (maximum number of tokens to sell), scaled by 1e18. + /// @param timeScale The steepness of the logistic curve, scaled by 1e18. /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is + /// @return int256 The target time the tokens should be sold by, scaled by 1e18, where the time is /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. function getTargetSaleTime(int256 logisticLimit, int256 timeScale, int256 sold) internal pure returns (int256) { unchecked { From bce4acb467f4699c506675094c8641dc851cef2f Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 7 Sep 2022 20:48:04 -0400 Subject: [PATCH 22/29] reorganize --- src/LinearVRGDA.sol | 44 -------------------- src/{dev => }/LinearVRGDALib.sol | 2 +- src/LogisticVRGDA.sol | 65 ------------------------------ src/{dev => }/LogisticVRGDALib.sol | 2 +- src/VRGDA.sol | 53 ------------------------ src/{dev => }/VRGDALib.sol | 2 +- src/VRGDAStruct.sol | 60 --------------------------- src/{dev => examples}/Contract.sol | 4 +- 8 files changed, 5 insertions(+), 227 deletions(-) delete mode 100644 src/LinearVRGDA.sol rename src/{dev => }/LinearVRGDALib.sol (97%) delete mode 100644 src/LogisticVRGDA.sol rename src/{dev => }/LogisticVRGDALib.sol (97%) delete mode 100644 src/VRGDA.sol rename src/{dev => }/VRGDALib.sol (98%) delete mode 100644 src/VRGDAStruct.sol rename src/{dev => examples}/Contract.sol (93%) diff --git a/src/LinearVRGDA.sol b/src/LinearVRGDA.sol deleted file mode 100644 index 814d060..0000000 --- a/src/LinearVRGDA.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import {unsafeWadDiv} from "./utils/SignedWadMath.sol"; - -import {VRGDA} from "./VRGDA.sol"; - -/// @title Linear Variable Rate Gradual Dutch Auction -/// @author transmissions11 -/// @author FrankieIsLost -/// @notice VRGDA with a linear issuance curve. -abstract contract LinearVRGDA is VRGDA { - /*////////////////////////////////////////////////////////////// - PRICING PARAMETERS - //////////////////////////////////////////////////////////////*/ - - /// @dev The total number of tokens to target selling every full unit of time. - /// @dev Represented as an 18 decimal fixed point number. - int256 internal immutable perTimeUnit; - - /// @notice Sets pricing parameters for the VRGDA. - /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. - /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. - /// @param _perTimeUnit The number of tokens to target selling in 1 full unit of time, scaled by 1e18. - constructor( - int256 _targetPrice, - int256 _priceDecayPercent, - int256 _perTimeUnit - ) VRGDA(_targetPrice, _priceDecayPercent) { - perTimeUnit = _perTimeUnit; - } - - /*////////////////////////////////////////////////////////////// - PRICING LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. - /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is - /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. - function getTargetSaleTime(int256 sold) public view override returns (int256) { - return unsafeWadDiv(sold, perTimeUnit); - } -} diff --git a/src/dev/LinearVRGDALib.sol b/src/LinearVRGDALib.sol similarity index 97% rename from src/dev/LinearVRGDALib.sol rename to src/LinearVRGDALib.sol index 681a2d5..30ab8a2 100644 --- a/src/dev/LinearVRGDALib.sol +++ b/src/LinearVRGDALib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {unsafeWadDiv} from "../utils/SignedWadMath.sol"; +import {unsafeWadDiv} from "./utils/SignedWadMath.sol"; import {VRGDALib, VRGDAx} from "./VRGDALib.sol"; diff --git a/src/LogisticVRGDA.sol b/src/LogisticVRGDA.sol deleted file mode 100644 index 2afff17..0000000 --- a/src/LogisticVRGDA.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import {wadLn, unsafeDiv, unsafeWadDiv} from "./utils/SignedWadMath.sol"; - -import {VRGDA} from "./VRGDA.sol"; - -/// @title Logistic Variable Rate Gradual Dutch Auction -/// @author transmissions11 -/// @author FrankieIsLost -/// @notice VRGDA with a logistic issuance curve. -abstract contract LogisticVRGDA is VRGDA { - /*////////////////////////////////////////////////////////////// - PRICING PARAMETERS - //////////////////////////////////////////////////////////////*/ - - /// @dev The maximum number of tokens of tokens to sell + 1. We add - /// 1 because the logistic function will never fully reach its limit. - /// @dev Represented as an 18 decimal fixed point number. - int256 public immutable logisticLimit; - - /// @dev The maximum number of tokens of tokens to sell + 1 multiplied - /// by 2. We could compute it on the fly each time but this saves gas. - /// @dev Represented as a 36 decimal fixed point number. - int256 public immutable logisticLimitDoubled; - - /// @dev Time scale controls the steepness of the logistic curve, - /// which affects how quickly we will reach the curve's asymptote. - /// @dev Represented as an 18 decimal fixed point number. - int256 internal immutable timeScale; - - /// @notice Sets pricing parameters for the VRGDA. - /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. - /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. - /// @param _maxSellable The maximum number of tokens to sell, scaled by 1e18. - /// @param _timeScale The steepness of the logistic curve, scaled by 1e18. - constructor( - int256 _targetPrice, - int256 _priceDecayPercent, - int256 _maxSellable, - int256 _timeScale - ) VRGDA(_targetPrice, _priceDecayPercent) { - // Add 1 wad to make the limit inclusive of _maxSellable. - logisticLimit = _maxSellable + 1e18; - - // Scale by 2e18 to both double it and give it 36 decimals. - logisticLimitDoubled = logisticLimit * 2e18; - - timeScale = _timeScale; - } - - /*////////////////////////////////////////////////////////////// - PRICING LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. - /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is - /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. - function getTargetSaleTime(int256 sold) public view override returns (int256) { - unchecked { - return -unsafeWadDiv(wadLn(unsafeDiv(logisticLimitDoubled, sold + logisticLimit) - 1e18), timeScale); - } - } -} diff --git a/src/dev/LogisticVRGDALib.sol b/src/LogisticVRGDALib.sol similarity index 97% rename from src/dev/LogisticVRGDALib.sol rename to src/LogisticVRGDALib.sol index c116d10..cfdefca 100644 --- a/src/dev/LogisticVRGDALib.sol +++ b/src/LogisticVRGDALib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {wadLn, unsafeDiv, unsafeWadDiv} from "../utils/SignedWadMath.sol"; +import {wadLn, unsafeDiv, unsafeWadDiv} from "./utils/SignedWadMath.sol"; import {VRGDALib, VRGDAx} from "./VRGDALib.sol"; diff --git a/src/VRGDA.sol b/src/VRGDA.sol deleted file mode 100644 index 4cd94d3..0000000 --- a/src/VRGDA.sol +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "./utils/SignedWadMath.sol"; -import {VRGDALibrary} from "./lib/VRGDALibrary.sol"; - -/// @title Variable Rate Gradual Dutch Auction -/// @author transmissions11 -/// @author FrankieIsLost -/// @notice Sell tokens roughly according to an issuance schedule. -abstract contract VRGDA { - /*////////////////////////////////////////////////////////////// - VRGDA PARAMETERS - //////////////////////////////////////////////////////////////*/ - - /// @notice Target price for a token, to be scaled according to sales pace. - /// @dev Represented as an 18 decimal fixed point number. - int256 public immutable targetPrice; - - /// @dev Precomputed constant that allows us to rewrite a pow() as an exp(). - /// @dev Represented as an 18 decimal fixed point number. - int256 internal immutable decayConstant; - - /// @notice Sets target price and per time unit price decay for the VRGDA. - /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. - /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. - constructor(int256 _targetPrice, int256 _priceDecayPercent) { - targetPrice = _targetPrice; - - decayConstant = wadLn(1e18 - _priceDecayPercent); - - // The decay constant must be negative for VRGDAs to work. - require(decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); - } - - /*////////////////////////////////////////////////////////////// - PRICING LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @notice Calculate the price of a token according to the VRGDA formula. - /// @param timeSinceStart Time passed since the VRGDA began, scaled by 1e18. - /// @param sold The total number of tokens that have been sold so far. - /// @return The price of a token according to VRGDA, scaled by 1e18. - function getVRGDAPrice(int256 timeSinceStart, uint256 sold) public view returns (uint256) { - return VRGDALibrary.getVRGDAPrice(targetPrice, decayConstant, timeSinceStart - getTargetSaleTime(toWadUnsafe(sold + 1))); - } - - /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. - /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is - /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. - function getTargetSaleTime(int256 sold) public view virtual returns (int256); -} diff --git a/src/dev/VRGDALib.sol b/src/VRGDALib.sol similarity index 98% rename from src/dev/VRGDALib.sol rename to src/VRGDALib.sol index a5d2787..11db14c 100644 --- a/src/dev/VRGDALib.sol +++ b/src/VRGDALib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "../utils/SignedWadMath.sol"; +import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "./utils/SignedWadMath.sol"; // TODO: rename to something better? // VRGDAx sounds badass tho diff --git a/src/VRGDAStruct.sol b/src/VRGDAStruct.sol deleted file mode 100644 index ed8b8a6..0000000 --- a/src/VRGDAStruct.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "./utils/SignedWadMath.sol"; -import {VRGDALibrary} from "./lib/VRGDALibrary.sol"; - -// TODO: rename to something better? -// VRGDAx sounds badass tho -struct VRGDAx { - int256 targetPrice; - int256 decayConstant; - function (int256) view returns (int256) getTargetSaleTime; -} - -/// @title Variable Rate Gradual Dutch Auction -/// @author transmissions11 -/// @author FrankieIsLost -/// @author saucepoint -/// @notice Sell tokens roughly according to an issuance schedule. -abstract contract MultiVRGDA { - /// look bro, no state 😈 - - /// @notice Sets target price and per time unit price decay for a VRGDA instance. - /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. (18 decimal fixed point number) - /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. (18 decimal fixed point number) - function createVRGDA(int256 _targetPrice, int256 _priceDecayPercent) internal pure returns (VRGDAx memory vrgda) { - // Precomputed constant that allows us to rewrite a pow() as an exp(). - int256 _decayConstant = wadLn(1e18 - _priceDecayPercent); - require(_decayConstant < 0, "NON_NEGATIVE_DECAY_CONSTANT"); - - vrgda = VRGDAx( - _targetPrice, // Target price for a token, to be scaled according to sales pace. - _decayConstant, // Precomputed constant that allows us to rewrite a pow() as an exp(). - getTargetSaleTime - ); - } - - /*////////////////////////////////////////////////////////////// - PRICING LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @notice Calculate the price of a token according to the VRGDA formula. - /// @param vrgda a VRGDAx struct representing a VRGDA instance - /// @param timeSinceStart Time passed since the VRGDA began, scaled by 1e18. - /// @param sold The total number of tokens that have been sold so far. - /// @return uint256 the price of a token according to VRGDA, scaled by 1e18. - function getVRGDAPrice(VRGDAx memory vrgda, int256 timeSinceStart, uint256 sold) internal view returns (uint256) { - return VRGDALibrary.getVRGDAPrice( - vrgda.targetPrice, - vrgda.decayConstant, - timeSinceStart - vrgda.getTargetSaleTime(toWadUnsafe(sold + 1)) - ); - } - - /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. - /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return int256 The target time the tokens should be sold by, scaled by 1e18, where the time is - /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. - function getTargetSaleTime(int256 sold) public view virtual returns (int256); -} diff --git a/src/dev/Contract.sol b/src/examples/Contract.sol similarity index 93% rename from src/dev/Contract.sol rename to src/examples/Contract.sol index e2b9beb..a90c02a 100644 --- a/src/dev/Contract.sol +++ b/src/examples/Contract.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {LinearVRGDALib, LinearVRGDAx} from "./LinearVRGDALib.sol"; -import {LogisticVRGDALib, LogisticVRGDAx} from "./LogisticVRGDALib.sol"; +import {LinearVRGDALib, LinearVRGDAx} from "../LinearVRGDALib.sol"; +import {LogisticVRGDALib, LogisticVRGDAx} from "../LogisticVRGDALib.sol"; import {toWadUnsafe} from "../utils/SignedWadMath.sol"; contract Contract { From 2fc0d12fba912f4d8fe97dea46e48b9dae412ad6 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 7 Sep 2022 23:23:12 -0400 Subject: [PATCH 23/29] replace all VRGDA impl with lib struct impl --- src/LinearVRGDALib.sol | 6 +-- src/LogisticVRGDALib.sol | 6 +-- src/examples/Contract.sol | 10 ++--- src/examples/LinearNFT.sol | 18 +++++---- src/examples/LogisticNFT.sol | 16 +++++--- src/examples/MultiNFT.sol | 69 ++++++++++---------------------- test/Contract.t.sol | 23 +++++++---- test/mocks/MockLinearVRGDA.sol | 27 +++++++++++-- test/mocks/MockLogisticVRGDA.sol | 27 +++++++++++-- 9 files changed, 117 insertions(+), 85 deletions(-) diff --git a/src/LinearVRGDALib.sol b/src/LinearVRGDALib.sol index 30ab8a2..f1e8845 100644 --- a/src/LinearVRGDALib.sol +++ b/src/LinearVRGDALib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {unsafeWadDiv} from "./utils/SignedWadMath.sol"; +import {unsafeWadDiv, toWadUnsafe} from "./utils/SignedWadMath.sol"; import {VRGDALib, VRGDAx} from "./VRGDALib.sol"; @@ -42,14 +42,14 @@ library LinearVRGDALib { /// @param timeSinceStart Units of time passed since the VRGDA began, scaled by 1e18. /// @param sold The number of tokens sold so far, scaled by 1e18. /// @return uint256 The price of a token according to VRGDA, scaled by 1e18. - function getVRGDAPrice(LinearVRGDAx memory self, int256 timeSinceStart, int256 sold) + function getVRGDAPrice(LinearVRGDAx memory self, int256 timeSinceStart, uint256 sold) internal pure returns (uint256) { int256 timeDelta; unchecked { - timeDelta = timeSinceStart - getTargetSaleTime(self.perTimeUnit, sold); + timeDelta = timeSinceStart - getTargetSaleTime(self.perTimeUnit, toWadUnsafe(sold + 1)); } return VRGDALib.getVRGDAPrice(self.vrgda.targetPrice, self.vrgda.decayConstant, timeDelta); } diff --git a/src/LogisticVRGDALib.sol b/src/LogisticVRGDALib.sol index cfdefca..bf50d9b 100644 --- a/src/LogisticVRGDALib.sol +++ b/src/LogisticVRGDALib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {wadLn, unsafeDiv, unsafeWadDiv} from "./utils/SignedWadMath.sol"; +import {wadLn, unsafeDiv, unsafeWadDiv, toWadUnsafe} from "./utils/SignedWadMath.sol"; import {VRGDALib, VRGDAx} from "./VRGDALib.sol"; @@ -45,14 +45,14 @@ library LogisticVRGDALib { /// @param timeSinceStart Units of time passed since the VRGDA began, scaled by 1e18. /// @param sold The number of tokens sold so far, scaled by 1e18. /// @return uint256 The price of a token according to VRGDA, scaled by 1e18. - function getVRGDAPrice(LogisticVRGDAx memory self, int256 timeSinceStart, int256 sold) + function getVRGDAPrice(LogisticVRGDAx memory self, int256 timeSinceStart, uint256 sold) internal pure returns (uint256) { int256 timeDelta; unchecked { - timeDelta = timeSinceStart - getTargetSaleTime(self.logisticLimit, self.timeScale, sold); + timeDelta = timeSinceStart - getTargetSaleTime(self.logisticLimit, self.timeScale, toWadUnsafe(sold + 1)); } return VRGDALib.getVRGDAPrice(self.vrgda.targetPrice, self.vrgda.decayConstant, timeDelta); } diff --git a/src/examples/Contract.sol b/src/examples/Contract.sol index a90c02a..093165f 100644 --- a/src/examples/Contract.sol +++ b/src/examples/Contract.sol @@ -12,8 +12,8 @@ contract Contract { uint256 startTime = block.timestamp; // dummy counters to represent "purchases" - int256 resourceA; // priced via Linear VRGDA - int256 resourceB; // priced via Logistic VRGDA + uint256 resourceA; // priced via Linear VRGDA + uint256 resourceB; // priced via Logistic VRGDA // define 2 VRGDAs to price the resources LinearVRGDAx internal linearAuction; @@ -22,7 +22,7 @@ contract Contract { constructor () { // initialize the VRGDAs linearAuction = LinearVRGDALib.createLinearVRGDA(1e18, 0.2e18, 1e18); - logAuction = LogisticVRGDALib.createLogisticVRGDA(1e18, 0.2e18, 100e18, 100e18); + logAuction = LogisticVRGDALib.createLogisticVRGDA(1e18, 0.2e18, 1000e18, 1000e18); } // purchase resourceA, according to the linear VRGDA @@ -31,7 +31,7 @@ contract Contract { uint256 price = linearAuction.getVRGDAPrice(timeSinceStart, resourceA); require(msg.value >= (price * amount), "Not enough ETH"); - resourceA += int256(amount); + unchecked { resourceA += amount; } } // purchase resourceB, according to the logistic VRGDA @@ -40,7 +40,7 @@ contract Contract { uint256 price = logAuction.getVRGDAPrice(timeSinceStart, resourceB); require(msg.value >= (price * amount), "Not enough ETH"); - resourceB += int256(amount); + unchecked { resourceB += amount; } } // view function for getting the price of resourceA diff --git a/src/examples/LinearNFT.sol b/src/examples/LinearNFT.sol index e705632..91cc05f 100644 --- a/src/examples/LinearNFT.sol +++ b/src/examples/LinearNFT.sol @@ -4,16 +4,17 @@ pragma solidity >=0.8.0; import {ERC721} from "solmate/tokens/ERC721.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {toDaysWadUnsafe} from "../utils/SignedWadMath.sol"; +import {toDaysWadUnsafe, toWadUnsafe} from "../utils/SignedWadMath.sol"; -import {LinearVRGDA} from "../LinearVRGDA.sol"; +import {LinearVRGDALib, LinearVRGDAx} from "../LinearVRGDALib.sol"; /// @title Linear VRGDA NFT /// @author transmissions11 /// @author FrankieIsLost /// @notice Example NFT sold using LinearVRGDA. /// @dev This is an example. Do not use in production. -contract LinearNFT is ERC721, LinearVRGDA { +contract LinearNFT is ERC721 { + using LinearVRGDALib for LinearVRGDAx; /*////////////////////////////////////////////////////////////// SALES STORAGE //////////////////////////////////////////////////////////////*/ @@ -22,6 +23,8 @@ contract LinearNFT is ERC721, LinearVRGDA { uint256 public immutable startTime = block.timestamp; // When VRGDA sales begun. + LinearVRGDAx public vrgda; // The VRGDA used to price the NFT. + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -31,12 +34,13 @@ contract LinearNFT is ERC721, LinearVRGDA { "Example Linear NFT", // Name. "LINEAR" // Symbol. ) - LinearVRGDA( + { + vrgda = LinearVRGDALib.createLinearVRGDA( 69.42e18, // Target price. 0.31e18, // Price decay percent. 2e18 // Per time unit. - ) - {} + ); + } /*////////////////////////////////////////////////////////////// MINTING LOGIC @@ -45,7 +49,7 @@ contract LinearNFT is ERC721, LinearVRGDA { function mint() external payable returns (uint256 mintedId) { unchecked { // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. - uint256 price = getVRGDAPrice(toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); + uint256 price = vrgda.getVRGDAPrice(toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. diff --git a/src/examples/LogisticNFT.sol b/src/examples/LogisticNFT.sol index df58694..a08a382 100644 --- a/src/examples/LogisticNFT.sol +++ b/src/examples/LogisticNFT.sol @@ -6,14 +6,15 @@ import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {toDaysWadUnsafe, toWadUnsafe} from "../utils/SignedWadMath.sol"; -import {LogisticVRGDA} from "../LogisticVRGDA.sol"; +import {LogisticVRGDALib, LogisticVRGDAx} from "../LogisticVRGDALib.sol"; /// @title Logistic VRGDA NFT /// @author transmissions11 /// @author FrankieIsLost /// @notice Example NFT sold using LogisticVRGDA. /// @dev This is an example. Do not use in production. -contract LogisticNFT is ERC721, LogisticVRGDA { +contract LogisticNFT is ERC721 { + using LogisticVRGDALib for LogisticVRGDAx; /*////////////////////////////////////////////////////////////// CONSTANTS //////////////////////////////////////////////////////////////*/ @@ -28,6 +29,8 @@ contract LogisticNFT is ERC721, LogisticVRGDA { uint256 public immutable startTime = block.timestamp; // When VRGDA sales begun. + LogisticVRGDAx public vrgda; // The VRGDA used to price the NFT. + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -37,14 +40,15 @@ contract LogisticNFT is ERC721, LogisticVRGDA { "Example Logistic NFT", // Name. "LOGISTIC" // Symbol. ) - LogisticVRGDA( + { + vrgda = LogisticVRGDALib.createLogisticVRGDA( 69.42e18, // Target price. 0.31e18, // Price decay percent. // Maximum # mintable/sellable. toWadUnsafe(MAX_MINTABLE), 0.1e18 // Time scale. - ) - {} + ); + } /*////////////////////////////////////////////////////////////// MINTING LOGIC @@ -56,7 +60,7 @@ contract LogisticNFT is ERC721, LogisticVRGDA { unchecked { // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. - uint256 price = getVRGDAPrice(toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); + uint256 price = vrgda.getVRGDAPrice(toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. diff --git a/src/examples/MultiNFT.sol b/src/examples/MultiNFT.sol index 57a68a5..a30b6fb 100644 --- a/src/examples/MultiNFT.sol +++ b/src/examples/MultiNFT.sol @@ -6,14 +6,17 @@ import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {toDaysWadUnsafe, toWadUnsafe, unsafeWadDiv, wadLn, unsafeDiv} from "../utils/SignedWadMath.sol"; -import {MultiVRGDA, VRGDAx} from "../VRGDAStruct.sol"; +import {LinearVRGDALib, LinearVRGDAx} from "../LinearVRGDALib.sol"; +import {LogisticVRGDALib, LogisticVRGDAx} from "../LogisticVRGDALib.sol"; /// @title Multi VRGDA NFT /// @author transmissions11 /// @author FrankieIsLost /// @notice Example NFT sold using BOTH Linear VRGDA and Logistic VRGDA. /// @dev This is an example. Do not use in production. -contract MultiNFT is ERC721, MultiVRGDA { +contract MultiNFT is ERC721 { + using LinearVRGDALib for LinearVRGDAx; + using LogisticVRGDALib for LogisticVRGDAx; /*////////////////////////////////////////////////////////////// SALES STORAGE //////////////////////////////////////////////////////////////*/ @@ -25,17 +28,14 @@ contract MultiNFT is ERC721, MultiVRGDA { uint256 public immutable publicStartTime = block.timestamp + 30 days; // When Logistic VRGDA sales begin. // -- VRGDA Objects -- - VRGDAx internal presaleVRGDA; - VRGDAx internal publicVRGDA; + LinearVRGDAx internal presaleVRGDA; + LogisticVRGDAx internal publicVRGDA; // -- Linear VRGDA Params -- int256 public perTimeUnit; // The number of tokens to target selling in 1 full unit of time, scaled by 1e18. // -- Logistic VRGDA Params -- uint256 public constant MAX_MINTABLE = 100; // Max supply. for logistic VRGDA - int256 public immutable logisticLimit; - int256 public immutable logisticLimitDoubled; - int256 internal immutable timeScale; /*////////////////////////////////////////////////////////////// CONSTRUCTOR @@ -50,50 +50,24 @@ contract MultiNFT is ERC721, MultiVRGDA { // ------------------------- // create a VRGDA to be used in presale // ------------------------- - presaleVRGDA = createVRGDA(69.42e18, 0.31e18); - perTimeUnit = 2e18; // additional state variable used for presaleVRGDA.getTargetSaleTime which points to this.getTargetSaleTime + presaleVRGDA = LinearVRGDALib.createLinearVRGDA( + 69.42e18, // Target price. + 0.31e18, // Price decay percent. + 2e18 // 2 units sold per unit of time + ); // ----------------------------- // create a VRGDA to used in public sale // note: we can reuse presaleVRGDA and overwrite the .getTargetSaleTime since // they are used during different times. However for the sake of example, let's define two independent VRGDAs // ----------------------------- - publicVRGDA = createVRGDA(69.42e18, 0.31e18); - - // Set additional state variables used for publicVRGDA.getTargetSaleTime which points to this.getLogisticTargetSaleTime - // Add 1 wad to make the limit inclusive of _maxSellable - logisticLimit = toWadUnsafe(MAX_MINTABLE) + 1e18; - - // Scale by 2e18 to both double it and give it 36 decimals. - logisticLimitDoubled = logisticLimit * 2e18; - - // Time scale - timeScale = 0.1e18; - - // override the target sale time function to use a logistic function - publicVRGDA.getTargetSaleTime = getLogisticTargetSaleTime; - } - - /*////////////////////////////////////////////////////////////// - VRGDA LOGIC - //////////////////////////////////////////////////////////////*/ - /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. - /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return int256 the target time the tokens should be sold by, scaled by 1e18, where the time is - /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. - function getTargetSaleTime(int256 sold) public view override returns (int256) { - return unsafeWadDiv(sold, perTimeUnit); - } - - /// @dev Logistic counterpart to the linear getTargetSaleTime - /// will be assigned to a VRGDAx's .getTargetSaleTime attribute - /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return int256 The target time the tokens should be sold by, scaled by 1e18, where the time is - /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. - function getLogisticTargetSaleTime(int256 sold) public view returns (int256) { - unchecked { - return -unsafeWadDiv(wadLn(unsafeDiv(logisticLimitDoubled, sold + logisticLimit) - 1e18), timeScale); - } + publicVRGDA = LogisticVRGDALib.createLogisticVRGDA( + 69.42e18, // Target price. + 0.31e18, // Price decay percent. + // Maximum # mintable/sellable. + toWadUnsafe(MAX_MINTABLE), + 0.1e18 // Time scale. + ); } /*////////////////////////////////////////////////////////////// @@ -102,15 +76,16 @@ contract MultiNFT is ERC721, MultiVRGDA { function mint() external payable returns (uint256 mintedId) { unchecked { + // mintedId = totalSold++; // conditionally set price based on time + VRGDA // i.e. if time < publicStartTime use presaleVRGDA else use publicVRGDA uint256 price; if (block.timestamp < publicStartTime) { // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. - price = getVRGDAPrice(presaleVRGDA, toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); + price = presaleVRGDA.getVRGDAPrice(toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); } else { - price = getVRGDAPrice(publicVRGDA, toDaysWadUnsafe(block.timestamp - publicStartTime), mintedId = totalSold++); + price = publicVRGDA.getVRGDAPrice(toDaysWadUnsafe(block.timestamp - publicStartTime), mintedId = totalSold++); } require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. diff --git a/test/Contract.t.sol b/test/Contract.t.sol index 8bd57fb..63fa6c9 100644 --- a/test/Contract.t.sol +++ b/test/Contract.t.sol @@ -1,11 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; +import "forge-std/Vm.sol"; + import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; -import {Contract} from "../src/dev/Contract.sol"; +import {Contract} from "../src/examples/Contract.sol"; -contract LinearNFTTest is DSTestPlus { +contract ContractTest is DSTestPlus { Contract c; function setUp() public { @@ -13,10 +15,10 @@ contract LinearNFTTest is DSTestPlus { } function rng(uint256 a, uint256 b) public pure returns (uint256) { - return uint256(keccak256(abi.encodePacked(a, b))) % 100; + return (uint256(keccak256(abi.encodePacked(a, b))) % 10) + 1; } - function testGas() public { + function testGasConsumption() public { uint256 price = c.getPriceLinear(); c.buyLinear{value: price}(1); @@ -24,17 +26,22 @@ contract LinearNFTTest is DSTestPlus { c.buyLogistic{value: price}(1); uint256 amount; - for (uint256 i = 0; i < 100; i++) { + uint256 total; + for (uint256 i = 0; i < 20; i++) { price = c.getPriceLinear(); amount = rng(i, price); - c.buyLinear{value: (price * amount)}(amount); + total = price * amount; + + c.buyLinear{value: total}(amount); hevm.warp(block.timestamp + rng(i, block.timestamp)); } - for (uint256 i = 0; i < 100; i++) { + for (uint256 i = 0; i < 20; i++) { price = c.getPriceLogistic(); amount = rng(i, price); - c.buyLogistic{value: (price * amount)}(amount); + total = price * amount; + + c.buyLogistic{value: total}(amount); hevm.warp(block.timestamp + rng(i, block.timestamp)); } } diff --git a/test/mocks/MockLinearVRGDA.sol b/test/mocks/MockLinearVRGDA.sol index aa4217e..7216dd8 100644 --- a/test/mocks/MockLinearVRGDA.sol +++ b/test/mocks/MockLinearVRGDA.sol @@ -1,12 +1,33 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {LinearVRGDA, VRGDA} from "../../src/LinearVRGDA.sol"; +import {LinearVRGDALib, LinearVRGDAx} from "../../src/LinearVRGDALib.sol"; + +contract MockLinearVRGDA { + using LinearVRGDALib for LinearVRGDAx; + LinearVRGDAx internal linearAuction; -contract MockLinearVRGDA is LinearVRGDA { constructor( int256 _targetPrice, int256 _priceDecreasePercent, int256 _perTimeUnit - ) LinearVRGDA(_targetPrice, _priceDecreasePercent, _perTimeUnit) {} + ) { + linearAuction = LinearVRGDALib.createLinearVRGDA( + _targetPrice, + _priceDecreasePercent, + _perTimeUnit + ); + } + + function targetPrice() public view returns (int256) { + return linearAuction.vrgda.targetPrice; + } + + function getVRGDAPrice(int256 timeSinceStart, uint256 sold) public view returns (uint256) { + return linearAuction.getVRGDAPrice(timeSinceStart, sold); + } + + function getTargetSaleTime(int256 sold) public view returns (int256) { + return LinearVRGDALib.getTargetSaleTime(linearAuction.perTimeUnit, sold); + } } diff --git a/test/mocks/MockLogisticVRGDA.sol b/test/mocks/MockLogisticVRGDA.sol index d1fac53..fde68ab 100644 --- a/test/mocks/MockLogisticVRGDA.sol +++ b/test/mocks/MockLogisticVRGDA.sol @@ -1,13 +1,34 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {LogisticVRGDA, VRGDA} from "../../src/LogisticVRGDA.sol"; +import {LogisticVRGDALib, LogisticVRGDAx} from "../../src/LogisticVRGDALib.sol"; -contract MockLogisticVRGDA is LogisticVRGDA { +contract MockLogisticVRGDA { + using LogisticVRGDALib for LogisticVRGDAx; + LogisticVRGDAx internal logAuction; constructor( int256 _targetPrice, int256 _priceDecreasePercent, int256 _maxSellable, int256 _timeScale - ) LogisticVRGDA(_targetPrice, _priceDecreasePercent, _maxSellable, _timeScale) {} + ) { + logAuction = LogisticVRGDALib.createLogisticVRGDA( + _targetPrice, + _priceDecreasePercent, + _maxSellable, + _timeScale + ); + } + + function targetPrice() public view returns (int256) { + return logAuction.vrgda.targetPrice; + } + + function getVRGDAPrice(int256 timeSinceStart, uint256 sold) public view returns (uint256) { + return logAuction.getVRGDAPrice(timeSinceStart, sold); + } + + function getTargetSaleTime(int256 sold) public view returns (int256) { + return LogisticVRGDALib.getTargetSaleTime(logAuction.logisticLimit, logAuction.timeScale, sold); + } } From 0bdab67403ae3f08054ee8f2135b08d205c95b15 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Thu, 8 Sep 2022 01:50:38 -0400 Subject: [PATCH 24/29] Sync with Master (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ LogisticToLinearVRGDA * 📝 Typo * merge in master, remove base files as originally intended * example implementation of a composite VRGDA * conversion of logisticToLinear NFT to use library * contract based logisticToLinear replaced with composite VRGDA in examples * convert mock contract to use library; add fixes to log2lin lib * add 1 wad for log2lin to follow the logistic behavior * snapshot Co-authored-by: t11s --- .gas-snapshot | 58 +++++++----- src/examples/LogisticToLinearNFT.sol | 93 +++++++++++++++++++ .../composite/LogisticToLinearVRGDALib.sol | 80 ++++++++++++++++ test/LinearVRGDA.t.sol | 4 +- test/LogisticToLinearNFT.t.sol | 39 ++++++++ test/LogisticToLinearVRGDA.t.sol | 75 +++++++++++++++ test/LogisticVRGDA.t.sol | 2 +- test/mocks/MockLinearVRGDA.sol | 4 +- test/mocks/MockLogisticToLinearVRGDA.sol | 41 ++++++++ test/mocks/MockLogisticVRGDA.sol | 4 +- 10 files changed, 367 insertions(+), 33 deletions(-) create mode 100644 src/examples/LogisticToLinearNFT.sol create mode 100644 src/examples/composite/LogisticToLinearVRGDALib.sol create mode 100644 test/LogisticToLinearNFT.t.sol create mode 100644 test/LogisticToLinearVRGDA.t.sol create mode 100644 test/mocks/MockLogisticToLinearVRGDA.sol diff --git a/.gas-snapshot b/.gas-snapshot index 5b256f0..9282def 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,25 +1,33 @@ -LinearNFTTest:testCannotUnderpayForNFTMint() (gas: 38074) -LinearNFTTest:testMintManyNFT() (gas: 4200340) -LinearNFTTest:testMintNFT() (gas: 84140) -LinearVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 10392, ~: 10392) -LinearVRGDATest:testPricingBasic() (gas: 10255) -LinearVRGDATest:testTargetPrice() (gas: 11032) -LogisticNFTTest:testCannotMintMoreThanMax() (gas: 4433171) -LogisticNFTTest:testCannotUnderpayForNFTMint() (gas: 38741) -LogisticNFTTest:testMintAllNFT() (gas: 4422338) -LogisticNFTTest:testMintNFT() (gas: 84829) -LogisticVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 11925, ~: 11925) -LogisticVRGDATest:testFailOverflowForBeyondLimitTokens(uint256,uint256) (runs: 256, μ: 10348, ~: 10348) -LogisticVRGDATest:testGetTargetSaleTimeDoesNotRevertEarly() (gas: 6147) -LogisticVRGDATest:testGetTargetSaleTimeRevertsWhenExpected() (gas: 8533) -LogisticVRGDATest:testNoOverflowForAllTokens(uint256,uint256) (runs: 256, μ: 11462, ~: 11462) -LogisticVRGDATest:testNoOverflowForMostTokens(uint256,uint256) (runs: 256, μ: 11635, ~: 11761) -LogisticVRGDATest:testPricingBasic() (gas: 10995) -LogisticVRGDATest:testTargetPrice() (gas: 12479) -MultiNFTTest:testCannotPublicMintMoreThanMax() (gas: 4486412) -MultiNFTTest:testCannotUnderpayForPresaleNFTMint() (gas: 46730) -MultiNFTTest:testCannotUnderpayForPublicNFTMint() (gas: 46405) -MultiNFTTest:testMintAllPublicNFT() (gas: 4477594) -MultiNFTTest:testMintManyPresaleNFT() (gas: 4269662) -MultiNFTTest:testMintPresaleNFT() (gas: 92830) -MultiNFTTest:testMintPublicNFT() (gas: 94982) +ContractTest:testGasConsumption() (gas: 574114) +LinearNFTTest:testCannotUnderpayForNFTMint() (gas: 44345) +LinearNFTTest:testMintManyNFT() (gas: 4229039) +LinearNFTTest:testMintNFT() (gas: 90366) +LinearVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 17055, ~: 17055) +LinearVRGDATest:testPricingBasic() (gas: 16739) +LinearVRGDATest:testTargetPrice() (gas: 17695) +LogisticNFTTest:testCannotMintMoreThanMax() (gas: 4468512) +LogisticNFTTest:testCannotUnderpayForNFTMint() (gas: 47145) +LogisticNFTTest:testMintAllNFT() (gas: 4457202) +LogisticNFTTest:testMintNFT() (gas: 93188) +LogisticToLinearNFTTest:testCannotUnderpayForNFTMint() (gas: 53571) +LogisticToLinearNFTTest:testMintManyNFT() (gas: 4359639) +LogisticToLinearNFTTest:testMintNFT() (gas: 99592) +LogisticToLinearVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 27228, ~: 27848) +LogisticToLinearVRGDATest:testPricingBasic() (gas: 25943) +LogisticToLinearVRGDATest:testSwitchSmoothness() (gas: 32405) +LogisticToLinearVRGDATest:testTargetPrice() (gas: 28466) +LogisticVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 20681, ~: 20681) +LogisticVRGDATest:testFailOverflowForBeyondLimitTokens(uint256,uint256) (runs: 256, μ: 18879, ~: 18879) +LogisticVRGDATest:testGetTargetSaleTimeDoesNotRevertEarly() (gas: 10360) +LogisticVRGDATest:testGetTargetSaleTimeRevertsWhenExpected() (gas: 12740) +LogisticVRGDATest:testNoOverflowForAllTokens(uint256,uint256) (runs: 256, μ: 19930, ~: 19930) +LogisticVRGDATest:testNoOverflowForMostTokens(uint256,uint256) (runs: 256, μ: 20103, ~: 20229) +LogisticVRGDATest:testPricingBasic() (gas: 19538) +LogisticVRGDATest:testTargetPrice() (gas: 21235) +MultiNFTTest:testCannotPublicMintMoreThanMax() (gas: 4476414) +MultiNFTTest:testCannotUnderpayForPresaleNFTMint() (gas: 44434) +MultiNFTTest:testCannotUnderpayForPublicNFTMint() (gas: 48312) +MultiNFTTest:testMintAllPublicNFT() (gas: 4467571) +MultiNFTTest:testMintManyPresaleNFT() (gas: 4240217) +MultiNFTTest:testMintPresaleNFT() (gas: 90511) +MultiNFTTest:testMintPublicNFT() (gas: 96866) diff --git a/src/examples/LogisticToLinearNFT.sol b/src/examples/LogisticToLinearNFT.sol new file mode 100644 index 0000000..2239c92 --- /dev/null +++ b/src/examples/LogisticToLinearNFT.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {ERC721} from "solmate/tokens/ERC721.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; + +import {toDaysWadUnsafe, toWadUnsafe} from "../utils/SignedWadMath.sol"; + +import {LogisticToLinearVRGDALib, LogisticToLinearVRGDAx} from "./composite/LogisticToLinearVRGDALib.sol"; + +/// @title Logistic To Linear VRGDA NFT +/// @author transmissions11 +/// @author FrankieIsLost +/// @notice Example NFT sold using LogisticToLinearVRGDA. +/// @dev This is an example. Do not use in production. +contract LogisticToLinearNFT is ERC721 { + using LogisticToLinearVRGDALib for LogisticToLinearVRGDAx; + /*////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////*/ + + /// @dev The day the switch from a logistic to translated linear VRGDA is targeted to occur. + /// @dev Represented as an 18 decimal fixed point number. + int256 internal constant SWITCH_DAY_WAD = 233e18; + + /// @notice The minimum amount of tokens that must be sold for the VRGDA issuance + /// schedule to switch from logistic to the "post switch" translated linear formula. + /// @dev Computed off-chain by plugging SWITCH_DAY_WAD into the uninverted pacing formula. + /// @dev Represented as an 18 decimal fixed point number. + int256 internal constant SOLD_BY_SWITCH_WAD = 8336.760939794622713006e18; + + /*////////////////////////////////////////////////////////////// + SALES STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 public totalSold; // The total number of tokens sold so far. + + uint256 public immutable startTime = block.timestamp; // When VRGDA sales begun. + + // Note: instead of defining a single composite VRGDA, we could define two separate VRGDA's and + // and use external conditions (sold units, time, etc.) to determine which VRGDA to use. + // but we'll use the composite for the sake of example + LogisticToLinearVRGDAx public vrgda; // The VRGDA used to price the NFT. + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor() + ERC721( + "Example Logistic To Linear NFT", // Name. + "LOGISTIC_TO_LINEAR" // Symbol. + ) + { + vrgda = LogisticToLinearVRGDALib.createLogisticToLinearVRGDA( + 4.2069e18, // Target price. + 0.31e18, // Price decay percent. + 9000e18, // Logistic asymptote. + 0.014e18, // Logistic time scale. + SOLD_BY_SWITCH_WAD, // Sold by switch. + SWITCH_DAY_WAD, // Target switch day. + 9e18 // Sales to target per day. + ); + } + + /*////////////////////////////////////////////////////////////// + MINTING LOGIC + //////////////////////////////////////////////////////////////*/ + + function mint() external payable returns (uint256 mintedId) { + unchecked { + // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. + uint256 price = vrgda.getVRGDAPrice(toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); + + require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. + + _mint(msg.sender, mintedId); // Mint the NFT using mintedId. + + // Note: We do this at the end to avoid creating a reentrancy vector. + // Refund the user any ETH they spent over the current price of the NFT. + // Unchecked is safe here because we validate msg.value >= price above. + SafeTransferLib.safeTransferETH(msg.sender, msg.value - price); + } + } + + /*////////////////////////////////////////////////////////////// + URI LOGIC + //////////////////////////////////////////////////////////////*/ + + function tokenURI(uint256) public pure override returns (string memory) { + return "https://example.com"; + } +} diff --git a/src/examples/composite/LogisticToLinearVRGDALib.sol b/src/examples/composite/LogisticToLinearVRGDALib.sol new file mode 100644 index 0000000..40e7bf4 --- /dev/null +++ b/src/examples/composite/LogisticToLinearVRGDALib.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {wadLn, unsafeDiv, unsafeWadDiv, toWadUnsafe} from "../../utils/SignedWadMath.sol"; + +import {LogisticVRGDALib} from "../../LogisticVRGDALib.sol"; +import {VRGDALib, VRGDAx} from "../../VRGDALib.sol"; + +struct LogisticToLinearVRGDAx { + int256 logisticLimit; + int256 timeScale; + int256 soldBySwitch; + int256 switchTime; + int256 perTimeUnit; + VRGDAx vrgda; +} + +/// @title Linear Variable Rate Gradual Dutch Auction +/// @author transmissions11 +/// @author FrankieIsLost +/// @notice VRGDA with a linear issuance curve. +library LogisticToLinearVRGDALib { + + /// @notice Create a Linear VRGDA using specified parameters. + /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. + /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. + /// @param _perTimeUnit The number of tokens to target sell in 1 full unit of time, scaled by 1e18. + /// @return logToLinearVRGDA The created Linear VRGDA (of type struct LogisticToLinearVRGDAx). + function createLogisticToLinearVRGDA( + int256 _targetPrice, + int256 _priceDecayPercent, + int256 _logisticAsymptote, + int256 _timeScale, + int256 _soldBySwitch, + int256 _switchTime, + int256 _perTimeUnit + ) internal pure returns (LogisticToLinearVRGDAx memory logToLinearVRGDA) { + logToLinearVRGDA = LogisticToLinearVRGDAx( + _logisticAsymptote + 1e18, // add 1 wad to make the limit inclusive + _timeScale, + _soldBySwitch, + _switchTime, + _perTimeUnit, + VRGDALib.createVRGDA(_targetPrice, _priceDecayPercent) + ); + } + + /*////////////////////////////////////////////////////////////// + PRICING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Calculate the price of a token according to the VRGDA formula. + /// @param self a VRGDA represented as the LogisticToLinearVRGDAx struct + /// @param timeSinceStart Units of time passed since the VRGDA began, scaled by 1e18. + /// @param sold The number of tokens sold so far, scaled by 1e18. + /// @return uint256 The price of a token according to VRGDA, scaled by 1e18. + function getVRGDAPrice(LogisticToLinearVRGDAx memory self, int256 timeSinceStart, uint256 sold) + internal + pure + returns (uint256) + { + int256 timeDelta; + unchecked { + timeDelta = timeSinceStart - getTargetSaleTime(self, toWadUnsafe(sold + 1)); + } + return VRGDALib.getVRGDAPrice(self.vrgda.targetPrice, self.vrgda.decayConstant, timeDelta); + } + + /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. + /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. + /// @return int256 The target time the tokens should be sold by, scaled by 1e18, where the time is + /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. + function getTargetSaleTime(LogisticToLinearVRGDAx memory self, int256 sold) internal pure returns (int256) { + if (sold < self.soldBySwitch) return LogisticVRGDALib.getTargetSaleTime(self.logisticLimit, self.timeScale, sold); + + unchecked { + return unsafeWadDiv(sold - self.soldBySwitch, self.perTimeUnit) + self.switchTime; + } + } +} diff --git a/test/LinearVRGDA.t.sol b/test/LinearVRGDA.t.sol index c106032..e518cd2 100644 --- a/test/LinearVRGDA.t.sol +++ b/test/LinearVRGDA.t.sol @@ -9,8 +9,6 @@ import {MockLinearVRGDA} from "./mocks/MockLinearVRGDA.sol"; uint256 constant ONE_THOUSAND_YEARS = 356 days * 1000; -uint256 constant MAX_SELLABLE = 6392; - contract LinearVRGDATest is DSTestPlus { MockLinearVRGDA vrgda; @@ -31,7 +29,7 @@ contract LinearVRGDATest is DSTestPlus { } function testPricingBasic() public { - // Our VRGDA targets this number of mints at given time. + // Our VRGDA targets this number of mints at the given time. uint256 timeDelta = 120 days; uint256 numMint = 239; diff --git a/test/LogisticToLinearNFT.t.sol b/test/LogisticToLinearNFT.t.sol new file mode 100644 index 0000000..56f6e3c --- /dev/null +++ b/test/LogisticToLinearNFT.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; + +import {LogisticToLinearNFT} from "../src/examples/LogisticToLinearNFT.sol"; + +contract LogisticToLinearNFTTest is DSTestPlus { + LogisticToLinearNFT nft; + + function setUp() public { + nft = new LogisticToLinearNFT(); + } + + function testMintNFT() public { + nft.mint{value: 4.231748564166457308e18}(); + + assertEq(nft.balanceOf(address(this)), 1); + assertEq(nft.ownerOf(0), address(this)); + } + + function testCannotUnderpayForNFTMint() public { + hevm.expectRevert("UNDERPAID"); + nft.mint{value: 4e18}(); + } + + function testMintManyNFT() public { + for (uint256 i = 0; i < 100; i++) { + nft.mint{value: address(this).balance}(); + } + + assertEq(nft.balanceOf(address(this)), 100); + for (uint256 i = 0; i < 100; i++) { + assertEq(nft.ownerOf(i), address(this)); + } + } + + receive() external payable {} +} diff --git a/test/LogisticToLinearVRGDA.t.sol b/test/LogisticToLinearVRGDA.t.sol new file mode 100644 index 0000000..65079dc --- /dev/null +++ b/test/LogisticToLinearVRGDA.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; + +import {toWadUnsafe, toDaysWadUnsafe, fromDaysWadUnsafe} from "../src/utils/SignedWadMath.sol"; + +import {MockLogisticToLinearVRGDA} from "./mocks/MockLogisticToLinearVRGDA.sol"; + +uint256 constant ONE_THOUSAND_YEARS = 356 days * 1000; + +int256 constant SWITCH_DAY_WAD = 233e18; + +int256 constant SOLD_BY_SWITCH_WAD = 8336.760939794622713006e18; + +contract LogisticToLinearVRGDATest is DSTestPlus { + MockLogisticToLinearVRGDA vrgda; + + function setUp() public { + vrgda = new MockLogisticToLinearVRGDA( + 4.2069e18, // Target price. + 0.31e18, // Price decay percent. + 9000e18, // Logistic asymptote. + 0.014e18, // Logistic time scale. + SOLD_BY_SWITCH_WAD, // Sold by switch. + SWITCH_DAY_WAD, // Target switch day. + 9e18 // Tokens to target per day. + ); + } + + function testTargetPrice() public { + // Warp to the target sale time so that the VRGDA price equals the target price. + hevm.warp(block.timestamp + fromDaysWadUnsafe(vrgda.getTargetSaleTime(1e18))); + + uint256 cost = vrgda.getVRGDAPrice(toDaysWadUnsafe(block.timestamp), 0); + assertRelApproxEq(cost, uint256(vrgda.targetPrice()), 0.00001e18); + } + + function testSwitchSmoothness() public { + uint256 switchTokenSaleTime = uint256(vrgda.getTargetSaleTime(8337e18) - vrgda.getTargetSaleTime(8336e18)); + + assertRelApproxEq( + uint256(vrgda.getTargetSaleTime(8336e18) - vrgda.getTargetSaleTime(8335e18)), + switchTokenSaleTime, + 0.0005e18 + ); + + assertRelApproxEq( + switchTokenSaleTime, + uint256(vrgda.getTargetSaleTime(8338e18) - vrgda.getTargetSaleTime(8337e18)), + 0.005e18 + ); + } + + function testPricingBasic() public { + // Our VRGDA targets this number of mints at the given time. + uint256 timeDelta = 60 days; + uint256 numMint = 3572; + + hevm.warp(block.timestamp + timeDelta); + + uint256 cost = vrgda.getVRGDAPrice(toDaysWadUnsafe(block.timestamp), numMint); + assertRelApproxEq(cost, uint256(vrgda.targetPrice()), 0.002e18); + } + + function testAlwaysTargetPriceInRightConditions(uint256 sold) public { + sold = bound(sold, 0, type(uint128).max); + + assertRelApproxEq( + vrgda.getVRGDAPrice(vrgda.getTargetSaleTime(toWadUnsafe(sold + 1)), sold), + uint256(vrgda.targetPrice()), + 0.00001e18 + ); + } +} diff --git a/test/LogisticVRGDA.t.sol b/test/LogisticVRGDA.t.sol index 99fd031..d7d3ed1 100644 --- a/test/LogisticVRGDA.t.sol +++ b/test/LogisticVRGDA.t.sol @@ -32,7 +32,7 @@ contract LogisticVRGDATest is DSTestPlus { } function testPricingBasic() public { - // Our VRGDA targets this number of mints at given time. + // Our VRGDA targets this number of mints at the given time. uint256 timeDelta = 120 days; uint256 numMint = 876; diff --git a/test/mocks/MockLinearVRGDA.sol b/test/mocks/MockLinearVRGDA.sol index 7216dd8..4e09f02 100644 --- a/test/mocks/MockLinearVRGDA.sol +++ b/test/mocks/MockLinearVRGDA.sol @@ -9,12 +9,12 @@ contract MockLinearVRGDA { constructor( int256 _targetPrice, - int256 _priceDecreasePercent, + int256 _priceDecayPercent, int256 _perTimeUnit ) { linearAuction = LinearVRGDALib.createLinearVRGDA( _targetPrice, - _priceDecreasePercent, + _priceDecayPercent, _perTimeUnit ); } diff --git a/test/mocks/MockLogisticToLinearVRGDA.sol b/test/mocks/MockLogisticToLinearVRGDA.sol new file mode 100644 index 0000000..8b47d8c --- /dev/null +++ b/test/mocks/MockLogisticToLinearVRGDA.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {LogisticToLinearVRGDALib, LogisticToLinearVRGDAx} from "../../src/examples/composite/LogisticToLinearVRGDALib.sol"; + +contract MockLogisticToLinearVRGDA { + using LogisticToLinearVRGDALib for LogisticToLinearVRGDAx; + LogisticToLinearVRGDAx internal logToLinearAuction; + + constructor( + int256 _targetPrice, + int256 _priceDecayPercent, + int256 _logisticAsymptote, + int256 _timeScale, + int256 _soldBySwitch, + int256 _switchTime, + int256 _perTimeUnit + ) + { + logToLinearAuction = LogisticToLinearVRGDALib.createLogisticToLinearVRGDA( + _targetPrice, + _priceDecayPercent, + _logisticAsymptote, + _timeScale, + _soldBySwitch, + _switchTime, + _perTimeUnit + ); + } + function targetPrice() public view returns (int256) { + return logToLinearAuction.vrgda.targetPrice; + } + + function getVRGDAPrice(int256 timeSinceStart, uint256 sold) public view returns (uint256) { + return logToLinearAuction.getVRGDAPrice(timeSinceStart, sold); + } + + function getTargetSaleTime(int256 sold) public view returns (int256) { + return logToLinearAuction.getTargetSaleTime(sold); + } +} diff --git a/test/mocks/MockLogisticVRGDA.sol b/test/mocks/MockLogisticVRGDA.sol index fde68ab..88705b4 100644 --- a/test/mocks/MockLogisticVRGDA.sol +++ b/test/mocks/MockLogisticVRGDA.sol @@ -8,13 +8,13 @@ contract MockLogisticVRGDA { LogisticVRGDAx internal logAuction; constructor( int256 _targetPrice, - int256 _priceDecreasePercent, + int256 _priceDecayPercent, int256 _maxSellable, int256 _timeScale ) { logAuction = LogisticVRGDALib.createLogisticVRGDA( _targetPrice, - _priceDecreasePercent, + _priceDecayPercent, _maxSellable, _timeScale ); From 952cb97883b042bfce7f2c86fb1841eb2ce23d99 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Thu, 8 Sep 2022 02:02:15 -0400 Subject: [PATCH 25/29] remove log2lin in favor of lib --- src/LogisticToLinearVRGDA.sol | 71 ----------------------------------- 1 file changed, 71 deletions(-) delete mode 100644 src/LogisticToLinearVRGDA.sol diff --git a/src/LogisticToLinearVRGDA.sol b/src/LogisticToLinearVRGDA.sol deleted file mode 100644 index 65f7873..0000000 --- a/src/LogisticToLinearVRGDA.sol +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -import {unsafeWadDiv} from "./utils/SignedWadMath.sol"; - -import {VRGDA} from "./VRGDA.sol"; -import {LogisticVRGDA} from "./LogisticVRGDA.sol"; - -/// @title Logistic To Linear Variable Rate Gradual Dutch Auction -/// @author transmissions11 -/// @author FrankieIsLost -/// @notice VRGDA with a piecewise logistic and linear issuance curve. -abstract contract LogisticToLinearVRGDA is LogisticVRGDA { - /*////////////////////////////////////////////////////////////// - PRICING PARAMETERS - //////////////////////////////////////////////////////////////*/ - - /// @dev The number of tokens that must be sold for the switch to occur. - /// @dev Represented as an 18 decimal fixed point number. - int256 internal immutable soldBySwitch; - - /// @dev The time soldBySwitch tokens were targeted to sell by. - /// @dev Represented as an 18 decimal fixed point number. - int256 internal immutable switchTime; - - /// @dev The total number of tokens to target selling every full unit of time. - /// @dev Represented as an 18 decimal fixed point number. - int256 internal immutable perTimeUnit; - - /// @notice Sets pricing parameters for the VRGDA. - /// @param _targetPrice The target price for a token if sold on pace, scaled by 1e18. - /// @param _priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. - /// @param _logisticAsymptote The asymptote (minus 1) of the pre-switch logistic curve, scaled by 1e18. - /// @param _timeScale The steepness of the pre-switch logistic curve, scaled by 1e18. - /// @param _soldBySwitch The number of tokens that must be sold for the switch to occur. - /// @param _switchTime The time soldBySwitch tokens were targeted to sell by, scaled by 1e18. - /// @param _perTimeUnit The number of tokens to target selling in 1 full unit of time, scaled by 1e18. - constructor( - int256 _targetPrice, - int256 _priceDecayPercent, - int256 _logisticAsymptote, - int256 _timeScale, - int256 _soldBySwitch, - int256 _switchTime, - int256 _perTimeUnit - ) LogisticVRGDA(_targetPrice, _priceDecayPercent, _logisticAsymptote, _timeScale) { - soldBySwitch = _soldBySwitch; - - switchTime = _switchTime; - - perTimeUnit = _perTimeUnit; - } - - /*////////////////////////////////////////////////////////////// - PRICING LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @dev Given a number of tokens sold, return the target time that number of tokens should be sold by. - /// @param sold A number of tokens sold, scaled by 1e18, to get the corresponding target sale time for. - /// @return The target time the tokens should be sold by, scaled by 1e18, where the time is - /// relative, such that 0 means the tokens should be sold immediately when the VRGDA begins. - function getTargetSaleTime(int256 sold) public view virtual override returns (int256) { - // If we've not yet reached the number of sales required for the switch - // to occur, we'll continue using the standard logistic VRGDA schedule. - if (sold < soldBySwitch) return LogisticVRGDA.getTargetSaleTime(sold); - - unchecked { - return unsafeWadDiv(sold - soldBySwitch, perTimeUnit) + switchTime; - } - } -} From 04529bba3be2638168a7f3e3a2d084b41ca7cec8 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Thu, 8 Sep 2022 09:09:26 -0400 Subject: [PATCH 26/29] use solmate signedWadMath --- src/LinearVRGDALib.sol | 2 +- src/LogisticVRGDALib.sol | 2 +- src/VRGDALib.sol | 2 +- src/examples/Contract.sol | 2 +- src/examples/MultiNFT.sol | 2 +- src/examples/composite/LogisticToLinearVRGDALib.sol | 2 +- src/lib/VRGDALibrary.sol | 2 +- test/MultiNFT.t.sol | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/LinearVRGDALib.sol b/src/LinearVRGDALib.sol index f1e8845..7e036ef 100644 --- a/src/LinearVRGDALib.sol +++ b/src/LinearVRGDALib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {unsafeWadDiv, toWadUnsafe} from "./utils/SignedWadMath.sol"; +import {unsafeWadDiv, toWadUnsafe} from "solmate/utils/SignedWadMath.sol"; import {VRGDALib, VRGDAx} from "./VRGDALib.sol"; diff --git a/src/LogisticVRGDALib.sol b/src/LogisticVRGDALib.sol index bf50d9b..509e33a 100644 --- a/src/LogisticVRGDALib.sol +++ b/src/LogisticVRGDALib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {wadLn, unsafeDiv, unsafeWadDiv, toWadUnsafe} from "./utils/SignedWadMath.sol"; +import {wadLn, unsafeDiv, unsafeWadDiv, toWadUnsafe} from "solmate/utils/SignedWadMath.sol"; import {VRGDALib, VRGDAx} from "./VRGDALib.sol"; diff --git a/src/VRGDALib.sol b/src/VRGDALib.sol index 11db14c..fc01790 100644 --- a/src/VRGDALib.sol +++ b/src/VRGDALib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "./utils/SignedWadMath.sol"; +import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "solmate/utils/SignedWadMath.sol"; // TODO: rename to something better? // VRGDAx sounds badass tho diff --git a/src/examples/Contract.sol b/src/examples/Contract.sol index 093165f..695d76d 100644 --- a/src/examples/Contract.sol +++ b/src/examples/Contract.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0; import {LinearVRGDALib, LinearVRGDAx} from "../LinearVRGDALib.sol"; import {LogisticVRGDALib, LogisticVRGDAx} from "../LogisticVRGDALib.sol"; -import {toWadUnsafe} from "../utils/SignedWadMath.sol"; +import {toWadUnsafe} from "solmate/utils/SignedWadMath.sol"; contract Contract { using LinearVRGDALib for LinearVRGDAx; diff --git a/src/examples/MultiNFT.sol b/src/examples/MultiNFT.sol index a30b6fb..47bdb3b 100644 --- a/src/examples/MultiNFT.sol +++ b/src/examples/MultiNFT.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.0; import {ERC721} from "solmate/tokens/ERC721.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {toDaysWadUnsafe, toWadUnsafe, unsafeWadDiv, wadLn, unsafeDiv} from "../utils/SignedWadMath.sol"; +import {toDaysWadUnsafe, toWadUnsafe, unsafeWadDiv, wadLn, unsafeDiv} from "solmate/utils/SignedWadMath.sol"; import {LinearVRGDALib, LinearVRGDAx} from "../LinearVRGDALib.sol"; import {LogisticVRGDALib, LogisticVRGDAx} from "../LogisticVRGDALib.sol"; diff --git a/src/examples/composite/LogisticToLinearVRGDALib.sol b/src/examples/composite/LogisticToLinearVRGDALib.sol index 40e7bf4..0bd7cd1 100644 --- a/src/examples/composite/LogisticToLinearVRGDALib.sol +++ b/src/examples/composite/LogisticToLinearVRGDALib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {wadLn, unsafeDiv, unsafeWadDiv, toWadUnsafe} from "../../utils/SignedWadMath.sol"; +import {wadLn, unsafeDiv, unsafeWadDiv, toWadUnsafe} from "solmate/utils/SignedWadMath.sol"; import {LogisticVRGDALib} from "../../LogisticVRGDALib.sol"; import {VRGDALib, VRGDAx} from "../../VRGDALib.sol"; diff --git a/src/lib/VRGDALibrary.sol b/src/lib/VRGDALibrary.sol index c3c4e93..48f3b32 100644 --- a/src/lib/VRGDALibrary.sol +++ b/src/lib/VRGDALibrary.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "../utils/SignedWadMath.sol"; +import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "solmate/utils/SignedWadMath.sol"; library VRGDALibrary { /// @notice Calculate the price of a token according to the VRGDA formula. diff --git a/test/MultiNFT.t.sol b/test/MultiNFT.t.sol index 4e7c702..2aaa06e 100644 --- a/test/MultiNFT.t.sol +++ b/test/MultiNFT.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; -import {fromDaysWadUnsafe} from "../src/utils/SignedWadMath.sol"; +import {fromDaysWadUnsafe} from "solmate/utils/SignedWadMath.sol"; import {MultiNFT} from "../src/examples/MultiNFT.sol"; contract MultiNFTTest is DSTestPlus { From 509ba3950e610a24e948cca692d588cfa0865aaa Mon Sep 17 00:00:00 2001 From: saucepoint Date: Thu, 8 Sep 2022 09:20:49 -0400 Subject: [PATCH 27/29] dep updates --- lib/forge-std | 2 +- lib/solmate | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/forge-std b/lib/forge-std index 25bff3b..82e6f53 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 25bff3b6c923edf941e5335767998e302925c20f +Subproject commit 82e6f5376c15341629ca23098e0b32d303f44f02 diff --git a/lib/solmate b/lib/solmate index 9cf1428..2781a2c 160000 --- a/lib/solmate +++ b/lib/solmate @@ -1 +1 @@ -Subproject commit 9cf1428245074e39090dceacb0c28b1f684f584c +Subproject commit 2781a2c7e9a7daa93372606493411ae92b628590 From f89803562f9c822e86f71845868df70b19207482 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Thu, 8 Sep 2022 11:47:32 -0400 Subject: [PATCH 28/29] reorder structs to shave 30 gas --- src/LinearVRGDALib.sol | 6 +++--- src/LogisticVRGDALib.sol | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/LinearVRGDALib.sol b/src/LinearVRGDALib.sol index 7e036ef..f7e84ed 100644 --- a/src/LinearVRGDALib.sol +++ b/src/LinearVRGDALib.sol @@ -6,8 +6,8 @@ import {unsafeWadDiv, toWadUnsafe} from "solmate/utils/SignedWadMath.sol"; import {VRGDALib, VRGDAx} from "./VRGDALib.sol"; struct LinearVRGDAx { - int256 perTimeUnit; VRGDAx vrgda; + int256 perTimeUnit; } /// @title Linear Variable Rate Gradual Dutch Auction @@ -28,8 +28,8 @@ library LinearVRGDALib { int256 _perTimeUnit ) internal pure returns (LinearVRGDAx memory linearVRGDA) { linearVRGDA = LinearVRGDAx( - _perTimeUnit, - VRGDALib.createVRGDA(_targetPrice, _priceDecayPercent) + VRGDALib.createVRGDA(_targetPrice, _priceDecayPercent), + _perTimeUnit ); } diff --git a/src/LogisticVRGDALib.sol b/src/LogisticVRGDALib.sol index 509e33a..5976ddf 100644 --- a/src/LogisticVRGDALib.sol +++ b/src/LogisticVRGDALib.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {wadLn, unsafeDiv, unsafeWadDiv, toWadUnsafe} from "solmate/utils/SignedWadMath.sol"; +import {wadMul, wadLn, unsafeDiv, unsafeWadDiv, toWadUnsafe} from "solmate/utils/SignedWadMath.sol"; import {VRGDALib, VRGDAx} from "./VRGDALib.sol"; struct LogisticVRGDAx { + VRGDAx vrgda; int256 logisticLimit; int256 timeScale; - VRGDAx vrgda; } /// @title Logistic Variable Rate Gradual Dutch Auction @@ -30,9 +30,9 @@ library LogisticVRGDALib { int256 _timeScale ) internal pure returns (LogisticVRGDAx memory logisticVRGDAx) { logisticVRGDAx = LogisticVRGDAx( + VRGDALib.createVRGDA(_targetPrice, _priceDecayPercent), _maxSellable + 1e18, // add 1 wad to make the limit inclusive of _maxSellable - _timeScale, - VRGDALib.createVRGDA(_targetPrice, _priceDecayPercent) + _timeScale ); } From fed9831aaadfc4698c7051853bdaafb4ddd07382 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Thu, 8 Sep 2022 11:52:47 -0400 Subject: [PATCH 29/29] reorder struct of log2in VRGDA; update gas snapshot --- .gas-snapshot | 62 +++++++++---------- .../composite/LogisticToLinearVRGDALib.sol | 6 +- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 9282def..eceb74f 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,33 +1,33 @@ -ContractTest:testGasConsumption() (gas: 574114) -LinearNFTTest:testCannotUnderpayForNFTMint() (gas: 44345) -LinearNFTTest:testMintManyNFT() (gas: 4229039) -LinearNFTTest:testMintNFT() (gas: 90366) -LinearVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 17055, ~: 17055) -LinearVRGDATest:testPricingBasic() (gas: 16739) -LinearVRGDATest:testTargetPrice() (gas: 17695) -LogisticNFTTest:testCannotMintMoreThanMax() (gas: 4468512) -LogisticNFTTest:testCannotUnderpayForNFTMint() (gas: 47145) -LogisticNFTTest:testMintAllNFT() (gas: 4457202) -LogisticNFTTest:testMintNFT() (gas: 93188) -LogisticToLinearNFTTest:testCannotUnderpayForNFTMint() (gas: 53571) -LogisticToLinearNFTTest:testMintManyNFT() (gas: 4359639) -LogisticToLinearNFTTest:testMintNFT() (gas: 99592) -LogisticToLinearVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 27228, ~: 27848) -LogisticToLinearVRGDATest:testPricingBasic() (gas: 25943) -LogisticToLinearVRGDATest:testSwitchSmoothness() (gas: 32405) -LogisticToLinearVRGDATest:testTargetPrice() (gas: 28466) -LogisticVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 20681, ~: 20681) -LogisticVRGDATest:testFailOverflowForBeyondLimitTokens(uint256,uint256) (runs: 256, μ: 18879, ~: 18879) +ContractTest:testGasConsumption() (gas: 571342) +LinearNFTTest:testCannotUnderpayForNFTMint() (gas: 44315) +LinearNFTTest:testMintManyNFT() (gas: 4226039) +LinearNFTTest:testMintNFT() (gas: 90336) +LinearVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 17028, ~: 17028) +LinearVRGDATest:testPricingBasic() (gas: 16709) +LinearVRGDATest:testTargetPrice() (gas: 17668) +LogisticNFTTest:testCannotMintMoreThanMax() (gas: 4464882) +LogisticNFTTest:testCannotUnderpayForNFTMint() (gas: 47109) +LogisticNFTTest:testMintAllNFT() (gas: 4453602) +LogisticNFTTest:testMintNFT() (gas: 93152) +LogisticToLinearNFTTest:testCannotUnderpayForNFTMint() (gas: 53541) +LogisticToLinearNFTTest:testMintManyNFT() (gas: 4356639) +LogisticToLinearNFTTest:testMintNFT() (gas: 99562) +LogisticToLinearVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 27174, ~: 27794) +LogisticToLinearVRGDATest:testPricingBasic() (gas: 25913) +LogisticToLinearVRGDATest:testSwitchSmoothness() (gas: 32261) +LogisticToLinearVRGDATest:testTargetPrice() (gas: 28412) +LogisticVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 20645, ~: 20645) +LogisticVRGDATest:testFailOverflowForBeyondLimitTokens(uint256,uint256) (runs: 256, μ: 18849, ~: 18849) LogisticVRGDATest:testGetTargetSaleTimeDoesNotRevertEarly() (gas: 10360) LogisticVRGDATest:testGetTargetSaleTimeRevertsWhenExpected() (gas: 12740) -LogisticVRGDATest:testNoOverflowForAllTokens(uint256,uint256) (runs: 256, μ: 19930, ~: 19930) -LogisticVRGDATest:testNoOverflowForMostTokens(uint256,uint256) (runs: 256, μ: 20103, ~: 20229) -LogisticVRGDATest:testPricingBasic() (gas: 19538) -LogisticVRGDATest:testTargetPrice() (gas: 21235) -MultiNFTTest:testCannotPublicMintMoreThanMax() (gas: 4476414) -MultiNFTTest:testCannotUnderpayForPresaleNFTMint() (gas: 44434) -MultiNFTTest:testCannotUnderpayForPublicNFTMint() (gas: 48312) -MultiNFTTest:testMintAllPublicNFT() (gas: 4467571) -MultiNFTTest:testMintManyPresaleNFT() (gas: 4240217) -MultiNFTTest:testMintPresaleNFT() (gas: 90511) -MultiNFTTest:testMintPublicNFT() (gas: 96866) +LogisticVRGDATest:testNoOverflowForAllTokens(uint256,uint256) (runs: 256, μ: 19894, ~: 19894) +LogisticVRGDATest:testNoOverflowForMostTokens(uint256,uint256) (runs: 256, μ: 20067, ~: 20193) +LogisticVRGDATest:testPricingBasic() (gas: 19502) +LogisticVRGDATest:testTargetPrice() (gas: 21199) +MultiNFTTest:testCannotPublicMintMoreThanMax() (gas: 4472784) +MultiNFTTest:testCannotUnderpayForPresaleNFTMint() (gas: 44404) +MultiNFTTest:testCannotUnderpayForPublicNFTMint() (gas: 48276) +MultiNFTTest:testMintAllPublicNFT() (gas: 4463971) +MultiNFTTest:testMintManyPresaleNFT() (gas: 4237217) +MultiNFTTest:testMintPresaleNFT() (gas: 90481) +MultiNFTTest:testMintPublicNFT() (gas: 96830) diff --git a/src/examples/composite/LogisticToLinearVRGDALib.sol b/src/examples/composite/LogisticToLinearVRGDALib.sol index 0bd7cd1..06bfddc 100644 --- a/src/examples/composite/LogisticToLinearVRGDALib.sol +++ b/src/examples/composite/LogisticToLinearVRGDALib.sol @@ -7,12 +7,12 @@ import {LogisticVRGDALib} from "../../LogisticVRGDALib.sol"; import {VRGDALib, VRGDAx} from "../../VRGDALib.sol"; struct LogisticToLinearVRGDAx { + VRGDAx vrgda; int256 logisticLimit; int256 timeScale; int256 soldBySwitch; int256 switchTime; int256 perTimeUnit; - VRGDAx vrgda; } /// @title Linear Variable Rate Gradual Dutch Auction @@ -36,12 +36,12 @@ library LogisticToLinearVRGDALib { int256 _perTimeUnit ) internal pure returns (LogisticToLinearVRGDAx memory logToLinearVRGDA) { logToLinearVRGDA = LogisticToLinearVRGDAx( + VRGDALib.createVRGDA(_targetPrice, _priceDecayPercent), _logisticAsymptote + 1e18, // add 1 wad to make the limit inclusive _timeScale, _soldBySwitch, _switchTime, - _perTimeUnit, - VRGDALib.createVRGDA(_targetPrice, _priceDecayPercent) + _perTimeUnit ); }