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

[DISCUSSION] Account extensions #478

Open
firatNEAR opened this issue Apr 26, 2023 · 9 comments
Open

[DISCUSSION] Account extensions #478

firatNEAR opened this issue Apr 26, 2023 · 9 comments
Labels
WG-protocol Protocol Standards Work Group should be accountable

Comments

@firatNEAR
Copy link

firatNEAR commented Apr 26, 2023

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.
  • Even if we compose contracts, storage staking for the contracts is economically infeasible for certain users.
  • Certain contracts are deployed over and over again, still costing the same for each user.
  • There is no way of acting behalf of another account in the current design.
  • There is no way for contracts to call each other synchronously.

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:

  • Multisig
  • Dead man switch
  • Neth controller
  • Lockup contract
  • NFT
  • FT
  • Multi-token

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

  • We need to have the subscribed contract wasm at the time of the execution. If we are not storing this on the account state, where do we get it? Do we store it on a special account? A special shard that every validator tracks? Or do we get with the function call?
  • Can contracts on different namespaces access each others' states?
  • To what extend the permissions be defined? Action level? Namespace level? Account level?
  • The economics of 'contract subscriptions'.
  • Backwards compatibility of namespaces with current account structure.
@firatNEAR firatNEAR added the WG-protocol Protocol Standards Work Group should be accountable label Apr 26, 2023
@ilblackdragon
Copy link
Member

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:

  • block receival of not interesting assets or from non whitelisted/black listed accounts
  • prevent account from transferring out specific types of assets (eg borrowed NFTs)
  • Specify usage limits on the account

@bowenwang1996
Copy link
Collaborator

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

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

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:

  • How do you do a flash loan if account extension contracts don't share state?
  • If they do share state how do you do actions synchronously, do you have to lock the contract across shards?
  • Is there significant demand for this feature outside of Aurora who already have a solution?
  • Do you have a single gas limit for all synchronous calls combined and how do contracts stay within that when they don't know how much their predecessor/successors will use?

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

  • We need to have the subscribed contract wasm at the time of the execution. If we are not storing this on the account state, where do we get it? Do we store it on a special account? A special shard that every validator tracks? Or do we get with the function call?
    Just store normal contracts

  • Can contracts on different namespaces access each others' states?
    Only to the extent that existing contracts can interact with one another

  • To what extend the permissions be defined? Action level? Namespace level? Account level?
    People can deploy proxy contracts with arbitrary rules and decide what works best for them.

  • The economics of 'contract subscriptions'.
    Each controlling contract can decide.

  • Backwards compatibility of namespaces with current account structure.
    Completely backwards compatible, because we don't change anything

@akhi3030
Copy link
Contributor

Some questions:

  • Is there a single one of this capability contract on the network or one per user?
  • what about the other contracts, e.g. multisig and neth? Is there a single one or one per user?
  • If there are one per user for the above contracts, can we not compose all of them into a single contract?

@DavidM-D
Copy link
Contributor

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

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

encody commented Jun 13, 2023

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
WG-protocol Protocol Standards Work Group should be accountable
Projects
None yet
Development

No branches or pull requests

6 participants