Agoric / agoric-sdk

monorepo for the Agoric Javascript smart contract platform
Apache License 2.0
327 stars 206 forks source link

EPIC: Zoe Design Doc - Paying for Metering #3294

Open katelynsills opened 3 years ago

katelynsills commented 3 years ago

For the purposes of this document, let's define:

Summary

(Note: Items have been marked as IN-SCOPE or OUT-OF-SCOPE for the metering testnet phase.)

  1. IN-SCOPE: A chargeAccount is a purse-like object known to Zoe that holds RUN and can be charged to pay for metering. https://github.com/Agoric/agoric-sdk/issues/3309 https://github.com/Agoric/agoric-sdk/issues/3322
  2. IN-SCOPE: Zoe charges the contract creator's chargeAccount for all execution that occurs in the contract instance ZCFVat.
  3. IN-SCOPE: The contract creator (in coordination with the contract developer) can electively choose to pass on costs to users of their contract instance by charging fees for the use of invitations, publicFacets, and other objects.
  4. IN-SCOPE: A contract developer can specify the fees for the use of invitations, publicFacets, and other objects, in relative terms. Zoe translates these relative terms into absolute RUN amounts and quote expiration Timestamps just-in-time before the user receives the invitations, publicFacets, and other objects. (See https://github.com/Agoric/agoric-sdk/issues/3399)
  5. OUT-OF-SCOPE: Dapps can use the user's chargeAccount to pay fees related to the user's use of the underlying contract instance, but not directly. The wallet creates proxy-like objects which wrap the user's chargeAccount and passes these objects onto the dapp. The dapp can attempt to call a method on the proxy-like object, and the wallet (and therefore user) can approve or disapprove translating this request into a call on the actual object within the wallet.
  6. OUT-OF-SCOPE: The wallet can automatically charge fees under a specified amount or for a particular dapp/contract if the user decides they do not want have to explicitly approve fees.
  7. OUT-OF-SCOPE: A lower level mechanism ("stamps") is necessary for preventing spam in calls that cannot charge a chargeAccount, such as the Zoe method to make a chargeAccount. However, stamps will be implemented at the kernel level and is out of scope for now.

An Aside: Best Practices for Charging Fees

Charging unanticipated and surprising high fees for services already provided makes users unhappy. A much better user experience is giving a quote ahead of providing the service, then only charging the quoted amount. The service provider covers any actual difference in the quoted price compared to the actual costs.

This means that whatever users agree to when they approve something in their wallet is what they should be charged. In other words, we shouldn't have a model where the users' purse or chargeAccount is charged the current price of execution, if that price is different than what the user agreed to. Volatility shouldn't be pushed onto the user after they've made a decision. (edit: there are at least two kinds of surprises: 1) an increase in the number of units of execution over what was estimated, and 2) an increase in the cost of a single unit of execution. We need to make sure to handle both cases.) Instead, the price that they decide to pay should already reflect the potential for future volatility while their transaction is executing. This requires 1) the contract developer be accurate in their relative estimations, and 2) the contract creator be willing to cover the difference.

Counter Argument: making the user think about fees is a huge burden on them

This is unavoidable, unless the dapp developer or contract creator choose to cover all fees and handle anti-spam measures themselves or hide the fees in the costs of other assets. That seems incredibly difficult.

But, even within this model, it is possible for the contract creator to cover all fees, merely by not setting a fee per offer or setting the fee for a publicFacet method call to 0. This may be appropriate for a short-lived contract like a covered call, but more analysis is required.

Details: Translating RUN and chargeAccounts to metering [TODO: fill in]

A walk through the user experience of creating a vault with metering/fees:

https://github.com/Agoric/agoric-sdk/issues/3399#issuecomment-868107542

Incidental changes

  1. The RUN IssuerKit must be made within makeZoe and not within a contract on top of Zoe.
  2. zcf.makeInvitation needs to take an invitation config because adding a fee with expiration is too many parameters. (Note: we could continue with simply having more parameters if that is less of a breaking change.)

More detailed considerations and alternatives

See this Google doc for this Github issue's previous content, which was walking through the design from first principles and considering alternatives.

rowgraus commented 3 years ago

This feels like the right start for a good user facing approach.

A design constraint I'd like to try to add: can we mostly or fully eliminate the chance that a user pays for activity that doesn't result in an actual transaction? I.e., paying for a price quote on the AMM is a non-starter (though the solution in this case is to not require a roundtrip to the chain for a price quote, so we can handle that separately). For all its flaws, the Ethereum model mostly achieves this by having users sign transactions ahead of time and only paying if they get processed.

