Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -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)
59 changes: 35 additions & 24 deletions src/LinearVRGDA.sol
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
/// @author FrankieIsLost <[email protected]>
/// @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);
}
Comment on lines +52 to +54

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might as well move this to a library, no? it's the same in LogisticVRGDA.sol

}
93 changes: 47 additions & 46 deletions src/LogisticVRGDA.sol
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
/// @author FrankieIsLost <[email protected]>
/// @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);
}
}
60 changes: 0 additions & 60 deletions src/VRGDA.sol

This file was deleted.

36 changes: 28 additions & 8 deletions src/examples/LinearNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {LinearVRGDA} from "../LinearVRGDA.sol";
/// @author FrankieIsLost <[email protected]>
/// @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
//////////////////////////////////////////////////////////////*/
Expand All @@ -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
//////////////////////////////////////////////////////////////*/
Expand All @@ -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
Expand All @@ -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.

Expand Down
Loading