Giter Site home page Giter Site logo

near / near-sdk-contract-tools Goto Github PK

View Code? Open in Web Editor NEW
36.0 3.0 9.0 1.42 MB

Helpful functions and macros for developing smart contracts on NEAR Protocol.

Home Page: https://crates.io/crates/near-sdk-contract-tools

License: GNU General Public License v3.0

Rust 99.80% Shell 0.20%
blockchain dapp near nearprotocol smart-contracts wasm webassembly

near-sdk-contract-tools's Introduction

near-sdk-contract-tools

NFT

use near_sdk::{
    borsh::{self, BorshDeserialize, BorshSerialize},
    near_bindgen, PanicOnDefault,
};
+ use near_sdk_contract_tools::nft::*;

#[derive(BorshSerialize, BorshDeserialize, PanicOnDefault)]
+ #[derive(NonFungibleToken)]
#[near_bindgen]
pub struct MyNftContract {}

#[near_bindgen]
impl MyNftContract {
    #[init]
    pub fn new() -> Self {
        let mut contract = Self {};

+         contract.set_contract_metadata(ContractMetadata::new(
+             "My NFT".to_string(),
+             "MNFT".to_string(),
+             None,
+         ));

        contract
    }
}

FT

use near_sdk::{
    borsh::{self, BorshDeserialize, BorshSerialize},
    near_bindgen, PanicOnDefault,
};
+ use near_sdk_contract_tools::ft::*;

#[derive(BorshSerialize, BorshDeserialize, PanicOnDefault)]
+ #[derive(FungibleToken)]
#[near_bindgen]
pub struct MyFtContract {}

#[near_bindgen]
impl MyFtContract {
    #[init]
    pub fn new() -> Self {
        let mut contract = Self {};

+         contract.set_metadata(&FungibleTokenMetadata::new(
+             "My Fungible Token".into(),
+             "MYFT".into(),
+             24,
+         ));

        contract
    }
}

What is it?

This package is a collection of common tools and patterns in NEAR smart contract development:

  • Storage fee management.
  • Escrow pattern and derive macro.
  • Owner pattern and derive macro.
  • Pause pattern and derive macro.
  • Role-based access control.
  • Derive macros for NEP standards:

Not to be confused with near-contract-standards, which contains official implementations of standardized NEPs. This crate is intended to be a complement to near-contract-standards.

You can think of this collection of common tools and patterns (mostly in the form of derive macros) as a sort of OpenZeppelin for NEAR.

Pro tip: Use the contract wizard to generate starter code for your next project.

Installation

cargo add near-sdk-contract-tools

Examples

See also: the full integration tests.

Owner

use near_sdk::{near_bindgen, AccountId};
use near_sdk_contract_tools::{owner::Owner, Owner};

#[derive(Owner)]
#[near_bindgen]
struct Contract {
    // ...
}

#[near_bindgen]
impl Contract {
    #[init]
    pub fn new(owner_id: AccountId) -> Self {
        let mut contract = Self {
            // ...
        };

        Owner::init(&mut contract, &owner_id);

        contract
    }

    pub fn owner_only(&self) {
        Self::require_owner();

        // ...
    }
}

The Owner derive macro exposes the following methods to the blockchain:

fn own_get_owner(&self) -> Option<AccountId>;
fn own_get_proposed_owner(&self) -> Option<AccountId>;
fn own_renounce_owner(&mut self);
fn own_propose_owner(&mut self, account_id: Option<AccountId>);
fn own_accept_owner(&mut self);

Events

The #[event] macro can be applied to structs or enums.

use near_sdk_contract_tools::{event, standard::nep297::Event};

#[event(standard = "nft", version = "1.0.0")]
pub struct MintEvent {
    pub owner_id: String,
    pub token_id: String,
}

let e = MintEvent {
    owner_id: "account".to_string(),
    token_id: "token_1".to_string(),
};

// Emits the event to the blockchain
e.emit();

Fungible Token

To create a contract that is compatible with the NEP-141, NEP-145, and NEP-148 standards, that emits standard-compliant (NEP-297) events.

use near_sdk_contract_tools::ft::*;
use near_sdk::near_bindgen;

#[derive(FungibleToken)]
#[near_bindgen]
struct MyFt {}

