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

EPIC: Zoe Design Doc - Paying for Metering #3294

Open
katelynsills opened this issue Jun 11, 2021 · 12 comments
Open

EPIC: Zoe Design Doc - Paying for Metering #3294

katelynsills opened this issue Jun 11, 2021 · 12 comments
Labels
enhancement New feature or request Epic metering charging for execution (was: package: tame-metering and transform-metering) Zoe package: Zoe

Comments

@katelynsills
Copy link
Contributor

katelynsills commented Jun 11, 2021

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

  • Contract developer: the person who writes the code of a smart contract and deploys it using E(zoe).install(...).
  • Contract creator: the person who starts an instance of a smart contract through E(zoe).startInstance(...)
  • User: the person who calls E(zoe).offer(...) with an invitation to this contract
  • Dapp developer: the person who makes the front-end that connects to an on-chain contract and sends suggested offers to the user's wallet
  • Computron: A unit of execution. XS gives us a computron measure, and we plan to use this directly [TODO: add more information about how XS calculates this].
  • Spam: unwanted usage with uncovered costs

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.
    Add feePurse #3309
    Add feePurse arg to Zoe methods. Also partially apply the feePurse to produce original Zoe API #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 Zoe Menu Design #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]

  • One meter per ZCFVat
  • Zoe refills the meter by minting computron units after charging RUN
  • The RUN/computron price is passed to makeZoe as an argument, and is static for now.

Kernel implementation

#3508

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

#3399 (comment)

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.

@katelynsills katelynsills added the enhancement New feature or request label Jun 11, 2021
@katelynsills katelynsills self-assigned this Jun 11, 2021
@rowgraus
Copy link

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
Copy link
Contributor Author

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
Copy link
Contributor

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
Copy link
Member

warner commented Jun 12, 2021

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

  • Every economic-action object (e.g. Offer, Invitation) has a price.
    • The price is declared by the contract when it creates this object, and delivered next to it (maybe as auxdata of the object?). The price is determined by code written by the original Contract Developer, as parameterized by the Contract Creator. This code may incorporate current exchange rates and contract state (e.g. queue depth).
    • The price is fixed for a given Offer/Invitation. If circumstances change and the contract no longer wishes to honor that price, the offer becomes invalid. But once the offer/invitation is claimed successfully, the price is fixed.
  • The user, or more likely their wallet/user-agent, can look at the declared price and decide whether they're willing to pay it or not. The wallet might automatically accept price below some user-configured threshold.
  • If they agree, they tell Zoe they're claiming/accepting the Offer/Invitation, and provide a chargeAccount to cover the price. This chargeAccount is likely to be long-lived, serves a single user, refundable, denominated in RUN, and synchronously owned by Zoe.
  • Zoe will deduct exactly the declared price from the user's chargeAccount, and increment the ZCF contract vat's meter by the corresponding number of computrons (sale/price mechanism TBD). Then Zoe will deliver the claim/accept message to the ZCF vat.
  • The Contract Creator also establishes their own chargeAccount as a backstop for the ZCF contract vat, provided as an argument to startInstance(). This is also owned by Zoe, and behaves just like the user's chargeAccount, except for the auto-reload feature described below.
  • The ZCF vat runs on a single meter. This meter has a balance and a reload theshold.
    • Each swingset delivery is allowed to use the full meter balance.
    • The balance is decremented by the consumed computron usage at the end of each crank.
    • If the balance drops below zero, the vat is terminated.
    • If the balance drops below the reload threshold, the kernel invokes vatAdmin, which can fire a Notifier, which notifies Zoe.
    • Zoe reacts by transferring some pre-configured amount from the Contract Creator's chargeAccount into the contract vat meter, if possible

