as presented by Evgeny Kuzyakov to the NEAR collective, Oct 2020
The voting contract was used by MainNet validators to enable transfers in October 2020.
This is significant because it made the network fully decentralized and moved the NEAR network from phase I to phase II
The transition to Phase II occurred because NEAR Protocol’s validators
indicated via an on-chain voting mechanism that they believe the network is
sufficiently secure, reliable and decentralized to remove transfer restrictions
and officially allow it to operate at full functionality. Accounts that are
subject to lockup contracts will now begin their unlocking timeline.The shift to Phase II means 3 important things:
Permissionlessness:
With the removal of transfer restrictions, it is now possible for anyone to
transfer NEAR tokens and participate in the network. Specifically, it is now
possible for anyone to send or receive tokens, to create accounts, to
participate in validation, to launch applications or to otherwise use the
network … all without asking for anyone’s permission. This means individuals,
exchanges, defi contracts or anyone else can utilize the NEAR token and the NEAR
network in an unrestricted fashion.Voting: The community-operated
NEAR Protocol successfully performed its first on-chain vote, indicating that
community governance is operational and effective. The exact mechanism will
change going forward but this is a substantial endorsement of the enthusiasm of
the community to participate.Decentralization: The dozens of
validators who participated in the vote were backed by over 120 million tokens
of delegated stake from over a thousand individual tokenholders and this vote
indicates that they believe the network is sufficiently secure and decentralized
to operate freely.In essence, NEAR is now fully ready to build on, ready
to use, and ready to grow to its full potential. While Bitcoin brought us Open
Money and Ethereum evolved that into the beginnings of Open Finance, we finally
have access to a platform with the potential to bridge the gap to a truly Open
Web.
Evgeny walked us through the contract code in detail. This article attempts to capture the most salient points for developers new to the NEAR ecosystem who want to learn to write contracts for NEAR protocol using the Rust programming language. NEAR also supports AssemblyScript as a contract development langauge and future articles will attempt similar treatment for that language.
At time of writing, NEAR recommends using Rust for high value contracts which manage mission-critical features or significant levels of capital due to the maturity of the Rust language, its community and the vast amount of attention paid to the Rust-to-WebAssembly toolchain.
The voting contract can be found here: https://github.com/near/core-contracts/tree/master/voting
The key files in the repository are listed below and will be introduced in this document.
voting
├── Cargo.toml
└── src
└── lib.rs
A brief note on compiling contracts for NEAR
A NEAR contract written in Rust must be compiled as a library. This is in contrast to an executable, the kind of program that runs directly on your computer. On NEAR, contracts run inside of a virtual machine called the NEAR VM that is optimized to work with the blockchain. Contracts require another program, this VM, in order to execute.
Technically speaking, there is no wasm32-wasi
target as with typical Rust programs that run directly on the operating system. Instead, Rust smart contracts targeting NEAR use the wasm32-unknown-unknown
interface. This creates a file that any runner (like NEAR VM, for example with Wasi support) can execute as if it was a normal binary.
If you try to compile a contract for NEAR and your Rust toolchain is missing the Wasm components, the compiler will report an error similar to this one:
error[E0463]: can't find crate for `core`
|
= note: the `wasm32-unknown-unknown` target may not be installed
Run this command to add the Wasm target to the Rust toolchain:
rustup target add wasm32-unknown-unknown
To develop a NEAR contract in Rust there are a few things you will always do – the boilerplate.
Cargo.toml
Familiar to Rust devs, this file specifies dependencies and other project flags including:
[package]
name = "voting-contract" # Build a Wasm file called voting-contract
[dependencies]
near-sdk = "2.0.0" # Use near-sdk-rs version 2.0.0
[profile.release]
opt-level = "s" # Optimize for small code size
lto = true # Optimize for small code size
debug = false # Do not include debug info
panic = "abort" # Terminate process on panic
overflow-checks = true # Panic on overflow
opt-level = "s"
Optimize for small code sizelto = true
Optimize for small code sizedebug = false
Do not include debug infopanic = "abort"
Terminate process on panicoverflow-checks = true
Panic on overflowsrc/lib.rs
src/lib.rs
)As of July 27, 2020 - ac06ac4
(excluding tests)
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{U128, U64};
use near_sdk::{env, near_bindgen, AccountId, Balance, EpochHeight};
use std::collections::HashMap;
#[global_allocator]
static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;
type WrappedTimestamp = U64;
/// Voting contract for unlocking transfers. Once the majority of the stake holders agree to
/// unlock transfer, the time will be recorded and the voting ends.
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct VotingContract {
/// How much each validator votes
votes: HashMap<AccountId, Balance>,
/// Total voted balance so far.
total_voted_stake: Balance,
/// When the voting ended. `None` means the poll is still open.
result: Option<WrappedTimestamp>,
/// Epoch height when the contract is touched last time.
last_epoch_height: EpochHeight,
}
impl Default for VotingContract {
fn default() -> Self {
env::panic(b"Voting contract should be initialized before usage")
}
}
#[near_bindgen]
impl VotingContract {
#[init]
pub fn new() -> Self {
assert!(!env::state_exists(), "The contract is already initialized");
VotingContract {
votes: HashMap::new(),
total_voted_stake: 0,
result: None,
last_epoch_height: 0,
}
}
/// Ping to update the votes according to current stake of validators.
pub fn ping(&mut self) {
assert!(self.result.is_none(), "Voting has already ended");
let cur_epoch_height = env::epoch_height();
if cur_epoch_height != self.last_epoch_height {
let votes = std::mem::take(&mut self.votes);
self.total_voted_stake = 0;
for (account_id, _) in votes {
let account_current_stake = env::validator_stake(&account_id);
self.total_voted_stake += account_current_stake;
if account_current_stake > 0 {
self.votes.insert(account_id, account_current_stake);
}
}
self.check_result();
self.last_epoch_height = cur_epoch_height;
}
}
/// Check whether the voting has ended.
fn check_result(&mut self) {
assert!(
self.result.is_none(),
"check result is called after result is already set"
);
let total_stake = env::validator_total_stake();
if self.total_voted_stake > 2 * total_stake / 3 {
self.result = Some(U64::from(env::block_timestamp()));
}
}
/// Method for validators to vote or withdraw the vote.
/// Votes for if `is_vote` is true, or withdraws the vote if `is_vote` is false.
pub fn vote(&mut self, is_vote: bool) {
self.ping();
if self.result.is_some() {
return;
}
let account_id = env::predecessor_account_id();
let account_stake = if is_vote {
let stake = env::validator_stake(&account_id);
assert!(stake > 0, "{} is not a validator", account_id);
stake
} else {
0
};
let voted_stake = self.votes.remove(&account_id).unwrap_or_default();
assert!(
voted_stake <= self.total_voted_stake,
"invariant: voted stake {} is more than total voted stake {}",
voted_stake,
self.total_voted_stake
);
self.total_voted_stake = self.total_voted_stake + account_stake - voted_stake;
if account_stake > 0 {
self.votes.insert(account_id, account_stake);
self.check_result();
}
}
/// Get the timestamp of when the voting finishes. `None` means the voting hasn't ended yet.
pub fn get_result(&self) -> Option<WrappedTimestamp> {
self.result.clone()
}
/// Returns current a pair of `total_voted_stake` and the total stake.
/// Note: as a view method, it doesn't recompute the active stake. May need to call `ping` to
/// update the active stake.
pub fn get_total_voted_stake(&self) -> (U128, U128) {
(
self.total_voted_stake.into(),
env::validator_total_stake().into(),
)
}
/// Returns all active votes.
/// Note: as a view method, it doesn't recompute the active stake. May need to call `ping` to
/// update the active stake.
pub fn get_votes(&self) -> HashMap<AccountId, U128> {
self.votes
.iter()
.map(|(account_id, stake)| (account_id.clone(), (*stake).into()))
.collect()
}
}
In general, keep in mind that we are operating in the resource-contrained environment of the NEAR Protocol Runtime.
For any single contract method invocation, all storage and compute operations must fit comfortably within a single block time of 1 second along with any other calls made on other contracts. It's useful to keep this in mind as we design contracts which operate as good and performant citizens of the network. You can read more about "thinking in gas" in our docs.
Also surprising for some new to contract development: the concepts of randomness and time are not available on the same terms as when running on bare metal. Randomness is especially tricky and the only real authority on time in this context is the blockchain so any notion of time here is tied directly to epochs, specifically EpochHeight
. The Rust SDK near-sdk-rs
also exposes env::random_seed
.
Let's put these ideas aside for the time being and focus on the code of this contract.
near_sdk_rs
provides support for writing performant contracts including a custom binary serialization / deserialization format called Borsh for storing contract state efficiently on chain to reduce both storage and gas costs.
The first line of the contract brings these tools into scope.
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
Reading on, clients of the contract may require a JSON format for arguments & return types. Since JSON is limited to JavaScript number support (10^53), we need to wrap Rust u64 and u128 with some helper functionality that returns these numbers as JSON strings. Notice the difference in the uppercase "U" on wrapped types vs the lowercase "u" on Rust native types.
use near_sdk::json_types::{U128, U64};
near-sdk-rs
exposes the virtual machine execution context through env
which developers can use to interrogate the NEAR Runtime for information like the signer of the current transaction, the state of the contract, and more. near_bindgen
will be explained where used lower in this document. AccountId
, Balance
and EpochHeight
are all types that are native to NEAR protocol and represent what you expect them to. Check out the nearcore source code for more details about these types like AccountId.
use near_sdk::{env, near_bindgen, AccountId, Balance, EpochHeight};
When choosing to organize how data is stored by your contract, it's important to first decide whether you want to use "Key-Value Storage" or "Singleton Storage".
Key-Value Storage is a key-value structure which is ideal for managing large contract state or cases with sparse access to contract data (ie. you only need a few pieces of data on occasion). This state is only deserialized when accessed and otherwise remains untouched. The contract will access storage directly using the Storage
interface and/or choose a more appropriate abstraction from among the list of available collections, like a PersistentVector or UnorderedMap.
Singleton Storage is a serialized representation of the contract struct. Using this type of state storage is more efficient when contract data is:
Since this state is deserialized in its entirety every time a contract method is called, it's important to keep it small.
The choice of std::collections::HashMap
here hints at a Singleton Storage strategy.
use std::collections::HashMap;
Pressing on …
Rust allows us to define our own custom memory allocation mechanism to make memory management as efficient as possible. wee_alloc
, the "Wasm-Enabled, Elfin (small) Allocator" has good performance when compiled to Wasm . In contrast, wee_alloc
would be a poor choice for a scenario where allocation is a performance bottleneck. It's small, less than a kilobyte, but doesn't have as good performance as the standard allocator. In fact it in O(n) is the number of allocated objects.
It's safe to consider these lines "boilerplate" and just include them in your contracts without much thought. If the choice of allocator changes for some reason, you'll probably hear about it.
#[global_allocator]
static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;
As mentioned earlier, near-sdk-rs
provides wrapped types that allow contracts to return JSON-compatible formats for big numbers. These types are required when contract methods return large numbers to a JavaScript execution context like the browser. Internal calls ignore these types and use the native Rust types as usual.
In the following case, the original "Something" is of type u64
but we want to make sure that we can serialize and deserialize it at the contract boundary when communicating with the outside world. So we "wrap" it by using the type U64
with uppercase "U", the same type but with conveniently automated serialization (a requirement for any contract method arguments).
type WrappedSomething = U64;
Wrap a Rust struct
in #[near_bindgen]
and it generates a smart contract compatible with the NEAR blockchain. That's it. Every Rust contract struct
must be preceded with this macro since it exposes the execution environment to the contract which allows it to receive method calls, send results and generally behave like a contract within the Runtime environment.
Each contract has its own Singleton Storage as part of the contract account where contract state will be serialized and stored on chain. Inspecting account storage will reveal a key named STATE
whose value is this Singleton Storage. The following lines define the struct which will become this Singleton representation of the contract. Only one public struct should be decorated with the near_bindgen
macro, along with the implementation of that same struct.
Contracts are efficiently serialized using Borsh, the binary representation described earlier in this document.
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct SomeContract {
/* code removed for clarity */
}
Within the contract struct
we can store any valid Rust type. This contract includes an example of storing a map of custom types with the HashMap
(specifically mapping AccountId
s to some related Balance
). It also includes an example of storing a custom type exposed by near-sdk-rs
called Balance
, an Option
type (meaning it may or may not have a value) and the EpochHeight
for keeping track of "time" from within this context (ie. while running on the blockchain as mentioned above).
/// How much each validator votes
votes: HashMap<AccountId, Balance>,
/// Total voted balance so far.
total_voted_stake: Balance,
/// When the voting ended. `None` means the poll is still open.
result: Option<WrappedTimestamp>,
/// Epoch height when the contract is touched last time.
last_epoch_height: EpochHeight,
This bit of code is part of a larger context where we have decided to control contract initialization by decorating a specific method with the #[init]
macro (see below).
By default all contract methods will try to initialize contract state if not already initialized (since it's possible to call any public methods on a deployed contract) but in this case we want to avoid that behavior.
impl Default for VotingContract {
fn default() -> Self {
env::panic(b"VotingContract should be initialized before usage")
}
}
Decorating an implementation section (impl
) with #[near_bindgen]
wraps all public methods with some extra code that handles serialization / deserialization for method arguments and return types, panics if money is attached unless the method is decorated with #[payable]
, and more.
#[near_bindgen]
impl VotingContract {
/* code removed for clarity */
}
Decorating a public method with #[init]
will add some machinery to "initialize the contract" by serializing and saving the returned value (in this case a struct
) into the contract's Singleton Storage.
You can add an #[init]
decorator to any public method to mark it as something like a "constructor" for the contract – note that there's nothing special about the new()
method name here, it could just as well be old()
and would work fine. new()
is a convention used by the NEAR community because it sets the correct expectation for anyone reading the code.
This implementation references env::state_exists()
to assert that the contract's Singleton Storage state has not been initialized, ie. it does not already exist. We don't want to accidentally re-initialize the contract at some later time and blow away any valuable state that has been captured!
The #[init]
macro expects that the method it decorates will return an instance of the struct
which is being referenced by this implementation section. The return value will be serialized on chain into the contract's Singleton Storage.
If you're new to Rust then you may not be familiar with "implicit returns", a language feature likely passed down through the influence of Ruby. It allows the last line of a method to be assigned as the method's return value. In Rust we get an implicit return of the value of the last line of a method if we just ignore the semicolon at the end of the last expression. In this case the last line returns an instance of the VotingContract
with all attributes of the struct
initialized to their default values. Rust calls these "expressions" rather than "implicit returns" (https://doc.rust-lang.org/book/ch03-03-how-functions-work.html#function-bodies-contain-statements-and-expressions)
Note that the method signature of new()
is returning an instance of Self
but could just as well have been VotingContract
except this way we will have to change fewer tokens in the code if we ever decide to rename the contract. A modest but meaningful gift to our future selves, this is.
#[init]
pub fn new() -> Self {
assert!(!env::state_exists(), "The contract is already initialized");
VotingContract {
votes: HashMap::new(),
total_voted_stake: 0,
result: None,
last_epoch_heigh: 0,
}
}
near-sdk-rs
Although the following lines are not specific to the Voting
contract, it may be instructive to cover these while we're thinking about the basics of contract design for NEAR Protocol.
These features are available to contract developers using Rust on NEAR Protocol.
Public methods (pub fn
) will be exposed as contract methods avaliable for clients to call.
// public methods will be exposed as contract methods avaliable for clients to call
pub fn do_something() {
// unimplemented!();
}
&self
If a method borrows self – that is, the first argument in the list is &self
, where the &
is a Rust modifier that basically means "safely take a temporary reference to …" – then you can be sure that it will interact with contract state but will NOT CHANGE contract state.
pub fn do_something_else(&self) {
// unimplemented!();
}
&mut self
Similar to the above, borrowing a mutable reference means the method will interact with contract state and WILL CHANGE contract state.
pub fn do_something_else_entirely(&mut self) {
// unimplemented!();
}
Going a level deeper here, it may be worth noting at this point that #[near_bindgen]
actually adds code to each method of the contract which reacts to the presence of self
or &mut self
.
This additional code first tries to deserialize Singleton Storage, ie. contract state. Recall that this Singleton contract state is stored as the value associated with a reserved key named STATE
. near-sdk-rs
will call Default()
if STATE
does not exist (detected by a call to env::state_exists()
), otherwise it will call Default()
as part of preparing to run a method.
If you're familiar with Rust macros, #[near_bindgen]
is very readable. If not, the author found that it helps to alternate between holding your breath and taking slow, deep breaths: counting 4 in then 8 out while staring at it for a good long while.
Methods can take arguments which will be deserialized from JSON and return types which will be serialized to JSON if called from an external context.
Making internal method calls within a Rust context doesn't require the same serialization / deserialization overhead.
Internally we can call contracts using positional arguments. Via any external interface (simulation tests, near-api-js
, etc), we have to pass JSON objects which get deserialized into the function arguments as needed.
In summary, if you call a method directly without Promise
or ext_contract
API then there is no serialization / deserialization happening. If you call from a frontend using near-api-js
or raw RPC, or using the cross-contract API, then serialization is needed.
pub fn do_one_more_thing(some_bool: bool) -> Option<SomeType>{
// unimplemented!();
}
If you want to attach (send) tokens to a contract method, add #[payable]
.
Decorating a contract method with the #[payable]
macro will allow the method to accept attached native NEAR tokens without a panic. If tokens are attached to a method call without adding the #[payable]
macro then the method will panic.
#[payable]
pub fn show_me_the_money() {
let tokens = env::attached_deposit()
}
If you want to hide methods from the contract's public interface, don't make them public.
Methods which are not marked as public will not be exposed as part of the contract interface
fn private_method(){
unimplemented!();
}
If you want to maintain restricted access to some methods so that only the contract account itself may call them, add #[private]
.
Sometimes a method should be available to the contract via promise call which requires that it is exposed on the contract but not callable by anyone other than the contract itself.
This is a common pattern in NEAR which can be hand-crafted with a few lines of code but this macro makes it more readable (see near-sdk-rs
readme: https://github.com/near/near-sdk-rs/blob/master/README.md)
(NOTE: this is a pending feature not available in near-sdk-rs
2.0.0 at time of writing)
#[private]
pub fn contract_private_method(){
// unimplemented!();
}
Beyond the basics of writing contracts for NEAR using Rust, there are a few critical design issues to be aware of as a contract author. These concerns stem from a single attribute of smart contracts – they are public interfaces that can be invoked by any valid transaction on the network. The implication is that we should be more defensive in our development of contracts and always assume the potential for malicious or adversarial behavior by bad actors. This defensive stance will lead to safer code and more robust contracts.
(1) For sensitive methods which are modifying contract state, it's important to check env::predecessor_account_id()
to prevent being called by the wrong account maliciously. In general, having a setter()
method that does not verify the callers authority or ownership before applying changes to contract state is an antipattern.
(2) All contract data is essentially public information and can be queried via the NEAR RPC API at will for free. This implies that any information stored by the contract should not open the contract for exploitation by bad actors.