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

Auction Standard #24

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open

Auction Standard #24

wants to merge 31 commits into from

Conversation

joshpainter
Copy link

This CHIP details a zero-counterparty-risk protocol for auctions of digital assets running on the Chia blockchain. Ideally any auction protocol will be interoperable with all existing and future assets that may be created on the Chia blockchain, and therefore it is worth discussing and designing a solid foundation with full community involvement.

This first draft details the problems and risks with the current-day auction process. The next update will add the first draft for the Conceptual Design and Specification sections - the fun part!

Special thanks to @danieljperry and @fizpawiz for their help so far!

@joshpainter joshpainter changed the title Auctions - Initial commit Auction Standard - Initial commit Jun 15, 2022
@joshpainter joshpainter changed the title Auction Standard - Initial commit Auction Standard Jun 15, 2022
@Ganbin
Copy link

Ganbin commented Jun 15, 2022

Nice draft.

My first thinking is about the delay introduced in the fact that when we are the highest bidder, we directly settle and pay the bid before the auction is finished.

  1. This mean that if another higher bid happen, will we be reimbursed directly in the same transaction block or do we have to wait the next transaction block to get our bid back?

  2. What happen if multiple bid arrive in the same block? I suppose the highest win and the others are rejected.

  3. Will this not be too much transactions to be done on L1 in the future when the chain will be busy?

  4. As a transaction block happen every ~52 seconds. Will it not make the process not so reactive as we could have only 1 bid per 52 seconds? Maybe this is enough for an auction.

  5. Will this CHIP propose a way to delay the auction close every time a new higher bid is placed? This can prevent sniping in the last moment.

@joshpainter
Copy link
Author

joshpainter commented Jun 15, 2022

Thank you for your feedback!

My first thinking is about the delay introduced in the fact that when we are the highest bidder, we directly settle and pay the bid before the auction is finished.

  1. This mean that if another higher bid happen, will we be reimbursed directly in the same transaction block or do we have to wait the next transaction block to get our bid back?

In my current thinking, all functionality is executed when a high bidder "spends" the auction singleton. This logic is contained in the PAY_HIGH_BID program. The first high bidder would "spend" the singleton by running PAY_HIGH_BID program, which is going recreate the singleton, now with the high bidder's information embedded. But the singleton will only allow itself to be spent if it sees announcements from all the coins that it knows need to be spent to fulfill the high bid obligations. See CALC_BUYERS_PREMIUM and CALC_COMMISSION. These programs will create the correct coin conditions needed for those features and PAY_HIGH_BID will do the same for the main high bid. In the end, the high bidder will not be able to "steal" (spend) the singleton unless they prove that they have made all the appropriate spends, asserted through announcements.

  1. What happen if multiple bid arrive in the same block? I suppose the highest win and the others are rejected.

I'm definitely not an expert here, but since high bidders will be competing to spend the same auction singleton, it will be "first come first served." Or more accurately - which spend bundle reached the farmer who eventually farmed that block first? The other one would have been rejected. But with dynamic/auto-extending auctions, this won't be a big deal. More than likely, the DAPP will just try again next block.

  1. Will this not be too much transactions to be done on L1 in the future when the chain will be busy?
  2. As a transaction block happen every ~52 seconds. Will it not make the process not so reactive as we could have only 1 bid per 52 seconds? Maybe this is enough for an auction.

The interesting thing about live auctions is it takes months of planning and then they may be over in 60 seconds and 12 bids. This all has to do with the auctioneer of course, but they are trained in the art of high pressure sales and their goal is to get as many bids as fast as possible in order to activate "auction fever" in the bidders. We have no need to do this and much more reason to slow things down and auto-extend times to prevent sniping, better user experience, etc. Modern digital auction users are especially used to these slower-paced auctions.

  1. Will this CHIP propose a way to delay the auction close every time a new higher bid is placed? This can prevent sniping in the last moment.

Yes! We are still very early. This would be defined in the CALC_HAMMER_TIME program and would probably be the most popular option. Other weird rules could be embedded here during the mint of the auction singleton though!

