Building with Movement
The Movement logo
🚧🚧🚧 Important Update In Progress 🚧🚧🚧
We are actively working on a new edition of this booklet in response to changes in our audience and to address issues stemming from the outdated nature of the original guide.
- Weeks 1 & 2: Existing content remains useful. We are enhancing these sections with more in-depth material specific to the Aptos Framework.
- Weeks 3 & 4: Stay tuned! We will soon add comprehensive content covering Sui Move and unique features of the Movement ecosystem, including MEVM.
Welcome to Building with Movement! Over the course of this booklet, we will be addressing topics related to developing for the Movement blockchain.
This booklet was originally developed for a month-long hackathon, and so has content covering four weeks. While we think this pacing is nice, the book is concise enough to do in far less time and resumable enough to spread out over more time.
By the end of this book, you will have the skills to build for the Movement blockchain and the contextual awareness to understand its advantages.
If you are looking for in-depth documentation of Movement or the movement
CLI please head to docs.movementlabs.xyz.
This book has an accompanying GitHub repository here. You can also navigate to it by clicking the icon on any page.
If you run into any issues, please use our Help Desk to file a bug report, request support, or search for additional documentation. We will attempt to provide timely support, prioritizing hackathon participants.
Week 1: Introduction to Move and Movement
This week kicks off our exploration of the Move language and Movement ecosystem. We will begin by introducing Move, setting up an appropriate developer environment, and covering the basics of the Move language. Many of the topics concerning the move language will be revisited in greater depth during Week 2.
Introduction to Move and Movement
This section is intended to orient the reader on the history of the language and various Move virtual machine implementations.
Libra/Diem
The Move programming language was originally developed by Facebook's Libra project, now known as Diem, to facilitate the creation of smart contracts on its blockchain platform. The language takes its name from the underlying concept of moving resources rather than copying them, aligning with the principles of resource-oriented programming. Move was designed to address the unique challenges of blockchain development, such as security, efficiency, and scalability.
Resource-orientation and the blockchain
Resource-orientation is a fundamental concept in programming languages like Move that greatly benefits the blockchain ecosystem. By aligning with the principles of resource-oriented programming, the blockchain can enhance security, efficiency, and reliability of smart contracts.
Stack Model Programming and Function Ownership
In resource-oriented programming, like Move, the stack model is employed to manage data ownership and control access. Take for example the following unsafe C program.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void printAndFree(char* data) {
printf("Data: %s\n", data);
free(data); // Function takes ownership and frees the memory
}
int main() {
char* value = (char*)malloc(strlen("Hello") + 1);
strcpy(value, "Hello");
printAndFree(value); // Pass ownership of 'value' to the function
// Attempt to access the value after it has been freed
printf("Data after freeing: %s\n", value); // Unsafe access!
return 0;
}
In Move, this kind of unsafe access would not be possible because of strict ownership conditions.
Each function owns any resources it creates and is responsible for its lifecycle. This ownership model ensures that resources are properly managed and prevents unauthorized access or modification, bolstering the security of blockchain-based applications.
Access Restriction All the Way Down
Resource-oriented programming languages like Move implement access restrictions at all levels of code execution. From the top-level contract to individual functions, access to resources is strictly controlled. This granular access control minimizes the risk of unauthorized operations and ensures that only authorized parties can interact with specific resources, promoting secure and auditable transactions on the blockchain.
Type Linearity and Ownership
linear type: a type with an enforced the restriction that variables or values of the type can be used exactly once. In other words, each linear value has a unique owner or consumer, and it must be used or consumed linearly without duplication or uncontrolled consumption.
f(a) -> g(a) -> h(a)
non-linear type: a type without an enforced the restriction that variables or values of the type can be used exactly once. Variables or values of non-linear types can be used or accessed multiple times without restrictions.
f(a) ->
g(a)
+ h(a) ->
c(a)
+ k(a)
+ p(a)
Type linearity is a crucial aspect of resource-oriented programming that enforces the linear use of resources. In Move, resources generally have linear types, meaning they can only be consumed or moved, not duplicated. This feature prevents resource duplication, reduces memory consumption, and eliminates the risk of double-spending, ensuring the integrity and accuracy of transactions on the blockchain.
How does this address common smart contract vulnerabilities?
The resource-orientation and type-linearity of the Move programming language play a significant role in avoiding common smart contract vulnerabilities. Here's how these features address specific vulnerabilities:
-
Reentrancy Attacks: In a reentrancy attack, a malicious contract calls back into the calling contract before the first execution completes, potentially leading to unexpected behavior or loss of funds. Move's resource-orientation ensures that resources (which include digital assets) cannot be duplicated and are used in a linear fashion. This linearity means that once a resource is moved, it cannot be accessed again within the same transaction, thereby mitigating reentrancy risks.
-
Integer Overflow and Underflow: These occur when an operation attempts to create a numerical value outside the range that can be represented with a given number of bits. Move's type system can enforce range checks on numeric values, reducing the risk of overflow and underflow errors.
-
Unintended Ether Loss: In Ethereum, contracts can be accidentally destroyed with ether still inside, leading to permanent loss of funds. Move's resource model can prevent this by ensuring that resources are accounted for at all times, making it much harder to lose them accidentally.
-
Frozen Ether: Ether can become frozen in a contract due to bugs. Move's stronger guarantees about the state and its manipulation help avoid such scenarios, as the language is designed to make the effects of code more predictable and transparent.
-
Timestamp Dependence and Miner Manipulation: Some contracts rely on block timestamps, which can be slightly manipulated by miners. Move's approach to resources and transactions doesn't inherently solve this, but its more predictable environment can help developers avoid relying on such external and manipulable factors.
-
Short Address/Parameter Attack: This happens due to inconsistent handling of input data length. Move's strong typing and explicit resource management can help avoid this by enforcing correct input handling and data lengths.
-
Denial of Service (DoS) via Block Gas Limit: Attackers might stuff blocks with expensive computations to exhaust a contract's gas. While this is more of a systemic issue, Move's efficiency and predictability in resource handling can mitigate some of the risks.
-
Unknown Function Calls: In Ethereum, sending Ether to unknown functions can lead to vulnerabilities. Move's explicit resource accounting can help avoid such scenarios by making it clear where and how resources are flowing.
Move’s focus on safety, predictability, and explicit resource management addresses these vulnerabilities effectively, helping developers write safer smart contracts. This is particularly crucial in blockchain environments, where contract bugs and vulnerabilities can lead to significant financial losses and are often irreversible due to the immutable nature of blockchain technology.
Developer Setup
This section examines tooling and provides setup instructions for working with the Movement blockchain and the various examples in covered in this booklet.
The Move ecosystem
The budding Move ecosystem sports a variety of developer friendly tools. As we list off tools that will be relevant to this booklet, keep in mind that there are number of projects we have not included.
Virtual machines
As previously discussed, there are several popular virtual machine implementations available for Move development--each of with is paired with particular blockchain. Besides the Movement VM
, the most well-known Move virtual machines are Move VM
, Aptos VM
, and Sui VM
We provide a comparison of these different virtual machines in our docs.
When selecting a virtual machine for development it's important to consider performance, ease of use, and stability. Aptos VM
built upon the original and stable Move VM
to provide an improved developer experience. The Movement VM
builds upon Aptos VM
to provide improved performance.
CLIs
There are three CLIs worthy of note in the Move language development space. All support building, testing, deploying, and running smart contracts.
move
: the original CLI for Move development.aptos
: the CLI for Aptos development.movement
: our very own CLI which is currently compatible with the Aptos CLI.
In this booklet we will be working with movement
. If you ever need help working with movement
you can run movement --help
or movement <command> --help
for more information.
Package managers
You can manage Move dependencies by adding them directly to your Move.toml
file.
[package]
name = "hello_world"
version = "0.0.0"
[dependencies]
AptosFramework = { git = "https://github.com/movemntdev/aptos-core.git", subdir = "aptos-vm/aptos-move/aptos-framework", rev = "testnet" }
[addresses]
std = "0x1"
hello_blockchain = "_"
Our version of the AptosFramework is slightly different from the upstream at https://github.com/aptos-labas/aptos-core.git. Please be mindful that, while our goal is to support the latest version of the Aptos Framework, we may occasionally lag behind the upstream--resulting in incompatibilities.
IDE
There are several useful development enviroments for Move. This book will be geared towards using VsCode because of the its developer container features and its Move analyzer. However, syntax highlighting has been implemented for other IDEs including Vim.
Our Setup
We'll be using the movement
CLI and VsCode most-often running the movement-dev
Docker container from (mvlbs/m1)[https://hub.docker.com/repository/docker/mvlbs/m1/general].
To get started...
- Clone the repo from which this book originates: https://github.com/movemntdev/movement-hack
- Open the repo in VsCode.
- Reopen the directory using the movement devcontainer.
Alternatively, when working with movement-dev
you may:
docker image pull mvlbs/m1
docker run -it -v "$(pwd):/workspace" mvlbs/m1 /bin/bash
We will also occasionally use Rust, TypeScript, and Python to complete various programming examples.
Setting Up Your Own Environment
While we recommend using the above, if you want to set up your own project environment, you can install the movement
CLI and then run movement init
in your chosen directory. This will create a .movement
profile. You can then add a Move.toml
and a sources directory to get started.
Basic Move Syntax
This section treats with basic Move syntax, comparing the language to Rust. This is merely intended to provide some guidance for booklet participants. More comprehensive guides can be found at move-book.com and move-language.github.io.
Allocation and the Move Memory Model
Move's resource-oriented language model leads to a unique memory model:
- Resources in Move are allocated and deallocated implicitly based on their defined lifespan and ownership semantics. This ensures proper resource management without the need for explicit memory allocation or deallocation.
- Resources are created and stored on the blockchain as part of a transaction, providing a persistent and tamper-resistant storage mechanism.
- Global storage in Move allows for the storage of resource data that can be accessed across multiple transactions and smart contracts.
While function ownership in Move is similar to Rust, it is less permissive and closer to being purely linear--restricting the set of possible borrows.
Expressions and Control Flow
Expressions
Move uses a similar expression-orientation to Rust. Expressions are evaluated to produce a value. Expressions can be used in many places, including:
#![allow(unused)] fn main() { fun eight() : u8 { 8 } }
if
Branching in Move is accomplished via if
and else
statements. There is not an else if
statement.
#![allow(unused)] fn main() { if (a) { debug::print<u8>(&0); } else { debug::print<u8>(&99); }; }
if
is an expression.
#![allow(unused)] fn main() { let value = if (true) { 8 } else {0}; // value set to 8 }
Move syntax for expressions and control flow shares similarities with Rust. Basic control flow constructs like if, while, and loop are present in Move as well. However, Move has a more limited set of expressions and control flow features compared to Rust.
while
and loop
Move supports while
and loop
looping constructs. while
loops while condition is true. loop
loops infinitely. There is no for
loop, both while
and loop
are roughly equally used as replacements.
#![allow(unused)] fn main() { // example of while loop while (i < 10) { Vector::push_back(&mut a, i); i = i + 1; }; }
Types
Primitives
- Move has the primitive types
boolean
,u8
,u64
,u128
,address
, andsigner
. - Move also supports hex- and byte-string literals.
#![allow(unused)] fn main() { let byte_val = b"hello, world!"; // byte string let hex_val = x"48656C6C6F210A"; // hex string }
- Integers can be type cast with the
as
keyword. signer
represents the sender of a transaction and is used for access control and authentication.signer
can be converted to address withsigner::address_of
.
Abilities
Type abilities in Move specify certain primitive memory behaviors and constraints for types. These abilities are perhaps most similar to different pointer types in Rust.
copy
: Thecopy
ability allows for the type's value to be copied.drop
: Thedrop
ability enables the necessary cleanup actions when the type goes out of scope.store
: Thestore
ability allows the type's value to be stored inside a struct in global storage.key
: Thekey
ability allows the type's value to be used as a unique identifier or index in the global storage of the Move blockchain.- Conditional abilities allow types to have different behaviors based on conditions.
Generic and behavior
- Move supports generics for structs and functions.
- It's possible to achieve polymorphic behavior with generics and phantom types.
- Often you will want to nest generic structures inside of resources to achieve polymorphism. See the
LiquidityPool
generic structure below for an example.
#![allow(unused)] fn main() { // polymorphic coin fee obtainment from liquidswap. /// Get fee for specific pool. public fun get_fee<X, Y, Curve>(): u64 acquires LiquidityPool { assert!(coin_helper::is_sorted<X, Y>(), ERR_WRONG_PAIR_ORDERING); assert!(exists<LiquidityPool<X, Y, Curve>>(@liquidswap_pool_account), ERR_POOL_DOES_NOT_EXIST); let pool = borrow_global<LiquidityPool<X, Y, Curve>>(@liquidswap_pool_account); pool.fee } }
Resources, References, and Mutation
- You can create a reference with
&
and&mut
. - You cannot use the
mut
keyword during assignment, .e.g.,let mut value = ...
will not compile. - Global storage operators
move_to
,move_from
,borrow_global_mut
,borrow_global
, andexists
in Move enable reading from and writing to resources stored in the blockchain's global storage. - The
acquires
keyword is used to specify which resources a function acquires ownership of a resource during execution.
#![allow(unused)] fn main() { module collection::collection { use std::signer; struct Item has store, drop {} struct Collection has key, store { items: vector<Item> } public fun add_item(account: &signer) acquires Collection { let collection = borrow_global_mut<Collection>(signer::address_of(account)); vector::push_back(&mut collection.items, Item {}); } } }
Move allows the creation of read-only references to resources, ensuring that functions cannot modify them. Here's a small code snippet demonstrating the use of Move syntax:
Misc. syntax
- The
public
keyword in Move indicates that a function can be invoked from outside the current module. - The
entry
keyword is used to declare functions that can be called via the RPC. - The
native
keyword is used to declare functions that are implemented in the blockchain runtime or in an external module. - There are VM specific directives; in Movement we will address
#[inline]
,#[view]
,#[test_only]
, and#[test]
.
Modules and Build System
This section treats with the basic functionality of the module and build system for Move.
Packages
A folder with a Move.toml
and compilable move code is a package. Packages may define external dependencies, which can be local or store at a remote repository. All Move clients used herein will automatically fetch and compile dependencies. When using movement
, we will also define package addresses in the Move.toml
.
[package]
name = "hello_world"
version = "0.0.0"
[dependencies]
# MoveNursery = { git = "https://github.com/move-language/move.git", subdir = "language/move-stdlib/nursery", rev = "main" }
MovementFramework = { git = "https://github.com/movemntdev/movement-subnet.git", subdir = "vm/aptos-vm/aptos-move/aptos-framework", rev = "main" }
[addresses]
std = "0x1"
hello_blockchain = "_"
Program types
Move has two different types of program: modules and scripts. As a general rule, you should use scripts for short proofs of concept or as an entrypoint, see Script Based Design. You can define multiple scripts and modules in the same file.
Modules
Modules are great for code reuse. They can be reference and linked. A module takes the following form.
#![allow(unused)] fn main() { module <address>::<identifier> { (<use> | <friend> | <type> | <function> | <constant>)* } }
When attempting to use logic in a module, you must specify the member function that you wish to call.
Scripts
Scripts are a slightly easier to use alternative to modules. They take the following form.
#![allow(unused)] fn main() { script { <use>* <constants>* fun <identifier><[type parameters: constraint]*>([identifier: type]*) <function_body> } }
Scripts can only have one function in their body.
Within the context of movement
, we will not be using scripts--instead preferring the module construct.
Building
When developing modules in Move you will need to publish the module before being able to run. The exception to this rule is when using movement
to run unit tests. movement move publish
will handle both the building and publication of modules in your current working directory. If you simply want to build the module to inspect its bytecode run movement move build
.
Below is an example bash script for publishing and running a function in a module end-to-end using the Movement CLI drawn from 💻 hello_blockchain
.
#!/bin/bash -e
# Function to echo text as cyan with emoji
function begin() {
echo -e "🔹 \033[36m$1\033[0m"
}
# Function to echo text as green with increased font-weight and emoji
function finish() {
echo -e "✅ \033[1;32m$1\033[0m"
}
begin "Funding account for hello_blockchain deployment and call..."
movement account fund-with-faucet --account default
finish "Funded account for hello_blockchain deployment and call!"
begin "Publishing hello_blockchain module..."
echo "y" | movement move publish --named-addresses hello_blockchain=default
finish "Published hello_blockchain module!"
begin "Setting hello_blockchain message to 'hello!'..."
echo "y" | movement move run --function-id default::message::set_message --args string:hello!
finish "Set hello_blockchain message to 'hello'!"
begin "Querying resources for account..."
movement account list --query resources --account default
finish "Queryed resourced for account!"
named_addresses
The Move build system enables the usage of named addresses to simplify addressing schemes. These will be replaced at compile time whether they are in the adrress position in a module <my_address_name>::my_module
or marked with an @<my_address_name>
elsewhere.
In your Move.toml
, you may specify these addresses as below.
[addresses]
std = "0x1"
<my_address_name> = "_"
You may then specify them when compiling using --<my_address_name>=<my_value>
.
Additional complexities that emerge when using named addresses are well documented in Diem's original documentation of the Move language.
💻 resource_roulette
dev-addresses
Resource roulette currently has a value in the [addresses]
block that is better suited to [dev-addresses]
. Find it an test the module.
Debugging Basic Smart Contracts
This section introduces a few basic smart contracts from this repository as a starting point for programming activites.
💻 hello_world
This very simple program for Movement Move VM
. You can find it at examples/movement/hello_world
prints values to debug console. Note the differences in encodings between the b""
and string::utf8
versions of the string.
Learnings leveraged:
- Basic Move syntax
- Byte strings
#![allow(unused)] fn main() { module hello_world::hello_world { use std::string; use std::signer; use aptos_std::debug; #[test(account = @0x1)] public entry fun hello_world(account: signer) { let addr = signer::address_of(&account); debug::print<address>(&addr); let message = b"Hello, world!"; debug::print(&message); let str_message = string::utf8(message); debug::print(&str_message); } } }
To test run...
movement move test
> [debug] @0x1
> [debug] 0x48656c6c6f2c20776f726c6421
> [debug] "Hello, world!"
If you want to double-check the output hex...
echo 48656c6c6f2c20776f726c6421 | xxd -r - p
💻 fib
The obligatory Move program that computes the nth Fibonacci number. We will refer to this later when we do 💻 MulticontractFib. You can find it and instructions to run it examples/movement/fib
.
Learnings leveraged:
- Basic Move syntax
- Move recursion.
💻 data_structures
From scratch implementation of a priority queue, a couple variations of a hash map, and a binary tree. This may be a useful reference point for building more challenging projects that require custom data strucures. You can find it at examples/movement/data_structures
.
Learnings leveraged:
- Basic Move syntax
- Vectors
- BCS
- Move idioms
💻 resource_roulette
A game of roulette on MoveVM. Place your address on an element in the vector. Contains methods public fun bid
and public fun spin
. Receive a payout if you placed your address on the correct cell. You can find it and instructions to run it at examples/movement/resource_roulette
.
Learnings leveraged:
- Basic Move syntax
- Signer and address types
- Borrows
- Initialization
- Move idioms
#![allow(unused)] fn main() { module resource_roulette::resource_roulette { use std::vector; use std::signer; const ENO_UNAUTHORIZED_ADDRESS : u64 = 0; // ResourceRoulette struct representing the contract state struct ResourceRoulette has key { bids: vector<vector<address>>, owner: address, state : u64 } struct RouletteWinnings has key { amount : u64 } // Initialization function for the ResourceRoulette contract public fun init(account: &signer) { assert!(signer::address_of(account) == @resource_roulette, ENO_UNAUTHORIZED_ADDRESS); let bids = vector::empty<vector<address>>(); let i = 0; while (i < 32) { vector::push_back(&mut bids, vector::empty<address>()); i = i + 1; }; move_to(account, ResourceRoulette { bids, owner: @resource_roulette, state : 17203943403948 }); } // Initializes winnings for a signer public fun init_winnings(account: &signer) { move_to(account, RouletteWinnings { amount: 0, }); } // Bid function to allow signers to bid on a specific slot public fun bid(account : &signer, slot: u8) acquires ResourceRoulette { if (!exists<RouletteWinnings>(signer::address_of(account))) { init_winnings(account); }; let self = borrow_global_mut<ResourceRoulette>(@resource_roulette); roll_state(self); let bids_size = vector::length(&self.bids); assert!(slot < (bids_size as u8), 99); let slot_bids = vector::borrow_mut(&mut self.bids, (slot as u64)); vector::push_back(slot_bids, signer::address_of(account)); } public fun total_bid() : u64 { // Make this more complex to support actual bidding return 100 } // rolls state using xoroshiro prng fun roll_state(self :&mut ResourceRoulette) { let state = (self.state as u256); let x = state; let y = state >> 64; let t = x ^ y; state = ((x << 55) | (x >> 9)) + y + t; y = y ^ x; state = state + ((y << 14) | (y >> 50)) + x + t; state = state + t; state = state % (2^128 - 1); self.state = (state as u64); } public fun get_noise() : u64 { 1 } fun empty_bids(self : &mut ResourceRoulette){ // empty the slots let bids = vector::empty<vector<address>>(); let i = 0; while (i < 32) { vector::push_back(&mut bids, vector::empty<address>()); i = i + 1; }; self.bids = bids; } // Roll function to select a pseudorandom slot and pay out all signers who selected that slot public fun spin() acquires ResourceRoulette, RouletteWinnings { let self = borrow_global_mut<ResourceRoulette>(@resource_roulette); // get the winning slot let bids_size = vector::length(&self.bids); roll_state(self); let winning_slot = (get_noise() * self.state % (bids_size as u64)) ; // pay out the winners let winners = vector::borrow(&self.bids, winning_slot); let num_winners = vector::length(winners); if (num_winners > 0){ let balance_per_winner = total_bid()/( num_winners as u64); let i = 0; while (i < num_winners) { let winner = vector::borrow(winners, i); let winnings = borrow_global_mut<RouletteWinnings>(*winner); winnings.amount = winnings.amount + balance_per_winner; i = i + 1; }; }; empty_bids(self); } // tests... } }
Movement Packages
This section examines the Move standard library and Aptos framework. These are the most common starting points in M1 development.
Disclaimer: there are many more useful modules in the standard library and framework. We will discuss some of them in this course and encourage you to explore them as you go.
aptos_std::debug
aptos_std::debug::print
serializes and prints a Move value to the console using a VM native
func. The associated [debug]
outputs are easiest to view when unit testing.
aptos_std::debug::print_stack_trace
prints the current stack.
The below is an example demonstrating print string nuances in 💻 hello_world
.
#![allow(unused)] fn main() { module hello_world::hello_world { use std::string; use std::signer; use aptos_std::debug; #[test(account = @0x1)] public entry fun sender_can_set_message(account: signer) { let addr = signer::address_of(&account); debug::print<address>(&addr); let message = b"Hello, world!"; debug::print(&message); let str_message = string::utf8(message); debug::print(&str_message); } } }
std::vector
and aptos_std::big_vector
A useful dynamically size collection with an aptos_std::
counterpart optimized for a large number of elements.
The below is an example of searching through std::vector
in open addressing hash map implementation from 💻 data_structures
.
#![allow(unused)] fn main() { public fun find<K, V>(map: &OaHashMap<K, V>, key: &K) : &Option<Entry<K, V>> { let index = compute_hash_index(key, map.size); let count = 0; loop { let option_value = vector::borrow(&map.entries, index % map.size); if (option::is_none(option_value)) { return option_value } else { let entry = option::borrow(option_value); if (key_equals(&entry.key, key)) { return option_value } }; index = index + 1; count = count + 1; if (count > map.size) { abort ENO_BUFFER_EXHAUSTED } } } }
aptos_std::table
An associative array.
#![allow(unused)] fn main() { fun run_table(account: signer) { let t = table::new<u64, u8>(); let key: u64 = 100; let error_code: u64 = 1; assert!(!table::contains(&t, key), error_code); assert!(*table::borrow_with_default(&t, key, &12) == 12, error_code); add(&mut t, key, 1); assert!(*table::borrow_with_default(&t, key, &12) == 1, error_code); move_to(&account, TableHolder{ t }); } }
std::option
This module defines the Option type and its methods to represent and handle an optional value. The implementation is based on a vector.
#![allow(unused)] fn main() { fun option_contains() { let none = option::none<u64>(); let some = option::some(5); let some_other = option::some(6); assert!(option::contains(&some, &5), 0); assert!(option::contains(&some_other, &6), 1); assert!(!option::contains(&none, &5), 2); assert!(!option::contains(&some_other, &5), 3); } }
aptos_framework::account
One of the most notable features of the aptos_framework
is its resource accounts. Per Aptos, "a resource account is a developer feature used to manage resources independent of an account managed by a user, specifically publishing modules and automatically signing for transactions." In some ways, you can think of a resource account as an administrator or service account. In most large projects, you will want to use resource accounts to--in the least--manage deployments.
Using resource accounts takes some practice. We encourage you to closely consider the code in 💻 mini_dex
to get a better sense of how to use resource accounts.
#![allow(unused)] fn main() { public entry fun initialize_lp_account( minidx_admin: &signer, lp_coin_metadata_serialized: vector<u8>, lp_coin_code: vector<u8> ) { assert!(signer::address_of(minidx_admin) == @mini_dex, EInvalidAccount); let (lp_acc, signer_cap) = account::create_resource_account(minidx_admin, b"LP_CREATOR_SEED"); aptos_framework::code::publish_package_txn( &lp_acc, lp_coin_metadata_serialized, vector[lp_coin_code] ); move_to(minidx_admin, CapabilityStorage { signer_cap }); } }
For many common coin and NFT operations, the Aptos framework provides a set of methods that will manage capabilities. For example, the below is a snippet from an aptos-core
example demonstrating how to initialize a coin:
#![allow(unused)] fn main() { /// initialize the module and store the signer cap, mint cap and burn cap within fun init_module(account: &signer) { // store the capabilities within `ModuleData` let resource_signer_cap = resource_account::retrieve_resource_account_cap(account, @source_addr); let (burn_cap, freeze_cap, mint_cap) = coin::initialize<ChloesCoin>( account, string::utf8(b"Chloe's Coin"), string::utf8(b"CCOIN"), 8, false, ); move_to(account, ModuleData { resource_signer_cap, burn_cap, mint_cap, }); // destroy freeze cap because we aren't using it coin::destroy_freeze_cap(freeze_cap); // regsiter the resource account with both coins so it has a CoinStore to store those coins coin::register<AptosCoin>(account); coin::register<ChloesCoin>(account); } }
However, you will also often have to define your own capabilities. In the Aptos Framework, you will usually find it most idiomatic and convenient to use use aptos_framework::account::SignerCapability
for this.
Review 💻 mini_dex
for an example of how to use SignerCapability
to manage unique capabilities. As a challenge, design your own simple module including resource account logic and a SignerCapability
.
Week 2: Advanced Move and Move for Movement
This week treats with more advanced concepts in the Move language and focuses our Move development efforts on the Movement blockchain. This is also intended as an opportunity to practice and revisit concepts from Week 1.
Generics, type constraints, and polymorphism
In this section we will examine Move's support for common type-level program models. Owing to the strictness of its programming model, many of the features found in modern programming languages are not available in Move.
We conclude with a programming example, 💻 MoveNav, that implements a polymorphic navigation module using Move.
Generics
Generics act as abstract stand-ins for concrete types and allow for type-independent code. A single function written with generics can be used for any type. In the Move language, generics can be applied to struct and function signatures.
#![allow(unused)] fn main() { // source: https://move-language.github.io/move/generics.html fun id<T>(x: T): T { // this type annotation is unnecessary but valid (x: T) } struct Foo<T> has copy, drop { x: T } let x = id<bool>(true); // ok! let x = id<u64>(true); // error! true is not a u64 struct Foo<T> has copy, drop { x: T } let foo = Foo<bool> { x: 0 }; // error! 0 is not a bool let Foo<address> { x } = foo; // error! bool is incompatible with address let bar = Foo<bool> { x : true }; // ok! }
Move does not support subtyping or composable type constraints
In Rust you can write code that composes traits to implement functionality similar to subtyping in other languages.
trait Animal { fn make_sound(&self); } trait Fly { fn fly(&self); } trait Swim { fn swim(&self); } trait FlyingSwimmingAnimal: Animal + Fly + Swim { fn do_flying_swimming_stuff(&self) { self.make_sound(); self.fly(); self.swim(); } } struct Duck { name: String, } impl Animal for Duck { fn make_sound(&self) { println!("The duck {} says quack!", self.name); } } impl Fly for Duck { fn fly(&self) { println!("The duck {} is flying.", self.name); } } impl Swim for Duck { fn swim(&self) { println!("The duck {} is swimming.", self.name); } } fn perform_actions<T: Animal + Fly + Swim>(animal: &T) { animal.make_sound(); animal.fly(); animal.swim(); } fn main() { let duck = Duck { name: String::from("Donald"), }; perform_actions(&duck); }
As part of the strictness of its programming model, Move does not support these constructs. Instead to achieve something similar to subtyping, it is often best to compose structs. That is, while you cannot establish an is relationship between types, you can establish a has relationship. This may be a more familiar model for those used to programming in C.
#![allow(unused)] fn main() { module 0x42::Animal { use std::string; use std::debug; struct Animal { sound : string; name : string; } struct FlyingAnimal { flies : bool; speed : u8; animal : Animal; } struct Bird { flying_animal : FlyingAnimal; feather_description : string; } fun make_sound(animal : Animal){ debug::print(animal.sound); } fun bird_make_sound(bird : Bird){ make_sound(bird.flying_animal.animal); } } }
Importantly, however, you cannot compose types to provide bounds on generic functions. An equivalent of fn perform_actions<T: Animal + Fly + Swim>(animal: &T)
does not exist.
Further below, we will discuss more advanced means of achieving similar subtyping and polymorphism. However, in most cases, you will be better off simply choosing a simpler programming model.
The Four Type Abilities: copy
, drop
, store
, and key
copy
: Thecopy
ability allows for the type's value to be cloned.drop
: Thedrop
ability enables the necessary cleanup actions when the type goes out of scope.store
: Thestore
ability allows the type's value to be stored inside a struct in global storage.key
: Thekey
ability allows the type's value to be used as a unique identifier or index in the global storage of the Move blockchain.
These abilites may be used to define type bounds for generic functions and structs.
#![allow(unused)] fn main() { // source: https://move-language.github.io/move/global-storage-operators.html#global-storage-operators-with-generics struct Box<T> has key { t: T } // Publish a Container storing a type T of the caller's choosing fun publish_generic_box<T>(account: &signer, t: T) { move_to<Box<T>>(account, Box { t }) } /// Publish a container storing a u64 fun publish_instantiated_generic_box(account: &signer, t: u64) { move_to<Box<u64>>(account, Box { t }) } }
Importantly, in the publish_generic_box
example above, the type T
must also have the has
and store
abilities owing to Move's rules of ability composition:
copy
: All fields must have copy.drop
: All fields must have drop.store
: All fields must have store.key
: All fields must have store.
Storage polymorphism
Storage polymorphism is the ability to index into global storage via a type parameter chosen at runtime. By leveraging storage polymorphism in combination with generics, Move developers can write generic algorithms, functions, and modules that can work with different resource types stored in the blockchain's storage.
#![allow(unused)] fn main() { // source: https://move-language.github.io/move/global-storage-operators.html#global-storage-operators-with-generics struct Box<T> has key { t: T } // Publish a Container storing a type T of the caller's choosing fun publish_generic_box<T>(account: &signer, t: T) { move_to<Box<T>>(account, Box { t }) } /// Publish a container storing a u64 fun publish_instantiated_generic_box(account: &signer, t: u64) { move_to<Box<u64>>(account, Box { t }) } }
While not as flexible as general polymorphism found in some languages, Move's storage polymorphism can quickly compose complex and useful operations for the blockchain. The below is an example of function for adding liquidity from Movement's dex router. Under the hood, global storage polymorphism is being used to enable to the publication of generic coins.
#![allow(unused)] fn main() { /// Add liquidity to pool `X`/`Y` with rationality checks. /// * `coin_x` - coin X to add as liquidity. /// * `min_coin_x_val` - minimum amount of coin X to add as liquidity. /// * `coin_y` - coin Y to add as liquidity. /// * `min_coin_y_val` - minimum amount of coin Y to add as liquidity. /// Returns remainders of coins X and Y, and LP coins: `(Coin<X>, Coin<Y>, Coin<LP<X, Y, Curve>>)`. /// /// Note: X, Y generic coin parameters must be sorted. public fun add_liquidity<X, Y, Curve>( coin_x: Coin<X>, min_coin_x_val: u64, coin_y: Coin<Y>, min_coin_y_val: u64, ): (Coin<X>, Coin<Y>, Coin<LP<X, Y, Curve>>) { assert!(coin_helper::is_sorted<X, Y>(), ERR_WRONG_COIN_ORDER); let coin_x_val = coin::value(&coin_x); let coin_y_val = coin::value(&coin_y); assert!(coin_x_val >= min_coin_x_val, ERR_INSUFFICIENT_X_AMOUNT); assert!(coin_y_val >= min_coin_y_val, ERR_INSUFFICIENT_Y_AMOUNT); let (optimal_x, optimal_y) = calc_optimal_coin_values<X, Y, Curve>( coin_x_val, coin_y_val, min_coin_x_val, min_coin_y_val ); let coin_x_opt = coin::extract(&mut coin_x, optimal_x); let coin_y_opt = coin::extract(&mut coin_y, optimal_y); let lp_coins = liquidity_pool::mint<X, Y, Curve>(coin_x_opt, coin_y_opt); (coin_x, coin_y, lp_coins) } }
Unused and phantom types
In order to enforce type constraints at compile-time, unused type parameters can be marked as phantom type parameters. Arguments to phantom type parameters won't be considered when determining the abilities of the generic type. Thus, this eliminates the need for spurious ability annotations.
Generics, along with storage polymorphism and phantom type parameters, offer flexibility, code reuse, and type safety. These features make it easier to create modular and reusable code components for safe contracts in the Move language. For instance, generics can be used to define generic data structures such as lists, maps, or queues that can store and manipulate values of any type. In addition, generics enable the creation of templatized algorithms which can operate on different types of data.
#![allow(unused)] fn main() { // source: https://move-language.github.io/move/generics.html?highlight=phantom%20types#unused-type-parameters module 0x2::m { // Currency Specifiers struct Currency1 {} struct Currency2 {} // A generic coin type that can be instantiated using a currency // specifier type. // e.g. Coin<Currency1>, Coin<Currency2> etc. struct Coin<Currency> has store { value: u64 } // Write code generically about all currencies public fun mint_generic<Currency>(value: u64): Coin<Currency> { Coin { value } } // Write code concretely about one currency public fun mint_concrete(value: u64): Coin<Currency1> { Coin { value } } } }
💻 MoveNav
💻 MoveNav implements Dijkstra's algorithm for navigating over a graph with different navigation types in move.
We'll use a simple approach to domain modeling outlined below.
#![allow(unused)] fn main() { // redacted version of examples/movement/MoveNav module 0x42::MoveNav { use std::vector; use std::option; use std::debug; use std::string; struct Graph { nodes: vector<Vector3>, edges: vector<Edge>, } struct Vector3 { x: u8, y: u8, z: u8, } struct Edge { source: u8, target: u8, weight: u8, } struct Navigation { graph: Graph, navigation_type: NavigationType, } struct NavigationType { name: string::String, speed: u8, } fun navigate(nav: Navigation, start: Vector3, end: Vector3): option::Option<vector::Vector3> { debug::print("Navigating from ", start, " to ", end); let nav_type = &nav.navigation_type; if nav_type.name == "Walk" { debug::print("Walking at speed ", nav_type.speed); // Perform walking navigation logic return option::none() } if nav_type.name == "Run" { debug::print("Running at speed ", nav_type.speed); // Perform running navigation logic return option::none() } if nav_type.name == "Fly" { debug::print("Flying at speed ", nav_type.speed); // Perform flying navigation logic return option::none() } else { debug::print("Unsupported navigation type"); return option::none() } } fun set_graph(nav: &mut Navigation, graph: Graph) { nav.graph = graph; } fun set_navigation_type(nav: &mut Navigation, nav_type: NavigationType) { nav.navigation_type = nav_type; } } }
To dive into the project, please clone this book's repo, and navigate to examples/movement/MoveNav
.
The future of Move type programming
The MSL specification provides for more advanced type-level constructs. For ambitious developers, these may be powerful contribution objectives!
Safety
🚧 Under renovation. Please check back later.
The Move Prover
Diagram of the Move Prover Architecture
The Move Prover is a formal verification tool specifically designed for the Move programming language. The Move Prover performs static analysis of Move programs, exploring all possible execution paths and applying various verification techniques. It can reason about program behaviors, ownership and borrowing rules, resource lifecycles, and other aspects of Move's type system and semantics. By leveraging the Move Prover, developers can gain increased confidence in the correctness and security of their Move smart contracts. This tool helps identify potential issues before deployment, reducing the risk of vulnerabilities, ensuring adherence to best practices, and promoting the development of robust and reliable blockchain applications.
Introduction to Smart Contracts
🚧 Under renovation. Please check back later.
Testing
This section examines testing strategies for Move smart contracts on Movement. We will cover both automated and manual strategies for testing functionality.
Movement profile configuration
movement init --network testnet
When building and testing an application, you will want to work against either the Movement testnet or a local network. The movement
devcontainer is equipped with the movement
CLI and uses docker-compose
to start a local network; both options are available without additional setup when working with the devcontainer.
When beginning a project, run movement init
and specify either testnet
or local
. The first profile you create will be called default
.
To create a new profile run movement init --profile <name-of-profile>
. You can then specify which profile you would like to use in various commands by --profile <name-of-profile>
, e.g., move run --profile default ...
.
You can inspect the details of all of your profiles at .movement/config.yaml
in your working directory.
# config.yaml
---
profiles:
default:
private_key: "0x23978a9c5a8c9291c3cb0283f5c4eee243f7ae81d62b3d3243aa06ac0fcde2cf"
public_key: "0xf6ad6834565bda0f3fa8a093311f1a1308855773d2108cd04dd770da9c078ecd"
account: 29f06cb1f4139484e8c3dcd9f915ad39acb2aee9a8e8064ee48cfc255ecf10ca
rest_url: "https://fullnode.devnet.aptoslabs.com/"
faucet_url: "https://faucet.devnet.aptoslabs.com/"
Automated testing
Movement's CLI movement
provides an aptos
-like interface for building and testing Move smart contracts. The built-in testing functionality is best suited for unit testing. You can define tests in the same module or separately.
#![allow(unused)] fn main() { // hello_blockchain.move module hello_blockchain::message { use std::error; use std::signer; use std::string; use aptos_framework::account; use aptos_framework::event; // ... #[test(account = @0x1)] public entry fun hello_world(account: signer) acquires MessageHolder { let addr = signer::address_of(&account); aptos_framework::account::create_account_for_test(addr); set_message(account, string::utf8(b"Hello, Blockchain")); assert!( get_message(addr) == string::utf8(b"Hello, Blockchain"), ENO_MESSAGE ); } } }
#![allow(unused)] fn main() { // hello_blockchain_test.move #[test_only] module hello_blockchain::message_tests { use std::signer; use std::unit_test; use std::vector; use std::string; use hello_blockchain::message; fun get_account(): signer { vector::pop_back(&mut unit_test::create_signers_for_testing(1)) } #[test] public entry fun sender_can_set_message() { let account = get_account(); let addr = signer::address_of(&account); aptos_framework::account::create_account_for_test(addr); message::set_message(account, string::utf8(b"Hello, Blockchain")); assert!( message::get_message(addr) == string::utf8(b"Hello, Blockchain"), 0 ); } } }
You can then run tests for the package containing modules from the movement CLI.
movement move test --named-addresses hello_blockchain=default
For advanced use of movement
for automated testing, such as coverage, see the movement
CLI documentation.
Manual testing
Often, automated unit testing will be insufficient to determine that your smart contracts are ready for production. You will want to apply a set of end-to-end strategies to ensure smart contract quality. At the moment, all of these strategies are manual; however, automation can be built on-top of them.
Contribution {: .contributor-block} Help us develop better tools for automated e2e and integration testing.
With movement
There three key instructions for manual testing using movement
:
movement move publish
: publishes modules and scripts to the Movement blockchain.movement move run
: runs a module or script.movement account list
: lists resources values.
When testing manually, you will typically adopt a flow of publish->run->list
. In the examples provided with this book's repository, you will commonly see bash scripts for running and testing Movement smart contract that orchestrate these three commands. The following is an example from our hello_blockchain
contract:
#!/bin/bash -e
# Function to echo text as cyan with emoji
function begin() {
echo -e "🔹 \033[36m$1\033[0m"
}
# Function to echo text as green with increased font-weight and emoji
function finish() {
echo -e "✅ \033[1;32m$1\033[0m"
}
begin "Funding account for hello_blockchain deployment and call..."
movement account fund-with-faucet --account default
finish "Funded account for hello_blockchain deployment and call!"
begin "Publishing hello_blockchain module..."
echo "y" | movement move publish --named-addresses hello_blockchain=default
finish "Published hello_blockchain module!"
begin "Setting hello_blockchain message to 'hello!'..."
echo "y" | movement move run --function-id default::message::set_message --args string:hello!
finish "Set hello_blockchain message to 'hello'!"
begin "Querying resources for account..."
movement account list --query resources --account default
finish "Queryed resourced for account!"
begin "Setting hello_blockchain message to 'goodbye!'..."
echo "y" | movement move run --function-id default::message::set_message --args string:goodbye!
finish "Set hello_blockchain message to 'goodbye'!"
begin "Querying resources for account..."
movement account list --query resources --account default
finish "Queryed resourced for account!"
Semi-automation
In many cases, you will find opportunities to automate the inspection of resources via bash
, python
, and other scripts. As you develop any of these testing strategies, we encourage you to share them with us so that we might make improvements to our CLI's capabilities.
Share Share your semi-automated workflows with us!
Testing directives
Movement provides several directives for testing which are important to understand.
#[test]
Marks a test function. Can be provided with arguments.
When testing procedures that require signers, you will need set their values in the directive. Take the example below from 💻 ResourceRoulette
#![allow(unused)] fn main() { #[test(account = @resource_roulette, bidder_one = @0x3)] #[expected_failure(abort_code = FLAG_WINNER)] public fun test_wins(account : &signer, bidder_one : &signer) acquires }
Here our test expects both resource account, i.e., resource_roulette
, and a bidder signer, i.e., bidder_one
. We will discuss how these are used below.
#[test_only]
Test only is used for defining symbols that will only be compiled when testing. It can be useful for creating mocks and stubs, test boundaries and more.
#[expect_failure]
Allows you to check if a routine aborts as expected, i.e., matching a certain error code.
In addition to asserting intended failures, you can use this behavior to define more complex tests that are based on boundary conditions being crossed. The example below from 💻 Resource Roulette uses this pattern to test whether winners emerge from the pseudorandom spinner.
#![allow(unused)] fn main() { #[test_only] const BOUNDARY_WINNER : u64 = 1; // Under the current state rolling implementation this will work // More robust testing would calculate system dynamics #[test(account = @resource_roulette, bidder_one = @0x3)] #[expected_failure(abort_code = FLAG_WINNER)] public fun test_wins(account : &signer, bidder_one : &signer) acquires ResourceRoulette, RouletteWinnings { init(account); let i : u64 = 0; while (i < 1_000) { bid(bidder_one, 7); spin(); let winnings = borrow_global<RouletteWinnings>(signer::address_of(bidder_one)); if (winnings.amount > 0) { abort BOUNDARY_WINNER }; i = i + 1; }; } }
Mocks, stubs, and state-based simulation
In order to simulate and control the behavior of dependencies or external systems during testing, you may whish to apply mocking, stubbing, and stated-based simulation strategies.
Mocks and stubs
Mocks and stubs are both techniques used to simulate or replace certain components of a system being tested. A mock is a fake implementation of a method or an object, meant to simulate its real behavior. Stubs, on the other hand, are simplified implementations that do not imitate the real behavior. Instead, stubs produce a predefined response to a specific method call or input. Thus, mocks verify the behavior of code and stubs verify the state of the code.
Some of the modules in the standard library and framework will be suitable for mocking. The example below uses a resource account function to mock a specialized publishing process. A good strong understanding of the standard library can result in much cleaner solutions to mocking problems.
#![allow(unused)] fn main() { #[test_only] public entry fun set_up_test(origin_account: &signer, resource_account: &signer) { use std::vector; account::create_account_for_test(signer::address_of(origin_account)); // create a resource account from the origin account, mocking the module publishing process resource_account::create_resource_account(origin_account, vector::empty<u8>(), vector::empty<u8>()); init_module(resource_account); } }
State-based simulation
State-based simulation is a testing technique that focuses on verifying a program based the correctness of its state transitions. First, one must identify and define the states that the program can be in. Next, the events or actions that trigger a transition between states must be defined. Using this information, proper test cases should be generated to explore different state transitions and scenarios.
For movement
Beyond the test
and test_only
directives, Movement does not not provide any additional ergonomics for mocking, stubbing, or state-based simulation. However, opting for a common environment module may be suitable for more complex cases. The example below uses storage polymorphism to implement a common environment store.
#![allow(unused)] fn main() { address 0x42::Environment { // Unused type for global storage differentiation struct VariableA {} struct VariableB {} // A generic variable type that can be instantiated with different types struct VariableStore<phantom K, V> has store { value: V, } // Set the value of a variable public fun set_variable<K, V>(value: V) acquires VariableStore<K, V> { move_to<VariableStore<K, V>>(account, VariableSore { value }) } // Get the value of a variable public fun get_variable<K, V>(): V acquires VariableStore<K, V> { borrow_global<VariableStore<K, V>>(addr).value } } }
When setting up your tests, you would then want to run something like the below. You'll likely want to simply create a type bridge in the module above to enable external sets from the CLI.
#!/bin/bash -e
# set environment
begin "Setting environment to slow..."
echo "y" | movement move run --function-id default::message::set_slow_variable --args string:slow
finish "Set environment to slow!"
Contribution Help us develop mocking and stubbing tools for Movement.
Design Patterns
🚧 Under renovation. Please check back later.
Smart Contracts development and staging
🚧 Under renovation. Please check back later.
Week 3: M1 and DeFi Applications
M1 is Movement's first L1. This week is be all about building applications, particular DeFi applications, with Movement.
Deploying on M1
At the time of writing, only the M1 testnet is available. To set up for deployment you only need to make sure you have a testnet profile configured.
movement init --network testnet
You may then use the movement
CLI to publish your package.
movement move publish --named-addresses hello_blockchain=default
We encourage however to take a look at aptos_framework::resource_account
and aptos_framework::resource_group
for more advanced publication options.
💻 MulticontractFib
We encourage you to checkout examples/movement/multicontract_fib
for an easy to inspect deployment. Once you've deployed, simply run the command below to check out the on-chain resources.
movement account list --query resources --account default
Security
This section provides a high-level overview of common blockchain concerns and points to features of Movement or modules from the Movement standard library that can be used to help.
M1
When working with M1 you will be able benefit from Avalanche's adaptive security and Move's strict programming model to avoid many of the vulnerabilities you would commonly encounter in smart contract development. You should, of course, remain aware of the vectors of attack.
Attack Surface
Blockchain
Network layer
The network layer of a blockchain application is susceptible to various attacks, such as distributed denial-of-service (DDoS) attacks, eclipse attacks, and Sybil attacks. These attacks can disrupt the network's functionality and compromise its security.
Consensus
The consensus mechanism used in a blockchain application can be targeted by attackers through attacks like 51% attacks, where they gain majority control over the network's computing power and manipulate transactions or block validation.
Thankfully, Avalanche consensus--which underpins movement--is designed to resist various attacks, including sybil attacks, distributed denial-of-service (DDoS) attacks, and collusion attacks. Its probabilistic nature ensures that the consensus outcome converges to the desired state, even when the network is under attack.
Blockchain protocol:
The underlying blockchain protocol may have vulnerabilities that can be exploited by attackers. Weaknesses in the protocol's cryptographic algorithms, consensus algorithms, or validation mechanisms can lead to security breaches.
Smart contracts
Environment
Smart contracts rely on the execution environment in which they run. Insecure environments can expose smart contracts to attacks, such as sandbox escapes or unauthorized access to system resources.
External dependencies
Smart contracts often interact with external systems or data sources. These dependencies can introduce security risks, such as malicious data feeds or vulnerabilities in the connected systems.
Code vulnerabilities
Flaws in the code of smart contracts can lead to various vulnerabilities, including reentrancy attacks, integer overflow/underflow, and logic errors. These vulnerabilities can be exploited to manipulate contract behavior or steal funds.
Thankfully, thanks to its type-system, resource-orientation, and linear programming model, the Move language makes it difficult to publish code with many of the common smart contract vulnerabilities.
Upgradability
The ability to upgrade smart contracts introduces potential security risks. Unauthorized or malicious upgrades can compromise the integrity of the contract or introduce vulnerabilities that can be exploited.
Thankfully, the Move language introduces the concept of modules, which are self-contained units of code that encapsulate the functionality of smart contracts. Unlike traditional smart contracts, Move modules can be upgraded without disrupting the entire system or requiring complex migration processes.
dApps
Integration risks
Decentralized applications (dApps) often integrate with external services or APIs. Insecure integration points can expose dApps to security risks, such as unauthorized data access or injection of malicious code.
Handling User Data
Data minimization
Blockchain applications should follow the principle of data minimization, collecting only the necessary data and avoiding the storage of sensitive information that is not required for the application's functionality.
Access control
Proper access control mechanisms should be implemented to ensure that only authorized individuals or entities can access sensitive user data. This includes authentication, authorization, and secure role-based access control.
Move uses an account-based ownership model where each account has its own address and associated permissions. Access control can be enforced at the account level, ensuring that only authorized users can perform specific actions.
Encryption
Sensitive user data stored in the blockchain or associated systems should be encrypted to protect it from unauthorized access. Encryption algorithms and protocols should be carefully chosen and implemented.
Pseudonymization
To enhance privacy, blockchain applications can pseudonymize user data by replacing personally identifiable information with pseudonyms or cryptographic identifiers. This makes it difficult to directly link user identities to their data.
DoS
Denial-of-Service (DoS) attacks aim to disrupt the availability of a blockchain application or the entire network by overwhelming the system with an excessive amount of requests or by exploiting vulnerabilities in the network's infrastructure. DoS attacks can result in service unavailability or degradation, impacting the application's functionality and user experience. Implementing robust DoS mitigation strategies is essential to protect against such attacks.
As mentioned above, Avalanche's adaptive security approach it difficult to successfully deny network service.
Common blockchain system design patterns
Oracles
Oracles play a crucial role in blockchain applications by providing a bridge between the blockchain and the external world. They are trusted entities or mechanisms that bring off-chain data onto the blockchain, making it accessible to smart contracts and decentralized applications. Oracles enable the blockchain to interact with real-world events, data feeds, APIs, and other external systems. By leveraging oracles, blockchain applications can access and process external information in a reliable and secure manner.
We built 💻 NewsMoves,
examples/movement/new_moves
, as a toy oracle contract that allows a trusted entity to write the latest Google news articles onto the blockchain. Please checkout the directory to publish the contract and run the Python data entity.
#![allow(unused)] fn main() { module news_moves::news_moves { use std::signer; use std::vector; // Struct representing a news article entry struct Article has copy { timestamp: u64, title: vector<u8>, content: vector<u8>, } // NewsMoves struct representing the contract state struct NewsMoves { articles: vector<Article>, } // Initialization function for the NewsMoves contract public fun init() { move_to(@news_moves, NewsMoves { articles: vector::empty<Article>(), }); } public fun update<X, Y, Curve>( account: &signer, timestamp: u64, title: vector<u8>, content: vector<u8>, ) acquires NewsMoves { // update the contract at the account let account_addr = signer::address_of(account); let self = borrow_global_mut<NewsMoves>(account_addr); // add the new article vector::push_back(&mut self.articles, Article { timestamp: timestamp, title: title, content: content, }); } // Function to get the latest news article from the contract public fun getLatestArticle(): Article { // Get the latest article from the contrac let moves = borrow_global<NewsMoves>(@news_moves); let len = vector::length(&articles); assert(len > 0, 98); // Ensure there is at least one article let latestArticleIndex = len - 1; *moves.articles[latestArticleIndex] } } }
Rollups
Rollups are a layer 2 scaling solution for blockchains that aim to improve scalability and reduce transaction costs. They work by aggregating multiple transactions off-chain and then submitting a summary or proof of those transactions to the main chain. This reduces the burden on the main chain, allowing for faster and more efficient processing. Rollups can significantly increase transaction throughput and enable complex applications to run smoothly on the blockchain while maintaining a high level of security.
We added a toy dispute mechanism 💻 NewsMoves,
examples/movement/new_moves
to demonstrate how part of a rollup could implemented.
Tokenization
Tokenization is the process of representing real-world or digital assets as tokens on a blockchain. It enables the creation of digital representations (tokens) that can be owned, transferred, and managed on the blockchain. Tokenization has broad applications, ranging from representing physical assets like real estate or artwork to creating digital assets like utility tokens or security tokens. By tokenizing assets, blockchain-based systems can provide increased liquidity, fractional ownership, and facilitate seamless transferability of assets in a secure and transparent manner.
We implement a toy tokenization scheme and separately used our framework's
aptos_token_objects
to augment 💻 NewsMoves,examples/movement/new_moves
and demonstrate Movement token utilities.
State Channels
State channels are a scalability solution in blockchain that allows off-chain execution of transactions between participants. They enable fast and low-cost transactions by keeping most of the interactions off-chain, while the final state is settled on the main blockchain. State channels are particularly useful for frequent and fast interactions, such as microtransactions or gaming applications, as they reduce congestion and improve transaction throughput.
Side Chains
Side chains are separate blockchains that are connected to the main blockchain, often referred to as the "main chain" or "parent chain." They provide an additional layer of scalability and flexibility by allowing specific use cases or applications to operate on their own chain while still being interoperable with the main chain. Side chains can handle transactions and smart contracts independently, reducing the load on the main chain and enabling specialized functionalities.
Collaborative Governance
Collaborative governance refers to the process of making collective decisions and managing blockchain networks through the participation and collaboration of multiple stakeholders. It involves mechanisms such as voting, consensus-building, and community-driven decision-making to govern the rules, upgrades, and overall direction of a blockchain network. Collaborative governance aims to ensure inclusivity, transparency, and alignment of interests among network participants.
aptos_framework::aptos_governance
provides a host of out of the box tools for handling proposals, dynamic voting systems, and member rewards. Using it you can implement a DAO and tests in a about a thousand lines of code.
Atomic Swaps
Atomic swaps are a mechanism that allows the exchange of different cryptocurrencies or digital assets between two parties without the need for an intermediary or trusted third party. It enables secure peer-to-peer transactions directly between participants, ensuring that either the entire transaction is executed or none of it occurs. Atomic swaps enhance interoperability and facilitate decentralized exchanges by eliminating the need for centralized intermediaries.
Proofs and Zero-Knowledge
Zero-knowledge proofs are cryptographic techniques that enable a party to prove knowledge of certain information without revealing the actual information itself. They allow for privacy-preserving transactions and interactions on the blockchain, where participants can validate the correctness of a statement or the possession of certain data without disclosing sensitive details. Zero-knowledge proofs enhance confidentiality and confidentiality in blockchain applications, ensuring that privacy-sensitive information remains secure while still being verifiable.
At Movement, we're working on a Zero-Knowledge VM powered by the Move Prover.
Access Control
This section briefly covers some considerations of blockchain access control and highlights places where Movement and this repo may be helpful.
Signatures and certificates
Signatures and certificates play a crucial role in access control on the blockchain. They enable authentication and ensure that only authorized entities can perform specific actions or access certain resources. Signatures provide proof of ownership and authenticity, while certificates verify the identity and permissions of participants. By utilizing strong cryptographic signatures and certificates, blockchain applications can establish secure and tamper-proof access control mechanisms.
Avalanche uses Transport Layer Security, TLS, to protect node-to-node communications from eavesdroppers. The Avalanche virtual machine uses elliptic curve cryptography, specifically
secp256k1
, for its signatures on the blockchain, and signs all messages.
ACLs, RBAC, and ABAC
Access Control Lists (ACLs), Role-Based Access Control (RBAC), and Attribute-Based Access Control (ABAC) are common frameworks used to manage and enforce access control policies in blockchain applications. ACLs define permissions based on a list of entities and their associated access rights. RBAC assigns permissions to roles, allowing for more centralized management of access control. ABAC grants access based on attributes, such as user attributes or environmental conditions. Each framework has its strengths and can be tailored to meet specific access control requirements in a blockchain application.
A rudimentary pattern in Move and Movement development is to assert a contract owner. Using named addresses, @named_address
.
#![allow(unused)] fn main() { script owner_address::important_script { const ERNO_OWNER_ONLY : u64 = 0 fun do_important_thing(signer : address){ assert!(signer == @owner_address, ERNO_OWNER_ONLY); // do thing... } } }
Manipulation of addresses often serves as the basis for Movement access control mechanisms.
The Move language provides host of unique safe ways to implement access controls for resources.
aptos_framework::aptos_governance
provides an easy to use module for governance which can allow for decentralized managed of these control lists.
Permissioned chains
Permissioned chains are blockchain networks that restrict participation to authorized entities only. Unlike public or permissionless blockchains, permissioned chains require participants to obtain explicit permission or be part of a predefined set of trusted entities. Permissioned chains are often employed in enterprise or consortium settings, where privacy, confidentiality, and control over network participants are paramount. By leveraging permissioned chains, blockchain applications can enforce stricter access control and maintain a trusted network environment.
While the Movement testnet is not a permissioned chain, our virtual machines strong access controls--due to the Move language--make it easy to restrict access to select resources.
DeFi
This section treats with a general overview DeFi problems and solution, and the faculties of Movement M1 to help create these solutions.
What is DeFi?
Decentralized Finance (DeFi) refers to a category of financial applications and platforms built on blockchain networks that aim to provide open, permissionless, and decentralized alternatives to traditional financial systems. DeFi leverages the transparency, immutability, and programmability of blockchain technology to enable financial activities without the need for intermediaries or centralized authorities.
Why Decentralize?
Decentralization in DeFi brings several advantages, including increased transparency, censorship resistance, improved accessibility, and reduced reliance on trusted third parties. By removing intermediaries and enabling peer-to-peer transactions, decentralization can enhance efficiency, reduce costs, and empower individuals to have more control over their financial activities.
Financial Applications of Decentralized Systems
Purchasing
Decentralized systems allow for the direct peer-to-peer purchase of digital assets, cryptocurrencies, and other goods or services without the need for intermediaries or central authorities.
Lending
DeFi platforms enable individuals to lend and borrow funds directly from other users through smart contracts, removing the need for traditional financial intermediaries such as banks.
Check out
examples/movement/gimme_mini
for a toy implementation of both a direct and peer-pooled lending platform.
#![allow(unused)] fn main() { // example supply function from gimme_mini direct lending platform public fun supply( account: &signer, market_obj: Object<Market>, underlying_fa: FungibleAsset ): FungibleAsset acquires Vault, Market { assert!( fungible_asset::asset_metadata(&underlying_fa) == fungible_asset::store_metadata(market_obj), ERR_MARKET_MISMATCH ); let underlying_amount = fungible_asset::amount(&underlying_fa); // update market fungible store fungible_asset::deposit(market_obj, underlying_fa); // mint ctoken let ctoken_amount = underlying_to_ctoken(market_obj, underlying_amount); let market = borrow_global_mut<Market>(object::object_address(&market_obj)); let ctoken = fungible_asset::mint(&market.ctoken_mint_ref, ctoken_amount); // update user vault init_vault_if_not_exists(account); let vault = borrow_global_mut<Vault>(signer::address_of(account)); if (!vector::contains(&vault.collaterals, &market_obj)) { vector::push_back(&mut vault.collaterals, market_obj); }; ctoken } }
#![allow(unused)] fn main() { module peer_pooled_lend::peer_pooled_lend { use std::signer; friend mini_lend::LoanOfficer; /// Lends funds to liquidity pool. /// 1. LoanOfficer will... /// 1. Check if account has enough funds to lend. /// 2. Check for suspicious activity. /// 2. If account has enough funds to lend... /// 1. MiniLend will transfer funds from account to liquidity pool. public fun lend(account : signer, amount : u64){ // ... } /// Allows lender to seek repayment from liquidity pool. /// 1. LoanOfficer will... /// 1. Determine whether account is lender. /// 2. Determine loan period is up. /// 2. If the account is a valid lender and the loan period is up... /// 1. MiniLend will transfer funds from liquidity pool to account or self-collateralize. public fun seek_repayment(account : signer, amount : u64){ // ... } /// Borrows funds from liquidity pool. /// 1. LoanOfficer will... /// 1. Check if account has enough collateral /// 2. Check account credit. /// 3. If account has enough collateral and credit... /// 2. If account has enough collateral and credit... /// 1. MiniLend will borrow funds from liquidity pool /// 3. Whether or not the account will successully borrow funds, run the audit function. public fun borrow(account : signer, amount : u64){ // ... } public fun repay(account : signer, amount : u64){ // ... } /// Looks over loan tables and dispatches events to manage loans /// Anyone can call this function enabling decentralized book keeping. public fun audit(account : signer){ // ... } } }
Trading
Decentralized exchanges (DEXs) facilitate trustless trading of digital assets directly between users, eliminating the need for centralized order books and custody of funds by intermediaries.
Movement has an ready-made DEX.
DeFi Phenomena
Yield Farming
Yield farming involves leveraging various DeFi protocols to maximize returns on cryptocurrencies or digital assets by providing liquidity, staking, or participating in other activities to earn additional rewards.
Check out
examples/movement/yield_gardner
for a toy implementation of a yield farmer.
Flash Loans
Flash loans are uncollateralized loans that allow users to borrow funds temporarily for specific transactions within a single blockchain transaction. They exploit the composability and fast transaction finality of smart contracts.
Automated Market Making
Automated market makers (AMMs) are decentralized protocols that use mathematical formulas to determine asset prices and provide liquidity for trading. They have revolutionized liquidity provision in DeFi by eliminating the need for order books and enabling continuous trading.
Check out
examples/movement/gimme_mini
for a toy implementation of a lending platform built atop theliquidswap
AMM.
Coins
Coins are typically created and recorded on the blockchain through a consensus mechanism, ensuring their authenticity and immutability. They can be transferred between participants on the network, used for transactions, and sometimes serve additional purposes such as voting rights or access to certain functionalities within decentralized applications (dApps) built on the blockchain. Coins play a fundamental role in enabling economic activity and incentivizing participation within the blockchain ecosystem.
Movement provides built-in utilties to easily create and managed coins at varying levels of abstraction.
#![allow(unused)] fn main() { // A managed coin. //:!:>sun module sun_coin::sun_coin { struct SunCoin {} fun init_module(sender: &signer) { aptos_framework::managed_coin::initialize<SunCoin>( sender, b"Sun Coin", b"SUN", 6, false, ); } } //<:!:sun }
#![allow(unused)] fn main() { // Handling coin transactions script { use aptos_framework::coin; use std::vector; // There are two ways to approach this problem // 1. Withdraw the total then distribute the pieces by breaking it up or // 2. Transfer for each amount individually fun main<CoinType>(sender: &signer, split : vector<address>, amount: u64) { let i = 0; let len = vector::length(split); let coins = coin::withdraw<CoinType>(sender, amount); while (i < len) { let coins_pt = coin::extract(&mut coins, amount / len); coin::deposit( vector::borrow(split, i), coins_pt ); }; } } }
Stablecoins are a type of coin that aims to maintain a stable value, typically pegged to a fiat currency like the US Dollar or a basket of assets. They provide price stability, making them suitable for various use cases within the decentralized finance ecosystem.
Important DeFi Algorithms
Constant Product Market Maker (CPMM) and Constant Mean Market Maker (CMMM)
These algorithms are used in AMMs to maintain liquidity and determine prices based on the constant product or mean principles.
Fraud Proof and Binary Search
These algorithms enhance security in DeFi protocols by detecting and mitigating potential fraud or malicious activities. Binary search is often used to optimize search and validation processes.
Modern Portfolio Theory (MPT)
MPT is applied in DeFi to optimize asset allocation and portfolio management, considering risk, returns, and correlations among different assets.
Risk Models
DeFi relies on various risk models and methodologies to assess and manage risks associated with lending, borrowing, and other financial activities in decentralized systems.
Week 4: Projects
Week 4 is all about building projects. If you're reading through this notebook outside of our hackathon, we hope you find the resources useful for building your first Movement project.
Project Requirements and Recommendations
In this section we discuss project requirements and recommendations. Many of the recommendations involve working on technologies that we're keen on developing and incubating for Movement!
Requirements
For all hackathon participants, we expect...
- a pitch deck,
- a paper,
- links to source developed during the hackathon.
We've included templates for LaTeX
and PPT
in the templates/presentation
folder. You will make five minute pitches using your deck at the end of the hackathon.
Project recommendations
There are a variety of ways to use and contribute to the Movement ecosystem. These include dApp development, infrastructure development, bounties, and direct contributions to our software.
dApps
The content of this course should have prepared you to work on dApps such as the following.
- Continue with 💻 GimmeMini,
examples/movement/gimme_mini
- Continue with 💻 MiniDex,
examples/movement/mini_dex
. - Create a mini oracle provider, rollup provider, or validator set management system. You may wish to continue from 💻 NewsMoves
examples/movement/new_moves
- Decentralized Twitter clone. You may fish to continue from 💻 MyFirstDapp
examples/movement/my_first_dapp
.
Infrastructure
We're all about continuing to build out the Movement ecosystem to empower more awesome developers like you. At the time of writing, the following are all things we have a close eye on.
- Connect a new wallet! Right now we only support Pontem on testnet.
- Create an L2 that allows us to deposit levered funds in another protocol.
- Create a framework for borrowing funds against an asset.
- Create a framework that allows for customizable liquidity strategies.
- Build integrations with Perp.
- Develop a stable swap with another subnet
Bounties
We've also put together a zealy.io community. Some of our bounties include.
- Report UI/UX bug
- Report backend bug
- Deploy App with 100 unique smart contract interactions
- Community Award To 3 Most used dApps
- Screenshot using App
- Deploy project from Aptos / Sui
- Contribute to Champions
Contribute
We welcome contributions to our source, including the movement-hack repo from which this book is sourced. Generally speaking we encourage you to...
- Develop new starter code and example modules.
- Submit PRs against open issues.
- Improve documentation.
Guidelines
This section outlines some best practices for the project and Movement success, as well as how to get in touch with the team for support.
Requirements Gathering
Requirements gathering is a critical phase in the software development process that involves capturing, analyzing, and documenting the needs and expectations of stakeholders. It lays the foundation for a successful project by ensuring a clear understanding of what needs to be built. Here are some steps for successful requirements gathering:
-
Identify Stakeholders: Identify and involve all relevant stakeholders, including clients, end-users, subject matter experts, and other project team members. Each stakeholder brings unique perspectives and insights into the project requirements.
-
Define the Scope: Clearly define the scope of the project by outlining its objectives, boundaries, and limitations. This helps set realistic expectations and ensures that the requirements gathering process remains focused.
-
Conduct Interviews and Workshops: Engage in one-on-one interviews and group workshops with stakeholders to gather their input, understand their needs, and identify any existing challenges or pain points. Encourage open and honest communication to capture comprehensive requirements.
-
Document Requirements: Document the gathered requirements in a structured manner, ensuring clarity, completeness, and traceability. Use techniques such as use cases, user stories, functional and non-functional requirements, and process flows to capture different aspects of the system.
-
Validate and Verify: Validate the gathered requirements by reviewing them with stakeholders to ensure accuracy and alignment with their expectations. Seek their feedback and address any concerns or discrepancies. Verify the requirements against the project objectives and constraints.
-
Prioritize Requirements: Prioritize the requirements based on their importance, urgency, and impact on project success. Collaborate with stakeholders to establish a clear understanding of the most critical and high-priority features.
-
Iterate and Refine: Requirements gathering is an iterative process. Continuously refine and iterate the requirements based on feedback, changing project needs, and evolving stakeholder requirements. Regularly communicate and collaborate with stakeholders to ensure ongoing alignment.
Remember, successful requirements gathering involves effective communication, active listening, and collaboration with stakeholders throughout the process. By following these steps, developers can gather comprehensive and accurate requirements that form a solid foundation for successful software development. If you require further assistance or guidance, feel free to reach out to Movement Labs using the contact information below in Support.
Pitching and presenting
Crafting a paper and deck
Crafting a concise and visually appealing pitch deck is crucial for effectively presenting your small blockchain project at the end of a hackathon. Structure your deck to highlight the problem your project solves, the solution you've developed, the target market, and the unique value it offers. Emphasize the key features and benefits of your project, along with any notable achievements during the hackathon. Use clear and concise language, visual aids, and a consistent design to enhance the overall presentation.
We've provided a templates for LaTeX and PPT in
presentations/templates
.
The pitch
During the pitch, focus on delivering a compelling and concise message that captures the attention of the judges and participants. Clearly articulate the problem your project addresses, the solution it provides, and how it leverages blockchain technology. Highlight the practical applications, benefits, and potential market opportunities of your project. Demonstrate how your project differentiates itself from existing solutions and how it aligns with the hackathon's theme or challenges. Be confident, enthusiastic, and passionate about your project, conveying the potential impact and value it brings.
The presentation
When delivering your presentation, aim for clarity, professionalism, and effective communication. Begin with a concise and attention-grabbing introduction to hook the audience. Provide a brief overview of your project's development process, focusing on the key milestones achieved during the hackathon. Showcase any live demos, prototypes, or working features of your project, demonstrating its functionality and value proposition. Highlight the technical aspects of your project, such as the blockchain technology used, any smart contracts implemented, and the scalability or security measures in place. Conclude your presentation with a compelling call-to-action, inviting the judges and participants to engage with your project and explore its potential.
Support
We're happy to support all developers on Movement. Whatever your problems, needs or desires, we hope we can help out.
General outreach
For general questions, comments, and concerns please email liam@movementlabs.xyz or open up a new discussion in movemntdev/movement-hack.
Bugs
If you believe you've identified a bug, please create an issue in movemntdev/movement-hack. We will triage accordingly.
Next Steps
Now that you've completed this book and your projects, we invite you to provide your feedback and stay in touch!