Skip to content

Commit

Permalink
Merge pull request #1 from script3/review
Browse files Browse the repository at this point in the history
Review
  • Loading branch information
mootz12 authored Apr 5, 2024
2 parents 79f4a99 + e6bf2e3 commit eebe8eb
Show file tree
Hide file tree
Showing 50 changed files with 1,307 additions and 1,982 deletions.
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. <[email protected]>"]
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

0 comments on commit eebe8eb

Please sign in to comment.