Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Premium Price with EDA integration #33

Merged
merged 5 commits into from
Jun 14, 2024
Merged
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
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,38 @@ The core functionality of Base Usernames should look familiar to anyone that's l

In addition to replicating the base behavior of the ENS protocol, we are offering a series of promotional discounts associcated with various Coinbase product integrations. As such, the Base Usernames ETH Registrar Controller allows users to perform discounted registrations while passing along integration-specific `validationData`.

### Functional Diagrams
## Functional Diagrams

### Premium Pricing Chart

To visualize the pricing decay for premium prices, you can run the Python script located in the py directory. The script generates a pricing chart based on user input.

### Prerequisites

* Python 3.x
* matplotlib library

### Running the Script

1. Navigate to the py directory

```shell
$ cd py
```

2. Run the Python script

```shell
$ python3 Price.py
```

3. Follow the prompts to input the required values:

* Start Premium: The initial premium price
* Total days: The number of days it takes for the price to reach its end value
* Number of days: The number of days over which price should decay

The script will generate and display the pricing chart based on the provided inputs.

## Usage

Expand Down
54 changes: 54 additions & 0 deletions py/Price.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import math
import matplotlib.pyplot as plt

PRECISION = 10 ** 18
SECONDS_PER_DAY = 86400

def decayed_premium(start_premium, elapsed_seconds, seconds_in_period, per_period_decay_percent_wad):
ratio = elapsed_seconds / seconds_in_period

percent_wad_remaining_per_period = (PRECISION - per_period_decay_percent_wad) / PRECISION
multiplier = (percent_wad_remaining_per_period ** ratio)

price = (start_premium * multiplier)
return price

def calculate_prices(start_premium, end_value, num_days, seconds_in_period, per_period_decay_percent_wad):
elapsed_times = []
prices = []

for day in range(num_days):
for i in range(10):
elapsed_time = (day + i / 10) * SECONDS_PER_DAY

premium = decayed_premium(start_premium, elapsed_time, seconds_in_period, per_period_decay_percent_wad)

price = max(premium, end_value) / PRECISION
elapsed_times.append(day + i / 10)
prices.append(price)

return elapsed_times, prices

start = int(input("Input a value for the start premium: "))
total = int(input("How many days would it take for the price to reach its end value: "))
num = int(input("Input a value for the number of days over which the price should decay: "))

start_premium = start * (10 ** 18)
total_days = total
seconds_in_period = SECONDS_PER_DAY
per_period_decay_percent = 50

per_period_decay_percent_wad = int(per_period_decay_percent * PRECISION / 100)
end_value = start_premium >> total_days

num_days = num

elapsed_times, prices = calculate_prices(start_premium, end_value, num_days, seconds_in_period, per_period_decay_percent_wad)
plt.figure(figsize=(start, num))
plt.plot(elapsed_times, prices, marker='o')
plt.xlabel('Elapsed Time (days)')
plt.ylabel('Premium Price')
plt.title('Pricing Chart')
plt.grid(True)
plt.xticks(range(num_days + 1))
plt.show()
98 changes: 15 additions & 83 deletions src/L2/ExponentialPremiumPriceOracle.sol
Original file line number Diff line number Diff line change
@@ -1,116 +1,48 @@
//SPDX-License-Identifier: MIT
Keshavrajsinghal marked this conversation as resolved.
Show resolved Hide resolved
// SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;

import "./StablePriceOracle.sol";
import {GRACE_PERIOD} from "src/util/Constants.sol";
import {EDAPrice} from "src/lib/EDAPrice.sol";
import "solady/utils/FixedPointMathLib.sol";
import {Test, console} from "forge-std/Test.sol";