The parameters detailed here and even high-level ideas around using singletons are up for debate. I'm certainly not an expert in any of this but not many of us are, so let's start somewhere and figure it out together! :)

@joshpainter
Copy link
Author

Updated with more community feedback, updates and credit section

@joshpainter
Copy link
Author

I had a fantastic chat on Keybase with @danieljperry and learned a whole lot plus we may have figured out a solution to a problem I didn't even know I had yet lol. Reposted here so others can learn too - thanks again Dan!!

Dan Perry:
Looks like you're getting some good traction on the auction CHIP. Congrats!
There is a lot to build here, but I think most of it is technically not a problem, with one exception? How do you enforce the hammer time? We don't allow you to invalidate a spend directly on-chain:
https://github.com/Chia-Network/chia-blockchain/wiki/FAQ#how-can-i-make-a-coin-that-may-only-be-spent-until-a-certain-timestamp-or-block-height
There are some proposed solutions, but they still require a human to spend something. Have you given any thought to doing this in a trustless manner? This might be a good question for our public channels, as once you solve this, I think building the rest of the protocol will be straightforward, though still a lengthy process.

Josh Painter:
Thank you! Yes, feedback seems to be great so far and I even got to talk to Bram about it in a twitter spaces today. He had some good feedback too but no big holes in the concept yet. As for hammer time, I always assumed this would be the last spend of the singleton by the owner to melt it and claim the prize inside. So imagine the auction itself as an egg with the item for sale inside. We are stealing the egg from each other (as long as we pay for it properly) until nobody wants to steal it anymore and after a certain amount of time of no stealing, the current owner can now make the "CLAIM_ASSET" spend to melt the singleton and get at the juicy center of the egg :)

Dan Perry:
That's great you even got Bram's attention on this. The thing to think about it the concept of "after a certain amount of time of no stealing". Think about how this will be enforced. Of course a human could get involved, but it would be great to do it in a trustless manner. That's what I'm unsure of how to do.

Josh Painter:
Well even if the winner never makes that last spend to “crack the egg,” they still own the egg and nobody else can crack it except the owner after that sliding timeout. So when anybody tries to execute “claim asset” we first check to see if they are owner and next check to see if auction is over. If both true, claim asset works. Otherwise it fails. Am I still not understanding your concern or am I missing something that I should be concerned about?

Dan Perry:
The concern is the "sliding timeout". How is that enforced?

Josh Painter:
Ah - each time the egg is stolen, we update the auction hammer time to be now plus five minutes (or whatever time period they chose at mint). So now that auction end time is saved in the solution reveal of that spend and the next spend can pass in that date and it can be asserted through lineage proofs (i think?). Now the current spend can see if it is still “during auction” or “after auction”

Dan Perry:
If you look at our condition codes, we have (for example) ASSERT_SECONDS_ABSOLUTE: https://chialisp.com/docs/coins_spends_and_wallets
This could be used to validate the final spend, perhaps? But we don't have a way to invalidate a spend. So I'm wondering, you have the end time curried into the solution, but what do you do with it? What you need is some way to say "If this timestamp has not been reached, then it is valid to spend the singleton to a new temporary owner and reset the timestamp. Else, the timestamp has been reached so the auction is over. Spend the singleton to the final owner." The "else" part seems straightforward, but I'm not sure how the "if" part would work. How would this be enforced on-chain?
One simple way to do this would be to pass in the current timestamp into the solution, then compare it against the curried timestamp. But then you're relying on the timestamp, which was obtained off-chain. So there is still some degree of trust involved. Maybe this is OK, as long as you can be certain that the current timestamp is unfakeable. But how to use the blockchain, and nothing else, to enforce this rule is the part I'm not seeing.

