near / NEPs

The Near Enhancement Proposals repository
https://nomicon.io
217 stars 140 forks source link

[DISCUSSION] Account extensions #478

Open firatNEAR opened 1 year ago

firatNEAR commented 1 year ago

The main idea for this proposal is to increase the usability of the network by introducing new features that decrease the barrier of entry for end-users.

TLDR: allow accounts to have more than one contract on them that can communicate in sync, where users/developers can subscribe to contracts by deploying just the hash of a smart contract instead of full binary to different namespaces on the account.

Motivation

Currently we are faced with multiple problems:

Composing contracts is non-trivial

As an end-user, who doesn’t know how to write smart-contracts, there is no way to compose multiple contracts and deploy them onto your account. Why is this relevant? Imagine a case where a user wants to deploy a multi-sig contract on their account but also wants to have the dead man switch at the same time on the account. Currently there is no way that they can do this without either learning intricacies of smart-contract development or asking the community to develop a smart contract.

The contract upgrade workflow would also be improved by the introduction of composable contracts: an upgrade component could control the upgrades of individual components instead of the entire contract (which risks bricking an account if a bad contract is deployed).

Even if we compose contracts, storage staking for the contracts is economically infeasible for certain users.

Let’s assume that the end-user found a way to compose these contracts manually. The multi-sig core contract currently is 333kB, which costs ~3N in storage staking. If we keep composing contracts their size will grow and the cost of storage staking for the contract will also keep increasing. For some users, it is not feasible to keep this amount of NEAR on their accounts just to achieve these kinds of basic functionalities.

Certain contracts are deployed over and over again, still costing the same for each user

