Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MESH-1170/ POLYX vesting smart contract #1388

Merged
merged 17 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions contracts/polyx-vesting/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Ignore build artifacts from the local tests sub-crate.
/target/

# Ignore backup files creates by cargo fmt.
**/*.rs.bk

# Remove Cargo.lock when creating an executable, leave it for libraries
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
Cargo.lock
35 changes: 35 additions & 0 deletions contracts/polyx-vesting/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "polyx-vesting"
version = "0.1.0"
authors = ["Polymesh Association"]
edition = "2021"

[dependencies]
ink_primitives = { version = "3.3", default-features = false }
ink_metadata = { version = "3.3", default-features = false, features = ["derive"], optional = true }
ink_env = { version = "3.3", default-features = false }
ink_storage = { version = "3.3", default-features = false }
ink_lang = { version = "3.3", default-features = false }

scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }

[lib]
name = "polyx_vesting"
path = "lib.rs"
crate-type = [
# Used for normal contract Wasm blobs.
"cdylib",
]

[features]
default = ["std"]
std = [
"ink_metadata/std",
"ink_env/std",
"ink_storage/std",
"ink_primitives/std",
"scale/std",
"scale-info/std",
]
ink-as-dependency = []
38 changes: 38 additions & 0 deletions contracts/polyx-vesting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Example contract for Polyx vesting.

## Build

Install [`cargo-contract`](https://github.com/paritytech/cargo-contract).
```
cargo install cargo-contract --force
```

Build the contract:
`cargo +nightly contract build --release`

Contract file needed for deployment `./target/ink/polyx_vesting.contract`.

## Deployment and setup.

Needed:

1. Upload and deploy the contract file `polyx_vesting.contract`.
2. For deployment use the `new(beneficiary_address, start_timestamp, duration_seconds)` contructor with the beneficiary address, start and duration times.
3. Enter the Polyx amount that would be available for the contract.

Top up contract by sending funds to the contract address.

## Usage

1. Call `release()` function to release Polyx.
2. Call `releasable()` function to show the amount of releasable Polyx.
3. Call `vested_amount(timestamp)` function to calculates the amount of Polyx that has already vested.
4. Call `released()` function to return the amount of Polyx already released.
5. Call `start()` function to return the start timestamp.
6. Call `duration()` function to return the vesting duration.
7. Call `beneficiary()` function to return the beneficiary address.

## Test

Test the contract:
`cargo +nightly contract test`
248 changes: 248 additions & 0 deletions contracts/polyx-vesting/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
#![cfg_attr(not(feature = "std"), no_std)]

use ink_lang as ink;

#[ink::contract]
mod polyx_vesting {
use ink_storage::traits::SpreadAllocate;

/// Defines the storage of your contract.
#[ink(storage)]
#[derive(SpreadAllocate)]
pub struct PolyxVesting {
released: Balance,
beneficiary: AccountId,
start: Timestamp,
duration: Timestamp,
}

/// Event emitted when Polyx is released.
#[ink(event)]
pub struct PolyxReleased {
value: Balance,
}

/// The error types.
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
/// Returned if not enough balance to fulfill a request is available.
InsufficientBalance,
/// Funds are not released as yet.
FundsNotReleased,
/// Invalid start timestamp
InvalidStartTimestamp,
/// Duration Overflow
DurationOverflow,
/// Invalid timestamp
InvalidTimestamp,
}

/// The contract result type.
pub type Result<T> = core::result::Result<T, Error>;

impl PolyxVesting {
/// Constructor
#[ink(constructor, payable)]
pub fn new(
beneficiary_address: AccountId,
start_timestamp: Timestamp,
duration_milli_seconds: Timestamp,
) -> Self {
// We use unwrap as ink! <= v3.4 doesn't support returning Result
// But it is fixed in v4.0 https://github.com/paritytech/ink/pull/1446
ink_lang::utils::initialize_contract(|contract| {
Self::new_init(
contract,
beneficiary_address,
start_timestamp,
duration_milli_seconds,
)
})
.unwrap()
}

fn new_init(
&mut self,
beneficiary_address: AccountId,
start_timestamp: Timestamp,
duration_milli_seconds: Timestamp,
) -> Result<()> {
self.beneficiary = beneficiary_address;
self.start = start_timestamp;
self.duration = duration_milli_seconds;
self.validation()
}

fn validation(&self) -> Result<()> {
let add_result = self.start.checked_add(self.duration);
// Ensure start and duration not over type limit
if self.start < ink_env::block_timestamp::<ink_env::DefaultEnvironment>() {
return Err(Error::InvalidStartTimestamp);
} else if add_result.is_none() {
return Err(Error::DurationOverflow);
} else {
Ok(())
}
}

// Getters
/// Returns the vesting duration.
#[ink(message)]
pub fn duration(&self) -> Timestamp {
self.duration
}

/// Returns the start timestamp.
#[ink(message)]
pub fn start(&self) -> Timestamp {
self.start
}

/// Returns the beneficiary address.
#[ink(message)]
pub fn beneficiary(&self) -> AccountId {
self.beneficiary
}

/// Returns the amount of POLYX already released.
#[ink(message)]
pub fn released(&self) -> Balance {
self.released
}

/// Returns the amount of releasable POLYX.
#[ink(message)]
pub fn releasable(&self) -> Result<Balance> {
Ok(self
.vested_amount(self.env().block_timestamp().into())?
.saturating_sub(self.released()))
}

/// Release the native token (POLYX) that have already vested.
#[ink(message)]
pub fn release(&mut self) -> Result<()> {
let amount = self.releasable()?;
if amount == 0 {
return Err(Error::FundsNotReleased);
}
self.released += amount;
Self::env().emit_event(PolyxReleased { value: amount });
if self.env().transfer(self.beneficiary, amount).is_err() {
Err(Error::InsufficientBalance)
} else {
Ok(())
}
}

/// Calculates the amount of tokens that has already vested.
#[ink(message)]
pub fn vested_amount(&self, timestamp: Timestamp) -> Result<Balance> {
Ok(self.vesting_schedule(
self.env().balance().saturating_add(self.released),
timestamp,
)?)
}

/// This returns the amount vested.
fn vesting_schedule(
&self,
total_allocation: Balance,
timestamp: Timestamp,
) -> Result<Balance> {
if timestamp < self.start {
return Ok(0);
} else if timestamp > self.start.saturating_add(self.duration) {
return Ok(total_allocation.into());
} else {
return Ok(
(total_allocation * (timestamp.saturating_sub(self.start)) as u128)
/ self.duration as u128,
);
}
}
}

#[cfg(test)]
mod tests {
/// Imports all the definitions from the outer scope so we can use them here.
use super::*;

/// Imports `ink_lang` so we can use `#[ink::test]`.
use ink_lang as ink;

fn next_x_block(x: u8) {
for _i in 0..x {
ink_env::test::advance_block::<ink_env::DefaultEnvironment>();
}
}

fn beneficiary_balance(beneficiary_address: AccountId) -> Balance {
ink_env::test::get_account_balance::<ink_env::DefaultEnvironment>(beneficiary_address)
.expect("failed to get account balance")
}

/// We test if the new constructor does its job.
#[ink::test]
fn new_works() {
let accounts = ink_env::test::default_accounts::<ink_env::DefaultEnvironment>();
// Constructor works.
let polyx_vesting = PolyxVesting::new(accounts.alice, 24, 80);
// Ensure the values are stored correctly
assert_eq!(polyx_vesting.beneficiary(), accounts.alice);
assert_eq!(polyx_vesting.start(), 24);
assert_eq!(polyx_vesting.duration(), 80);
assert_eq!(polyx_vesting.released(), 0);
}

/// We test if vesting does its job.
#[ink::test]
fn vesting_works() {
let accounts = ink_env::test::default_accounts::<ink_env::DefaultEnvironment>();
// Constructor works.
let mut polyx_vesting = PolyxVesting::new(accounts.alice, 24, 80);

// Ensure error when calling relase before start of vesting period
assert_eq!(polyx_vesting.release(), Err(Error::FundsNotReleased));

// Check beneficiary current balance
let old_balance = beneficiary_balance(accounts.alice);

next_x_block(5);

// Release Polyx
assert_eq!(polyx_vesting.release(), Ok(()));
JMoore96 marked this conversation as resolved.
Show resolved Hide resolved

// Check beneficiary updated balance
let new_balance = beneficiary_balance(accounts.alice);

assert_eq!(polyx_vesting.released(), new_balance - old_balance);

next_x_block(6);

assert_eq!(polyx_vesting.release(), Ok(()));
}

#[ink::test]
fn validation_works() {
let accounts = ink_env::test::default_accounts::<ink_env::DefaultEnvironment>();
// Constructor works.
let mut polyx_vesting_1 = PolyxVesting::new(accounts.alice, 24, 80);
let mut polyx_vesting_2 = PolyxVesting::new(accounts.alice, 24, 80);

next_x_block(5);

// Ensure error when calling release() with start timestamp over 1 year.
assert_eq!(
polyx_vesting_1.new_init(accounts.alice, 1, 80),
Err(Error::InvalidStartTimestamp)
);

// Ensure error when calling release() with duration timestamp over 1 year.
assert_eq!(
polyx_vesting_2.new_init(accounts.alice, Timestamp::MAX, 80),
Err(Error::DurationOverflow)
);
}
}
}