Josh Painter:
I think I must still not understand the difference between a spend being "not valid" and a spend being "invalidated." But yes, I was assuming that user would pass in current time and then we would evaluate current time against the curried-in time, and I'm kind of starting to understand your point there I think. Assuming that trust is ok, wouldn't the CLAIM_ASSET then just "throw an exception" so to speak if the current time is not yet "after" the auction?
Yeah reading through the condition codes again I think I see what you are saying - we can assert time on this coin (when it was created vs now) but we can't assert a time on this coin's parent coin, which is what we'd need here I think - is that right?
Although...a sliding expiration is still fine there cause we don't actually care about the parent's coin/solution reveal now that I think about it. Each time the "egg" is stolen, we are really recreating the whole egg when we "steal" it, so at that point a new "egg timer" (lol) starts on that egg. So each "egg" in the chain has a lifespan during which it can be stolen and recreated as a new egg (ASSERT_SECONDS_RELATIVE = true) OR stolen and cracked (ASSERT_SECONDS_RELATIVE = false). So maybe I understand now by talking it out - are you saying that there is not a way to ASSERT_SECONDS_RELATIVE = false but still execute the spend?

Dan Perry:
"wouldn't the CLAIM_ASSET then just "throw an exception" so to speak if the current time is not yet "after" the auction?" -- yes this is the easy part. That is enforceable with ASSERT_SECONDS_RELATIVE (or ABSOLUTE).
"can be stolen and recreated as a new egg (ASSERT_SECONDS_RELATIVE = true) OR stolen and cracked (ASSERT_SECONDS_RELATIVE = false)" This is the problem. If ASSERT_SECONDS_RELATIVE is true, then the egg can be stolen and cracked. This means that the timestamp has been reached and the auction is over. However, if ASSERT_SECONDS_RELATIVE is false, then the egg may not be stolen at all. It's not spendable yet.
You could, however, have multiple branches of logic, where ASSERT_SECONDS_RELATIVE only exists in one branch.

Josh Painter:
Does it help that…yes exactly. CLAIM_ASSET and PAY_HIGH_BID are alternate spends in my mind with different conditions

Dan Perry:
ok good. So the final spend is simple enough. But what goes in the other branch? The one with "can be stolen and recreated with a new egg"? You can't use ASSERT_SECONDS_RELATIVE here. So we're back to the point where you pass in a timestamp in the solution and compare it with a curried timestamp. This might be fine. Run it past the Chia devs to see what they think. My point is that this is introducing some trust into the system. As far as I know, there's not a way to do this trustlessly. If you can figure one out, then you're golden.
One thing to keep in mind is ASSERT_SECONDS_RELATIVE doesn't allow you to go down two different logical paths. It's either true or false. If it's true, then the spend is allowed (as long as there are no other false conditions). If it's false, then the spend fails.

Josh Painter:
Yep I definitely was not thinking through that part - in my mind I could treat these asserts as "if" statements but now I understand that they are not quite that - I should imagine it more like an if condition with a built-in failure mode for "else" right?
you know an interesting improvement there could be an extra parameter you pass into the ASSERTs that is a lambda that runs on failure of assert before the main "exception"? dunno if that opens up other holes tho

Dan Perry:
This might be possible for sending messages. I'm not sure, though. The thing to think about with conditions is you can have as many as you want, and as long as they all pass, the coin spend passes. But if one fails, then the coin spend fails as well. If you have no conditions at all, then the coin spend automatically passes. Also, the only thing you can do with a coin is spend it. It's not possible to modify the coin. This is why a singleton would work well for auctions -- you can spend it and recreate it with new rules.

Josh Painter:
Yep ok thank you for that, I was definitely missing that nuance. Let me think some more - heading back to Tulsa from Austin today so I have 7 hours to think about it 😂

Dan Perry:
Sounds good, have a nice trip!

--later--

Josh Painter:
Ok, had some time to think. What I really need is a way to "OR" conditions together, but that doesn't seem to be an option. But what about this: every time we make new high bid, we curry in new HAMMER_TIME block height to new singleton, like normal. But we also ask the user to pass in the blockheight for which their spend is valid as well as the blockheight of the last high bid (which we verify of course). This "valid blockheight" always must be after the last high bid's blockheight but before the HAMMER_TIME block height. Once we pass that check, we just ASSERT_HEIGHT_RELATIVE using the passed-in blockheight.

