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

Add Launch Auction Price Oracle with reduced half life #70

Merged
merged 25 commits into from
Aug 14, 2024
Merged
Changes from 9 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7dd9033
Add parameterized auction half life
stevieraykatz Jul 21, 2024
6b37b28
Merge branch 'main' into launch-auction
stevieraykatz Aug 5, 2024
facd077
Revert to prod exp. price auction
stevieraykatz Aug 5, 2024
0e056b4
Add Launch Auction pricing contract
stevieraykatz Aug 5, 2024
edea68f
Fix tests, get ready for review
stevieraykatz Aug 5, 2024
0a6684a
Cleanup return to avoid overshadowing
stevieraykatz Aug 5, 2024
4774c14
Fix pragma
stevieraykatz Aug 5, 2024
6fb4de2
rename to avoid overshadowing
stevieraykatz Aug 5, 2024
a2f7087
Rename constant
stevieraykatz Aug 5, 2024
17a5a48
Add unit tests
stevieraykatz Aug 7, 2024
0f0589f
Lint
stevieraykatz Aug 7, 2024
d4534f7
Fix comments in test
stevieraykatz Aug 7, 2024
9f22d48
Fix tests per PR
stevieraykatz Aug 8, 2024
ea08a84
Add comment for base prices
stevieraykatz Aug 8, 2024
61c873e
lint
stevieraykatz Aug 8, 2024
1563df6
Change halflife to 1.5 hours, allow auction duration to be specified …
stevieraykatz Aug 9, 2024
f5826c9
Fix typo in natspec
stevieraykatz Aug 9, 2024
71394e9
Switch to Error from require
stevieraykatz Aug 9, 2024
74310cb
Add test for new error message
stevieraykatz Aug 9, 2024
1e46dc1
Fix unit conversion issue in bitshift operation
stevieraykatz Aug 11, 2024
01ac27f
Add launch auction integration tests
stevieraykatz Aug 12, 2024
e9b954d
reorder setup to mimic expected ordering
stevieraykatz Aug 12, 2024
eba4cfa
lint
stevieraykatz Aug 12, 2024
ac267c2
Update src/L2/LaunchAuctionPriceOracle.sol
stevieraykatz Aug 13, 2024
11936e7
Fix some comments, cleanup tests
stevieraykatz Aug 13, 2024
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
83 changes: 83 additions & 0 deletions src/L2/LaunchAuctionPriceOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";

import {EDAPrice} from "src/lib/EDAPrice.sol";
import {StablePriceOracle} from "src/L2/StablePriceOracle.sol";

