diff --git a/.gas-snapshot b/.gas-snapshot index e0eedd1..66d8e94 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,3 +1,5 @@ +LinearBoundedVRGDATest:testMinPrice() (gas: 9826) +LinearBoundedVRGDATest:testPricingBasic() (gas: 10043) LinearNFTTest:testCannotUnderpayForNFTMint() (gas: 37841) LinearNFTTest:testMintManyNFT() (gas: 4177040) LinearNFTTest:testMintNFT() (gas: 83907) @@ -20,6 +22,6 @@ LogisticVRGDATest:testFailOverflowForBeyondLimitTokens(uint256,uint256) (runs: 2 LogisticVRGDATest:testGetTargetSaleTimeDoesNotRevertEarly() (gas: 6102) LogisticVRGDATest:testGetTargetSaleTimeRevertsWhenExpected() (gas: 8488) LogisticVRGDATest:testNoOverflowForAllTokens(uint256,uint256) (runs: 256, μ: 11161, ~: 11161) -LogisticVRGDATest:testNoOverflowForMostTokens(uint256,uint256) (runs: 256, μ: 11280, ~: 11117) +LogisticVRGDATest:testNoOverflowForMostTokens(uint256,uint256) (runs: 256, μ: 11283, ~: 11117) LogisticVRGDATest:testPricingBasic() (gas: 10718) LogisticVRGDATest:testTargetPrice() (gas: 12157) diff --git a/src/BoundedVRGDA.sol b/src/BoundedVRGDA.sol new file mode 100644 index 0000000..3e0a3b6 --- /dev/null +++ b/src/BoundedVRGDA.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {wadExp, wadLn, wadMul, unsafeWadMul, toWadUnsafe} from "solmate/utils/SignedWadMath.sol"; + +import {VRGDA} from "./VRGDA.sol"; + +/// @title Bounded Variable Rate Gradual Dutch Auction +/// @author transmissions11 +/// @author FrankieIsLost +/// @notice Sell tokens roughly according to an issuance schedule. + +abstract contract BoundedVRGDA is VRGDA { + /*////////////////////////////////////////////////////////////// + VRGDA PARAMETERS + //////////////////////////////////////////////////////////////*/ + + /// @dev The minimum sale price of a token. + /// @dev Represented as an 18 decimal fixed point number. + uint256 public immutable min; + + /// @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 _min minimum price to be paid for a token, scaled by 1e18 + constructor( + int256 _targetPrice, + int256 _priceDecayPercent, + uint256 _min + ) VRGDA(_targetPrice, _priceDecayPercent) { + min = _min; + } + + /*////////////////////////////////////////////////////////////// + PRICING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Calculate the price of a token according to the VRGDA formula, bounded by min value. + /// @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 virtual override returns (uint256) { + uint256 VRGDAPrice = super.getVRGDAPrice(timeSinceStart, sold); + + return VRGDAPrice > min ? VRGDAPrice : min; + } +} diff --git a/test/LinearBoundedVRGDA.t.sol b/test/LinearBoundedVRGDA.t.sol new file mode 100644 index 0000000..41d79d5 --- /dev/null +++ b/test/LinearBoundedVRGDA.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; + +import {toWadUnsafe, toDaysWadUnsafe, fromDaysWadUnsafe} from "solmate/utils/SignedWadMath.sol"; + +import {MockLinearBoundedVRGDA} from "./mocks/MockLinearBoundedVRGDA.sol"; + +uint256 constant ONE_THOUSAND_YEARS = 356 days * 1000; + +contract LinearBoundedVRGDATest is DSTestPlus { + MockLinearBoundedVRGDA vrgda; + + function setUp() public { + vrgda = new MockLinearBoundedVRGDA( + 69.42e18, // Target price. + 0.31e18, // Price decay percent. + 1e18, // Min price + 2e18 // Per time unit. + ); + } + + function testPricingBasic() public { + // Our VRGDA targets this number of mints at the given time. + uint256 timeDelta = 120 days; + uint256 numMint = 239; + + hevm.warp(block.timestamp + timeDelta); + + uint256 cost = vrgda.getVRGDAPrice(toDaysWadUnsafe(block.timestamp), numMint); + assertRelApproxEq(cost, uint256(vrgda.targetPrice()), 0.00001e18); + } + + function testMinPrice() public { + uint256 timeDelta = 120 days; + uint256 numMint = 216; + + // Warp to a sale time where the decreased VRGDA price should be less than the min. + hevm.warp(block.timestamp + timeDelta); + + uint256 cost = vrgda.getVRGDAPrice(toDaysWadUnsafe(block.timestamp), numMint); + assertEq(cost, uint256(vrgda.min())); + } +} diff --git a/test/mocks/MockLinearBoundedVRGDA.sol b/test/mocks/MockLinearBoundedVRGDA.sol new file mode 100644 index 0000000..a3938b5 --- /dev/null +++ b/test/mocks/MockLinearBoundedVRGDA.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {unsafeWadDiv} from "solmate/utils/SignedWadMath.sol"; +import {BoundedVRGDA} from "../../src/BoundedVRGDA.sol"; + +contract MockLinearBoundedVRGDA is BoundedVRGDA { + int256 internal immutable perTimeUnit; + + constructor( + int256 _targetPrice, + int256 _priceDecayPercent, + uint256 _min, + int256 _perTimeUnit + ) BoundedVRGDA(_targetPrice, _priceDecayPercent, _min) { + perTimeUnit = _perTimeUnit; + } + + function getTargetSaleTime(int256 sold) public view virtual override returns (int256) { + return unsafeWadDiv(sold, perTimeUnit); + } +}