This repository has been archived by the owner on Nov 15, 2023. It is now read-only.
Deposit based storage incentivation for pallet_contracts
#9807
Labels
J0-enhancement
An additional feature request.
Z4-involved
Can be fixed by an expert coder with good knowledge of the codebase.
Discussed in #9740
Motivation
Up until recently
pallet_contracts
did prevent unbound state growth by charging rent or a deposit from the contract itself. This system was removed for various reasons: #9669. Read the linked PR description before engaging in this discussion. We want a replacement for that system. In the following I describe one idea at a high level and then go into detail for the various areas where questions might arise.Overview
One rather obvious alternative to charging the contract for its own storage is to charge the caller that is responsible for creating this storage. This is distinct from and in addition to gas whose purpose it is to charge for execution time. Using ongoing payments (rent) is not really viable so it would be purely deposit based like in any other pallet: Calling a contract will transfer balance from the caller to the contract or vise versa depending on whether the call increased or decreased the storage usage. The balance is reserved in the contract's account so it cannot be used or moved away by the contract. The
deposit
made by the caller is calculated like this:Note that
deposit
can be negative which constitutes a refund from the contract the caller who is removing this storage. This is not necessarily the original depositor.One major criticism of this approach when compared with the contract based rent is that it makes contract providers inflexible with regard to their financing model because the storage is always payed by the caller. In theory, the old system allowed the contract authors to come up with their own financing model by for example pumping their own money into the contract to keep it afloat. In practice however, there are still gas costs that need to be payed by the caller so it wasn't never enough to allow fee less usage of contracts while making contract (language) development much harder.
I argue that the financing model of a contract should not live in the contract itself but should be provided by other means. This allows contract authors to concentrate on the business logic. Companies could provide proxy contracts to customers that are restricted in what they can do.
Implementation Details
This is a rather simple system from the distance but there are challenges to solve in some areas.
Contract Termination
A contract can decide to remove itself by calling the
seal_terminate
host function. As of right now a contract can call this function at any time in order to remove itself and all its associated storage.This function will continue to work mostly like it did. It will do the following when used by a contract:
beneficiary
(this is unchanged)tl;dr:
seal_terminate
will work fine. Free balance sent tobeneficiary
, storage deposits to the caller.Code sharing
There are two kinds of storage whose size is controlled by users:
instantiate_with_code
extrinsic and can be shared between different contracts: A contract can be instantiated without uploading any new code to the chain. Instead, it can reference an existing code by hash.The latter is what this section is about. The code can be shared between different contract instantiations. Questions arise around the removal of those contracts. We clearly want to allow the removal of those contracts for uploaders to regain their deposit. There are two distinct challenges with regard to code sharing:
Code blobs cannot be removed due to active users
We cannot remove code blobs which have contracts associated with them for obvious reasons. To enforce this invariant we have a reference count associated with each code blob. However a problem arises when 3rd parties start using a code blob uploaded by someone else. The uploader cannot delete the code to regain the deposit because of contracts it doesn't control.
The solution to this problem would be for contract authors to deny contract creation by entities they don't control. This can be done easily in the constructor of the contract. IMHO this is an elegant solution because it does not require baking in any logic into
pallet_contracts
. The drawback with this approach is that the default behavior would be to allow instantiation by anyone. However, that could be easily solved by contract languages that force authors to make an explicit decision in constructors. This does require any changes whatsoever to thepallet_contracts
.tl;dr: We do nothing and tell contract authors to be wary about who they allow to instantiate their code blob. Also we always refund the deposit to the original uploader and not to the remover (this is different from contract storage).
Race between upload and instantiation
Right now there is no way to "just upload" a code blob. You need to call
instantiate_with_code
which instantiates the first contract right away. The code blob is automatically deleted when the last contract that uses it is removed. This is an elegant solution because it does not require a separate extrinsic to remove an orphaned code blob.However, there are requests for adding an extrinsic that can upload a code blob without an associated contract (patractlabs/redspot#136). That causes a race: Someone could delete the code hash in between the upload and instantiation.
To resolve this situation we only allow the original uploader of a code blob to remove it. The uploader has an incentive to remove it to get the deposit back. This means that code is no longer removed automatically when the refcount drops to zero but require the submission of a
remove_code
transaction that checks whether the refcount is zero and that the submitter is equal to the original uploader.Whenever some account uploads a code some balance gets reserved at their account (depending on the code size). This reserve is released when they delete the code. This is different from balance that is used for contract storage which is transferred to the contract and reserved there. This is because this balance changes hands when someone else removed the storage which is not possible for code blobs where the remover must be the original uploader.
tl;dr: We bring pack
upload_code
andremove_code
. The latter can only be called by the original uploader.User experience
With this change a user would pay for two distinct things when calling a contract:
The gas is is taken from the caller as transaction fee exactly like with any other transaction. The caller can limit the amount of gas that can be used by a call using a
gas_limit
.For the storage a deposit is made to the contract and reserved there. Both values can be estimated by pre-running the call as RPC before submitting it as a transaction. It is called an estimation because between pre-running and transaction submission the state of the chain can change and with it the behavior of the call (this is why we have
gas_limit
).In order to allow the same for storage we add a
storage_limit
field which limits how much balance is allowed to be deposited as part of the call.tl;dr We add a
storage_limit
to the call arguments and astorage_deposit
to the call RPC result. The latter one should be used by UIs together withgas_used
to give the user a cost estimation. Note thatdeposit_used
can be negative (refund).Alternative Solutions
State Expiry
One noteable solution to unbounded storage growth is something called state expiry 1 2 3 4. The tl;dr is that full nodes / collators only need to hold state for a fixed period of time. After that the caller / transactor is required to provide a witness for transactions that touch older states.
This is the solution which Ethereum plans to use for solving their storage growth issues. However, state expiry merely mitigates the long term consequences of wasteful behavior without discouraging it. Whether state expiry will work out is not a clear cut and might further away than we like because it requires changes to the substrate storage layer. The proposal here can be implemented relatively quickly and can be removed if it turns out too be to big of a hurdle. I am not so sure that there will be acceptance to add a deposit system into an active contract ecosystem when state expiry won't suffice. On the other hand, state expiry can be added on top at a later time without breaking existing contracts. I speculate that this is the main reason why Ethereum opts for such a solution rather than incentivizing good behavior from contracts.
The text was updated successfully, but these errors were encountered: