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

The Burn: spend or burn excess ETH, every N Noun mints #803

Draft
wants to merge 130 commits into
base: master
Choose a base branch
from

Conversation

eladmallel
Copy link
Collaborator

No description provided.

many tests and refinements TODO
trying to assess auction house gas v1 vs v2
best to use what's used on mainnet now
taking a more tolerant appraoch since auction house can work fine
without the oracle
settleAuction is protected from re-entry by requiring `settled` to be false
and setting it to true first thing after the require statements
should make it clearer that this function can't be abused via reentry

msg.value is required to be higher than the previous amount + min increment

by setting `auction.amount` right after that requirement, it's clear the next
reentry will have to provide a higher amount and so on
created AuctionV2 that fits into 2 storage slots instead of 5 previously
added a migration contract between V1 and V2 that rewrites auction data for V2
@davidbrai davidbrai changed the base branch from verbs-auctionv2-infinite-oracle to master October 10, 2023 12:18
@netlify
Copy link

netlify bot commented Oct 11, 2023

Deploy Preview for nouns-testnet ready!

Name Link
🔨 Latest commit 3c7725f
🔍 Latest deploy log https://app.netlify.com/sites/nouns-testnet/deploys/6539152e133c1800082c237d
😎 Deploy Preview https://deploy-preview-803--nouns-testnet.netlify.app/
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@netlify
Copy link

netlify bot commented Oct 11, 2023

Deploy Preview for nouns-home ready!

Name Link
🔨 Latest commit 3c7725f
🔍 Latest deploy log https://app.netlify.com/sites/nouns-home/deploys/6539152e83f2c800082fb14c
😎 Deploy Preview https://deploy-preview-803--nouns-home.netlify.app/
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

eladmallel and others added 3 commits October 11, 2023 10:56
it's not bit-packed when placed after the auction struct
and for now we prefer to not force it into the auction struct

esp. since bigger gas savings are coming from descriptor storage read optimizations
following feedback from 9999, we're doing a small modification to the burn spec in regards to when it can be invoked 🔥

The problem 9999 helped us identify:
Let's say the current nextBurnNounId is 900
If at noun id 900 there is no excess ETH (which is also the point of the mechanism), then at any moment from then when there is excess ETH, a burn can be invoked immediately, because the current noun id is > 900
We think this defeats the purpose of predictable burn times

The suggested fix:
There will be a burn window of a few nouns, e.g. 3, during which a burn can be invoked
If there was no burn in that window, then the next window will be at a multiple of nounIdsBetweenBurns

