Cover photo

Getting Started with Aptos Blockchain: A Simple Guide to Writing Smart Contracts

Introduction

This article is designed to help you start writing smart contracts on the Aptos Blockchain. Along the way, we'll be creating a "token-vesting" program. The "Token-Vesting" program was the first open-source contract from the Mokshya Protocol. You can view the complete code here:

https://github.com/mokshyaprotocol/aptos-token-vesting

Understanding Aptos and Move

Play Video

Aptos is a Layer 1 blockchain known for its high throughput and reliability. It leverages a novel smart contract language called Move, which is designed for secure and verifiable execution. Move is designed to prevent common vulnerabilities in smart contract programming and provides a safe, high-performance environment for writing blockchain applications.

Key Differences Between Aptos and Ethereum

Aptos and Ethereum differ significantly in their underlying technologies and design philosophies. Aptos uses a novel consensus protocol called AptosBFT, derived from DiemBFT, which aims for high throughput and low latency. In contrast, Ethereum is transitioning from Proof of Work (PoW) to Proof of Stake (PoS) with Ethereum 2.0 to enhance scalability and energy efficiency.

Aptos employs the Move programming language, designed for security and verifiability, making it easier to write and verify safe smart contracts. Ethereum, on the other hand, primarily uses Solidity and Vyper, which are widely adopted but have been prone to security vulnerabilities.

Why move?

https://aptos.dev/en/build/smart-contracts/why-move

Installation Guide for Move and Aptos CLI

https://aptos.dev/en/build/get-started/developer-setup

Before we dive into the token-vesting program, we need to set up our development environment.

  1. Install Rust: Move language is developed using Rust, so we need to install Rust first.

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    source $HOME/.cargo/env
    
  2. Install Move: Clone the Move repository and install Move CLI.

    git clone https://github.com/move-language/move.git
    cd move/language
    cargo install --path .
    
  3. Install Aptos CLI: Install Aptos CLI using the following command:

  4. Configure Aptos CLI: Initialize Aptos CLI with a new account.

    aptos init
    

What is Token Vesting?

Aptos defines commonly known and used tokens as “coins” and its native token, “APT”, is defined in the aptos_coin module. You can create, mint, freeze, transfer, and destroy your own tokens through the coin module.

In Aptos, Modules are like smart contracts. We can create and publish “modules” which are independent entities with logic that can be called from the frontend for executing various transactions.

Token Vesting Concept

Suppose Alice needs to pay Bob 1000 tokens (coin) named DeveloperDAO Token "CODE" on a different schedule:

  • Jul 1: 200 CODE

  • Jul 30: 300 CODE

  • Aug 12: 400 CODE

  • Aug 21: 100 CODE

Bob needs assurance that payments will be made on time, and Alice worries that if she pays everything upfront, Bob might not complete the work. A better solution is a contract that ensures scheduled payments. This is the fundamental concept of token vesting - trustless scheduled payments from one party to another.

Play Video

How Does Token Vesting Work?

Alice sends the schedule with the release amounts to the Aptos blockchain, which is stored. Alice also deposits 1000 CODE tokens in a resource account acting as a trustless escrow. Bob can withdraw the designated amounts as per the schedule. We need two main functions for this:

  1. Create Vesting: Define the schedule, payments, receiver, and deposit the total amount. (create_vesting function)

  2. Release Fund: Verify the receiver and pay the amount due on the specified date. (release_fund function)

Initializing and Importing Dependencies

post image

First, create a folder named token-vesting. In your terminal inside the token-vesting folder, run the following command:

aptos move init --name token-vesting

This initializes a basic structure for your Move project. Replace the content of Move.toml with the following:

[package]
name = 'token-vesting'
version = '1.0.0'

[addresses]
token_vesting = "_"
Std = "0x1"
aptos_std = "0x1"

[dependencies]
AptosFramework = { local = "../../aptos-core/aptos-move/framework/aptos-framework" }

Now you are ready to start writing the smart contract. In the sources folder, create a file named token-vesting.move.

Defining the Module

We start by defining the module with the identifier token_vesting. The module name must match the name in the addresses segment of Move.toml.

module token_vesting::vesting {
}

Inside the module, we define all the dependencies we will be using:

use std::signer;
use aptos_framework::account;
use std::vector;
use aptos_framework::managed_coin;
use aptos_framework::coin;
use aptos_std::type_info;
use aptos_std::simple_map::{Self, SimpleMap};

Defining the Structure

To save data regarding the vesting contract, Aptos provides the struct option which can be used to define various data structures. In our case:

