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

[NEP-591]: Global Contracts #591

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Changes to the protocol specification and standards are called NEAR Enhancement
| [0536](https://github.com/near/NEPs/blob/master/neps/nep-0536.md) | Reduce the number of gas refunds | @evgenykuzyakov @bowenwang1996 | Final |
| [0539](https://github.com/near/NEPs/blob/master/neps/nep-0539.md) | Cross-Shard Congestion Control | @wacban @jakmeier | Final |
| [0568](https://github.com/near/NEPs/blob/master/neps/nep-0569.md) | Resharding V3 | @staffik @Longarithm @Trisfald @marcelo-gonzalez @shreyan-gupta @wacban | Final |
| [0591](https://github.com/near/NEPs/blob/master/neps/nep-0591.md) | Global Contracts | @bowenwang1996 @pugachag @stedfn | Final |


## Specification

Expand Down
221 changes: 221 additions & 0 deletions neps/nep-0591.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
---
NEP: 591
Title: Global Contracts
Authors: Bowen Wang <[email protected]>, Anton Puhach <[email protected]>, Stefan Neamtu <[email protected]>
Status: New
DiscussionsTo: https://github.com/nearprotocol/neps/pull/591
Type: Protocol
Replaces: 491
Version: 1.0.0
Created: 2025-02-11
LastUpdated: 2025-03-17
---

## Summary

This proposal introduces global contracts, a new mechanism that allows smart contracts to be deployed once and reused by any account without incurring high storage costs.

Currently, deploying the same contract multiple times on different accounts leads to significant storage fees.
Global contracts solve this by making contract code available globally, allowing multiple accounts to reference it instead of storing their own copies.

Rather than requiring full storage costs for each deployment, accounts can simply link to an existing global contract, reducing redundancy and improving scalability. This approach optimizes storage, lowers costs, and ensures efficient contract distribution across the network.

## Motivation

A common use case on NEAR is to deploy the same smart contract many times on many different accounts. For example, a multisig contract is a frequently deployed contract.
However, today each time such a contract is deployed, a user has to pay for its storage and the cost is quite high. For a 300kb contract the cost is 3N.

With the advent of chain signatures, the smart contract wallet use case will become more ubiquitous.
As a result, it is very desirable to be able to reuse already deployed contract without having to pay for the storage cost again.

Additionally, global contracts cover the underlying use case for [NEP-491](https://github.com/near/NEPs/pull/491): [#12818](https://github.com/near/nearcore/pull/12818).
Some businesses onboard their users by creating an account on their behalf and deploying a contract to it. Such a use case is susceptible to refund abuse, where there is a financial incentive to repeatedly create and destroy an account, cashing in on the storage fee that was initially paid by the business to deploy the contract.

## Specification

Global contract can be deployed in 2 ways: either by its hash or by owner account id.
Contracts deployed by hash are effectively immutable and cannot be updated.
When deployed by account id the owner can redeploy the contract updating it for all its users.
Comment on lines +36 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we talk about when to use each way? For instance, when should a user use hash instead of account id, and vice versa?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, added in bfad2e3

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to jump in, but I’m curious, will the deployed-by account ID receive a hash?

I don’t see a reason to allow someone to change my code and potentially take everything from my contract. But if a globally deployed account also gets a hash that I can lock onto (and update later if needed), that would be a nice DevX improvement.

I do see a downside, though that every update essentially creates a new copy of the code instead of replacing it. So I’m curious, how is this going to be addressed?

Copy link
Contributor Author

@pugachAG pugachAG Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@akorchyn Nope, in this case we store account id, so the owner can update contract for all current users. There are underlying use cases for that, for example application developers onboarding users on NEAR.
Also it is possible to lock such contract if owner deletes all keys from the account, effectively making it impossible to make any changes.

Users can use contracts deployed by hash if they prefer having control over contract updates. In order to update the contract user would have to explicitly switch to a different version of the contract deployed under a different hash.
Contracts deployed by account id should be used when user trusts contract developers to update the contract for them. For example if user accounts are created specifically for some application to be onchain.

We introduce new receipt action for deploying global contracts:

```rust
struct DeployGlobalContractAction {
code: Vec<u8>,
deploy_mode: GlobalContractDeployMode,
}

enum GlobalContractDeployMode {
/// Contract is deployed under its code hash.
/// Users will be able reference it by that hash.
/// This effectively makes the contract immutable.
CodeHash,
/// Contract is deployed under the owner account id.
/// Users will be able reference it by that account id.
/// This allows the owner to update the contract for all its users.
AccountId,
Comment on lines +55 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does deploying a global contract by account id deploy said contract to the owner account as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, it does not do that

}
```

User pays for storage by burning NEAR tokens from its balance depending on the deployed contract size.

Also new action is added for using previously deployed global contract:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify that this action is the same / similar as deploying the contract directly to the account?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in c528f22


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to "unlink" the global contract from the account? Is it even possible for regular contracts? What about cleaning the state of the contract after it's removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know it is not possible to unlink the regular contract, so we don't have to solve that for the global contracts. The same applies for the state of the contracts.

```rust
struct UseGlobalContractAction {
contract_identifier: GlobalContractIdentifier,
}

enum GlobalContractIdentifier {
CodeHash(CryptoHash),
AccountId(AccountId),
}
```

This action is similar to deploying a regular contract to an account, except user does not cover storage deposit.

## Reference Implementation

### Storage

In order to have global contracts available to users on all shards we store a copy in each shard's trie.
A new trie key is introduced for that:

```rust
pub enum TrieKey {
...
GlobalContractCode {
identifier: GlobalContractCodeIdentifier,
},
}

pub enum GlobalContractCodeIdentifier {
CodeHash(CryptoHash),
AccountId(AccountId),
}
```

The value is contract code bytes, similar to `TrieKey::ContractCode`.

### Distribution

Global contract has to be distributed to all shards after being deployed.
This is implemented with a dedicated receipt type:
Comment on lines +104 to +105
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this accounted for in the BandwidthScheduler?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implemented as part of near/nearcore#13147


```rust
enum ReceiptEnum {
...
GlobalContractDistribution(GlobalContractDistributionReceipt),
}

enum GlobalContractDistributionReceipt {
V1(GlobalContractDistributionReceiptV1),
}

struct GlobalContractDistributionReceiptV1 {
id: GlobalContractIdentifier,
target_shard: ShardId,
already_delivered_shards: Vec<ShardId>,
code: Arc<[u8]>,
}
```

`GlobalContractDistribution` receipt is generated as a result of processing `DeployGlobalContractAction`.
Such receipt contains destination shard `target_shard` as well as list of already visited shard ids `already_delivered_shards`.
Applying `GlobalContractDistribution` receipt updates the corresponding `TrieKey::GlobalContractCode` in the trie for the current shard.
It also generates distribution receipt for the next shard in the current shard layout. This process continues until `already_delivered_shards` contains all shards.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One consequence of this approach is that global contract deployment will take at least num_shards blocks to get onto all shards. In the future we may need some way of checking if the contract is fully deployed / updated. Nothing to worry about now, I'm sure the contract developers will ask about it once they need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed with more shards in the future propagating global contract updates might take quite some time.
We can extend the functionality in the future by adding some deployment progress indicator as part of owners account id data if that would be required.

This way we ensure that at any point in time there is at most one `GlobalContractDistribution` receipt in flight for a given deployment and eventually it will reach all shards.

Note that `GlobalContractDistribution` does not target any specific account (`system` is used as a placeholder) and `target_shard` is used for receipt routing.

### Usage

We change `Account` struct to make it possible to reference global contracts.
`AccountV2` is introduced changing `code_hash: CryptoHash` field to more generic `contract: AccountContract`:

```rust
enum AccountContract {
None,
Local(CryptoHash),
Global(CryptoHash),
GlobalByAccount(AccountId),
}
```
Comment on lines +135 to +145
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it be possible to deploy / use global contracts from accounts created before AccountV2 is released? Is there any migration / lazy update necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AccountV2 is released along with global contract.
User account is upgraded to V2 lazily when global contract is used, so no migration is required.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Applying `UseGlobalContractAction` updates user account `contract` field accordingly.

`FunctionCall` action processing is updated to respect global contracts. This includes updating [contract preparation pipeline](https://github.com/near/nearcore/blob/fb95d7b7740d1fda9245afa498ce4e9ac145c8af/runtime/runtime/src/pipelining.rs#L24) as well as [recording of the executed contract to be included in the state witness](https://github.com/near/nearcore/blob/fb95d7b7740d1fda9245afa498ce4e9ac145c8af/core/store/src/trie/update.rs#L338).

### Costs

For global contracts deployment we burn tokens for storage instead of locking like what we do regular contracts today.
The cost per byte of global contract code `global_contract_storage_amount_per_byte` is set as 10x the storage staking cost `storage_amount_per_byte`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it set to 10x symbolically or do you just set its value to be 10x? I would suggest decoupling them so that they can be updated independently.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to this comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

global_contract_storage_amount_per_byte is a separate runtime parameter completely independent from storage_amount_per_byte, here I meant that initial values is set to be 10 * storage_amount_per_byte


Additionally we add action costs for the global contract related actions:

* `action_deploy_global_contract` is exactly the same as `action_deploy_contract`
* `action_deploy_global_contract_per_byte`:
* send costs are the same as `action_deploy_contract_per_byte`
* execution costs should cover distribution of the contract to all shards:
* this is pretty expensive for the network, so want want to charge significant amount of gas for that
* we still want to be able to fit max allowed contracts size into single chunk space: `max_gas_burnt = 300_000_000_000_000`, `max_contract_size = 4_194_304`, so it should be at most `max_gas_burnt / max_contract_size = 71_525_573`
* we need to allow for some margin for other costs, so we can round it down to `70_000_000`

Using a global contract incurs two costs, as follows:

* `action_use_global_contract`
* mirrors `action_deploy_contract`
* base cost for processing a usage action
* `action_use_global_contract_per_identifier_byte`
* mirrors `action_deploy_contract_per_byte`, but based on the global contract identifier length
* introduced because the `AccountId` in `GlobalByAccount(AccountId)` variant can vary in length, unlike `Global(CryptoHash)` with a fixed size of 32 bytes

Referencing a global contract locks tokens for storage in accordance with `storage_amount_per_byte` based on global contract identifier length, calculated similarly to `action_use_global_contract_per_identifier_byte`.

Although the cost structure is similar to that of single shard contract deployments, the overall fees are significantly lower because only a few bytes are stored for the reference. This is desired, because referencing a global contract is not an expensive operation for the network.

## Security Implications

One potential issue is increasing infrastructure cost for global contracts with growing number of shards.
A global contract is effectively replicated on every shard, so with increase in number of shards each global contract uses more storage.
This can be potentially addressed in the future by making deployment costs parametrized with the number of shards in the current epoch, but it still wouldn't address the issue for the already deployed contracts.

## Alternatives

In [the original proposal](https://github.com/near/NEPs/issues/556) we considered storing global contracts in a separate global trie (managed at the block level) and introducing a dedicated distribution mechanism.
We decided not to proceed with this approach as it requires significantly higher effort to implement and also introduces new critical dependencies for the protocol.

## Future possibilities

Global contracts can potentially be used as part of the sharded contracts effort. Sharded contract code should be available on all shards, so using global contracts for that might be a good choice.

## Changelog

[The changelog section provides historical context for how the NEP developed over time. Initial NEP submission should start with version 1.0.0, and all subsequent NEP extensions must follow [Semantic Versioning](https://semver.org/). Every version should have the benefits and concerns raised during the review. The author does not need to fill out this section for the initial draft. Instead, the assigned reviewers (Subject Matter Experts) should create the first version during the first technical review. After the final public call, the author should then finalize the last version of the decision context.]

### 1.0.0 - Initial Version

> Placeholder for the context about when and who approved this NEP version.

#### Benefits

> List of benefits filled by the Subject Matter Experts while reviewing this version:

* Benefit 1
* Benefit 2

#### Concerns

> Template for Subject Matter Experts review for this version:
> Status: New | Ongoing | Resolved

| # | Concern | Resolution | Status |
| --: | :------ | :--------- | -----: |
| 1 | | | |
| 2 | | | |

## Copyright

Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).