Skip to content
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
35 changes: 21 additions & 14 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
[workspace]
resolver = "2"
[package]
name = "token-lockup"
version = "1.0.0"
authors = ["Script3 Ltd. <gm@script3.io>"]
license = "AGPL-3.0"
edition = "2021"
publish = false

members = [
"token-lockup",
"tests"
]
[lib]
crate-type = ["cdylib", "rlib"]
doctest = false

[profile.release-with-logs]
inherits = "release"
debug-assertions = true
[features]
testutils = ["soroban-sdk/testutils"]

[dependencies]
soroban-sdk = "20.5.0"

[dev_dependencies]
soroban-sdk = { version = "20.5.0", features = ["testutils"] }

[profile.release]
opt-level = "z"
Expand All @@ -20,8 +29,6 @@ panic = "abort"
codegen-units = 1
lto = true

[workspace.dependencies.soroban-sdk]
version = "20.5.0"

[workspace.dependencies.sep-41-token]
version = "0.3.0"
[profile.release-with-logs]
inherits = "release"
debug-assertions = true
9 changes: 2 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,10 @@ test: build
build:
mkdir -p target/wasm32-unknown-unknown/optimized

cargo rustc --manifest-path=token-lockup/Cargo.toml --crate-type=cdylib --target=wasm32-unknown-unknown --release --features standard
cargo rustc --manifest-path=Cargo.toml --crate-type=cdylib --target=wasm32-unknown-unknown --release
soroban contract optimize \
--wasm target/wasm32-unknown-unknown/release/token_lockup.wasm \
--wasm-out target/wasm32-unknown-unknown/optimized/standard_token_lockup.wasm

cargo rustc --manifest-path=token-lockup/Cargo.toml --crate-type=cdylib --target=wasm32-unknown-unknown --release --features blend --no-default-features
soroban contract optimize \
--wasm target/wasm32-unknown-unknown/release/token_lockup.wasm \
--wasm-out target/wasm32-unknown-unknown/optimized/blend_token_lockup.wasm
--wasm-out target/wasm32-unknown-unknown/optimized/token_lockup.wasm

cd target/wasm32-unknown-unknown/optimized/ && \
for i in *.wasm ; do \
Expand Down
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
# soroban-token-lockup

Soroban token lockup is a library implementing a smart contract designed for token via token lockups. It has two implementations:

Standard Lockup Contracts which implement basic lockup functionality by specifying a series of unlocks and the percent of total tokens that can be claimed at each lockup. These can be used as vesting contracts by retaining the admin role, or into lockup contracts by revoking it.

and

Blend Lockup Contracts which enable interactions with Blend Protocols backstop contract. These are used for Blend's ecosystem distribution, but could be adapted for usage with other protocols if teams wish to issue a lockup but still allow the tokens to be utilized in their protocol.
Lockup contract for any SEP-0041 compatible token. The lockup functionality is defined by a series of unlocks and the percent of total tokens that can be claimed at each lockup. These can be used as vesting contracts by retaining the admin role, or into lockup contracts by revoking it.
Binary file removed dependencies/backstop.wasm
Binary file not shown.
Binary file removed dependencies/comet.wasm
Binary file not shown.
Binary file removed dependencies/emitter.wasm
Binary file not shown.
Binary file removed dependencies/pool.wasm
Binary file not shown.
Binary file removed dependencies/pool_factory.wasm
Binary file not shown.
111 changes: 111 additions & 0 deletions src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use crate::{errors::TokenLockupError, storage, types::Unlock, validation::require_valid_unlocks};
use soroban_sdk::{
contract, contractimpl, panic_with_error, token::TokenClient, unwrap::UnwrapOptimized, Address,
Env, Vec,
};

#[contract]
pub struct TokenLockup;