In discussing reducing roundtrips to the chain for performance reasons during the last testnet phase, we talked about an offer model where the wallet might present an offer to the user upfront for pre-approval, which seems to push in the same direction here.

katelynsills commented 3 years ago

A design constraint I'd like to try to add: can we mostly or fully eliminate the chance that a user pays for activity that doesn't result in an actual transaction? I.e., paying for a price quote on the AMM is a non-starter (though the solution in this case is to not require a roundtrip to the chain for a price quote, so we can handle that separately). For all its flaws, the Ethereum model mostly achieves this by having users sign transactions ahead of time and only paying if they get processed.

Let's talk this through, because the version above does require the user to pay for a price quote on the AMM, which requires a query sent to the chain, so that would be (part of) a transaction. The only ways I can see to avoid that are: 1) the dapp has a backend that does the query where the dapp developer pays for the transaction on the user's behalf, and the dapp requires the user to login such that it can cut off access if necessary, or 2) we create a mechanism to read the chain at the JavaScript level without creating transactions. Perhaps event logging could provide this, but we don't have anything like this now.

zarutian commented 3 years ago

A few questions:

  1. I assume the purse in const { purse, chargeAccount } = E(zoe).makeChargeAccount(); has the ERTP purse interface, correct?
  2. will be there a way for a smart contract instance to change which chargeAccount it is currently running under?
  3. Would that be expressed in a ?priority list? where the last chargeAccount gets charged first and if it is exhausted then the next to last one gets charged and so on?
  4. Is the price, in RUN, of each computron fixed?
  5. could chargeAccount have two methods .resolveWhenCurrentAmountIsLessThan(amount) and .resolveWhenCurrentAmountIsGreaterThan(amount) that each return a promise that get resolved when their condition is met? This would be usefull for notifying when the chargeAccount needs top up or has been sufficently been topped up.

That is all for now.

warner commented 3 years ago

Ideas from today's meeting (@katelynsills @dtribble @mhofman @warner):

The swingset support is:

Other notes:

warner commented 3 years ago

The current vatAdmin API is:

(the vatAdmin object is in its own vat, and all remote method invocations return a Promise, but it's worth pointing out that vat creation and termination will be delayed by more than just the inter-vat messaging queues)

In #3308 I'm proposing to augment that to:

@katelynsills would that be sufficient for the rest of the chargeAccount work to be implemented on the Zoe side? Zoe already holds the vatAdmin facet so it can create dynamic ZCF vats. This API would give Zoe complete control over the computron credits made available to all the vats it creates (in particular it makes Zoe responsible for any notion of scarcity or exchange rate). I expect we'll come up with a more refined model later (shaped more like ERTP, with computron-denominated purses and a more-closely-held Mint facet), but I'm betting this will be enough to get us started.

When Zoe creates a new ZCF vat, it would do:

const meter = await E(vatAdmin).createMeter({ capacity, notificationThreshold });
const { adminNode, root } = await E(vatAdmin).createVat(code, { meter });

then adminNode.notifier() gets you a Promise that fires when the capacity drops below the threshold, and Zoe can do something like:

async function react() {
  const computronsBought = await sellRUN(contractOwnerChargeAccount, RUNToSell);
  await E(meter).addCapacity(computronsBought);
}

And when the user code initiates a new action (claiming an offer or something), Zoe does something similar, but selling RUN from the user's chargeAccount instead of the contract owner's.

Let me know what you think, and @mhofman and I will get started on implementing the kernel-side pieces.

katelynsills commented 3 years ago

@warner, this sounds good to me. There may be hiccups when implementing that may necessitate some small changes but this sounds like a great place to start.

katelynsills commented 3 years ago

Some more thoughts on pricing:

The user and the contract creator have opposing desires regarding the timing of fee menus. For instance, the user would like to know the cost of making an offer with a particular invitation, as far ahead of time as possible. Ideally for the user, the fee would be immutable and in the invitation's details along with the instance and installation.

This is the opposite of what is good for the contract creator. With potentially fluctuating prices for computrons, and with potentially fluctuating computrons per offerHandler (for instance, a particular offer sets off more processing within the contract), the contract creator desires to put off pricing as late as possible.

This makes sense if we view the quoted fee as an option. Options over longer periods of time are more costly for the entity offering the option, because that entity is taking on more risk and giving up more opportunities.

So where and when should Zoe require quotes for fees (menus)? Here are some possibilities:

1) requiring the contract creator to quote the fee when the contract code calls zcf.makeInvitation. This quote is good forever more.

