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
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.
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
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.
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/envInstall Move: Clone the Move repository and install Move CLI.
git clone https://github.com/move-language/move.git cd move/language cargo install --path .Install Aptos CLI: Install Aptos CLI using the following command:
Configure Aptos CLI: Initialize Aptos CLI with a new account.
aptos init
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.
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.
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:
Create Vesting: Define the schedule, payments, receiver, and deposit the total amount. (
create_vestingfunction)Release Fund: Verify the receiver and pay the amount due on the specified date. (
release_fundfunction)

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.
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};
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, andcoin_typeare addresses representing the sender, receiver, and type of token.release_timesis a vector of UNIX timestamps in ascending order.release_amountsis the scheduled amount corresponding to the times.total_amountis the sum of all release amounts.resource_capis the signer capability of the escrow or resource account.released_amountkeeps 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.
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);
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;
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"
You can find the test code inside the tests folder in the repository.
// 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);
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