// All information required for Vesting
struct VestingSchedule has key, store {
    sender: address,  
    receiver: address, 
    coin_type: address,
    release_times: vector<u64>,   // Times for token release
    release_amounts: vector<u64>, // Corresponding amounts for release
    total_amount: u64,            // Total amount
    resource_cap: account::SignerCapability, // Signer
    released_amount: u64,         // Sum of released amounts
}
  • sender, receiver, and coin_type are addresses representing the sender, receiver, and type of token.

  • release_times is a vector of UNIX timestamps in ascending order.

  • release_amounts is the scheduled amount corresponding to the times.

  • total_amount is the sum of all release amounts.

  • resource_cap is the signer capability of the escrow or resource account.

  • released_amount keeps track of the amount already withdrawn by Bob.

Abilities in Move define the boundaries of a data struct. In our case, VestingSchedule has the abilities store and key.

// Map to store seed and corresponding resource account address
struct VestingCap has key {
    vestingMap: SimpleMap<vector<u8>, address>,
}

This struct saves the seed and corresponding resource address. SimpleMap is used here for efficiency.

// Errors
const ENO_INSUFFICIENT_FUND: u64 = 0;
const ENO_NO_VESTING: u64 = 1;
const ENO_SENDER_MISMATCH: u64 = 2;
const ENO_RECEIVER_MISMATCH: u64 = 3;
const ENO_WRONG_SENDER: u64 = 4;
const ENO_WRONG_RECEIVER: u64 = 5;

Each error is defined with a u64 number for easy identification during transaction execution.

Create Vesting Function

In Aptos, different identifiers are used to define a function based on the access granted. Our function needs to be called by the user Alice, so it is defined as an entry function. In Aptos Move, the transaction signer comes as the input of the entry function as &signer, which in our case is Alice. As Move requires the struct to be pre-defined, we acquire VestingCap. CoinType in our case is the Mokshya coin.

public entry fun create_vesting<CoinType>(
    account: &signer,
    receiver: address,
    release_amounts: vector<u64>,
    release_times: vector<u64>,
    total_amount: u64,
    seeds: vector<u8>
) acquires VestingCap {
}

Firstly, we generate a resource account that will act as the escrow, i.e., vesting. Alice's account and seeds are used to create a resource account. The !exists<VestingCap>(account_addr) command verifies whether the VestingCap struct already exists in Alice’s account. If it doesn’t, move_to moves the struct into Alice’s account. borrow_global_mut brings the mutable reference of the struct inside Alice’s account, and the seed and corresponding vesting address are added to the simple map for future access.

let account_addr = signer::address_of(account);
let (vesting, vesting_cap) = account::create_resource_account(account, seeds); // Resource account
let vesting_address = signer::address_of(&vesting);
if (!exists<VestingCap>(account_addr)) {
    move_to(account, VestingCap { vestingMap: simple_map::create() })
};
let maps = borrow_global_mut<VestingCap>(account_addr);
simple_map::add(&mut maps.vestingMap, seeds, vesting_address);

vesting_signer_from_cap is the signer capability of the resource account vesting.

let vesting_signer_from_cap = account::create_signer_with_capability(&vesting_cap);

Next, we verify the length of release_amounts and release_times, and ensure release_amounts is equal to the total amount.

let length_of_schedule = vector::length(&release_amounts);
let length_of_times = vector::length(&release_times);
assert!(length_of_schedule == length_of_times, ENO_INSUFFICIENT_FUND);
let i = 0;
let total_amount_required = 0;
while (i < length_of_schedule) {
    let tmp = *vector::borrow(&release_amounts, i);
    total_amount_required = total_amount_required + tmp;
    i = i + 1;
};
assert

!(total_amount_required == total_amount, ENO_INSUFFICIENT_FUND);

We then derive the coin_address using a helper function and save all the information in the resource account vesting.

let released_amount = 0;
let coin_address = coin_address<CoinType>();
move_to(&vesting_signer_from_cap, VestingSchedule {
    sender: account_addr,
    receiver,
    coin_type: coin_address, 
    release_times,
    release_amounts,
    total_amount,
    resource_cap: vesting_cap,
    released_amount,
});

The helper function for deriving coin_address is defined below.

/// A helper function that returns the address of CoinType.
fun coin_address<CoinType>(): address {
    let type_info = type_info::type_of<CoinType>();
    type_info::account_address(&type_info)
}

Finally, we transfer the coin from Alice to the vesting escrow. We first register the coin in the resource account, then transfer the CODE token to the vesting resource account.

managed_coin::register<CoinType>(&vesting_signer_from_cap); 
coin::transfer<CoinType>(account, vesting_address, total_amount);

Release Fund Function