We can throw this possibility out. Invitations might be held for years, during which the costs to the contract developer might change drastically.

2) The above but with some sort of deadline after which the quoted price (and invitation in general?) is no good. 3) The above but the contract creator sends in a getFee function to zcf.makeInvitation. The price is entirely dynamic.

This is unacceptable from the user's perspective, for a number of reasons. First, maybe they had to pay some money to get the invitation in the first place, and they probably can't get a quote ahead of time for how much using the invitation would cost them. Now the contract creator can charge exorbitant prices for using the invitation, and the user has two bad choices: walk away having spent money on the invitation, or go forward paying even more fees. Second, if the fee function is entirely controlled by the contract creator, it's not clear what the user is agreeing to. Let's say the user queries for the current menu of prices, gets a price, and then makes an offer. In the meantime, the contract creator jacks up the prices, and the user's chargeAccount is charged the much higher price.

Side note: a great attack from the perspective of the contract creator against users would be to take something like the Vault, and make it cheap for users to escrow their collateral, and then dynamically make it prohibitively expensive for them to withdraw it, and take their collateral or wait for it to liquidate.

4) Users specify the fee they are willing to pay and the contract code can reject it.

This follows the pattern of offers. Users are free to make whatever offers they want, and the contract code is free to reject it by exiting the seat immediately. Users are assisted in putting their offer together by dapps. The downside is that there is no ergonomic way to specify the fee you are willing to pay, when calling methods on the publicFacet. This only really works for offers, and if you're specifying the fee, you might as well send a payment directly rather than a chargeAccount.

5) We reify the "quote" such that the user actually gets a price and a deadline for how long it's good. This gives the contract creator and the user maximum flexibility. The contract creator can make the deadline short or long, and the user can know that the quote is 100% good until the deadline. The downside is that it seems like it would require creating a lot of objects.

katelynsills commented 3 years ago

Another potential attack: if fees are charged no matter what, and fees are high, a contract creator could trick users into paying fees for an opportunity that doesn't actually exist. For example, the contract creator would present something that looks like great opportunity, such as token sold at a low price, get users to make an offer, then subtly exit the seat so that the user gets their original allocation, but is charged the fee. In this case, offer safety still holds, but does not include a refund of the fee. For the attack to work, the fact that the seat is immediately exited will have to be obscured somehow. Our usage of installations with petnames might mitigate this somewhat.

zarutian commented 3 years ago

Hmm... if computrons was its own currency (fungible ERTP right) then its fluctuating price in RUN would not be an issue, I hold forth. Puts me in mind of those postage stamps measured in grams/kilometers or just standard letter prepaid-ness. (Buy such a stamp and use it years later to send a letter, even if the postage cost is higher in USD the letter is dilvered)

However, the problem of exercise of an invitation or use needing more computron fuel to burn due to more activity for that use, is bit harder to solve.

dckc commented 3 years ago

@nathan-at-least asked What are the costs for using the network and who pays them? Are they transaction fees? As a contract developer, are there issues I need to be aware of around these costs? Do I need to consider optimization at the JS source code level?I think this issue is most relevant to the question, but other metering issues are likely to be relevant as well.

One perspective I learned in a meeting last week is that fees come in roughly two kinds:

  1. fees to protect the chain against contracts
  2. fees to protect contracts against users / clients

Contracts run on a meter, and when the meter runs out, they stop computing. (Whether this is a fatal or recoverable error is TBD - someone could perhaps refill their meter to allow them to resume.) Contracts can choose how much of this cost to pass on to users / clients vs. using, for example, auction fees.

Yes, JS optimization is relevant. Execution fees are intended to incentivize efficient computation. The JavaScript engine we use, XS, is instrumented to meter every step in a JavaScript computation.

Note that while the chain must be protected from runaway computation and from spam / griefing, we don't intend that execution fees are the primary incentive for validators / stakers. Stakers should get rewarded for value produced rather than for labor spent. Hm... our Economy & Network page doesn't tell that story as well as I'd like. I think Dean's Dec 2020 Pillar Series: Public Chain and Economy talk is better.

We aim to use the escalator algorithm from Drexler and Miller 1988. It allows clients to pay for priority. #3530 is scoped more precisely to escalators.

katelynsills commented 3 years ago

Here's the documentation we have on fees and metering: https://agoric.com/documentation/zoe/api/fees-and-metering.html

nathan-at-least commented 3 years ago

Thanks. This is very helpful!