Dan Perry:
Why don't you just make the "valid block height" and the "last high bid" the same?
Here's how I'm visualizing it:
curry a new hammer_time with each spend
when a "bid" is placed, pass current_time into the solution. There are two possibilities:

  1. last_high_bid_time < current_time < hammer_time
    if hammer_time - current_time > 0
    recreate singleton (auction continues)
  2. last_high_bid_time < hammer_time <= current_time
    This probably requires an (automated) spend from the holding coin to the final destination.
    else
    ASSERT_SECONDS_RELATIVE (verify that hammer_time has been reached)
    finalize spending (auction has ended)
    The "else" clause has ASSERT_SECONDS_RELATIVE, which guarantees that the auction has actually ended.

Josh Painter:
but how do we trust that current_time is true?

Dan Perry:
However, there is no equivalent in the "if" clause. As long as current_time has not been faked, the spend is secure. The hard part is making sure current_time is legit.

Josh Painter:
yup ok

Dan Perry:
Yes exactly.
If you can find an answer to this question, I think everything else will fall into place.

Josh Painter:
well in my idea, we ask the user to pass in last_bid_blockheight, their "valid spendheight" for their bid, plus the hammer time spendheight. Then we do the same logic with blockheights instead of times. But the nice there here is we can ASSERT_HEIGHT_RELATIVE on the "valid spend height" number that user passed in to validate that it is indeed this block.
(well, this block or the ones after it - but before hammer time cause we already checked that)

Dan Perry:
Using your idea, let's say the hammer_time block height has been reached. The auction is over. But then someone attempts to place a bid . ASSERT_HEIGHT_RELATIVE will pass because "valid spendheight" has been reached, so the bid will go through. Right?
well before we ASSERT_HEIGHT_RELATIVE here we would have verified that valid spendheight < hammer time
ASSERT_HEIGHT_RELATIVE(valid spendheight) --> this will pass as soon as valid spendheight has been reached. It will still pass after hammer time has been reached.
One idea to fix this logic would be to execute the final spend of the singleton as soon as hammer time has been reached. But this requires someone or some program to execute that spend. I don't know of a way to guarantee that bids can no longer be placed between hammer_time and that final spend. So someone could potentially come in and snipe a bid after hammer_time but before that final spend.

Josh Painter:
Right but can't we short-circuit the spend BEFORE we ASSERT_HEIGHT_RELATIVE?
IF (valid_spendheight >= hammer_time) return (x)
ELSE ASSERT_HEIGHT( valid_spendheight)
I suppose this would work for times too (if it works for blockheights)

Dan Perry:
right, that's essentially the same logic I used above
can be time or blockheight

Josh Painter:
Yep I see - basically same problem cause user could pass in fake "valid spendheight" that is in between last high bid and hammer time and ASSERT_RELATIVE will still be true. Ok well I didn't want to do this, but we just switch over to Absolute for "valid spendheight." So now we still check that it is after last_bid_absolute_height and before hammer_time_absolute_height and we also ASSERT_HEIGHT_ABSOLUTE and the user's bid is only valid for a single block, but the DAPP could be responsible for this complexity of incrementing/resubmitting bids on exact block assertion failures.

Dan Perry:
Unless I'm not understanding something, it's still the same problem. Coins can only become valid. They can never become invalid. So if you make absolute assertions, you're still relying on the spender to pass in a correct block height or timestamp.

Josh Painter:
Well the problem with using RELATIVE is that the user can pass in any height after the last_bid_height and it will be valid. but absolute means that this spend is only valid for a single block, ever, right? So user must pass in the exact blockheight for which their bid is valid (obviously the next one that is about to be farmed). We will validate that it is between last_bid_height and hammer_time_bid_height first, then we will ASSERT_HEIGHT_ABSOLUTE on what they gave us. If they are giving us a fake value, it won't work. If they are trying to bid on an absolute_height outside the window, it won't work.
if DAPP gets an error cause we missed absolute height this time, it can resubmit and try for next block