This function is called by Bob to get his vested fund. As the function needs information on both the structs, they need to be acquired at the function definition.

public entry fun release_fund<CoinType>(
    receiver: &signer,
    sender: address,
    seeds: vector<u8>
) acquires VestingSchedule, VestingCap {
}

We borrow the VestingSchedule from the vesting resource account and its signer capability to release the funds. Similarly, we verify the sender and receiver.

let receiver_addr = signer::address_of(receiver);
assert!(exists<VestingCap>(sender), ENO_NO_VESTING);
let maps = borrow_global<VestingCap>(sender);
let vesting_address = *simple_map::borrow(&maps.vestingMap, &seeds);
assert!(exists<VestingSchedule>(vesting_address), ENO_NO_VESTING);  
let vesting_data = borrow_global_mut<VestingSchedule>(vesting_address); 
let vesting_signer_from_cap = account::create_signer_with_capability(&vesting_data.resource_cap);
assert!(vesting_data.sender == sender, ENO_SENDER_MISMATCH);
assert!(vesting_data.receiver == receiver_addr, ENO_RECEIVER_MISMATCH);

Next, we calculate the amount of funds that the receiver can receive up to this time. For instance, if the date is Aug 12, the amount to be released should be the sum of all amounts, i.e., 900 CODE.

let length_of_schedule = vector::length(&vesting_data.release_amounts);
let i = 0;
let amount_to_be_released = 0;
let now = aptos_framework::timestamp::now_seconds();
while (i < length_of_schedule) {
    let tmp_amount = *vector::borrow(&vesting_data.release_amounts, i);
    let tmp_time = *vector::borrow(&vesting_data.release_times, i);
    if (tmp_time <= now) {
        amount_to_be_released = amount_to_be_released + tmp_amount;
    };
    i = i + 1;
};
amount_to_be_released = amount_to_be_released - vesting_data.released_amount;

If Bob has already received, say, on July 30, the amount released at that time (500 CODE) must be deducted. So, the amount to be released will be 400.

Finally, we register the CODE token in Bob’s account and transfer the funds from the vesting resource account to Bob’s address.

if (!coin::is_account_registered<CoinType>(receiver_addr)) {
    managed_coin::register<CoinType>(receiver); 
};
coin::transfer<CoinType>(&vesting_signer_from_cap, receiver_addr, amount_to_be_released);
vesting_data.released_amount = vesting_data.released_amount + amount_to_be_released;

Publishing the Module

Now we are ready to publish the module. Use the following command in the terminal:

aptos move publish --named-addresses token_vesting="0xaddress_obtained_in_above_command"

Interacting with the Module

You can find the test code inside the tests folder in the repository.

Creating Vesting

// Alice is account 1 and Bob is account 2
await faucetClient.fundAccount(account1.address(), 1000000000); // Airdropping
// Time and Amounts
const now = Math.floor(Date.now() / 1000);
// Any discrete amount and corresponding time 
// can be provided to get a variety of payment schedules
const release_amount = [10000, 50000, 10000, 30000];
const release_time_increment = [3, 20, 30];
var release_time = [BigInt(now)];
release_time_increment.forEach((item) => {
    let val = BigInt(now + item);
    release_time.push(val);
});
const create_vesting_payloads = {
    type: "entry_function_payload",
    function: pid + "::vesting::create_vesting",
    type_arguments: ["0x1::aptos_coin::AptosCoin"],
    arguments: [account2.address(), release_amount, release_time, 100000, "xyz"],
};
let txnRequest = await client.generateTransaction(account1.address(), create_vesting_payloads);
let bcsTxn = AptosClient.generateBCSTransaction(account1, txnRequest);
await client.submitSignedBCSTransaction(bcsTxn);

Release Funds

await faucetClient.fundAccount(account2.address(), 1000000000); // Airdropping
// The receiver gets allocated funds as required
const create_getfunds_payloads = {
    type: "entry_function_payload",
    function: pid + "::vesting::release_fund",
    type_arguments: ["0x1::aptos_coin::AptosCoin"],
    arguments: [account1.address(), "xyz"],
};
let txnRequest = await client.generateTransaction(account2.address(), create_getfunds_payloads);
let bcsTxn = AptosClient.generateBCSTransaction(account2, txnRequest);
await client.submitSignedBCSTransaction(bcsTxn);

Watch the video below if you want to learn more about programming Move

Play Video

References

  1. Aptos Official Documentation

  2. Move Language Documentation

  3. Aptos CLI GitHub Repository

  4. Mokshya Protocol GitHub Repository

  5. Aptos Token Standards

  6. Rust Programming Language