-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from script3/review
Review
- Loading branch information
Showing
50 changed files
with
1,307 additions
and
1,982 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
mod test_claim; | ||
mod test_initialize; | ||
mod test_set_unlocks; |
Oops, something went wrong.