#[contractimpl]
impl TokenLockup {
/********** Constructor **********/

/// Initialize the contract
///
/// ### Arguments
/// * `admin` - The admin of the lockup contract
/// * `owner` - The owner of the lockup contract
/// * `token` - The token to lock up
/// * `unlocks` - A vector of unlocks. Percentages represent the portion of the lockups token balance can be claimed
/// at the given unlock time. If multiple unlocks are claimed at once, the percentages are applied in order.
///
/// ### Errors
/// * AlreadyInitializedError - The contract has already been initialized
/// * InvalidUnlocks - The unlock times do not represent a valid unlock sequence
pub fn initialize(e: Env, admin: Address, owner: Address, unlocks: Vec<Unlock>) {
if storage::get_is_init(&e) {
panic_with_error!(&e, TokenLockupError::AlreadyInitializedError);
}
storage::extend_instance(&e);

require_valid_unlocks(&e, &unlocks);
storage::set_unlocks(&e, &unlocks);
storage::set_admin(&e, &admin);
storage::set_owner(&e, &owner);

storage::set_is_init(&e);
}

/********** Read-Only **********/

/// Get unlocks for the lockup
pub fn unlocks(e: Env) -> Vec<Unlock> {
storage::get_unlocks(&e).unwrap_optimized()
}

/// Get the admin address
pub fn admin(e: Env) -> Address {
storage::get_admin(&e)
}

/// Get the owner address
pub fn owner(e: Env) -> Address {
storage::get_owner(&e)
}

/********** Write **********/

/// (Only admin) Set new unlocks for the lockup. The new unlocks must retain
/// any existing unlocks that have already passed their unlock time.
///
/// ### Arguments
/// * `new_unlocks` - The new unlocks to set
///
/// ### Errors
/// * UnauthorizedError - The caller is not the admin
/// * InvalidUnlocks - The unlock times do not represent a valid unlock sequence
pub fn set_unlocks(e: Env, new_unlocks: Vec<Unlock>) {
storage::get_admin(&e).require_auth();

require_valid_unlocks(&e, &new_unlocks);

storage::set_unlocks(&e, &new_unlocks);
}

/// (Only owner) Claim the unlocked tokens. The tokens are transferred to the owner.
///
/// ### Arguments
/// * `tokens` - A vector of tokens to claim
///
/// ### Errors
/// * UnauthorizedError - The caller is not the owner
/// * NoUnlockedTokens - There are not tokens to claim for a given asset
pub fn claim(e: Env, tokens: Vec<Address>) {
let owner = storage::get_owner(&e);
owner.require_auth();

let unlocks = storage::get_unlocks(&e).unwrap_optimized();
let is_fully_unlocked = unlocks.last_unchecked().time <= e.ledger().timestamp();

for token in tokens.iter() {
let mut claim_amount = 0;
let token_client = TokenClient::new(&e, &token);
let mut balance = token_client.balance(&e.current_contract_address());
if is_fully_unlocked {
claim_amount = balance;
} else {
let last_asset_claim = storage::get_last_claim(&e, &token);
for unlock in unlocks.iter() {
if unlock.time > last_asset_claim && unlock.time <= e.ledger().timestamp() {
let transfer_amount = (balance * unlock.percent as i128) / 10000_i128;
balance -= transfer_amount;
claim_amount += transfer_amount;
}
}
}
storage::set_last_claim(&e, &token, &e.ledger().timestamp());
token_client.transfer(&e.current_contract_address(), &owner, &claim_amount);
}
}
}
6 changes: 3 additions & 3 deletions token-lockup/src/errors.rs → src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub enum TokenLockupError {
BalanceError = 10,
OverflowError = 12,

InvalidPercentError = 100,
InvalidUnlockSequenceError = 101,
InvalidClaimToError = 102,
InvalidUnlocks = 100,
NoUnlockedTokens = 101,
AlreadyUnlocked = 102,
}
14 changes: 14 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#![no_std]

pub mod contract;
mod errors;
mod storage;
mod types;
mod validation;

