From 8ea9f05254f5ee3f973ce5b3b6a6076614df8f08 Mon Sep 17 00:00:00 2001 From: beans Date: Tue, 30 Aug 2022 15:53:25 -0400 Subject: [PATCH 1/7] fix underflow --- .gas-snapshot | 2 +- src/examples/LinearNFT.sol | 4 ++-- src/examples/LogisticNFT.sol | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index b88bd7d..299ef76 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -13,6 +13,6 @@ LogisticVRGDATest:testFailOverflowForBeyondLimitTokens(uint256,uint256) (runs: 2 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:testNoOverflowForMostTokens(uint256,uint256) (runs: 256, μ: 11402, ~: 11528) LogisticVRGDATest:testPricingBasic() (gas: 10762) LogisticVRGDATest:testTargetPrice() (gas: 12246) diff --git a/src/examples/LinearNFT.sol b/src/examples/LinearNFT.sol index a0ca0e6..aaa2e71 100644 --- a/src/examples/LinearNFT.sol +++ b/src/examples/LinearNFT.sol @@ -43,8 +43,8 @@ contract LinearNFT is ERC721, LinearVRGDA { //////////////////////////////////////////////////////////////*/ function mint() external payable returns (uint256 mintedId) { - // Note: By using toDaysWadUnsafe(startTime - block.timestamp) we are establishing that 1 "unit of time" is 1 day. - uint256 price = getVRGDAPrice(toDaysWadUnsafe(startTime - block.timestamp), mintedId = totalSold++); + // 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++); require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. diff --git a/src/examples/LogisticNFT.sol b/src/examples/LogisticNFT.sol index 08ca603..c793904 100644 --- a/src/examples/LogisticNFT.sol +++ b/src/examples/LogisticNFT.sol @@ -54,8 +54,8 @@ contract LogisticNFT is ERC721, LogisticVRGDA { // Note: We don't need to check totalSold < MAX_MINTABLE, because getVRGDAPrice will // revert if we're over the max mintable limit we set when constructing LogisticVRGDA. - // Note: By using toDaysWadUnsafe(startTime - block.timestamp) we are establishing that 1 "unit of time" is 1 day. - uint256 price = getVRGDAPrice(toDaysWadUnsafe(startTime - block.timestamp), mintedId = totalSold++); + // 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++); require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. From 3e87d11205ee9b48af71abc9cc8af28651e60b66 Mon Sep 17 00:00:00 2001 From: beans Date: Tue, 30 Aug 2022 23:08:27 -0400 Subject: [PATCH 2/7] uncheck --- .gas-snapshot | 14 +++++++------- src/examples/LinearNFT.sol | 8 ++++++-- src/examples/LogisticNFT.sol | 8 ++++++-- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 299ef76..5c40300 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,13 +1,13 @@ -LinearNFTTest:testCannotUnderpayForNFTMint() (gas: 40100) -LinearNFTTest:testMintManyNFT() (gas: 4204940) -LinearNFTTest:testMintNFT() (gas: 86166) +LinearNFTTest:testCannotUnderpayForNFTMint() (gas: 39973) +LinearNFTTest:testMintManyNFT() (gas: 4192240) +LinearNFTTest:testMintNFT() (gas: 86039) LinearVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 10109, ~: 10109) LinearVRGDATest:testPricingBasic() (gas: 9972) LinearVRGDATest:testTargetPrice() (gas: 10749) -LogisticNFTTest:testCannotMintMoreThanMax() (gas: 4437960) -LogisticNFTTest:testCannotUnderpayForNFTMint() (gas: 40767) -LogisticNFTTest:testMintAllNFT() (gas: 4426938) -LogisticNFTTest:testMintNFT() (gas: 86855) +LogisticNFTTest:testCannotMintMoreThanMax() (gas: 4425133) +LogisticNFTTest:testCannotUnderpayForNFTMint() (gas: 40640) +LogisticNFTTest:testMintAllNFT() (gas: 4414238) +LogisticNFTTest:testMintNFT() (gas: 86728) LogisticVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 11692, ~: 11692) LogisticVRGDATest:testFailOverflowForBeyondLimitTokens(uint256,uint256) (runs: 256, μ: 10278, ~: 10278) LogisticVRGDATest:testGetTargetSaleTimeDoesNotRevertEarly() (gas: 6147) diff --git a/src/examples/LinearNFT.sol b/src/examples/LinearNFT.sol index aaa2e71..9e12de8 100644 --- a/src/examples/LinearNFT.sol +++ b/src/examples/LinearNFT.sol @@ -43,8 +43,12 @@ contract LinearNFT is ERC721, LinearVRGDA { //////////////////////////////////////////////////////////////*/ function mint() external payable returns (uint256 mintedId) { - // 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; + + unchecked { + // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. + price = 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 c793904..5b548ea 100644 --- a/src/examples/LogisticNFT.sol +++ b/src/examples/LogisticNFT.sol @@ -54,8 +54,12 @@ contract LogisticNFT is ERC721, LogisticVRGDA { // Note: We don't need to check totalSold < MAX_MINTABLE, because getVRGDAPrice will // revert if we're over the max mintable limit we set when constructing LogisticVRGDA. - // 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; + + unchecked { + // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. + price = getVRGDAPrice(toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); + } require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. From 9f3cd40b1c49ba74a7362c95771ed05112ee9a66 Mon Sep 17 00:00:00 2001 From: beans Date: Wed, 31 Aug 2022 01:55:53 -0400 Subject: [PATCH 3/7] unchecked block --- src/examples/LinearNFT.sol | 10 +++------- src/examples/LogisticNFT.sol | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/examples/LinearNFT.sol b/src/examples/LinearNFT.sol index 9e12de8..8da72c3 100644 --- a/src/examples/LinearNFT.sol +++ b/src/examples/LinearNFT.sol @@ -43,18 +43,14 @@ contract LinearNFT is ERC721, LinearVRGDA { //////////////////////////////////////////////////////////////*/ function mint() external payable returns (uint256 mintedId) { - uint256 price; - unchecked { // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. - price = getVRGDAPrice(toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); - } + uint256 price = getVRGDAPrice(toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); - require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. + require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. - _mint(msg.sender, mintedId); // Mint the NFT using mintedId. + _mint(msg.sender, mintedId); // Mint the NFT using mintedId. - unchecked { // 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. diff --git a/src/examples/LogisticNFT.sol b/src/examples/LogisticNFT.sol index 5b548ea..fdd29d5 100644 --- a/src/examples/LogisticNFT.sol +++ b/src/examples/LogisticNFT.sol @@ -54,18 +54,14 @@ contract LogisticNFT is ERC721, LogisticVRGDA { // Note: We don't need to check totalSold < MAX_MINTABLE, because getVRGDAPrice will // revert if we're over the max mintable limit we set when constructing LogisticVRGDA. - uint256 price; - unchecked { // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. - price = getVRGDAPrice(toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); - } + uint256 price = getVRGDAPrice(toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); - require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. + require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. - _mint(msg.sender, mintedId); // Mint the NFT using mintedId. + _mint(msg.sender, mintedId); // Mint the NFT using mintedId. - unchecked { // 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. From dca5b247d899d44fae292fed87dd6d2aea91876f Mon Sep 17 00:00:00 2001 From: beans Date: Tue, 6 Sep 2022 22:48:07 -0400 Subject: [PATCH 4/7] move vrgda logic over to lib --- src/LinearVRGDA.sol | 59 +++++++++++--------- src/LogisticVRGDA.sol | 93 ++++++++++++++++---------------- src/examples/LinearNFT.sol | 36 ++++++++++--- src/examples/LogisticNFT.sol | 43 +++++++++++---- test/mocks/MockLinearVRGDA.sol | 23 ++++++-- test/mocks/MockLogisticVRGDA.sol | 33 ++++++++++-- 6 files changed, 193 insertions(+), 94 deletions(-) diff --git a/src/LinearVRGDA.sol b/src/LinearVRGDA.sol index 814d060..fc955bd 100644 --- a/src/LinearVRGDA.sol +++ b/src/LinearVRGDA.sol @@ -1,44 +1,55 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {unsafeWadDiv} from "./utils/SignedWadMath.sol"; - -import {VRGDA} from "./VRGDA.sol"; +import {wadExp, wadLn, wadMul, unsafeWadMul, unsafeWadDiv, toWadUnsafe} from "./utils/SignedWadMath.sol"; /// @title Linear Variable Rate Gradual Dutch Auction /// @author transmissions11 /// @author FrankieIsLost /// @notice VRGDA with a linear issuance curve. -abstract contract LinearVRGDA is VRGDA { +library LinearVRGDA { /*////////////////////////////////////////////////////////////// - PRICING PARAMETERS + PRICING LOGIC //////////////////////////////////////////////////////////////*/ - /// @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; + /// @notice Calculate the price of a token according to the VRGDA formula. + /// @param timeSinceStart Time passed since the VRGDA began, scaled by 1e18. + /// @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 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, + int256 targetPrice, + int256 priceDecayPercent, + int256 perTimeUnit, + uint256 sold + ) public pure returns (uint256) { + unchecked { + // prettier-ignore + return uint256(wadMul(targetPrice, wadExp(unsafeWadMul(computeDecayConstant(priceDecayPercent), + // 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), 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. + /// @param perTimeUnit The number of tokens to target selling in 1 full unit of time, scaled by 1e18. /// @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) { + function getTargetSaleTime(int256 sold, int256 perTimeUnit) public pure returns (int256) { return unsafeWadDiv(sold, perTimeUnit); } + + /// @dev Calculate constant that allows us to rewrite a pow() as an exp(). + /// @param priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. + /// @return The computed constant represented as an 18 decimal fixed point number. + function computeDecayConstant(int256 priceDecayPercent) public pure returns (int256) { + return wadLn(1e18 - priceDecayPercent); + } } diff --git a/src/LogisticVRGDA.sol b/src/LogisticVRGDA.sol index 2afff17..3b6ad2b 100644 --- a/src/LogisticVRGDA.sol +++ b/src/LogisticVRGDA.sol @@ -1,65 +1,66 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {wadLn, unsafeDiv, unsafeWadDiv} from "./utils/SignedWadMath.sol"; +import {wadExp, wadLn, wadMul, unsafeWadMul, unsafeWadDiv, unsafeDiv, toWadUnsafe} from "./utils/SignedWadMath.sol"; -import {VRGDA} from "./VRGDA.sol"; - -/// @title Logistic Variable Rate Gradual Dutch Auction +/// @title Linear Variable Rate Gradual Dutch Auction /// @author transmissions11 /// @author FrankieIsLost -/// @notice VRGDA with a logistic issuance curve. -abstract contract LogisticVRGDA is VRGDA { +/// @notice VRGDA with a linear issuance curve. +library LogisticVRGDA { /*////////////////////////////////////////////////////////////// - PRICING PARAMETERS + PRICING LOGIC //////////////////////////////////////////////////////////////*/ - /// @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; + /// @notice Calculate the price of a token according to the VRGDA formula. + /// @param timeSinceStart Time passed since the VRGDA began, scaled by 1e18. + /// @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. + /// @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, + int256 targetPrice, + int256 priceDecayPercent, + int256 maxSellable, + int256 timeScale, + uint256 sold + ) public pure returns (uint256) { + unchecked { + // prettier-ignore + return uint256(wadMul(targetPrice, wadExp(unsafeWadMul(computeDecayConstant(priceDecayPercent), + // 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), maxSellable, 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. + /// @param maxSellable The maximum number of tokens to sell, scaled by 1e18. + /// @param timeScale The steepness of the logistic curve, scaled by 1e18. /// @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) { + function getTargetSaleTime( + int256 sold, + int256 maxSellable, + int256 timeScale + ) public pure returns (int256) { + int256 logisticLimit = maxSellable + 1e18; + unchecked { - return -unsafeWadDiv(wadLn(unsafeDiv(logisticLimitDoubled, sold + logisticLimit) - 1e18), timeScale); + return -unsafeWadDiv(wadLn(unsafeDiv(logisticLimit * 2e18, sold + logisticLimit) - 1e18), timeScale); } } + + /// @dev Calculate constant that allows us to rewrite a pow() as an exp(). + /// @param priceDecayPercent The percent price decays per unit of time with no sales, scaled by 1e18. + /// @return The computed constant represented as an 18 decimal fixed point number. + function computeDecayConstant(int256 priceDecayPercent) public pure returns (int256) { + return wadLn(1e18 - priceDecayPercent); + } } diff --git a/src/examples/LinearNFT.sol b/src/examples/LinearNFT.sol index 8da72c3..2666906 100644 --- a/src/examples/LinearNFT.sol +++ b/src/examples/LinearNFT.sol @@ -13,7 +13,7 @@ import {LinearVRGDA} from "../LinearVRGDA.sol"; /// @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 { /*////////////////////////////////////////////////////////////// SALES STORAGE //////////////////////////////////////////////////////////////*/ @@ -22,6 +22,21 @@ contract LinearNFT is ERC721, LinearVRGDA { uint256 public startTime = block.timestamp; // When VRGDA sales begun. + /*////////////////////////////////////////////////////////////// + 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; + + /// @dev The percent price decays per unit of time with no sales, scaled by 1e18 + int256 internal immutable priceDecayPercent; + + /// @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; + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -31,12 +46,11 @@ contract LinearNFT is ERC721, LinearVRGDA { "Example Linear NFT", // Name. "LINEAR" // Symbol. ) - LinearVRGDA( - 69.42e18, // Target price. - 0.31e18, // Price decay percent. - 2e18 // Per time unit. - ) - {} + { + targetPrice = 69.42e18; + priceDecayPercent = 0.31e18; + perTimeUnit = 2e18; + } /*////////////////////////////////////////////////////////////// MINTING LOGIC @@ -45,7 +59,13 @@ 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 = LinearVRGDA.getVRGDAPrice( + toDaysWadUnsafe(block.timestamp - startTime), + targetPrice, + priceDecayPercent, + perTimeUnit, + mintedId = totalSold++ + ); require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. diff --git a/src/examples/LogisticNFT.sol b/src/examples/LogisticNFT.sol index fdd29d5..d75cceb 100644 --- a/src/examples/LogisticNFT.sol +++ b/src/examples/LogisticNFT.sol @@ -13,7 +13,7 @@ import {LogisticVRGDA} from "../LogisticVRGDA.sol"; /// @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 { /*////////////////////////////////////////////////////////////// CONSTANTS //////////////////////////////////////////////////////////////*/ @@ -28,6 +28,24 @@ contract LogisticNFT is ERC721, LogisticVRGDA { uint256 public startTime = block.timestamp; // When VRGDA sales begun. + /*////////////////////////////////////////////////////////////// + PRICING 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 The steepness of the logistic curve, scaled by 1e18. + int256 internal immutable timeScale; + + /// @dev The percent price decays per unit of time with no sales, scaled by 1e18 + int256 internal immutable priceDecayPercent; + + /// @notice The maximum number of tokens to sell, scaled by 1e18. + /// @dev Used to calculate logisticLimit and logisticLimitDoubled. + int256 public immutable maxSellable; + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -37,14 +55,12 @@ contract LogisticNFT is ERC721, LogisticVRGDA { "Example Logistic NFT", // Name. "LOGISTIC" // Symbol. ) - LogisticVRGDA( - 69.42e18, // Target price. - 0.31e18, // Price decay percent. - // Maximum # mintable/sellable. - toWadUnsafe(MAX_MINTABLE), - 0.1e18 // Time scale. - ) - {} + { + targetPrice = 69.42e18; + priceDecayPercent = 0.31e18; + maxSellable = toWadUnsafe(MAX_MINTABLE); + timeScale = 0.1e18; + } /*////////////////////////////////////////////////////////////// MINTING LOGIC @@ -56,7 +72,14 @@ 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 = LogisticVRGDA.getVRGDAPrice( + toDaysWadUnsafe(block.timestamp - startTime), + targetPrice, + priceDecayPercent, + maxSellable, + timeScale, + mintedId = totalSold++ + ); require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. diff --git a/test/mocks/MockLinearVRGDA.sol b/test/mocks/MockLinearVRGDA.sol index aa4217e..760b9f8 100644 --- a/test/mocks/MockLinearVRGDA.sol +++ b/test/mocks/MockLinearVRGDA.sol @@ -1,12 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {LinearVRGDA, VRGDA} from "../../src/LinearVRGDA.sol"; +// import {LinearVRGDA, VRGDA} from "../../src/LinearVRGDA.sol"; +import {LinearVRGDA} from "../../src/LinearVRGDA.sol"; + +contract MockLinearVRGDA { + int256 internal immutable perTimeUnit; + int256 internal immutable priceDecayPercent; + int256 public immutable targetPrice; -contract MockLinearVRGDA is LinearVRGDA { constructor( int256 _targetPrice, int256 _priceDecreasePercent, int256 _perTimeUnit - ) LinearVRGDA(_targetPrice, _priceDecreasePercent, _perTimeUnit) {} + ) { + targetPrice = _targetPrice; + priceDecayPercent = _priceDecreasePercent; + perTimeUnit = _perTimeUnit; + } + + function getTargetSaleTime(int256 sold) public view returns (int256) { + return LinearVRGDA.getTargetSaleTime(sold, perTimeUnit); + } + + function getVRGDAPrice(int256 timeSinceStart, uint256 sold) public view returns (uint256) { + return LinearVRGDA.getVRGDAPrice(timeSinceStart, targetPrice, priceDecayPercent, perTimeUnit, sold); + } } diff --git a/test/mocks/MockLogisticVRGDA.sol b/test/mocks/MockLogisticVRGDA.sol index d1fac53..287570a 100644 --- a/test/mocks/MockLogisticVRGDA.sol +++ b/test/mocks/MockLogisticVRGDA.sol @@ -1,13 +1,40 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import {LogisticVRGDA, VRGDA} from "../../src/LogisticVRGDA.sol"; +import {LogisticVRGDA} from "../../src/LogisticVRGDA.sol"; + +contract MockLogisticVRGDA { + int256 internal immutable priceDecreasePercent; + int256 internal immutable timeScale; + + int256 public immutable targetPrice; + int256 public immutable maxSellable; -contract MockLogisticVRGDA is LogisticVRGDA { constructor( int256 _targetPrice, int256 _priceDecreasePercent, int256 _maxSellable, int256 _timeScale - ) LogisticVRGDA(_targetPrice, _priceDecreasePercent, _maxSellable, _timeScale) {} + ) { + targetPrice = _targetPrice; + priceDecreasePercent = _priceDecreasePercent; + maxSellable = _maxSellable; + timeScale = _timeScale; + } + + function getTargetSaleTime(int256 sold) public view returns (int256) { + return LogisticVRGDA.getTargetSaleTime(sold, maxSellable, timeScale); + } + + function getVRGDAPrice(int256 timeSinceStart, uint256 sold) public view returns (uint256) { + return + LogisticVRGDA.getVRGDAPrice( + timeSinceStart, + targetPrice, + priceDecreasePercent, + maxSellable, + timeScale, + sold + ); + } } From 5f489325693caf22f273165535106868666f8210 Mon Sep 17 00:00:00 2001 From: beans Date: Tue, 6 Sep 2022 22:50:31 -0400 Subject: [PATCH 5/7] merge --- .gas-snapshot | 49 +++++++++++++----------------------- src/examples/LinearNFT.sol | 4 --- src/examples/LogisticNFT.sol | 4 --- 3 files changed, 18 insertions(+), 39 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 6027905..65a54c6 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,31 +1,18 @@ -<<<<<<< HEAD -LinearNFTTest:testCannotUnderpayForNFTMint() (gas: 39973) -LinearNFTTest:testMintManyNFT() (gas: 4192240) -LinearNFTTest:testMintNFT() (gas: 86039) -LinearVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 10109, ~: 10109) -LinearVRGDATest:testPricingBasic() (gas: 9972) -LinearVRGDATest:testTargetPrice() (gas: 10749) -LogisticNFTTest:testCannotMintMoreThanMax() (gas: 4425133) -LogisticNFTTest:testCannotUnderpayForNFTMint() (gas: 40640) -LogisticNFTTest:testMintAllNFT() (gas: 4414238) -LogisticNFTTest:testMintNFT() (gas: 86728) -======= -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) ->>>>>>> 8e820a76fe37253f0a5b14dfeea7c07072b305c4 -LogisticVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 11692, ~: 11692) -LogisticVRGDATest:testFailOverflowForBeyondLimitTokens(uint256,uint256) (runs: 256, μ: 10278, ~: 10278) -LogisticVRGDATest:testGetTargetSaleTimeDoesNotRevertEarly() (gas: 6147) -LogisticVRGDATest:testGetTargetSaleTimeRevertsWhenExpected() (gas: 8533) -LogisticVRGDATest:testNoOverflowForAllTokens(uint256,uint256) (runs: 256, μ: 11229, ~: 11229) -LogisticVRGDATest:testNoOverflowForMostTokens(uint256,uint256) (runs: 256, μ: 11402, ~: 11528) -LogisticVRGDATest:testPricingBasic() (gas: 10762) -LogisticVRGDATest:testTargetPrice() (gas: 12246) +LinearNFTTest:testCannotUnderpayForNFTMint() (gas: 41772) +LinearNFTTest:testMintManyNFT() (gas: 4325717) +LinearNFTTest:testMintNFT() (gas: 87846) +LinearVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 14854, ~: 14854) +LinearVRGDATest:testPricingBasic() (gas: 14050) +LinearVRGDATest:testTargetPrice() (gas: 15494) +LogisticNFTTest:testCannotMintMoreThanMax() (gas: 4576176) +LogisticNFTTest:testCannotUnderpayForNFTMint() (gas: 42690) +LogisticNFTTest:testMintAllNFT() (gas: 4563891) +LogisticNFTTest:testMintNFT() (gas: 88788) +LogisticVRGDATest:testAlwaysTargetPriceInRightConditions(uint256) (runs: 256, μ: 16765, ~: 16765) +LogisticVRGDATest:testFailOverflowForBeyondLimitTokens(uint256,uint256) (runs: 256, μ: 14359, ~: 14359) +LogisticVRGDATest:testGetTargetSaleTimeDoesNotRevertEarly() (gas: 9460) +LogisticVRGDATest:testGetTargetSaleTimeRevertsWhenExpected() (gas: 11677) +LogisticVRGDATest:testNoOverflowForAllTokens(uint256,uint256) (runs: 256, μ: 15489, ~: 15489) +LogisticVRGDATest:testNoOverflowForMostTokens(uint256,uint256) (runs: 256, μ: 15662, ~: 15788) +LogisticVRGDATest:testPricingBasic() (gas: 15022) +LogisticVRGDATest:testTargetPrice() (gas: 17319) diff --git a/src/examples/LinearNFT.sol b/src/examples/LinearNFT.sol index c5eb82a..b9aa3aa 100644 --- a/src/examples/LinearNFT.sol +++ b/src/examples/LinearNFT.sol @@ -59,7 +59,6 @@ contract LinearNFT is ERC721 { 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. -<<<<<<< HEAD uint256 price = LinearVRGDA.getVRGDAPrice( toDaysWadUnsafe(block.timestamp - startTime), targetPrice, @@ -67,9 +66,6 @@ contract LinearNFT is ERC721 { perTimeUnit, mintedId = totalSold++ ); -======= - uint256 price = getVRGDAPrice(toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); ->>>>>>> 8e820a76fe37253f0a5b14dfeea7c07072b305c4 require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. diff --git a/src/examples/LogisticNFT.sol b/src/examples/LogisticNFT.sol index 4962d0f..fd3ce90 100644 --- a/src/examples/LogisticNFT.sol +++ b/src/examples/LogisticNFT.sol @@ -72,7 +72,6 @@ contract LogisticNFT is ERC721 { unchecked { // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. -<<<<<<< HEAD uint256 price = LogisticVRGDA.getVRGDAPrice( toDaysWadUnsafe(block.timestamp - startTime), targetPrice, @@ -81,9 +80,6 @@ contract LogisticNFT is ERC721 { timeScale, mintedId = totalSold++ ); -======= - uint256 price = getVRGDAPrice(toDaysWadUnsafe(block.timestamp - startTime), mintedId = totalSold++); ->>>>>>> 8e820a76fe37253f0a5b14dfeea7c07072b305c4 require(msg.value >= price, "UNDERPAID"); // Don't allow underpaying. From 9c2b70fea9cf6954bcc062cff5fbd50a0d6ab811 Mon Sep 17 00:00:00 2001 From: beans Date: Tue, 6 Sep 2022 22:58:36 -0400 Subject: [PATCH 6/7] remove comment --- test/mocks/MockLinearVRGDA.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/test/mocks/MockLinearVRGDA.sol b/test/mocks/MockLinearVRGDA.sol index 760b9f8..563013f 100644 --- a/test/mocks/MockLinearVRGDA.sol +++ b/test/mocks/MockLinearVRGDA.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -// import {LinearVRGDA, VRGDA} from "../../src/LinearVRGDA.sol"; import {LinearVRGDA} from "../../src/LinearVRGDA.sol"; contract MockLinearVRGDA { From 32f975b1344a08aaed1afa52d247b8eea5220684 Mon Sep 17 00:00:00 2001 From: beans Date: Tue, 6 Sep 2022 22:59:28 -0400 Subject: [PATCH 7/7] remove base vrgda impl --- src/VRGDA.sol | 60 --------------------------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 src/VRGDA.sol diff --git a/src/VRGDA.sol b/src/VRGDA.sol deleted file mode 100644 index 7a7cc36..0000000 --- a/src/VRGDA.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"; - -/// @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) { - 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)) - )))); - } - } - - /// @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); -}