#[near_bindgen]
impl MyFt {
    #[init]
    pub fn new() -> Self {
        let mut contract = Self {};

        contract.set_metadata(&FungibleTokenMetadata::new(
            "My Fungible Token".to_string(),
            "MYFT".to_string(),
            24,
        ));

        contract
    }
}

Standalone macros for each individual standard also exist.

Non-fungible Token

Use the NonFungibleToken derive macro to implement NEP-145, NEP-171, NEP-177, NEP-178, and NEP-181, with NEP-297 events.

use near_sdk::{
    borsh::{self, BorshSerialize, BorshDeserialize},
    PanicOnDefault,
    near_bindgen,
};
use near_sdk_contract_tools::nft::*;

#[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, NonFungibleToken)]
#[near_bindgen]
pub struct MyNft {}

Macro Combinations

One may wish to combine the features of multiple macros in one contract. All of the macros are written such that they will work in a standalone manner, so this should largely work without issue. However, sometimes it may be desirable for the macros to work in combination with each other. For example, to make a fungible token pausable, use the fungible token hooks to require that a contract be unpaused before making a token transfer:

use near_sdk_contract_tools::{
    ft::*,
    pause::{*, hooks::PausableHook},
    Pause,
};
use near_sdk::{
    borsh::{self, BorshSerialize, BorshDeserialize},
    PanicOnDefault,
    near_bindgen,
};

#[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, FungibleToken, Pause)]
#[fungible_token(all_hooks = "PausableHook")]
#[near_bindgen]
struct Contract {}

Custom Crates

If you are a library developer, have modified a crate that one of the near-sdk-contract-tools macros uses (like serde or near-sdk), or are otherwise using a crate under a different name, you can specify crate names in macros like so:

#[event(
    // ...
    crate = "near_sdk_contract_tools",
    macros = "near_sdk_contract_tools_macros",
    serde = "serde",
)]
// ...

#[derive(Owner)]
#[owner(
    // ...
    near_sdk = "near_sdk",
)]

Other Tips

Internal methods are not available to be callable via the blockchain. External ones are public and can be called by other contracts.

Proposing ownership (rather than transferring directly) is a generally good practice because it prevents you from accidentally transferring ownership to an account that nobody has access to, bricking the contract.

cargo expand will generate one huge Rust file with all of the macros have been processed:

cargo install cargo-expand
cargo expand > expanded.rs

See src/slot.rs. Slots are thin wrappers over a storage key.

near_sdk::assert_one_yocto() is a function that requires a full access key (by requiring a deposit of one yoctonear, the smallest possible unit of NEAR).

If a user connects their NEAR account to a dapp, the dapp will still not be able to call functions that call assert_one_yocto(), since function call access keys are not allowed to transfer native tokens. These function will require a signature from a full access key, usually involving a confirmation screen in the user's wallet.

Contributing

Setup

Run git config core.hooksPath hooks/ to set up commit hooks.

Build and test

Install cargo-make if it is not installed already:

cargo install cargo-make cargo-nextest

Run tests:

cargo nextest run
cargo test --doc
cd workspaces-tests
cargo make nextest

Audit

Version 1.0.0 of this library has been audited by Kudelski Security. (May 2023)

Authors


(Formerly known as near-contract-tools.)

near-sdk-contract-tools's People

Contributors

austinabell avatar dndll avatar encody avatar kenobijon avatar ryancwalsh avatar twitu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

near-sdk-contract-tools's Issues

RFC: Backed token / Treasury manager component

For smart contracts that manage multiple NEP-141 tokens in a treasury and issue a backed token. Inspired by the Skyward Finance hack. Should work well in conjunction with the existing NEP-141 infrastructure in this package (optional).

  • Provides mint and redeem actions. Each action could be exposed / limited / permissioned by the contract author as desired.
  • Mint action is backed by something (token, NEAR native, etc.). This would be specified by the contract author, probably via some sort of hook.
  • Redeem action would return the backing asset(s) to the redeemer.
  • Mechanism for controlled / limited release (e.g. time release, cliff / vest schedule, etc.)

Request for comments

  • Design suggestions
  • Should the component that would solve the Skyward issue be a "backed token" (i.e. NEP-141 extension of sorts) or a "treasury manager" (contract-agnostic multi-token (NEP-141 only?) manager)
  • Is this component in-scope for this project? It is definitely a little bit more "niche" in use-case, but I think if the component is sufficiently generalized, it could be useful for implementing:
    • Arbitrary token lockups (cliff/vest schedule)
    • Backed tokens (single- and multi-asset)
    • DAO treasuries