/// @title Launch Auction Price Oracle
///
/// @notice The mechanism by which names are auctioned upon Basenames public launch. The RegistrarController
/// Passes the `launchTime` in place of expiry for all new names. The half life of this contract is hard-coded
/// to 1 hour, accomplished by bitshifting the `endValue` and by passing this period into the exponential decay
stevieraykatz marked this conversation as resolved.
Show resolved Hide resolved
/// calculation.
///
/// Inspired by the `ExponentialPremiumPriceOracle` implemented by ENS:
/// https://github.com/ensdomains/ens-contracts/blob/staging/contracts/ethregistrar/ExponentialPremiumPriceOracle.sol
///
/// @author Coinbase (https://github.com/base-org/usernames)
contract LaunchAuctionPriceOracle is StablePriceOracle {
Copy link
Contributor

Choose a reason for hiding this comment

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

will there be a separate PR that utilizes this new oracle?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure I follow. In case this context helps, the flow for launch will be:

  1. Deploy this new LaunchAuctionPriceOracle along with the GA Contracts
  2. Use this oracle for the launch auction duration
  3. After it concludes, switch over to using the audited ExponentialPremiumPriceOracle

Copy link
Contributor

Choose a reason for hiding this comment

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

How and when does 2. happen?

Copy link
Collaborator Author

@stevieraykatz stevieraykatz Aug 8, 2024

Choose a reason for hiding this comment

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

It's set as the price oracle upon deployment of the GA Registrar Controller:
https://github.com/base-org/basenames/blob/b147ced84476b0d93b71e3500f1a90823b60a00f/src/L2/RegistrarController.sol#L273

Then, we do a pair of transactions in quick succession to "turn on" registrations.

  1. Set the Launch Time using: https://github.com/base-org/basenames/blob/b147ced84476b0d93b71e3500f1a90823b60a00f/src/L2/RegistrarController.sol#L330
  2. Set the RegistrarController as the controller for the BaseRegistrar via: https://github.com/base-org/basenames/blob/b147ced84476b0d93b71e3500f1a90823b60a00f/src/L2/BaseRegistrar.sol#L208

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @notice Starting premium for the dutch auction.
uint256 public immutable startPremium;

/// @notice Ending value of the auction, calculated on construction.
uint256 public immutable endValue;
Comment on lines +21 to +28
Copy link
Collaborator

Choose a reason for hiding this comment

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

Immutable variables are not actually part of contract's storage if that's the intention of STORAGE comment.


/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTANTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @notice The half-life of the premium price decay
uint256 constant PRICE_PREMIUM_HALF_LIFE = 1 hours;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* IMPLEMENTATION */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @notice Construction of the premium pricing oracle.
///
/// @param rentPrices The base prices passed to construction of the StablePriceOracle.
/// @param startPremium_ The starting price for the dutch auction, denominated in wei.
/// @param totalDays The total duration (in days) for the dutch auction.
constructor(uint256[] memory rentPrices, uint256 startPremium_, uint256 totalDays) StablePriceOracle(rentPrices) {
startPremium = startPremium_;
endValue = startPremium >> (totalDays * 24); // 1 hour halflife
Copy link
Collaborator

Choose a reason for hiding this comment

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

There seems to be a relationship between the half-life constant and the math here. Could we rewrite so that this relationship is codified using the constant vs relying on comment?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is another case of trying to minimize code changes while achieving the desired functionality. Originally, I unified them (and parameterized half life) but I think that the risk introduced from a larger tear-up isn't worth the squeeze.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ended up unifying these concepts. It became necessary once the units of the halflife and the units of the auction were both denominated in hours. Check it out and lmk what you think.

}

/// @notice The internal method for calculating pricing premium
///
/// @dev This method handles three cases:
/// 1. The name is not yet expired, premium = 0.
/// 2. The name is expired and in the auction window, premium = calculated decayed premium.
/// 3. The name is expired and outside of the auction window, premium = 0.
///
/// @param expires Timestamp of when the name will expire.
///
/// @return Price premium denominated in wei.
function _premium(string memory, uint256 expires, uint256) internal view override returns (uint256) {
if (expires > block.timestamp) {
return 0;
}
uint256 elapsed = block.timestamp - expires;
uint256 premium_ = decayedPremium(elapsed);
if (premium_ > endValue) {
return premium_ - endValue;
}
return 0;
Comment on lines +70 to +78
Copy link
Collaborator

@ilikesymmetry ilikesymmetry Aug 5, 2024

Choose a reason for hiding this comment

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

I am really struggling to get a clear grasp of this code and how it behaves at the boundaries even after a few rereads. What do you think of rewriting without the endValue abstraction and just focusing on using timestamps like your comment describes? Rename totalDays to auctionDuration, store it as an immutable and skip using endValue entirely (unless it's needed somewhere else?).

Suggested change
if (expires > block.timestamp) {
return 0;
}
uint256 elapsed = block.timestamp - expires;
uint256 premium_ = decayedPremium(elapsed);
if (premium_ > endValue) {
return premium_ - endValue;
}
return 0;
if (expires > block.timestamp || block.timestamp - expires > auctionDuration) {
return 0;
}
return decayedPremium(block.timestamp - expires);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

While I generally agree that this code could use some cleanup, it's a forked and audited contract from ENS that we're making a slight tweak to. Not sure that the cleanup is worth the risk of introducing a regression.

}

/// @notice The mechanism for calculating the decayed premium.
///
/// @param elapsed Seconds elapsed since the auction started.
///
/// @return Dacayed price premium denominated in wei.
Copy link
Collaborator

Choose a reason for hiding this comment

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

"Dacayed" -> "Decayed"

function decayedPremium(uint256 elapsed) public view returns (uint256) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If I understand the computation mechanism premium_ - endValue in _premium() correctly, our frontend should take whatever value is returned by this and subtract endValue on the UI to show users. By my estimates, endValue should be 100 ether * 0.5 ** (36 / 1.5) => 0.0000059605 ether. I recall we have some refund mechanism built into the contracts because the decay will reduce the price even further by the time the transaction validates so this isn't a big deal then.

Noting this more to confirm my own understanding, but maybe worth a dev natspec on this function that this value is an overestimate of what the user actually needs to pay given those two factors.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

F/E doesn't need to do anything special. The RegistrarController does the heavy lifting parsing the response. See:
https://github.com/base-org/basenames/blob/b147ced84476b0d93b71e3500f1a90823b60a00f/src/L2/RegistrarController.sol#L389-L398

I think that the natspec here is accurate. This method only returns the pure price premium. The implementation handles the use-case specific logic.

/// @dev 50% decay per period in wad format
uint256 perPeriodDecayPercentWad = FixedPointMathLib.WAD / 2;
return EDAPrice.currentPrice(startPremium, elapsed, PRICE_PREMIUM_HALF_LIFE, perPeriodDecayPercentWad);
}
}