The swingset support is:

  • the kernel maintains a table of meters, indexed by an integer, each with a balance and a notification threshold (both in computrons)
  • each dynamic vat points to a meter index (unit tests might define unmetered dynamic vats, but userspace should probably not have that option)
  • each delivery to a metered vat uses the current meter balance as a worker limit (possibly the min() of the current balance and some fixed safety limit)
  • each successful delivery decrements the meter balance by the usage report
    • the meter should still be above zero, otherwise the delivery would have failed with a metering fault
    • if the meter is below the notification threshold, queue a message to vatAdmin
  • let vatAdmin create meters, manipulate their balances, provide balance access and underflow notification to subscribers, and assign a single meter to each new dynamic vat

Other notes:

  • in addition to these meters (for expensive operations), each message delivery everywhere should consume a "stamp": a fixed-price token that must be provided by the message sender. This should increment the receiving vat's compute meter by some fixed amount. All vat code can safely execute up to this fixed amount of work for each delivery without fear of running out of funds, even if an attacker spams them with junk messages.
  • Contract Creators manage their chargeAccounts (which might be shared among multiple contracts) and the reload thresholds / amounts. We're thinking meters are non-refundable, so they won't want to auto-reload too much or too early, else they might spend more RUN than they want. But if they set the threshold too low, or the reload amount too low, they run a higher risk of having their contract vat be terminated, which is bad for business.
  • subcontracting: contract A might use contract B, and contract B's offers have their own price. One example is a Vault delegating to a Liquidation, which itself delegates to an AMM. If one contract decides to liquidate, it gets a sell offer from the AMM, which includes an execution price. The contract must decide whether this price is worth it or not (e.g. if the price is anything less than the expected proceeds from the sale), and provide Zoe with a chargeAccount (probably same one used to auto-reload the contract's vat itself) to cover it.
  • billable facets: to cover the costs of non-contract services (like a price oracle), Zoe could wrap these objects with a chargeAccount-billing facet. Callers would pay for invocation just like interacting with a contract.
  • proxies: the contract decides a price and gives objects to Zoe, but never gets access to the user's chargeAccount. Zoe has access to the chargeAccount and gives wrapped objects to the user (to their wallet), so the user cannot talk to the contract directly (this gives the contract confidence that its expensive-to-run methods will not be invoked without zoe first depositing the fee in its meter). The wallet produces a wrapped offer for the dapp. The dapp never sees the user's chargeAccount. The dapp can propose methods to invoke on the offer, but the user (or their wallet/user-agent) must approve them, and provide the funds.

@warner
Copy link
Member

warner commented Jun 13, 2021

The current vatAdmin API is:

  • typedef dynamicOptions: { description, metered, managerType, vatParameters, enableSetup, enablePipelining, virutalObjectCacheSize, useTranscript }
  • typedef adminNode: { terminateWithFailure(reason) => undefined, adminData() => stats, done() => Promise<reason> }
  • createVat(code, dynamicOptions) => Promise<{ adminNode, root }>
  • createVatByName(bundleName, dynamicOptions) => Promise<{ adminNode, root }>

(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:

  • typedef Computrons: Nat
  • typedef Meter: { addCapacity(Computrons) => Computrons, setCapacity(Computrons) => undefined, getCapacity() => Computrons, setNotificationThreshold(Computrons) => undefined, getNotificationThreshold() => Computrons, getNotifier() => Notifier<Computrons> }
  • define the following new method on vatAdmin:
    • createMeter({ capacity?: Computrons, notificationThreshold?: Computrons }) => Meter
  • add meter: Meter? to dynamicOptions
  • add meter: Meter? to adminNode (maybe, not sure it's important/useful)

@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
Copy link
Contributor Author

@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
Copy link
Contributor Author

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.

  1. The above but with some sort of deadline after which the quoted price (and invitation in general?) is no good.
  2. 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.

  1. 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.

  1. 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
Copy link
Contributor Author

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
Copy link
Contributor

zarutian commented Jun 22, 2021

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
Copy link
Member

dckc commented Oct 26, 2021

@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
Copy link
Contributor Author

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

@nathan-at-least
Copy link

Thanks. This is very helpful!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request Epic metering charging for execution (was: package: tame-metering and transform-metering) Zoe package: Zoe
Projects
None yet
Development

No branches or pull requests

7 participants