Dan Perry:
The "absolute" in ASSERT_HEIGHT_ABSOLUTE only refers to the height that this spend becomes valid. It's a block number, whereas with RELATIVE it would be "number of blocks since this coin's creation". Once the coin becomes valid after the specified block, it remains valid forever. It's not valid for just that single block.

Josh Painter:
OHH ok, so they both are "and everything after" type of operators

Dan Perry:
yes

Josh Painter:
hmmmm
i'm gonna need some more CHIPS lol

Dan Perry:
coins can only become valid. If we allowed them to become invalid, then they could be bricked

Josh Painter:
yes I see that now - it's like we only allow coins to die (be spent) because of a deterministic reason, not just random old age lol

Dan Perry:
right

Josh Painter:
well that's not even right really, cause "spent" isn't the same as "die"

Dan Perry:
Coins can never be bricked or burned in CHIA

Josh Painter:
yup ok I'm getting it

Dan Perry:
Even if I send a coin to a "burn" address, for which nobody has the private key, it's still technically not burned. Someone could figure out the key and spend it, however unlikely that is.

Josh Painter:
in my case, I do want to "brick" one of the alternate spends but not the other and the system doesn't have a way of knowing my intent - that this coin will never be bricked, just one of its spends will be

Dan Perry:
But if a coin became valid after a certain block or time, then it would always remain invalid, with no way whatsoever to spend it. This is not allowed in Chia.
Yes, you're seeing it now.

Josh Painter:
Neo fighting Smith

Dan Perry:
I didn't know you had a gif of Gene and Bob handy...

Josh Painter:
hahahhaa

--later--

Josh Painter:
Actually - are we overthinking this? What is wrong with a ruleset that says "anybody can steal the uncracked egg at any time" plus "the current owner can crack the egg (melt singleton) after the set end date" which will get pushed forward by during the "high bid" spend. So current high bidder always has to wait at least X amount of time to crack it, but if X amount of time passes and they (or their DAPP) doesn't crack the egg before someone else steals, that's their fault. Otherwise, maybe they are speculating on the egg and they don't mind leaving it up past it's "auction end time" cause they will be reimbursed a bit more than they paid for it when someone steals (via market forces under value of asset or even directly with fun rules where new high bidder has to pay last high bidder back plus a bit more). Regardless, the current egg owner can crack the egg at any time after their cool-off period OR leave it up to be stolen
In other words, who are we to say when the auction should end - the current owner of the auction egg should get to decide that but only after a set cooloff period to give other bidders the opportunity to steal. if the current owner doesn't want to "cash in" right after cooloff for whatever reason, more power to them? The original owner/auction house/last high bidders have all been made whole already.
Plus there isn't much incentive for anybody to snipe a high bid right at end of cool-off period. Would be too risky - you'd want to get your bid in well before end of cool-off period to make sure you don't miss it in case owner is waiting for that exact block height
The sniper has become the snipee lol

Dan Perry:
Yes, I think you figured it out.
Curry the current owner's pubkey and time the coin becomes spendable
owner must pass in pubkey to spend

Josh Painter:
yup perfect
the moral here is when you are up against obstacles, sometimes maybe you should just go find some different obstacles and see if those are easier
🤣
gonna think about it some more but it feels good so far - thanks!

Dan Perry:
if curried pubkey matches owner's pubkey, then verify timestamp reached and AGG_SIG_ME and the owner can spend the coin to move funds out of the auction singleton
else anyone can spend but they must increase the coin price by x value and relock it inside the singleton (plus more logic)
However, upon recreating the singleton, you have to make sure the correct end time and pubkey are curried in. This could be problematic in Chialisp , so you should implement a prototype that we can audit. If you're not careful, a farmer could modifiy these values.

Josh Painter:
awesome, yep, this all makes sense and that will give me a headstart on the actual code now that the concept itself seems solid in my head - thank you! Do you mind if I post this chat history in the Github discussion? I think it's great for people to see how to think through a problem in Chialisp together