contract ExponentialPremiumPriceOracle is StablePriceOracle {
uint256 immutable startPremium;
uint256 immutable endValue;
uint256 public immutable startPremium;
uint256 public immutable endValue;
uint256 constant PRECISION = 1e18;

constructor(uint256[] memory rentPrices, uint256 startPremium_, uint256 totalDays) StablePriceOracle(rentPrices) {
startPremium = startPremium_;
endValue = startPremium_ >> totalDays;
endValue = startPremium >> totalDays;
}

uint256 constant PRECISION = 1e18;
uint256 constant bit1 = 999989423469314432; // 0.5 ^ 1/65536 * (10 ** 18)
uint256 constant bit2 = 999978847050491904; // 0.5 ^ 2/65536 * (10 ** 18)
uint256 constant bit3 = 999957694548431104;
uint256 constant bit4 = 999915390886613504;
uint256 constant bit5 = 999830788931929088;
uint256 constant bit6 = 999661606496243712;
uint256 constant bit7 = 999323327502650752;
uint256 constant bit8 = 998647112890970240;
uint256 constant bit9 = 997296056085470080;
uint256 constant bit10 = 994599423483633152;
uint256 constant bit11 = 989228013193975424;
uint256 constant bit12 = 978572062087700096;
uint256 constant bit13 = 957603280698573696;
uint256 constant bit14 = 917004043204671232;
uint256 constant bit15 = 840896415253714560;
uint256 constant bit16 = 707106781186547584;

/**
* @dev Returns the pricing premium in internal base units.
*/

function _premium(string memory, uint256 expires, uint256) internal view override returns (uint256) {
expires = expires + GRACE_PERIOD;
if (expires > block.timestamp) {
return 0;
}

uint256 elapsed = block.timestamp - expires;
uint256 premium = decayedPremium(elapsed);
if (premium > endValue) {
return premium - endValue;
}
return 0;
}

/**
* @dev Returns the premium price at current time elapsed
* @param elapsed time past since expiry
*/
function decayedPremium(uint256 elapsed) public view returns (uint256) {
uint256 daysPast = (elapsed * PRECISION) / 1 days;
uint256 intDays = daysPast / PRECISION;
uint256 premium = startPremium >> intDays;
uint256 partDay = (daysPast - intDays * PRECISION);
uint256 fraction = (partDay * (2 ** 16)) / PRECISION;
uint256 totalPremium = _addFractionalPremium(fraction, premium);
return totalPremium;
}

function _addFractionalPremium(uint256 fraction, uint256 premium) internal pure returns (uint256) {
if (fraction & (1 << 0) != 0) {
premium = (premium * bit1) / PRECISION;
}
if (fraction & (1 << 1) != 0) {
premium = (premium * bit2) / PRECISION;
}
if (fraction & (1 << 2) != 0) {
premium = (premium * bit3) / PRECISION;
}
if (fraction & (1 << 3) != 0) {
premium = (premium * bit4) / PRECISION;
}
if (fraction & (1 << 4) != 0) {
premium = (premium * bit5) / PRECISION;
}
if (fraction & (1 << 5) != 0) {
premium = (premium * bit6) / PRECISION;
}
if (fraction & (1 << 6) != 0) {
premium = (premium * bit7) / PRECISION;
}
if (fraction & (1 << 7) != 0) {
premium = (premium * bit8) / PRECISION;
}
if (fraction & (1 << 8) != 0) {
premium = (premium * bit9) / PRECISION;
}
if (fraction & (1 << 9) != 0) {
premium = (premium * bit10) / PRECISION;
}
if (fraction & (1 << 10) != 0) {
premium = (premium * bit11) / PRECISION;
}
if (fraction & (1 << 11) != 0) {
premium = (premium * bit12) / PRECISION;
}
if (fraction & (1 << 12) != 0) {
premium = (premium * bit13) / PRECISION;
}
if (fraction & (1 << 13) != 0) {
premium = (premium * bit14) / PRECISION;
}
if (fraction & (1 << 14) != 0) {
premium = (premium * bit15) / PRECISION;
}
if (fraction & (1 << 15) != 0) {
premium = (premium * bit16) / PRECISION;
}
function decayedPremium(uint256 elapsed) public view returns (uint256) {
/// @dev The half-life of the premium price decay
uint256 secondsInPeriod = 1 days;
/// @dev 50% decay per period in wad format
uint256 perPeriodDecayPercentWad = FixedPointMathLib.WAD / 2;
uint256 premium = EDAPrice.currentPrice(startPremium, elapsed, secondsInPeriod, perPeriodDecayPercentWad);
return premium;
}
}
39 changes: 39 additions & 0 deletions src/lib/EDAPrice.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import {Test, console} from "forge-std/Test.sol";
import "solady/utils/FixedPointMathLib.sol";

library EDAPrice {
/// @notice returns the current price of an exponential price decay auction defined by the passed params

/// @dev reverts if perPeriodDecayPercentWad >= 1e18
/// @dev reverts if uint256 secondsInPeriod = 0
/// @dev reverts if startPrice * multiplier overflows
/// @dev reverts if lnWad(percentWadRemainingPerPeriod) * ratio) overflows

/// @param startPrice the starting price of the auction
/// @param secondsElapsed the seconds elapsed since auction start
/// @param secondsInPeriod the seconds over which the price should decay perPeriodDecayPercentWad
/// @param perPeriodDecayPercentWad the percent the price should decay during secondsInPeriod, 100% = 1e18

/// @return price the current auction price

function currentPrice(
uint256 startPrice,
uint256 secondsElapsed,
uint256 secondsInPeriod,
uint256 perPeriodDecayPercentWad
) internal pure returns (uint256) {
uint256 ratio = FixedPointMathLib.divWad(secondsElapsed, secondsInPeriod);
uint256 percentWadRemainingPerPeriod = FixedPointMathLib.WAD - perPeriodDecayPercentWad;

// percentWadRemainingPerPeriod can be safely cast because < 1e18
// ratio can be safely cast because will not overflow unless ratio > int256.max,
// which would require secondsElapsed > int256.max, i.e. > 5.78e76 or 1.8e69 years

int256 multiplier = FixedPointMathLib.powWad(int256(percentWadRemainingPerPeriod), int256(ratio));
uint256 price = (startPrice * uint256(multiplier)) / FixedPointMathLib.WAD;
return price;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;

import {Test, console} from "forge-std/Test.sol";
import {ExponentialPremiumOracleBase} from "./ExponentialPremiumOracleBase.t.sol";
import "solady/utils/FixedPointMathLib.sol";

contract ExponentialPremiumFuzzTest is ExponentialPremiumOracleBase {
function test_decayedPremium_decreasingPrices(uint256 elapsed) public view {
vm.assume(elapsed <= 365 days);
uint256 actualPremium = oracle.decayedPremium(elapsed);
assert(actualPremium <= startPremium);
}

function test_decayedPremium_boundaryValues(uint256 elapsed) public view {
uint256[] memory boundaryValues = new uint256[](3);
boundaryValues[0] = 0;
boundaryValues[1] = 1 days;
boundaryValues[2] = 365 days;

for (uint256 i = 0; i < boundaryValues.length; i++) {
elapsed = boundaryValues[i];
uint256 actualPremium = oracle.decayedPremium(elapsed);
assert(actualPremium <= startPremium);
}
}

function test_decayedPremium_alwaysDecreasing(uint256 elapsed1, uint256 elapsed2) public view {
vm.assume(elapsed1 <= elapsed2);
vm.assume(elapsed2 <= 365 days);

uint256 premium1 = oracle.decayedPremium(elapsed1);
uint256 premium2 = oracle.decayedPremium(elapsed2);

assert(premium1 >= premium2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;

import {Test, console} from "forge-std/Test.sol";
import {StablePriceOracle} from "src/L2/StablePriceOracle.sol";
import {ExponentialPremiumPriceOracle} from "src/L2/ExponentialPremiumPriceOracle.sol";

contract ExponentialPremiumOracleBase is Test {
ExponentialPremiumPriceOracle oracle;

uint256 rent1;
uint256 rent2;
uint256 rent3;
uint256 rent4;
uint256 rent5;
uint256 rent10;

uint256 startPremium = 1e18;
uint256 totalDays = 365 days;

function setUp() public {
uint256[] memory rentPrices = new uint256[](6);

rent1 = 1e18;
rent2 = 2e18;
rent3 = 3e18;
rent4 = 4e18;
rent5 = 5e18;
rent10 = 6e18;

rentPrices[0] = rent1;
rentPrices[1] = rent2;
rentPrices[2] = rent3;
rentPrices[3] = rent4;
rentPrices[4] = rent5;
rentPrices[5] = rent10;

oracle = new ExponentialPremiumPriceOracle(rentPrices, startPremium, totalDays);
}

function test_constructor() public view {
assertEq(oracle.startPremium(), startPremium);
assertEq(oracle.endValue(), startPremium >> totalDays);
assertEq(oracle.price1Letter(), rent1);
assertEq(oracle.price2Letter(), rent2);
assertEq(oracle.price3Letter(), rent3);
assertEq(oracle.price4Letter(), rent4);
assertEq(oracle.price5Letter(), rent5);
assertEq(oracle.price10Letter(), rent10);
}
}
29 changes: 29 additions & 0 deletions test/ExponentialPremiumPriceOracle/decayedPremium.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;

import {Test, console} from "forge-std/Test.sol";
import {ExponentialPremiumOracleBase} from "./ExponentialPremiumOracleBase.t.sol";
import "solady/utils/FixedPointMathLib.sol";

contract DecayedPremium is ExponentialPremiumOracleBase {
function test_decayedPremium_zeroElapsed() public view {
uint256 elapsed = 0;
uint256 expectedPremium = startPremium;
uint256 actualPremium = oracle.decayedPremium(elapsed);
assertEq(actualPremium, expectedPremium);
}

function test_decayedPremium_halfPeriod() public view {
uint256 elapsed = 1 days / 2;
uint256 expectedPremium = 707106781186547524; // Calculated expected value for premium price after 1/2 day
uint256 actualPremium = oracle.decayedPremium(elapsed);
assertEq(actualPremium, expectedPremium);
}

function test_decayedPremium_threePeriods() public view {
uint256 elapsed = 3 days;
uint256 expectedPremium = 124999999999999999; // Calculated expected value for premium price after 3 days
uint256 actualPremium = oracle.decayedPremium(elapsed);
assertEq(actualPremium, expectedPremium);
}
}