Multisig component

A few ways to implement multisig:

  1. Follow a preexisting pattern (e.g. copy/translate from core-contracts/multisig2).
    • Pros: Logic & structures predefined, pattern is somewhat well-known (at least, not new)
    • Cons: Code is not very well-maintained, large, complex (complexity = opportunity for error), not NEP-standardized, and such a standard would likely take a long time due to the complexity
  2. Implement our own: state machine (similar logic to core-contracts version)
    • Pros: We can clean up the logic from core-contracts a lot and simplify as necessary
    • Cons: Still relatively complex, and that comes with the same NEP approval difficulties.
  3. Implement our own: tx queue/approval. The idea is to accept an arbitrary transaction-like blob and simply sign&send when quorum is reached.
    • Pros: Simpler than state machine model, since we don't have to re-implement every possible action in our own custom data structures. Management actions (e.g. add member) could be implemented as normal #[private] functions which are called via normal, reflexive FUNCTION_CALL proposal blobs. Possibly easier to standardize as NEP since the logic is simpler.
    • Cons: Contract will have to do a little extra legwork (tx blob parsing) if it wants to implement any sort of limits on what sort of actions can be proposed, since this implementation would be written for generically signing opaque transaction blobs. I'm not aware if this pattern has been implemented before (on NEAR), so this would be new technology. Requires more testing, and I'm not even absolutely sure it's possible since I've not written a PoC.
  4. Implement our own: transaction signing protocol. Accept a transaction blob and an array of signatures (generated off-chain).
    • Pros: Probably the most straightforward implementation of multisig.
    • Cons: Suffers from the same lack of control as option 3 since it accepts an opaque blob. Requires some sort of off-chain mechanism for generating signatures.

Automatic `reference_hash` calculation at compile-time

Standards like NEP-141 have a field called reference_hash, described as:

the base64-encoded sha256 hash of the JSON file contained in the reference field.

With compile-time macros, it is possible to automatically download the reference field and calculate the hash. This would be immensely convenient for quickly adding an additional layer of security. Currently, not very many fungible tokens use the reference, and therefore the reference_hash fields, but the ones that do use reference don't seem to use reference_hash. This may be due to projects wanting to keep non-volatile metadata on-chain and volatile metadata off-chain, where it's easier to maintain control of and update.

Regardless, this would be a really cool feature to have.

Add unchecked versions of functions with no side-effects or permissioning

For example, ownership::Ownership::propose_owner_internal would not call require_owner or emit an OwnershipEvent::Propose event.

For Owner, it might make more sense to just have a non-event-emitting version of update_owner and update_proposed. For Pause, set_is_paused already doesn’t emit events or have any require! calls, so it wouldn’t require updating for this issue. I think the method names could use some work though, i.e. just by looking at the name of the method I should be able to tell whether it emits events & performs, for example, predecessor checks, and also the method that does perform those things should appear to the untrained eye as the “natural” choice. Hence, I had initially thought using names like update_owner_internal would be a good idea, but maybe update_owner_unchecked would be a better choice.

Which components will need NEPs to merge with near-sdk-rs?

This is essentially the question: Which derive macros expose external methods that are not yet standardized via an NEP?

Component Exposes Interface? NEP Standard
standard::*
Event
Migrate
Owner
Pause
Rbac
Slot
utils::*
Approval / SimpleMultisig

In order to be merged into SDK, a component's Exposes Interface? column should match its NEP Standard column.

Feature idea: Build ID

Macro that adds an external method that returns the commit hash of the build.

This should be possible because macros run at compile time.

Existing similar NEP: https://github.com/near/NEPs/blob/master/neps/nep-0330.md

Possible Example Usage

trait BuildId {
    fn build_id(&self) -> String;
}

#[derive(BuildId)]
#[derive(BorshSerialize, BorshDeserialize, PanicOnDefault)]
#[near_bindgen]
pub struct MyContract {
    // ...
}

let contract = MyContract::new();
println!("{}", contract.build_id()); // 6a5992033dc4a092a5d45d64b4f205ce9aeb2939

Add `rust-toolchain.toml` to lock rustc version for workspace

rls fails with just the previous version of rustc.

[ERROR rust_analyzer::lsp_utils] failed to run build scripts

error: package `near-jsonrpc-primitives v0.15.0` cannot be built because it requires rustc 1.64.0 or newer, while the currently active rustc version is 1.63.0

