Skip to content
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

Closed
athei opened this issue Sep 17, 2021 Discussed in #9740 · 0 comments · Fixed by #10082
Closed

Deposit based storage incentivation for pallet_contracts #9807

athei opened this issue Sep 17, 2021 Discussed in #9740 · 0 comments · Fixed by #10082
Assignees
Labels
J0-enhancement An additional feature request. Z4-involved Can be fixed by an expert coder with good knowledge of the codebase.

Comments

@athei
Copy link
Member

athei commented Sep 17, 2021

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:

deposit = storage_used_before - storage_used_after

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:

  • The free balance is transferred to the specified beneficiary (this is unchanged)
  • The storage deposits that are stored as reserved balance in the contract are transferred to the caller.

tl;dr: seal_terminate will work fine. Free balance sent to beneficiary, storage deposits to the caller.

Code sharing

There are two kinds of storage whose size is controlled by users:

  • The key value storage of a contract that can only be accessed by the contract it belongs to. This storage is created during contract execution.
  • The contract code which is a wasm blob that is uploaded to chain using the 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 the pallet_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 and remove_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:

  • Gas (weight / execution time)
  • Storage

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 a storage_deposit to the call RPC result. The latter one should be used by UIs together with gas_used to give the user a cost estimation. Note that deposit_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.

@athei athei added this to To Do (Ordered by priority) in Smart Contracts via automation Sep 17, 2021
@athei athei added J0-enhancement An additional feature request. U1-asap No need to stop dead in your tracks, however issue should be addressed as soon as possible. Z4-involved Can be fixed by an expert coder with good knowledge of the codebase. and removed U1-asap No need to stop dead in your tracks, however issue should be addressed as soon as possible. labels Sep 17, 2021
@athei athei self-assigned this Sep 17, 2021
@athei athei moved this from To Do (Ordered by priority) to In Progress in Smart Contracts Sep 17, 2021
Smart Contracts automation moved this from In Progress to Done Dec 7, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
J0-enhancement An additional feature request. Z4-involved Can be fixed by an expert coder with good knowledge of the codebase.
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

1 participant