Example:
If we set the initial burn noun id to 1000, and the nounIdsBetweenBurns to 100, and the burn window to 3
The first burn can happen while the auctioned noun has id in the range 1000-1003.
Only one burn can happen in a window
Next burns can happen in the windows 1100-1103, 1200-1203, etc. regardless of whether a burn happened or not
Comment on lines 334 to 340
function warmUpSettlementState(uint256[] calldata nounIds) external {
for (uint256 i = 0; i < nounIds.length; ++i) {
if (settlementHistory[nounIds[i]].blockTimestamp == 0) {
settlementHistory[nounIds[i]] = SettlementState({ blockTimestamp: 1, amount: 0, winner: address(0) });
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's likely worth optimizing this function since it may be called to warm up thousands of slots.

e.g.

function warmUpSettlementState(uint256[] calldata nounIds) external {
    uint256 nounIdCount = nounIds.length;
    SettlementState storage settlementState;
    for (uint256 i; i < nounIdCount;) {
        settlementState = settlementHistory[nounIds[i]];
        if (settlementState.blockTimestamp == 0) {
            settlementState.blockTimestamp = 1;
        }
        unchecked {
            ++i;
        }
    }
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

fixed: 5d1bea6

Comment on lines +402 to +407
// Skip Nouner reward Nouns, they have no price
// Also skips IDs with no price data
if (settlementState.winner == address(0)) {
--latestNounId;
continue;
}
Copy link
Collaborator

@solimander solimander Oct 19, 2023

Choose a reason for hiding this comment

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

I'm not so sure that we should skip auctioned nouns with no price data. Any gaps in historical population may return unexpected price results that are confusing at best and could affect the burn at worst.

Copy link
Collaborator

Choose a reason for hiding this comment

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

let's discuss this

Copy link
Collaborator

Choose a reason for hiding this comment

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

@solimander At first I thought you meant nouns that are auctioned but got no bids, and therefore burned.
But now I think I understand you meant historical data gaps for which we didn't populate data.

Trying to build a more concrete example:

  • We deploy this and start saving data starting from noun 900
  • We populate past data for nouns 800-850
  • Latest noun is at 925 and another contract calls getPrices(50), then they get the prices for nouns 825-850 & 900-925 (ignoring nounder rewards for now), which is not what they're expecting.
  1. Is this what you meant? if not, could you provide an example that demonstrates the issue?
  2. Thinking what would be a better behavior in this case. Some options:
    a. Don't skip the ids, but also don't return data for them. e.g. for getPrices(50), returns only prices for nouns 900-925. for getPrices(100) it would return prices for nouns 825-850 and 900-925.
    b. Return 0 as the price for ids with no data. e.g. getPrices(50) would get prices for nouns 900-925 and 25 zeroes. Then the caller of the function would need to decide what to do with zeroes.
    c. Any other ideas?
  3. In both suggestions above, my understanding is that you think we should skip nounder rewards nouns, is that correct? To do that I think we would need to add the id <= 1820 && id % 10 == 0 logic here, right?

Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. Is this what you meant? if not, could you provide an example that demonstrates the issue?

Yes, that's the case I'm referring to.

  1. Thinking what would be a better behavior in this case. Some options:
    a. Don't skip the ids, but also don't return data for them. e.g. for getPrices(50), returns only prices for nouns 900-925. for getPrices(100) it would return prices for nouns 825-850 and 900-925.
    b. Return 0 as the price for ids with no data. e.g. getPrices(50) would get prices for nouns 900-925 and 25 zeroes. Then the caller of the function would need to decide what to do with zeroes.
    c. Any other ideas?

I think that's an improvement on the existing logic. Alternatively, we could revert if there aren't enough consecutive prices populated to service the query (excluding Nounder nouns). Perhaps we expose both and let the consumer decide which fits their use-case.

  1. In both suggestions above, my understanding is that you think we should skip nounder rewards nouns, is that correct? To do that I think we would need to add the id <= 1820 && id % 10 == 0 logic here, right?

Yes, that's right.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Another case to consider:
A noun auctioned that got no bids, I think it this case it does make sense to skip, because it's not missing data.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it this case it does make sense to skip

Wouldn't it make more sense to include that price in the response as it was the actual settlement price?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thoughts on the following? -

function getPrices(uint256 auctionCount) external view returns (uint256[] memory prices) {
    uint256 latestNounId = auctionStorage.nounId;
    if (!auctionStorage.settled && latestNounId > 0) {
        latestNounId -= 1;
    }

    prices = new uint256[](auctionCount);
    SettlementState memory settlementState;
    uint256 actualCount;
    for (uint256 id = latestNounId; id > 0 && actualCount < auctionCount; --id) {
        if (id % 10 == 0 && id <= 1820) continue; // Skip Nounder nouns.

        settlementState = settlementHistory[id];
        if (settlementState.blockTimestamp <= 1) revert NOT_ENOUGH_CONSECUTIVE_PRICES();

        prices[actualCount] = uint64PriceToUint256(settlementState.amount);
        unchecked {
            ++actualCount;
        }
    }

    if (auctionCount > actualCount) {
        // this assembly trims the observations array, getting rid of unused cells
        assembly {
            mstore(prices, actualCount)
        }
    }
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

@solimander although unlikely, in case an auction doesn't get any bids and eventually the noun is burned, I think it's would be incorrect to assume the price there is zero. Since there was no actual transaction, I think the correct thing would be to skip it.
example: maybe there are no bids cause for some reason ethereum was down or gas prices were extreme.
wdyt?

Copy link
Collaborator

@solimander solimander Oct 24, 2023

Choose a reason for hiding this comment

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

I see your point. Perhaps a more likely case is when a reserve price has been set and is not met in an auction.

Okay, +1 for skip when there is no winner.


prices = new uint256[](auctionCount);
uint256 actualCount = 0;
while (actualCount < auctionCount && latestNounId > 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

May be worth optimizing this loop as it could run 90+ times in the proposed design.

e.g.

SettlementState memory settlementState;
uint256 actualCount;
for (uint256 id = latestNounId; id > 0 && actualCount < auctionCount; --id) {
    if (id % 10 == 0 && id <= 1820) continue; // Skip Nounder nouns.

    settlementState = settlementHistory[id];
    if (settlementState.winner == address(0)) continue; // Skip IDs with no price data.

    prices[actualCount] = uint64PriceToUint256(settlementState.amount);
    unchecked {
        ++actualCount;
    }
}

*/
function treasuryValueInETH() public view returns (uint256) {
address owner_ = owner();
return owner_.balance + stETH.balanceOf(owner_) + wETH.balanceOf(owner_) + rETHBalanceInETH(owner_);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thoughts on including token buyer's ETH in this calculation as well? 10% of the treasury currently sits in the token buyer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

let's discuss this.
to consider: token buyer's ETH is not part of the forked assets, so it doesn't participate in the BV

Copy link
Collaborator

Choose a reason for hiding this comment

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

@solimander we haven't considered including token buyer's ETH in the calculation so far.
agree that currently a big chunk of the treasury is there, but I think that's an anomaly rather than the norm. We're got to much ETH sent there because many were looking at the state of token buyer as a candidate and those transactions haven't since updated. I feel like this is a more of a bug with token buyer rather than a normal state. Perhaps token buyer should send ETH back to the DAO if it has too much? haven't thought about it enough.

Regardless, I think there's some logic in excluding it from the calculation here, because it doesn't participate in the assets sent to a fork. I don't feel strongly about it, so curious to hear your thoughts here.
also @eladmallel

Copy link
Collaborator

Choose a reason for hiding this comment

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

Okay, makes sense. I agree it likely doesn't make sense to add since it's not included in BV, but we should probably address this issue at some point.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I thought about it for a brief moment some weeks ago and decided to leave it out for the reason you mentioned - since that ETH is anyway currently not part of the forking mechanism.

but since it has the potential to hold significant amounts of ETH, and has the potential to be used to DAO spending, it can be a weird way of gaming the burn against our intentions, and because it seems fairly simple to take it into account - I am now leaning towards including its ETH in this calculation.

@davidbrai let's just add it?

* @notice Get the mean auction price of the last `numberOfPastAuctionsForMeanPrice` auctions.
* @dev Reverts if there is not enough auction history to calculate the mean auction price.
*/
function meanAuctionPrice() public view returns (uint256) {
Copy link
Collaborator

@solimander solimander Oct 19, 2023

Choose a reason for hiding this comment

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

May be worth adding a mean view function to the auction house itself to remove the need to re-loop over all prices. It also feels like it may be desirable for others who want to build on Nouns.

e.g. getMeanPrice(auctionCount)

nounIdsBetweenBurns = newNounIdsBetweenBurns;
}

function setBurnWindowSize(uint16 newBurnWindowSize) external onlyOwner {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Add comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants