Token Vesting Strategies
Sui에서 토큰을 출시할 계획이라면, 토큰의 장기적 전망을 강화하기 위해 베스팅 전략을 구현하는 것을 고려할 수 있다. 베스팅 전략은 일반적으로 토큰을 한 번에 전부 배포하는 대신, 팀 구성원, 투자자 또는 기타 초기 이해관계자에게 시간을 두고 순차적으로 토큰을 배포한다.
베스팅 전략을 구현하고 그 세부 사항을 공개하는 것은 다음과 같은 효과가 있다:
- 토큰에 대한 장기적인 참여를 보장한다.
- 시장 덤핑을 방지한다.
- 대량의 초기 토큰을 즉시 인출하여 토큰 가치를 떨어뜨리는 러그 풀(토큰 가치를 해치는 초기 토큰의 대량 즉시 인출)에 대한 우려를 완화한다.
- 이해관계자의 인센티브를 프로젝트의 성공과 정렬시킨다.
Vesting options
토큰 출시 시 사용할 수 있는 다양한 베스팅 전략이 있다. 프로젝트에 가장 적합한 옵션은 프로젝트와 그 목표에 고유한 여러 요소에 따라 달라진다.
다음 섹션에서는 Sui 네트워크에서 토큰을 출시할 때 고려할 수 있는 몇 가지 옵션을 설명한다.
Cliff vesting
클리프 베스팅은 전체 토큰 또는 자산이 일정 기간(“cliff”)이 지난 후 한 번에 사용 가능해지는 상황을 의미한다. 클리프 기간이 도래하기 전까지는 어떠한 토큰도 Release되지 않는다.
한 프로젝트의 10명 직원 각각은 1년 클리프 조건으로 1,000개의 토큰을 부여받는다. 1년이 지나면 이들은 1,000개의 토큰 전체를 받는다. 1년이 지나기 전에는 이 토큰에 접근할 수 없다.
다음 smart contract는 토큰 Release를 위한 클리프 베스팅 일정을 구현한다. 이 모듈에는 베스팅할 코인의 총합과 클리프 시점을 타임스탬프로 전달하는 new_wallet 함수가 포함돼 있다. 그 후 클리프 날짜가 과거 시점이라면 claim 함수를 호출해 Wallet에서 토큰을 회수할 수 있다.
위 예시 시나리오를 고려하면, 각 직원마다 별도의 wallet이 존재하도록 new_wallet을 10번 호출하면 된다. 각 호출마다 1,000개의 토큰을 포함해 wallet에 필요한 자금을 적재하면 된다. 해당 wallet을 사용한 이후의 claim호출은 현재 시각과 Wallet object의 cliff_time을 비교해, 클리프 시간이 현재 시각보다 늦은 경우 wallet에서 토큰을 반환한다.
Details
cliff.move
/// ===========================================================================================
/// Module: cliff
/// Description:
/// This module defines a vesting strategy in which the entire amount is vested after
/// a specific time has passed.
///
/// Functionality:
/// - Defines a cliff vesting schedule.
/// ===========================================================================================
module vesting::cliff;
use sui::balance::Balance;
use sui::clock::Clock;
use sui::coin::{Self, Coin};
// === Errors ===
#[error]
const EInvalidCliffTime: vector<u8> = b"Cliff time must be in the future.";
// === Structs ===
/// [Owned] Wallet contains coins that are available for claiming over time.
public struct Wallet<phantom T> has key, store {
id: UID,
// Amount of coins remaining in the wallet
balance: Balance<T>,
// Cliff time when the entire amount is vested
cliff_time: u64,
// Amount of coins that have been claimed
claimed: u64,
}
// === Public Functions ===
/// Create a new wallet with the given coins and cliff time.
/// Note that full amount of coins is stored in the wallet when it is created;
/// it is just that the coins need to be claimable after the cliff time.
///
/// @aborts with `EInvalidCliffTime` if the cliff time is not in the future.
public fun new_wallet<T>(
coins: Coin<T>,
clock: &Clock,
cliff_time: u64,
ctx: &mut TxContext,
): Wallet<T> {
assert!(cliff_time > clock.timestamp_ms(), EInvalidCliffTime);
Wallet {
id: object::new(ctx),
balance: coins.into_balance(),
cliff_time,
claimed: 0,
}
}
/// Claim the coins that are available for claiming at the current time.
public fun claim<T>(self: &mut Wallet<T>, clock: &Clock, ctx: &mut TxContext): Coin<T> {
let claimable_amount = self.claimable(clock);
self.claimed = self.claimed + claimable_amount;
coin::from_balance(self.balance.split(claimable_amount), ctx)
}
/// Calculate the amount of coins that can be claimed at the current time.
public fun claimable<T>(self: &Wallet<T>, clock: &Clock): u64 {
let timestamp = clock.timestamp_ms();
if (timestamp < self.cliff_time) return 0;
self.balance.value()
}
/// Delete the wallet if it is empty.
public fun delete_wallet<T>(self: Wallet<T>) {
let Wallet { id, balance, cliff_time: _, claimed: _ } = self;
id.delete();
balance.destroy_zero();
}
// === Accessors ===
/// Get the balance of the wallet.
public fun balance<T>(self: &Wallet<T>): u64 {
self.balance.value()
}
/// Get the cliff time of the vesting schedule.
public fun cliff_time<T>(self: &Wallet<T>): u64 {
self.cliff_time
}
Graded vesting
점진적 베스팅은 베스팅 기간 동안 토큰을 종종 동일한 비율로 점진적으로 Release할 수 있도록 한다.
한 직원이 1,200개의 토큰을 받으며, 4년에 걸쳐 매년 300개의 토큰이 베스팅된다. 매년 말마다 300개의 토큰이 사용 가능해진다.
다음 Hybrid vesting 섹션에는 점진적 베스팅을 수행하는 방법을 보여 주는 smart contract가 포함돼 있다.
Hybrid vesting
하이브리드 베스팅은 클리프 베스팅과 단계적 베스팅과 같은 서로 다른 베스팅 모델을 결합한다. 이를 통해 토큰이 시간에 따라 어떻게 풀릴지에 대한 유연성을 제공한다.
전체 토큰의 50%는 1년 클리프 이후에 풀리고, 나머지는 이후 3년에 걸쳐 선형적으로 분배된다.
다음 smart contract는 하이브리드 베스팅 모델을 생성한다. 클리브 베스팅 smart contract와 마찬가지로, 하이브리드 모델은 각 이해관계자의 모든 토큰을 보관하기 위한 Walletstruct를 정의한다. 그러나 이 월렛은 실제로 서로 다른 베스팅 규칙을 따르는 두 개의 별도 월렛을 포함하고 있다. 이 contract에 대해new_wallet을 호출할 때는 클리프가 종료되는 타임스탬프, 선형 스케줄이 시작되는 타임스탬프, 그리고 선형 베스팅이 종료되어야 하는 타임스탬프를 제공한다. 이후 claim 을 호출하면 이 파라미터 범위에 해당하는 토큰의 합계를 반환한다.
Details
hybrid.move
/// ===========================================================================================
/// Module: hybrid
/// Description:
/// This module defines a vesting strategy in which half of the tokens are cliff vested,
/// and the other half are linearly vested.
///
/// Functionality:
/// - Defines a hybrid vesting schedule.
/// ===========================================================================================
module vesting::hybrid;
use sui::clock::Clock;
use sui::coin::{Self, Coin};
use vesting::cliff;
use vesting::linear;
// === Structs ===
/// [Owned] Wallet contains coins that are available for claiming over time.
public struct Wallet<phantom T> has key, store {
id: UID,
// A wallet that uses cliff vesting for the first half of the balance
cliff_vested: cliff::Wallet<T>,
// A wallet that uses linear vesting for the second half of the balance
linear_vested: linear::Wallet<T>,
}
// === Public Functions ===
/// Create a new wallet with the given coins and vesting duration.
/// Note that full amount of coins is stored in the wallet when it is created;
/// it is just that the coins need to be claimed over time.
/// The first half of the coins are cliff vested, which takes start_cliff time to vest.
/// The second half of the coins are linearly vested, which starts at start_linear time
public fun new_wallet<T>(
coins: Coin<T>,
clock: &Clock,
start_cliff: u64,
start_linear: u64,
duration_linear: u64,
ctx: &mut TxContext,
): Wallet<T> {
let mut balance = coins.into_balance();
let balance_cliff = balance.value() * 50 / 100;
Wallet {
id: object::new(ctx),
cliff_vested: cliff::new_wallet(
coin::from_balance(balance.split(balance_cliff), ctx),
clock,
start_cliff,
ctx,
),
linear_vested: linear::new_wallet(
coin::from_balance(balance, ctx),
clock,
start_linear,
duration_linear,
ctx,
),
}
}
/// Claim the coins that are available for claiming at the current time.
public fun claim<T>(self: &mut Wallet<T>, clock: &Clock, ctx: &mut TxContext): Coin<T> {
let mut coin_cliff = self.cliff_vested.claim(clock, ctx);
let coin_linear = self.linear_vested.claim(clock, ctx);
coin_cliff.join(coin_linear);
coin_cliff
}
/// Calculate the amount of coins that can be claimed at the current time.
public fun claimable<T>(self: &Wallet<T>, clock: &Clock): u64 {
self.cliff_vested.claimable(clock) + self.linear_vested.claimable(clock)
}
/// Delete the wallet if it is empty.
public fun delete_wallet<T>(self: Wallet<T>) {
let Wallet {
id,
cliff_vested,
linear_vested,
} = self;
cliff_vested.delete_wallet();
linear_vested.delete_wallet();
id.delete();
}
// === Accessors ===
/// Get the balance of the wallet.
public fun balance<T>(self: &Wallet<T>): u64 {
self.cliff_vested.balance() + self.linear_vested.balance()
}
Backloaded vesting
Backloaded vesting은 시간에 걸쳐 균등하게 분배하기보다는 베스팅 기간 말에 대부분의 토큰을 분배한다. 이 접근 방식은 다량의 토큰이 언락되기 전에 생태계가 더 성숙해지도록 돕는다. 팀 구성원과 이해관계자는 초기에도 보상을 받을 수 있지만, 프로젝트와 더 오랜 기간을 함께한 사람일수록 가장 큰 보상이 주어진다.
한 직원의 토큰은 다음 스 케줄에 따라 Release된다:
- 첫 3년 동안 10%
- 4년 차에 90%
Backloaded vesting을 위한 smart contract는 베스팅할 모든 토큰을 담고 있는 상위 wallet 안에 두 개의 Wallet object들을 생성한다. 각 자식 wallet은 자신의 베스팅 스케줄을 담당한다. new_wallet을 호출할 때는 베스팅할 코인과 start_front, start_back, duration, back_percentage 값을 함께 전달한다. 제공한 값에 따라, wallet 소유자가 claim 함수를 호출했을 때 contract는 얼마나 많은 토큰을 반환할지를 결정한다.
예시 시나리오에서는 frontload 시작 타임스탬프와 backload의 시작 타임스탬프(frontload 시작 후 3년)를 전달할 수 있다. 또한 4년의 기간 (126230400000)과 back_percentage 값으로 90을 전달한다.
Details
backloaded.move
/// ===========================================================================================
/// Module: backloaded
/// Description:
/// This module defines a vesting strategy in which the majority amount is vested
/// near the end of a vesting period.
/// The vesting schedule is split into two portions: the front portion and the back portion.
/// Each portion is implemented using linear vesting schedules.
///
/// Functionality:
/// - Defines a backloaded vesting schedule.
/// ===========================================================================================
module vesting::backloaded;
use sui::clock::Clock;
use sui::coin::{Self, Coin};
use vesting::linear;
// === Errors ===
#[error]
const EInvalidBackStartTime: vector<u8> =
b"Start time of back portion must be after front portion.";
#[error]
const EInvalidPercentageRange: vector<u8> = b"Percentage range must be between 50 to 100.";
#[error]
const EInsufficientBalance: vector<u8> = b"Not enough balance for vesting.";
#[error]
const EInvalidDuration: vector<u8> = b"Duration must be long enough to complete back portion.";
// === Structs ===
/// [Owned] Wallet contains coins that are available for claiming over time.
public struct Wallet<phantom T> has key, store {
id: UID,
// A wallet that stores the front (first) portion of the balance
front: linear::Wallet<T>,
// A wallet that stores the back (last) portion of the balance
back: linear::Wallet<T>,
// Time when the vesting started
start_front: u64,
// Time when the back portion of the vesting started; start_front < start_back
start_back: u64,
// Total duration of the vesting schedule
duration: u64,
// Percentage of balance that is vested in the back portion; value is between 50 and 100
back_percentage: u8,
}
// === Public Functions ===
/// Create a new wallet with the given coins and vesting duration.
/// Full amount of coins is stored in the wallet when it is created;
/// but the coins are claimed over time.
///
/// When the front portion is vested over a short period of time
/// such that `duration - start_back > start_back - start_front`, then
/// more coins might be claimed in the front portion than the back portion.
/// To prevent this case, make sure that the back portion has higher percentage of the balance
/// via `back_percentage`.
///
/// @aborts with `EInvalidBackStartTime` if the back start time is before the front start time.
/// @aborts with `EInvalidPercentageRange` if the percentage range is not between 50 to 100.
/// @aborts with `EInvalidDuration` if the duration is not long enough to complete the back portion.
/// @aborts with `EInsufficientBalance` if the balance is not enough to split into front and back portions.
public fun new_wallet<T>(
coins: Coin<T>,
clock: &Clock,
start_front: u64,
start_back: u64,
duration: u64,
back_percentage: u8,
ctx: &mut TxContext,
): Wallet<T> {
assert!(start_back > start_front, EInvalidBackStartTime);
assert!(back_percentage > 50 && back_percentage <= 100, EInvalidPercentageRange);
assert!(duration > start_back - start_front, EInvalidDuration);
let mut balance = coins.into_balance();
let balance_back = balance.value() * (back_percentage as u64) / 100;
let balance_front = balance.value() - balance_back;
assert!(balance_front > 0 && balance_back > 0, EInsufficientBalance);
Wallet {
id: object::new(ctx),
front: linear::new_wallet(
coin::from_balance(balance.split(balance_front), ctx),
clock,
start_front,
start_back - start_front,
ctx,
),
back: linear::new_wallet(
coin::from_balance(balance, ctx),
clock,
start_back,
duration - (start_back - start_front),
ctx,
),
start_front,
start_back,
duration,
back_percentage,
}
}
/// Claim the coins that are available for claiming at the current time.
public fun claim<T>(self: &mut Wallet<T>, clock: &Clock, ctx: &mut TxContext): Coin<T> {
let mut coin_front = self.front.claim(clock, ctx);
let coin_back = self.back.claim(clock, ctx);
coin_front.join(coin_back);
coin_front
}
/// Calculate the amount of coins that can be claimed at the current time.
public fun claimable<T>(self: &Wallet<T>, clock: &Clock): u64 {
self.front.claimable(clock) + self.back.claimable(clock)
}
/// Delete the wallet if it is empty.
public fun delete_wallet<T>(self: Wallet<T>) {
let Wallet {
id,
front,
back,
start_front: _,
start_back: _,
duration: _,
back_percentage: _,
} = self;
front.delete_wallet();
back.delete_wallet();
id.delete();
}
// === Accessors ===
/// Get the balance of the wallet.
public fun balance<T>(self: &Wallet<T>): u64 {
self.front.balance() + self.back.balance()
}
/// Get the start time of the vesting schedule.
public fun start<T>(self: &Wallet<T>): u64 {
self.start_front
}
/// Get the duration of the vesting schedule.
public fun duration<T>(self: &Wallet<T>): u64 {
self.duration
}
Milestone- or performance-based vesting
성과 기반 베스팅에서는 매출 목표 달성이나 프로젝트 단계 진행과 같이 특정 목표나 지표를 달성할 때 베스팅 이벤트가 발생한다.
한 팀의 토큰은 월간 활성 사용자(MAU) 수에 비례해 베스팅된다. 플랫폼이 1,000만 MAU 목표를 달성하면 모든 토큰이 베스팅된다.
마찬가지로 마일스톤 기반 베스팅은 시간 기반 조건이 아니라 특정 프로젝트 또는 개인 마일스톤이 달성될 때 베스팅 이벤트를 생성한다.
블록체인 프로젝트의 Mainnet이 출시될 때 토큰이 언락된다.
다른 예와 마찬가지로, 다음 스마트 컨트랙트는 분배할 코인을 보관할 월렛을 생성한다. 그러나 다른 예와 달리 이 Wallet object에는 마일스톤 진행 상황을 업데이트할 권한을 가진 계정의 address를 설정하는 milestone_controller 필드가 포함된다. 무결성 검사로 지갑이 마일스톤 업데이트 권한이 있는 엔티티와 동일한 address를 갖는 경우 new_wallet 함수 호출은 오류로 중단된다.
마일스톤 업데이트 권한을 가진 주체는 update_milestone_percentage 를 호출해 완료 비율 값을 갱신할 수 있다. 베스팅된 토큰 월렛의 소유자는 현재 완료 비율 값에 따라 언락된 토큰을 얻기 위해 claim을 호출할 수 있다. 첫 번째 예시 시나리오를 기준으로 할 때, 프로젝트가 MAU 100만 명을 달성할 때마다 마일스톤 값을 10퍼센트씩 갱신할 수 있다. 두 번째 시나리오에서도 같은 컨트랙트를 사용하되, mainnet이 출시된 이후에 한 번만 비율을 100으로 업데이트할 수 있다.
Details
milestone.move
/// ===========================================================================================
/// Module: milestone
/// Description:
/// This module defines a vesting strategy that allows users to claim coins
/// as the milestones are achieved.
///
/// Functionality:
/// - Defines a milestone-based vesting schedule.
/// ===========================================================================================
module vesting::milestone;
use sui::balance::Balance;
use sui::coin::{Self, Coin};
// === Errors ===
#[error]
const EOwnerIsController: vector<u8> = b"Owner cannot be the milestone controller.";
#[error]
const EUnauthorizedOwner: vector<u8> = b"Unauthorized owner.";
#[error]
const EUnauthorizedMilestoneController: vector<u8> = b"Unauthorized milestone controller.";
#[error]
const EMilestonePercentageRange: vector<u8> = b"Invalid milestone percentage.";
#[error]
const EInvalidNewMilestone: vector<u8> =
b"New milestone must be greater than the current milestone.";
// === Structs ===
/// [Shared] Wallet contains coins that are available for claiming.
public struct Wallet<phantom T> has key, store {
id: UID,
// Amount of coins remaining in the wallet
balance: Balance<T>,
// Amount of coins that have been claimed
claimed: u64,
// Achieved milestone in percentage from 0 to 100
milestone_percentage: u8,
// Owner of the wallet
owner: address,
// Milestone controller of the wallet
milestone_controller: address,
}
// === Public Functions ===
/// Create a new wallet with the given coins and vesting duration.
/// Note that full amount of coins is stored in the wallet when it is created;
/// it is just that the coins need to be claimed as the milestones are achieved.
///
/// @aborts with `EOwnerIsController` if the owner is same as the milestone controller.
public fun new_wallet<T>(
coins: Coin<T>,
owner: address,
milestone_controller: address,
ctx: &mut TxContext,
) {
assert!(owner != milestone_controller, EOwnerIsController);
let wallet = Wallet {
id: object::new(ctx),
balance: coins.into_balance(),
claimed: 0,
milestone_percentage: 0,
owner,
milestone_controller,
};
transfer::share_object(wallet);
}
/// Claim the coins that are available based on the current milestone.
///
/// @aborts with `EUnauthorizedUser` if the sender is not the owner of the wallet.
public fun claim<T>(self: &mut Wallet<T>, ctx: &mut TxContext): Coin<T> {
assert!(self.owner == ctx.sender(), EUnauthorizedOwner);
let claimable_amount = self.claimable();
self.claimed = self.claimed + claimable_amount;
coin::from_balance(self.balance.split(claimable_amount), ctx)
}
/// Calculate the current amount of coins that can be claimed.
public fun claimable<T>(self: &Wallet<T>): u64 {
// Convert the balance to u128 to account for overflow in the calculation
let claimable: u128 =
(self.balance.value() + self.claimed as u128) * (self.milestone_percentage as u128) / 100;
// Adjust the claimable amount by subtracting the already claimed amount
(claimable as u64) - self.claimed
}
/// Update the milestone percentage of the wallet.
///
/// @aborts with `EUnauthorizedMilestoneController` if the sender is not the milestone controller.
/// @aborts with `EMilestonePercentageRange` if the new milestone percentage is invalid.
/// @aborts with `EInvalidNewMilestone` if the new milestone is not greater than the current milestone.
public fun update_milestone_percentage<T>(
self: &mut Wallet<T>,
percentage: u8,
ctx: &mut TxContext,
) {
assert!(self.milestone_controller == ctx.sender(), EUnauthorizedMilestoneController);
assert!(percentage > 0 && percentage <= 100, EMilestonePercentageRange);
assert!(percentage > self.milestone_percentage, EInvalidNewMilestone);
self.milestone_percentage = percentage;
}
/// Delete the wallet if it is empty.
public fun delete_wallet<T>(self: Wallet<T>) {
let Wallet {
id,
balance,
claimed: _,
milestone_percentage: _,
owner: _,
milestone_controller: _,
} = self;
id.delete();
balance.destroy_zero();
}
// === Accessors ===
/// Get the remaining balance of the wallet.
public fun balance<T>(self: &Wallet<T>): u64 {
self.balance.value()
}
/// Get the start time of the vesting schedule.
public fun milestone<T>(self: &Wallet<T>): u8 {
self.milestone_percentage
}
/// Get the owner of the wallet.
public fun get_owner<T>(self: &Wallet<T>): address {
self.owner
}
/// Get the milestone controller of the wallet.
public fun get_milestone_controller<T>(self: &Wallet<T>): address {
self.milestone_controller
}
Linear vesting
선형 베스팅에서는 미리 정해진 기간에 걸쳐 토큰이 점진적으로 풀린다.
한 프로젝트의 10명 직원 각각은 1년 클리프 조건으로 1,000개의 토큰을 부여받는다.
선형 베스팅 smart contract는 start와 duration 필드를 가진 Wallet object를 생성한다. Contract는 이러한 값과 현재 시간을 함께 사용해 베스팅된 토큰의 수를 결정한다. 이 경우 현재 시간은 월렛 소유자가 claim 함수를 호출하는 시점을 의미한다.
예시 시나리오에서는 1,000개의 토큰, 직원 시작일의 타임스탬프, 그리고 1년(31557600000)의 기간을 동안 wallet( new_wallet)을 생성한다.
Details
linear.move
/// ===========================================================================================
/// Module: linear
/// Description:
/// This module defines a vesting strategy that allows users to claim coins linearly over time.
///
/// Functionality:
/// - Defines a linear vesting schedule.
/// ===========================================================================================
module vesting::linear;
use sui::balance::Balance;
use sui::clock::Clock;
use sui::coin::{Self, Coin};
// === Errors ===
#[error]
const EInvalidStartTime: vector<u8> = b"Start time must be in the future.";
// === Structs ===
/// [Owned] Wallet contains coins that are available for claiming over time.
public struct Wallet<phantom T> has key, store {
id: UID,
// Amount of coins remaining in the wallet
balance: Balance<T>,
// Time when the vesting started
start: u64,
// Amount of coins that have been claimed
claimed: u64,
// Total duration of the vesting schedule
duration: u64,
}
// === Public Functions ===
/// Create a new wallet with the given coins and vesting duration.
/// Note that full amount of coins is stored in the wallet when it is created;
/// it is just that the coins need to be claimed over time.
///
/// @aborts with `EInvalidStartTime` if the start time is not in the future.
public fun new_wallet<T>(
coins: Coin<T>,
clock: &Clock,
start: u64,
duration: u64,
ctx: &mut TxContext,
): Wallet<T> {
assert!(start > clock.timestamp_ms(), EInvalidStartTime);
Wallet {
id: object::new(ctx),
balance: coins.into_balance(),
start,
claimed: 0,
duration,
}
}
/// Claim the coins that are available for claiming at the current time.
public fun claim<T>(self: &mut Wallet<T>, clock: &Clock, ctx: &mut TxContext): Coin<T> {
let claimable_amount = self.claimable(clock);
self.claimed = self.claimed + claimable_amount;
coin::from_balance(self.balance.split(claimable_amount), ctx)
}
/// Calculate the amount of coins that can be claimed at the current time.
public fun claimable<T>(self: &Wallet<T>, clock: &Clock): u64 {
let timestamp = clock.timestamp_ms();
if (timestamp < self.start) return 0;
if (timestamp >= self.start + self.duration) return self.balance.value();
let elapsed = timestamp - self.start;
// Convert the balance to u128 to account for overflow in the calculation
// Note that the division by zero is not possible because when duration is zero, the balance is returned above
let claimable: u128 =
(self.balance.value() + self.claimed as u128) * (elapsed as u128) / (self.duration as u128);
// Adjust the claimable amount by subtracting the already claimed amount
(claimable as u64) - self.claimed
}
/// Delete the wallet if it is empty.
public fun delete_wallet<T>(self: Wallet<T>) {
let Wallet { id, start: _, balance, claimed: _, duration: _ } = self;
id.delete();
balance.destroy_zero();
}
// === Accessors ===
/// Get the remaining balance of the wallet.
public fun balance<T>(self: &Wallet<T>): u64 {
self.balance.value()
}
/// Get the start time of the vesting schedule.
public fun start<T>(self: &Wallet<T>): u64 {
self.start
}
/// Get the duration of the vesting schedule.
public fun duration<T>(self: &Wallet<T>): u64 {
self.duration
}
Immediate vesting
즉시 베스팅에서는 모든 토큰이 할당되는 즉시 베스팅되며, 곧바로 전량이 사용 가능해진다.
초기 투자자는 구매 시점에 할당된 토큰 전량을 받는다.
즉시 베스팅에서는 언제든지 토큰을 수동으로 address로 전송하는 방식으로 처리할 수 있다. 그러나 smart contract 방식을 선택하면 수동 전송보다 여러 가지 이점을 얻을 수 있다.
- Transaction이 온체인에 저장되어 이해관계자가 검증할 수 있으므로 투명성과 책임성이 향상된다. Smart contract 로직이 Transaction의 정확한 목적을 식별한다.
- 특정 조건을 강제할 수 있다. 예를 들어 계약 조건 수락과 같은 일부 조건이 충족된 이후에만 100% 완료로 업데이트하는 milestone-based vesting contract를 만들 수 있다.
- 규정 준수 및 보고를 위한 감사 가능한 기록을 제공한다.
- agreement 조건이 변경될 때 claim 이전에 다른 토큰으로 변환하는 등의 다른 작업을 수행할 수 있는 유연성을 제공한다.
다음 테스트는 linear vesting smart contract 예제를 활용하여, 다른 베스팅 전략 smart contract 중 하나를 사용해 즉시 베스팅을 지원하는 방법을 보여 준다. 이 테스트는 vesting::linear의 Wallet object와 new_wallet 함수를 사용해 즉시 베스팅 시나리오를 수행한다. 테스트는 duration 값을 0으로 설정함으로써 이를 구현한다.
Details
immediate_tests.move
#[test_only]
module vesting::immediate_tests;
use sui::clock;
use sui::coin;
use sui::sui::SUI;
use sui::test_scenario as ts;
use vesting::linear::{new_wallet, Wallet};
public struct Token has key, store { id: UID }
const OWNER_ADDR: address = @0xAAAA;
const CONTROLLER_ADDR: address = @0xBBBB;
const FULLY_VESTED_AMOUNT: u64 = 10_000;
const VESTING_DURATION: u64 = 0;
const START_TIME: u64 = 1;
fun test_setup(): ts::Scenario {
let mut ts = ts::begin(CONTROLLER_ADDR);
let coins = coin::mint_for_testing<SUI>(FULLY_VESTED_AMOUNT, ts.ctx());
let now = clock::create_for_testing(ts.ctx());
let wallet = new_wallet(coins, &now, START_TIME, VESTING_DURATION, ts.ctx());
transfer::public_transfer(wallet, OWNER_ADDR);
now.destroy_for_testing();
ts
}
#[test]
fun test_immediate_vesting() {
let mut ts = test_setup();
ts.next_tx(OWNER_ADDR);
let mut now = clock::create_for_testing(ts.ctx());
let mut wallet = ts.take_from_sender<Wallet<SUI>>();
// vest immediately
now.set_for_testing(START_TIME);
assert!(wallet.claimable(&now) == FULLY_VESTED_AMOUNT);
assert!(wallet.balance() == FULLY_VESTED_AMOUNT);
let coins = wallet.claim(&now, ts.ctx());
transfer::public_transfer(coins, OWNER_ADDR);
assert!(wallet.claimable(&now) == 0);
assert!(wallet.balance() == 0);
ts.return_to_sender(wallet);
now.destroy_for_testing();
let _end = ts::end(ts);
}