Dan Perry:
Sure, keeping in mind that I'm still not sure it'll work... just that you figured out a way around the other problem.

Josh Painter:
hehe of course - well even if it doesn't work, I learned several things already so others might too :)

@iced-caffeine
Copy link

Hey guys,

I tried to read through this to figure out where the hangups are. Were you ever able to figure out if the last bid + blockheight obstacle could be overcome before the high bidder can crack the egg?

Is this actively being worked on at all?

@hoffmang9
Copy link
Member

Relevant and not available during the initial discussions above, NOT_VALID_AFTER is likely to show up as a CHIP in the next couple of months.

@joshpainter
Copy link
Author

I tried to read through this to figure out where the hangups are. Were you ever able to figure out if the last bid + blockheight obstacle could be overcome before the high bidder can crack the egg?

I ended up getting around this by changing the logic, which I think ended up better anyway. From above:

"Actually - are we overthinking this? What is wrong with a ruleset that says "anybody can steal the uncracked egg at any time" plus "the current owner can crack the egg (melt singleton) after the set end date" which will get pushed forward by during the "high bid" spend."

This makes it the high-bidder's responsibility to "end" the auction, but is only allowed AFTER a certain blockheight. This fits with the current "Valid after" asserts. This assumes the high-bidder (or their automated dApp) will be incentivized to end the auction ASAP so they can claim their win. But it also gives the high-bidder the weird option of "letting it ride" because of bidder's remorse, and hoping someone else will eventually outbid them, so they get reimbursed for their high bid. Remember that the original seller and even the auction house has already been reimbursed during every bid, so they don't mind if the auction continues indefinitely!

Is this actively being worked on at all?

Not currently, but it is on my list! And/or others too, hopefully. :)

@iced-caffeine
Copy link

Thanks for the comments.

In Re: "they don't mind if the auction continues indefinitely!" This is a spam vector. Someone can set the block height infinitely high with the first, low bid, thereby effectively burning the asset. The extend time with a new high bid should be a constant, known param in the program throughout the life of the auction.

@joshpainter
Copy link
Author

Yes, the extend time is definitely a parameter set by the creator when they "mint" the auction. In fact, I would like that parameter to be a program itself, so more custom logic could be used by the creator, such as extensions that get shorter and shorter over time, etc. But for simplicity, let's say it gets set to the equivalent of 10 minutes.

After the first bid is made, that high bidder effectively owns the asset. The original seller has been paid and the auction house has been paid out of that high bid. The only way that bidder will not continue to be the owner is if someone outbids them in the next 10 minutes (and therefore become the new effective owner).

After 10 minutes, the current high bidder can "crack the egg" and end the auction, or not. If they don't, someone else could come along 15 minutes later and steal it from them by making a high bid.

In other words: when the auction is active, anybody can always steal the "egg" by making a high bid. The only way for the auction to end is for the high bidder to end the auction after the extend time, as set by the original creator (not the bidder). So I don't think this is a spam vector, but please let me know if you still disagree!

@iced-caffeine
Copy link

Ah, this makes sense. I misunderstood the initial problem.

Basically after the auction "ends", the high bidder has the right to claim the asset. This sounds similar to how a solo farmer can claim his 1.75 XCH whenever. It's there on the chain for them, but they still need to go claim it.

@joshpainter
Copy link
Author

Ah, this makes sense. I misunderstood the initial problem.

Basically after the auction "ends", the high bidder has the right to claim the asset. This sounds similar to how a solo farmer can claim his 1.75 XCH whenever. It's there on the chain for them, but they still need to go claim it.

Exactly! An interesting side-effect of this is that it inverts auction sniping. Normally, an auction sniper will wait until the last possible moment to make a high-bid. This ends up pushing most of the action to the end of the auction. But in this model, you want to absolutely make sure your high bid is in well before the end time, cause you'll want to take into account traffic and confirmation time, etc.

Now, the current high-bidder is the "sniper" in that they are waiting to end that auction the moment they can so they can claim their win. The sniper has become the snipee!

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.

None yet

4 participants