Certain dApps require a ‘proxy’ contract to fully function due to the fact that there is no way of ‘acting on behalf of` another account without actually having a contract deployed on the account. Thus, dApps, such as Keypom, deploy the same contract on each of their users’ accounts. This means a huge amount of cost to the developers/businesses or a huge amount of cost on the end-users based on how the developers decide to put the costs on.

There is no way for contracts to call each other synchronously

Due to the asynchronous nature of NEAR protocol, even if the accounts are on the same shard, all cross contract interactions are async, which limits certain use-cases such as ‘flash loans’.

Proposal

The current proposal combines multiple sub-solutions that would address all of the problems that are mentioned in the Motivation section. Combination of these solutions would be called Account Extensions.

Account Namespaces: Current proposal with multiple solutions https://gov.near.org/t/proposal-account-extensions-contract-namespaces/34227

Contract Subscriptions: A “contract subscription” allows an account to deploy just the hash of a smart contract instead of the full binary. This, in conjunction with namespaces, would allow a user to pick and choose a set of account features to deploy with negligible gas and storage costs.

Example usage:

Wallet providers contain an “Add account extensions” section containing the following list:

Each option has a checkbox beside it. Users may select any combination of features, input minimal configuration details (e.g. list of account IDs and minimum approvals for multisig) and click “Save” to deploy the correct set of namespaced contracts on their account, without ever needing to touch any code or leave the wallet interface.

Sync execution: Namespaced contracts on the same account should be able to communicate with each other synchronously instead of asynchronously.

Permissions: An end-user subscribing to a contract would set permissions on a particular namespace. For example, restricting the namespace dao_multisig to interactions with dao.near only.

Open questions

ilblackdragon commented 1 year ago

Additional consideration as account extensions are designed is to also introduce on receipt creation and receival hooks.

Right now receipt receival just happens without any action on the receiving account. The only way to do something on receival is to implement ft_transfer_call type of calls but in the case of account extensions there are new vectors open up. There is also a way to define which function can key call but to define a specific set of actions one needs a proxy contract on the account right now.

A receipt issuance and receival hooks can allow to implement things like:

bowenwang1996 commented 1 year ago

Right now receipt receival just happens without any action on the receiving account. The only way to do something on receival is to implement ft_transfer_call type of calls but in the case of account extensions there are new vectors open up.

@ilblackdragon could you explain how account extension would enable receival hooks?

ilblackdragon commented 1 year ago

@bowenwang1996 I'm just suggesting to consider this as additional use case.

Given the account model is getting extended, for example, extra field where hooks are configured can be stored.

DavidM-D commented 1 year ago

I'd like to propose instead of this being a protocol feature we prototype this using a capabilities contract and only move over to protocol when we have discovered clear limits to this approach. This project is many months of protocol work, substantially & irreversibly complicates the programming model and may not be what our users need in the end.

Capabilities contract

An example of a possible capabilities contract is a contract initialized with state

Controllers: Account ID => [ Action Type ]

And the endpoint

take_actions: [ Action ] -> ()
take_actions actions =
  # What actions is this contract allowed to do
  allowed_actions = Controllers.get(predecessor_id)
  for (a in actions)
    if allowed_actions.includes(a)
      match a
        CreateAccount(createAccountAction) => create_account(createAccountAction),
        DeployContract(deployContractAction) => deploy_contract(deployContractAction),
        FunctionCall(functionCallAction) => function_call(functionCallAction),
        ...

This allows for contract composition based on the state of Controllers. An example initial configuration would be:

{
  "multisig.near": [AddKey],
  "neth.near": [FunctionCall, Stake],
  "dead_man.near": [DeleteAccount],
}

So how well does this satisfy the motivations for the protocol change?

Composing contracts is non-trivial

This solves that. We can just as easily wrap this up and expose it through a wallet. It has the same security model as account extensions in that you have to trust that any contracts you give control of your account to.

Even if we compose contracts, storage staking for the contracts is economically infeasible for certain users.

This can be a very small contract, likely 5-10x the size of an existing zero balance account. The per user cost doesn't grow with the complexity of the controlling contracts.

Certain contracts are deployed over and over again, still costing the same for each user

This is a problem, the nodes can trivially memoize the contract code but they will still over-charge the account creators for storage, but this is a big protocol change to fix a relatively small economic problem.

I'd like to first wait for there to be a demonstrable need for this saving, say a contract that has spent 10,000 NEAR being deployed to many different accounts. This spending can then be stemmed with a hack if popular_contract then don't charge followed by a more general rule.

There is no way for contracts to call each other synchronously

We don't solve this, but I'm not sure we want to.

I'd like to see a much clearer use case for this feature as it's a dramatic digression from our existing programming model. I have a few questions:

There might be an argument for not prohibiting (although not guaranteeing) that cross contract calls on the same shard can run on the same block. It's almost invisible to application developers and very lightweight.

Solutions to Open questions

akhi3030 commented 1 year ago

Some questions:

DavidM-D commented 1 year ago

The capability contract would be one per user. Multisig + NETH would be one per network/shard depending on usage If we did compose them all into a single contract, people wouldn't be able to write new contracts that control accounts in new and interesting ways. We could slim the capabilities contract down by moving much of it's logic into another contract though, but it probably saves minimal space while adding an extra hop and therefore latency.

akhi3030 commented 1 year ago

If we did compose them all into a single contract, people wouldn't be able to write new contracts that control accounts in new and interesting ways.

I suppose, I am thinking that many people will not want to do that and just want to use the functionality out of the box as is. So it might be good enough for them. For the more advanced users, I agree that a more flexible solution would be needed.

encody commented 1 year ago

@DavidM-D I agree: it does complicate the programming model. However, I don't think the alternative contract-layer solution solves the same set of problems that the protocol-layer solution does. In particular: storage sharding (all storage relating to a single application stays on that contract's account and is not sharded), zero-cost (or near-zero-cost) contract deployments (the proposed "capabilities contract" will have a non-zero size (and non-zero deployment cost); even some of the smallest contracts in the ecosystem still weigh a few kilobytes).

Contract Composition

Although the capabilities contract does work for the use-cases where a contract may wish to perform actions on behalf of a user (push actions), it does not allow for those contracts to receive interactions. There is no way to "compose" an NFT contract, for example, using this model.

Storage

Perhaps the full intent is not clear: what if Metamask users were able to transition to using a native NEAR account (controlled via neth) for free or nearly free: at the very least, without needing to separately purchase mainnet NEAR tokens (a la KeyPom trial accounts). If we imagine scaling this solution to, say, a billion users, a cost of even ~0.1 NEAR (10kb contract) / user becomes prohibitive.

It would also be nice if mass-data contracts (like social.near) were able to easily distribute their data across multiple accounts, especially so that once dynamic resharding is active, the load is easier to spread over multiple shards.

Repetitive Deployments

I believe @BenKurrek has firsthand experience with spending large amounts of NEAR deploying the same contract over and over, through the development of KeyPom.

Synchronous Execution

The synchronous execution model proposed by NEPs #480 and #481 is not perfect, but is also limited in such a way as to not change the fundamentally asynchronous nature of NEAR. It allows for highly sandboxed private submodules to be deployed under namespaces. Submodules can only be invoked synchronously (with gas limit) by a non-sandboxed module on the account. They do not have access to any VM host functions besides those needed to communicate with the module that invoked it. This model, though highly restricted, would allow for the possibility of deploying arbitrary foreign code as a synchronous submodule on another account, which could (primitively) simulate synchronous cross-contract execution.

This synchronous execution model is not terribly flexible, and probably will not be useful to the majority of projects. However, it is not a large addition (probably, given the PoC implementation in #481) if namespaced contracts are also implemented. @firatNEAR might be able to speak on this point in more detail.

DavidM-D commented 1 year ago

I'm going to move the discussion on Synchronous Execution over to #481 as it seems to be a more appropriate place.

So I understand that there is cost associated with deploying contracts. There are two sides to that, one is the dollar cost and the other is the on-boarding cost. The on-boarding cost is largely solved using relayers provided we can fix the faucet draining attacks.

This solution does have a higher cost per user (probably about 0.1 NEAR per user), but I assert that cost is outweighed by the benefit of having something we can release quickly and more importantly iterate on based on user feedback. Since the on node resource usage of a replicated contract is small, if 1 billion users show up on a Tuesday we can tweak the costs to reflect the resource usage on a dime.