diff --git a/.gas-snapshot b/.gas-snapshot index 8f2e464..65a54c6 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,18 +1,18 @@ -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) -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) +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/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/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); -} diff --git a/src/examples/LinearNFT.sol b/src/examples/LinearNFT.sol index e705632..b9aa3aa 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 immutable 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 df58694..fd3ce90 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 immutable 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..563013f 100644 --- a/test/mocks/MockLinearVRGDA.sol +++ b/test/mocks/MockLinearVRGDA.sol @@ -1,12 +1,28 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -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 + ); + } }