Account extensions
This proposal is the first in a series of upgrades to the NEAR Protocol known as “Account Extensions.”
The goals of Account Extensions are:
- Lowering the financial barrier to entry for new NEAR users.
- Making it easier for non-developers to deploy smart contracts on their accounts (e.g. multisig wallets).
- Making it cheaper for developers to deploy the same contract multiple times (e.g. factory contracts).
This proposal is the first step towards those goals.
Problem statement
Currently, it is possible to deploy a single smart contract to a NEAR account using a DEPLOY_CONTRACT
action. Subsequent DEPLOY_CONTRACT
actions completely replace existing code with new code, while leaving all storage intact. Contracts deployed on locked accounts (those with no full access keys deployed) therefore must implement an upgrade mechanism or remain static forever.
If a user wishes to create a smart contract that implements an NEP standard, the user must either write an implementation of the standard, or hope that one already exists that the user can use. When multiple standards are involved (e.g. NEP-141, NEP-145, NEP-148, NEP-197, NEP-330) in the same contract, plus some custom behaviors, this can mean that the user must effectively take on the role of developer to integrate everything together properly.
Composable contracts proposal
“Account Namespaces,” or, alternatively, “Composable Contracts,” attempts to solve this problem by implementing contract namespaces on each account. Whereas accounts currently have a 1:0…1 relationship with contracts, namespaces would expand that relationship to 1:*.
For example, this would allow alice.near
to deploy a multisig smart contract to alice.near:multisig
, and later deploy a dead man switch contract to alice.near:deadmanswitch
. Both contracts would be available for execution at the same time. Their storage would be separate.
This allows for developers to release “composable contracts” or “contract modules”: smart contracts that can be easily deployed to a namespace on an account without necessarily affecting the functionality of other smart contracts already deployed on that account. Contract modules would be attractive to non-developer users who wish to add more advanced features to their accounts without writing code.
This also allows for smart contracts to self-mutate one piece at a time, rather than all at once.
Terms & Notation
We will use the notation alice.near:multisig
to mean “the multisig
namespace on the account alice.near
.”
If a contract is not deployed to a specific namespace, we say it has been deployed to the “empty” or “default” namespace. This is the current status of all smart contracts on mainnet.
We will use the term “namespaced contract” to refer to contracts that are deployed to namespaces other than the default namespace.
“Legacy contracts” shall refer to contracts that are unaware of namespaces. This is the current status of all smart contracts on mainnet.
Implementation details
The DEPLOY_CONTRACT
action will be modified to include an optional namespace
field. The code attached to the action will be deployed to the specified namespace, replacing any existing code. Storage would remain untouched. If the namespace
field is missing or is the empty string ””
, the contract is deployed to the default namespace.
This feature faces a few important questions before it can be implemented:
- How do namespaced contracts interact with other contracts?
- How do other actors (users or contracts) interact with namespaced contracts?
The answers to these questions dictate the specific design decisions that will affect this feature.
The team at Pagoda has been discussing this proposal for some time, and has come up with two different routes for implementation:
Explicit namespaces
This route involves modifying the FUNCTION_CALL
action to include a new, optional namespace
field, which, if specified, will direct the call to the contract stored under that namespace on the receiving account. If the namespace
field is missing, the function call will fall back to the default namespace.
(Instead of introducing a new field, the namespace could equivalently be added to the receiver account ID field, however, this would probably break all existing contracts that use near_sdk::AccountId
, since the account ID / namespace separator character must be one that is currently disallowed in account IDs. The namespace could equivalently be added to the method name field (e.g. multisig->sign
instead of just sign
), but this would break existing contract standards. However, if the introduction of namespaces is accompanied by new contract standards, this might not be an issue.)
Contracts receiving function calls from namespaced contracts would still see the unadulterated account ID of the containing account in near_sdk::env::predecessor_account_id()
. However, near_sdk::env::predecessor_namespace()
would also be available, returning the namespace that the predecessor contract occupies.
Furthermore, future NEPs would be able to specify a “well-known namespace” for contract standards, which would identify a particular namespace (e.g. nep500
) under which standard-compliant namespaced contracts are expected to live.
Strengths & weaknesses
This route is generally “cleaner,” leading to a well-defined separation between namespaced and non-namespaced interactions.
However, it offers subpar backwards compatibility with existing contracts: a namespaced contract could potentially send interactions to a legacy contract which saves only the account ID, and fails to save the namespace. The legacy contract would then be unable to properly initiate interactions with the namespaced contract, possibly resulting in unexpected behavior.
Impact on stakeholders
Five different stakeholders have been identified. The impact on each is described below:
- End-users: low. Namespaces would be part of the transactions generated by dapps and smart contracts, etc. likely requiring little to no additional input from the average end-user.
- Legacy smart contracts: high. Legacy contracts are unaware of namespaces and will flat-out not work with new, namespaced contracts. This could lead to a fractured contract ecosystem, especially if the legacy contracts in question do not have a means of performing upgrades.
- Smart contract developer: medium. Cross-contract interactions now have an additional (albeit small) field to worry about.
- Protocol maintainers: medium. Storage and interactions are slightly more complex.
- Node operators: medium. A database migration is required.
Routing table
This route involves setting up a protocol-level routing table for dispatching incoming function calls. Routing tables map incoming method names to a namespace, method name pair. This eliminates namespaces from being a public construct. The routing table would establish a single, unified interface for all of the namespaced contracts to the public blockchain. Method names not found in the routing table fall back to the default namespace.
For example, let’s say alice.near
has deployed an NEP-141 contract to alice.near:ft
and an NEP-171 contract to alice.near:nft
. The routing table might look like this:
-
ft_transfer
→{ namespace: “ft”, method_name: “ft_transfer” }
-
ft_total_supply
→{ namespace: “ft”, method_name: “ft_total_supply” }
-
ft_new
→{ namespace: “ft”, method_name: “new” }
-
nft_transfer
→{ namespace: “nft”, method_name: “nft_transfer” }
-
nft_new
→{ namespace: “nft”, method_name: “new” }
- …
The routing table would be modified through DEPLOY_CONTRACT
actions, via a new routing_table
field, which contains entries which will be merged with the existing routing table, overwriting duplicates.
Strengths & weaknesses
The routing table approach completely hides namespaces from other contracts on the blockchain, allowing for perfect backward compatibility with legacy contracts, since this approach would not involve changing the receipts posted to the blockchain.
However, this comes at the expense of higher complexity for developers and users. Method name collisions are non-trivial to resolve. Although it is fairly easy to automatically generate a functional routing table, developers still must now include routing table design in the development process. Additionally, if a non-developer user tries to deploy a contract module whose routing table conflicts with a contract already deployed on that account, then the non-developer user is confronted with the task of designing a routing table: a task they probably really don’t want to do.
Impact on stakeholders
Five different stakeholders have been identified. The impact on each is described below:
- End-users: high. Most of the time, routing tables can be generated automatically without fear of collisions. If there is a collision, it could be the case that the user is trying to, for example, deploy a second fungible token smart contract on the same account, in which case the reason (and potential resolution) of the issue should be fairly clear to the user. However, if it is the case that there is a partial collision between two contracts (like two
new
methods), the resolution may not be immediately apparent to the end-user. Worse, if it is up to the end-user to resolve these conflicts, the user could very easily ruin their existing routing table, bricking the contracts (and potentially their account). - Legacy smart contracts: low. Routing tables are completely compatible with existing receipts.
- Smart contract developer: medium. It should be possible to automatically generate routing tables that are suitable for a large fraction of cases, requiring little extra work on the part of the developer.
- Protocol maintainers: medium. Accounts and interactions are slightly more complex.
- Node operators: medium. A database migration is required.
Benefits of namespaces: TL;DR
- Easier contract composability, e.g. just deploy the NEP-141 and NEP-330 modules and you’re set. Good for developers (esp. w.r.t. contract factories) and non-developer users (one-click deploy composable modules).
- Safer and easier contract upgrades, e.g. have an
upgrade_controller
namespace that manages upgrading the rest of the contract’s components, so a bad upgrade doesn’t brick the account.
In the future, contract namespaces could also integrate nicely with:
- Contract module 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.
- Permissions: It could be possible to set permissions on a particular namespace. For example, restricting the namespace
dao_multisig
to interactions withdao.near
only. - Synchronous execution. Namespaced contracts on the same account should be able to communicate with each other synchronously instead of asynchronously.
Feedback
The team at Pagoda is seeking community feedback on this proposal and the two proposed implementation approaches: explicit namespaces vs. routing tables.