The tooling can be informed of hard requirement by adding a rust-toolchain.toml with the following. Then it works properly.

[toolchain]     
channel = "1.64"

Package renaming

This issue is resolved when all of the below point are resolved or moot:

  • Should the name of the package be changed?
  • To what should the name of the package be changed?
  • All internal references/mentions have been updated, the repository has been moved, the old package has been deprecated on crates.io, and the renamed package has been published to crates.io.

Make `Rbac` iterable

This could mean:

  • Changing the current Rbac to support role holder enumeration. This would be a state-breaking change.
  • Adding an EnumerableRbac alternative that supports enumeration.

Disallow event name collisions within single macro invocation

#[event(standard = "x-name-collision", version = "1.0.0")]
enum NameCollision {
    First,
    #[nep297(name = "first")]
    Second,
}

println!("{}", NameCollision::First.to_event_string());
println!("{}", NameCollision::Second.to_event_string());

Output:

EVENT_JSON:{"standard":"x-name-collision","version":"1.0.0","event":"first","data":null}
EVENT_JSON:{"standard":"x-name-collision","version":"1.0.0","event":"first","data":null}

Though this technically has a "valid" use-case (wherein a single event's data can take multiple forms), it is almost always better to use another enum for the metadata type instead of having two events using the same name.

This issue is resolved when the above code produces a compiler error. Obviously it is much more difficult to make separate macro invocations produce a compiler error together, even if they are using the same standard field value.

Macro for upgradable contracts

It would be very neat to have a standardized way of upgrading a smart contract and migrating its state to mitigate common security vulnerabilities.

  • Should provide a callback for calling a migration state
  • Can only be executed with a full access key or by the contract owner.
  • Possibly support storing the previous state of the contract in case of an immediate rollback

Identities component

Like a generic version of Owner. Like Rbac, but there is a 1:0..1 relationship between an identity and an account ID. It will also be possible to retrieve the holder of an identity (enumeration is not possible in the current version of Rbac). Identity transfer (via propose/accept) and renunciation are optionally allowed.

Possible Example

enum Identity {
    Owner,
    Administrator,
    Manager,
}

#[derive(Identities)]
#[identities(identities = "Identity")]
#[near_bindgen]
struct MyContract {}

#[near_bindgen]
impl MyContract {
    #[init]
    fn init() -> Self {
        let mut contract = Self {};

        contract.initialize_identity(&Identity::Owner, env::predecessor_account_id());
        contract.initialize_identity(&Identity::Administrator, env::predecessor_account_id());
        contract.initialize_identity(&Identity::Manager, env::predecessor_account_id());
    }

    fn only_owner(&self, account: AccountId) {
        self.require_identity(&Identity::Owner);
    }

    fn only_administrator(&self, account: AccountId) {
        self.require_identity(&Identity::Administrator);
    }
}

impl IdentityHook<Identity> for MyContract {
    fn on_transfer(identity: &Identity, from: AccountId, to: AccountId) -> Result<(), ?> { }

    fn on_propose(identity: &Identity, account: AccountId) -> Result<(), ?> { }

    fn on_accept(identity: &Identity, account: AccountId) -> Result<(), ?> { }
}

Would expose the following interface to the blockchain:

(Caveat: Identities need a to/from string serialization)

trait IdentityExternal<Identity> {
    fn id_is(&self, account: AccountId, identity: Identity) -> bool;
    fn id_get(&self, identity: Identity) -> Option<AccountId>;
    fn id_renounce(&mut self, identity: Identity);
    fn id_propose(&mut self, identity: Identity, to: AccountId);
    fn id_accept(&mut self, identity: Identity, from: AccountId);
}

Improve Readme, add examples, etc

We should show examples about how to write a contract that uses this library.

For example, see #39 (comment)

We'll want to instruct how to make error messages human-readable, such as via:

self.approve_request(request_id)
    .map_err(|e| env::panic_str(&e.to_string()))
    .unwrap()

Fuller examples

Let's add (in a separate repo if necessary... and this one can link to it) complete examples of projects taking advantage of near-contract-tools.

E.g. https://github.com/NEARFoundation/near-contract-tools/blob/01a64242717af71a27fa49197a96912d1c5aeedb/README.md#fungible-token isn't enough to get me started. Part (most?) of it is that I'm new to Rust, but I'm having trouble knowing where to put these lines, what Cargo.toml needs to contain, how to build the project, etc. So a full working example would be helpful.

I looked at https://github.com/NEAR-Edu/near-certification-tools/blob/938255e1adbecbe0605c2749f9a7ebd9a9df317a/data-contract/src/contract.rs#L5 but couldn't figure it out. (Maybe certain types of macros are different from "derive" macros?

Multisig / Approval events

On proposal/request creation, approval (ensure there's something in the approval event that makes it clear when it is fully approved & can be executed), and execution.

Make internal functions more obviously internal

Currently, many of the components are composed of traits that contain methods that are primarily (nigh exclusively) for internal use (e.g. many of them have a root() function that returns a Slot). For someone who isn't super familiar with how this library works, these methods may appear to be freely usable, and may unintentionally cause the component to malfunction by their misuse.

There are a few ways we could think about fixing this problem:

  1. Just be really clear in the documentation which functions are for general use and which ones are internal.
  2. Rename the functions…
    a. …by prefixing with an underscore (e.g. root -> _root).
    b. …by prefixing with a word (e.g. root -> internal_root).
  3. We could consider using #[doc(hidden)], but I don't think that's really what that attribute is for.
  4. Split the internal functions off into a separate trait.
  5. Restructure the entire library such that hidden/private functions are actually hidden/private. Probably quite difficult to do at this point in the library's development, plus it would almost definitely make the derive macros more complicated to correctly implement.

New module checklist

Create a checklist that every new derive macro must accomplish before it can be merged / considered completed.

Hyperlink crate items in docs

Rust docs makes it easy to hyperlink doc items by name. This makes it easier to navigate the docs because the reader can quickly jump to the definition of an item being referred.

That being said hyperlinking everything everywhere can make the docs look noisy and hinder readability. This is somewhat subjective but some general rules of thumb -

  • Only need to hyperlink items in this crate
  • If an items is mentioned multiple times in a paragraph or couple of paragraphs, hyperlinking the first time it's mentioned is good enough
  • If a many items from a module are enumerated (like in bulleted list) hyperlinking the module is good enough

from discussion in #73

Write security invariants for every component

For assistance on the audit, every component should have a list of invariants / assumptions for use and for every function. For example, the Owner component's list would include:

  • The root storage key is not used for any other data.
  • Only the owner will ever be able to call own_renounce_owner, own_propose_owner.
  • Only the account most recently proposed (by the current owner, via own_propose_owner) will be able to call own_accept_owner.
  • After own_accept_owner has been called, the formerly "proposed owner" will become the owner, and the former owner will be removed. There will not be a "proposed owner".
  • Et cetera.

This list should appear in the module level documentation comments for each component.

Flags component

Support multiple, binary state flags.

Inspired by the functionality of the pause component in Aurora's NEAR plugins repo: https://github.com/aurora-is-near/near-plugins#pausable

Discussion Points

  • No function decorators if the decorator would be equivalent to a simple function call (see Owner::require_owner). We're trying to stay away from Solidity-style implementations in favor of Rust.
  • Flags should be implemented as an enum, similar to how roles are for Rbac.
  • It is probably worth investigating whether bit packing (e.g. using bitflags) is as gas-efficient as checking for storage key existence. If flags are not purely binary, then this point is moot (bit packing is impossible). For example, bit packing would be impossible:
    enum MyFlags {
        PausedOne(u8),
        PausedTwo,
    }

Proposed Usage

(derives on MyFlags as of yet undetermined)

enum MyFlags {
    PausedOne,
    PausedTwo,
}

#[derive(Flags)]
#[flags(flags = "MyFlags", storage_key = "StorageKey::Flags")]
struct Contract {
    // ...
}

impl Contract {
    fn one(&self) {
        Self::require_flag_unset(MyFlags::PausedOne);
        // ...
    }

    fn set_one(&self, on: bool) {
        if on {
            Self::set_flag(MyFlags::PausedOne);
        } else {
            Self::unset_flag(MyFlags::PausedOne);
        }
    }

    // similar for MyFlags::PausedTwo
}

Make events easier to use

Use owned values

It's a bit inconvenient to use borrowed values everywhere, but they are technically more memory-efficient. However, since they're pretty volatile (i.e. probably never appear in smart contract storage), maybe it is better to use owned values, since they'd be easier to read and work with.

Remove generic from Event trait

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.