Open katelynsills opened 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.
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.
A few questions:
const { purse, chargeAccount } = E(zoe).makeChargeAccount();
has the ERTP purse interface, correct?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.
Ideas from today's meeting (@katelynsills @dtribble @mhofman @warner):
startInstance()
. This is also owned by Zoe, and behaves just like the user's chargeAccount, except for the auto-reload feature described below.The swingset support is:
min()
of the current balance and some fixed safety limit)Other notes:
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> }
vatAdmin
:
createMeter({ capacity?: Computrons, notificationThreshold?: Computrons }) => Meter
meter: Meter?
to dynamicOptions
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.
@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.
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.
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.
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.
@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:
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.
Here's the documentation we have on fees and metering: https://agoric.com/documentation/zoe/api/fees-and-metering.html
Thanks. This is very helpful!
For the purposes of this document, let's define:
E(zoe).install(...)
.E(zoe).startInstance(...)
E(zoe).offer(...)
with an invitation to this contractSummary
(Note: Items have been marked as IN-SCOPE or OUT-OF-SCOPE for the metering testnet phase.)
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/3322An 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]
makeZoe
as an argument, and is static for now.Kernel implementation
https://github.com/Agoric/agoric-sdk/pull/3508
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
makeZoe
and not within a contract on top of Zoe.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.