#[cfg(test)]
extern crate std;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod testutils;
117 changes: 117 additions & 0 deletions src/storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use soroban_sdk::{Address, Env, Symbol, Vec};

use crate::types::Unlock;

/********** Ledger Thresholds **********/

const ONE_DAY_LEDGERS: u32 = 17280; // assumes 5 seconds per ledger

const LEDGER_BUMP: u32 = 120 * ONE_DAY_LEDGERS;
const LEDGER_THRESHOLD: u32 = LEDGER_BUMP - 20 * ONE_DAY_LEDGERS;

/********** Ledger Keys **********/

const OWNER_KEY: &str = "Owner";
const ADMIN_KEY: &str = "Admin";
const IS_INIT_KEY: &str = "IsInit";
const UNLOCKS_KEY: &str = "Unlocks";

/********** Ledger Thresholds **********/

/// Bump the instance lifetime by the defined amount
pub fn extend_instance(e: &Env) {
e.storage()
.instance()
.extend_ttl(LEDGER_THRESHOLD, LEDGER_BUMP);
}

/********** Instance **********/

/// Check if the contract has been initialized
pub fn get_is_init(e: &Env) -> bool {
e.storage().instance().has(&Symbol::new(e, IS_INIT_KEY))
}

/// Set the contract as initialized
pub fn set_is_init(e: &Env) {
e.storage()
.instance()
.set::<Symbol, bool>(&Symbol::new(e, IS_INIT_KEY), &true);
}

/// Get the owner address
pub fn get_owner(e: &Env) -> Address {
e.storage()
.instance()
.get::<Symbol, Address>(&Symbol::new(e, OWNER_KEY))
.unwrap()
}

/// Set the owner address
pub fn set_owner(e: &Env, owner: &Address) {
e.storage()
.instance()
.set::<Symbol, Address>(&Symbol::new(e, OWNER_KEY), &owner);
}

/// Get the admin address
pub fn get_admin(e: &Env) -> Address {
e.storage()
.instance()
.get::<Symbol, Address>(&Symbol::new(e, ADMIN_KEY))
.unwrap()
}

/// Set the admin address
pub fn set_admin(e: &Env, admin: &Address) {
e.storage()
.instance()
.set::<Symbol, Address>(&Symbol::new(e, ADMIN_KEY), &admin);
}

/********** Persistant **********/

/// Get the times of the lockup unlocks
pub fn get_unlocks(e: &Env) -> Option<Vec<Unlock>> {
let key = Symbol::new(e, UNLOCKS_KEY);
let result = e.storage().persistent().get::<Symbol, Vec<Unlock>>(&key);
if result.is_some() {
e.storage()
.persistent()
.extend_ttl(&key, LEDGER_THRESHOLD, LEDGER_BUMP);
}
result
}

/// Set the times of the lockup unlocks
pub fn set_unlocks(e: &Env, unlocks: &Vec<Unlock>) {
let key = Symbol::new(e, UNLOCKS_KEY);
e.storage()
.persistent()
.set::<Symbol, Vec<Unlock>>(&key, unlocks);
e.storage()
.persistent()
.extend_ttl(&key, LEDGER_THRESHOLD, LEDGER_BUMP);
}

/// Get the last claim time for a token
pub fn get_last_claim(e: &Env, token: &Address) -> u64 {
let result = e.storage().persistent().get::<Address, u64>(&token);
match result {
Some(last_claim) => {
e.storage()
.persistent()
.extend_ttl(&token, LEDGER_THRESHOLD, LEDGER_BUMP);
last_claim
}
None => 0,
}
}

/// Set the last claim time for a token
pub fn set_last_claim(e: &Env, token: &Address, time: &u64) {
e.storage().persistent().set::<Address, u64>(&token, time);
e.storage()
.persistent()
.extend_ttl(&token, LEDGER_THRESHOLD, LEDGER_BUMP);
}
3 changes: 3 additions & 0 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod test_claim;
mod test_initialize;
mod test_set_unlocks;
Loading