diff --git a/contracts/airdrop/src/contract.rs b/contracts/airdrop/src/contract.rs index afe707a81..dcf00bfac 100644 --- a/contracts/airdrop/src/contract.rs +++ b/contracts/airdrop/src/contract.rs @@ -5,8 +5,8 @@ use shade_protocol::{ QueryMsg, Config } }; -use crate::{state::{config_w, reward_w, claim_status_w}, - handle::{try_update_config, try_add_tasks, try_complete_task, try_claim}, +use crate::{state::{config_w, reward_w, claim_status_w, user_total_claimed_w, total_claimed_w}, + handle::{try_update_config, try_add_tasks, try_complete_task, try_claim, try_decay}, query }; use shade_protocol::airdrop::RequiredTask; @@ -34,12 +34,26 @@ pub fn init( return Err(StdError::GenericErr { msg: "tasks above 100%".to_string(), backtrace: None }) } + // Store the delegators list + let mut airdrop_total = Uint128::zero(); + for reward in msg.rewards { + let key = reward.address.to_string(); + + reward_w(&mut deps.storage).save(key.as_bytes(), &reward)?; + airdrop_total += reward.amount; + user_total_claimed_w(&mut deps.storage).save(key.as_bytes(), &Uint128::zero())?; + // Save the initial claim + claim_status_w(&mut deps.storage, 0).save(key.as_bytes(), &false)?; + } + let config = Config{ admin: match msg.admin { None => { env.message.sender.clone() } Some(admin) => { admin } }, + dump_address: msg.dump_address, airdrop_snip20: msg.airdrop_token.clone(), + airdrop_total, task_claim, start_date: match msg.start_time { None => env.block.time, @@ -50,14 +64,8 @@ pub fn init( config_w(&mut deps.storage).save(&config)?; - // Store the delegators list - for reward in msg.rewards { - let key = reward.address.to_string(); - - reward_w(&mut deps.storage).save(key.as_bytes(), &reward)?; - // Save the initial claim - claim_status_w(&mut deps.storage, 0).save(key.as_bytes(), &false)?; - } + // Initialize claim amount + total_claimed_w(&mut deps.storage).save(&Uint128::zero())?; Ok(InitResponse { messages: vec![], @@ -72,13 +80,16 @@ pub fn handle( ) -> StdResult { match msg { HandleMsg::UpdateConfig { - admin, start_date, end_date - } => try_update_config(deps, env, admin, start_date, end_date), + admin, dump_address, + start_date, end_date + } => try_update_config(deps, env, admin, dump_address, + start_date, end_date), HandleMsg::AddTasks { tasks } => try_add_tasks(deps, &env, tasks), HandleMsg::CompleteTask { address } => try_complete_task(deps, &env, address), HandleMsg::Claim { } => try_claim(deps, &env), + HandleMsg::Decay { } => try_decay(deps, &env), } } diff --git a/contracts/airdrop/src/handle.rs b/contracts/airdrop/src/handle.rs index 677734297..1dceeeebd 100644 --- a/contracts/airdrop/src/handle.rs +++ b/contracts/airdrop/src/handle.rs @@ -1,20 +1,21 @@ use cosmwasm_std::{to_binary, Api, Env, Extern, HandleResponse, Querier, StdError, StdResult, Storage, HumanAddr, Uint128}; -use crate::state::{config_r, config_w, reward_r, claim_status_w, claim_status_r}; +use crate::state::{config_r, config_w, reward_r, claim_status_w, claim_status_r, user_total_claimed_w, total_claimed_w, total_claimed_r}; use shade_protocol::airdrop::{HandleAnswer, RequiredTask}; use shade_protocol::generic_response::ResponseStatus; -use secret_toolkit::snip20::mint_msg; +use secret_toolkit::snip20::send_msg; pub fn try_update_config( deps: &mut Extern, env: Env, admin: Option, + dump_address: Option, start_date: Option, end_date: Option, ) -> StdResult { let config = config_r(&deps.storage).load()?; // Check if admin if env.message.sender != config.admin { - return Err(StdError::Unauthorized { backtrace: None }); + return Err(StdError::unauthorized()); } // Save new info @@ -23,6 +24,9 @@ pub fn try_update_config( if let Some(admin) = admin { state.admin = admin; } + if let Some(dump_address)= dump_address { + state.dump_address = Some(dump_address); + } if let Some(start_date) = start_date { state.start_date = start_date; } @@ -50,7 +54,7 @@ pub fn try_add_tasks( let config = config_r(&deps.storage).load()?; // Check if admin if env.message.sender != config.admin { - return Err(StdError::Unauthorized { backtrace: None }); + return Err(StdError::unauthorized()); } config_w(&mut deps.storage).update(|mut config| { @@ -64,7 +68,7 @@ pub fn try_add_tasks( } if count > Uint128(100) { - return Err(StdError::GenericErr { msg: "tasks above 100%".to_string(), backtrace: None }) + return Err(StdError::generic_err("tasks above 100%")) } Ok(config) @@ -95,7 +99,7 @@ pub fn try_complete_task( Ok(false) } else { - Err(StdError::Unauthorized { backtrace: None }) + Err(StdError::unauthorized()) } })?; @@ -109,7 +113,7 @@ pub fn try_complete_task( } // if not found - Err(StdError::NotFound { kind: "task".to_string(), backtrace: None }) + Err(StdError::not_found("task")) } pub fn try_claim( @@ -120,45 +124,90 @@ pub fn try_claim( // Check if airdrop started if env.block.time < config.start_date { - return Err(StdError::Unauthorized { backtrace: None }) + return Err(StdError::unauthorized()) } if let Some(end_date) = config.end_date { if env.block.time > end_date { - return Err(StdError::Unauthorized { backtrace: None }) + return Err(StdError::unauthorized()) } } let user = env.message.sender.clone(); let user_key = user.to_string(); - let eligible_amount = reward_r(&deps.storage).load( - user.to_string().as_bytes())?.amount; + let eligible_amount = reward_r(&deps.storage).load(user_key.as_bytes())?.amount; + let mut claimed_percentage = Uint128::zero(); let mut total = Uint128::zero(); for (i, task) in config.task_claim.iter().enumerate() { // Check if completed - let state = claim_status_r(&deps.storage, i).may_load(user_key.as_bytes())?; + let state = claim_status_r(&deps.storage, i).may_load( + user_key.as_bytes())?; match state { None => {} Some(claimed) => { + claimed_percentage += task.percent; if !claimed { - claim_status_w(&mut deps.storage, i).save(user_key.as_bytes(), &true)?; - total += task.percent.multiply_ratio(eligible_amount, Uint128(100)); + claim_status_w(&mut deps.storage, i).save( + user_key.as_bytes(), &true)?; + total += task.percent.multiply_ratio( + eligible_amount, Uint128(100)); } } }; } - // Redeem - let messages = vec![mint_msg(user, total, - None, 1, - config.airdrop_snip20.code_hash, - config.airdrop_snip20.address)?]; + // Update redeem info + let mut redeem_amount = total; + user_total_claimed_w(&mut deps.storage).update( + user_key.as_bytes(), |total_claimed| { + // Fix division issues + if claimed_percentage == Uint128(100) { + redeem_amount = (eligible_amount - total_claimed.unwrap())?; + } + Ok(total_claimed.unwrap() + redeem_amount) + })?; + + total_claimed_w(&mut deps.storage).update(|total_claimed| { + Ok(total_claimed + redeem_amount) + })?; Ok(HandleResponse { - messages, + messages: vec![send_msg(user, redeem_amount, + None, None, 0, + config.airdrop_snip20.code_hash, + config.airdrop_snip20.address)?], log: vec![], data: Some( to_binary( &HandleAnswer::Claim { status: ResponseStatus::Success } )? ) }) +} + +pub fn try_decay( + deps: &mut Extern, + env: &Env, +) -> StdResult { + let config = config_r(&deps.storage).load()?; + + // Check if airdrop ended + if let Some(end_date) = config.end_date { + if let Some(dump_address) = config.dump_address { + if env.block.time > end_date { + let send_total = (config.airdrop_total - total_claimed_r(&deps.storage).load()?)?; + let messages = vec![send_msg( + dump_address, send_total, None, None, + 1, config.airdrop_snip20.code_hash, + config.airdrop_snip20.address)?]; + + return Ok(HandleResponse { + messages, + log: vec![], + data: Some( to_binary( &HandleAnswer::Decay { + status: ResponseStatus::Success } )? ) + }) + } + } + } + + Err(StdError::unauthorized()) } \ No newline at end of file diff --git a/contracts/airdrop/src/query.rs b/contracts/airdrop/src/query.rs index b191ecb0b..10320e17a 100644 --- a/contracts/airdrop/src/query.rs +++ b/contracts/airdrop/src/query.rs @@ -1,11 +1,13 @@ use cosmwasm_std::{Api, Extern, Querier, StdResult, Storage, HumanAddr, Uint128}; use shade_protocol::airdrop::{QueryAnswer}; use crate::{state::{config_r, reward_r}}; -use crate::state::claim_status_r; +use crate::state::{claim_status_r, total_claimed_r, user_total_claimed_r}; pub fn config (deps: &Extern) -> StdResult { - Ok(QueryAnswer::Config { config: config_r(&deps.storage).load()? + Ok(QueryAnswer::Config { + config: config_r(&deps.storage).load()?, + total_claimed: total_claimed_r(&deps.storage).load()?, }) } @@ -23,7 +25,7 @@ pub fn airdrop_amount let eligible_amount = reward_r(&deps.storage).load(key.as_bytes())?.amount; let mut finished_tasks = vec![]; - let mut claimed = Uint128::zero(); + let claimed = user_total_claimed_r(&deps.storage).load(key.as_bytes())?; let mut unclaimed = Uint128::zero(); let config = config_r(&deps.storage).load()?; @@ -35,8 +37,8 @@ pub fn airdrop_amount let calc = task.percent.multiply_ratio(eligible_amount.clone(), Uint128(100)); match task_claimed { - true => claimed += calc, - false => unclaimed += calc + false => unclaimed += calc, + _ => {} }; } } diff --git a/contracts/airdrop/src/state.rs b/contracts/airdrop/src/state.rs index 1c7f6cd0e..6f4ce14b8 100644 --- a/contracts/airdrop/src/state.rs +++ b/contracts/airdrop/src/state.rs @@ -1,9 +1,11 @@ -use cosmwasm_std::Storage; +use cosmwasm_std::{Storage, Uint128}; use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton, bucket, Bucket, bucket_read, ReadonlyBucket}; use shade_protocol::airdrop::{Config, Reward}; pub static CONFIG_KEY: &[u8] = b"config"; pub static REWARDS_KEY: &[u8] = b"rewards"; +pub static TOTAL_CLAIMED_KEY: &[u8] = b"total_claimed"; +pub static USER_TOTAL_CLAIMED_KEY: &[u8] = b"user_total_claimed"; pub fn config_w(storage: &mut S) -> Singleton { singleton(storage, CONFIG_KEY) @@ -28,4 +30,22 @@ pub fn claim_status_r(storage: & S, index: usize) -> ReadonlyBucket< pub fn claim_status_w(storage: &mut S, index: usize) -> Bucket { bucket(&[index as u8], storage) +} + +// Total claimed +pub fn total_claimed_r(storage: &S) -> ReadonlySingleton { + singleton_read(storage, TOTAL_CLAIMED_KEY) +} + +pub fn total_claimed_w(storage: &mut S) -> Singleton { + singleton(storage, TOTAL_CLAIMED_KEY) +} + +// Total user claimed +pub fn user_total_claimed_r(storage: & S) -> ReadonlyBucket { + bucket_read(USER_TOTAL_CLAIMED_KEY, storage) +} + +pub fn user_total_claimed_w(storage: &mut S) -> Bucket { + bucket(USER_TOTAL_CLAIMED_KEY, storage) } \ No newline at end of file diff --git a/packages/network_integration/Cargo.toml b/packages/network_integration/Cargo.toml index c4c231cf5..071b80990 100644 --- a/packages/network_integration/Cargo.toml +++ b/packages/network_integration/Cargo.toml @@ -12,6 +12,7 @@ default = [] [dependencies] colored = "2.0.0" +chrono = "0.4.19" shade-protocol = { version = "0.1.0", path = "../shade_protocol" } secretcli = { version = "0.1.0", path = "../secretcli" } serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/packages/network_integration/src/contract_helpers/initializer.rs b/packages/network_integration/src/contract_helpers/initializer.rs index 6987dd6b0..9d872c369 100644 --- a/packages/network_integration/src/contract_helpers/initializer.rs +++ b/packages/network_integration/src/contract_helpers/initializer.rs @@ -4,7 +4,6 @@ use shade_protocol::{snip20::{InitialBalance}, snip20, initializer, initializer::Snip20ContractInfo}; use crate::{utils::{print_header, generate_label, print_contract, print_warning, STORE_GAS, GAS, VIEW_KEY, ACCOUNT_KEY, INITIALIZER_FILE}, - contract_helpers::governance::add_contract, contract_helpers::minter::get_balance}; use secretcli::{cli_types::NetContract, secretcli::{test_contract_handle, test_inst_init, list_contracts_by_code}}; diff --git a/packages/network_integration/src/main.rs b/packages/network_integration/src/main.rs deleted file mode 100644 index 1d20dd688..000000000 --- a/packages/network_integration/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Network integration test crate") -} \ No newline at end of file diff --git a/packages/network_integration/tests/testnet_integration.rs b/packages/network_integration/tests/testnet_integration.rs index 1fdd7c240..4a31909ce 100644 --- a/packages/network_integration/tests/testnet_integration.rs +++ b/packages/network_integration/tests/testnet_integration.rs @@ -1,11 +1,9 @@ use colored::*; use serde_json::Result; use cosmwasm_std::{HumanAddr, Uint128, to_binary}; -use secretcli::{cli_types::NetContract, - secretcli::{account_address, query_contract, test_contract_handle, - test_inst_init, list_contracts_by_code}}; -use shade_protocol::{snip20::{InitConfig, InitialBalance}, snip20, governance, staking, - micro_mint, band, oracle, asset::Contract, airdrop, +use secretcli::{secretcli::{account_address, query_contract, test_contract_handle, test_inst_init}}; +use shade_protocol::{snip20::{InitConfig}, snip20, governance, staking, + band, oracle, asset::Contract, airdrop, airdrop::{Reward, RequiredTask}, governance::{UserVote, Vote, ProposalStatus}, generic_response::ResponseStatus}; use network_integration::{utils::{print_header, print_warning, generate_label, print_contract, @@ -18,10 +16,17 @@ use network_integration::{utils::{print_header, print_warning, generate_label, p minter::{initialize_minter, setup_minters, get_balance}, stake::setup_staker}}; use std::{thread, time}; +use chrono; +use shade_protocol::snip20::InitialBalance; #[test] fn run_airdrop() -> Result<()> { let account = account_address(ACCOUNT_KEY)?; + let secondary_account = account_address("b")?; + + let half_airdrop = Uint128(500000); + let full_airdrop = Uint128(1000000); + let all_airdrop = Uint128(2000000); /// Initialize dummy snip20 print_header("\nInitializing snip20"); @@ -31,7 +36,9 @@ fn run_airdrop() -> Result<()> { admin: None, symbol: "TEST".to_string(), decimals: 6, - initial_balances: None, + initial_balances: Some(vec![InitialBalance{ + address: HumanAddr::from(account.clone()), + amount: all_airdrop }]), prng_seed: Default::default(), config: Some(InitConfig { public_total_supply: Some(true), @@ -49,31 +56,34 @@ fn run_airdrop() -> Result<()> { print_contract(&snip); { - let msg = snip20::HandleMsg::SetViewingKey { key: String::from(VIEW_KEY), padding: None }; + let msg = snip20::HandleMsg::SetViewingKey { + key: String::from(VIEW_KEY), + padding: None }; test_contract_handle(&msg, &snip, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; } - /// Assert that we start with nothing - assert_eq!(Uint128(0), get_balance(&snip, account.clone())); - - let half_airdrop = Uint128(500000); - let full_airdrop = Uint128(1000000); - print_header("Initializing airdrop"); + let now = chrono::offset::Utc::now().timestamp() as u64; + let duration = 180; + let airdrop_init_msg = airdrop::InitMsg { admin: None, + dump_address: Some(HumanAddr::from(account.clone())), airdrop_token: Contract { address: HumanAddr::from(snip.address.clone()), code_hash: snip.code_hash.clone() }, start_time: None, - end_time: None, + end_time: Some(now + duration), rewards: vec![Reward { address: HumanAddr::from(account.clone()), amount: full_airdrop + }, Reward { + address: HumanAddr::from(secondary_account), + amount: full_airdrop }], default_claim: Uint128(50), task_claim: vec![RequiredTask { @@ -86,6 +96,18 @@ fn run_airdrop() -> Result<()> { Some("test"))?; print_contract(&airdrop); + /// Assert that we start with nothing + { + test_contract_handle(&snip20::HandleMsg::Send { + recipient: HumanAddr::from(airdrop.address.clone()), + amount: all_airdrop, + msg: None, + memo: None, + padding: None + }, &snip, ACCOUNT_KEY, Some(GAS), Some("test"), None)?; + } + assert_eq!(Uint128(0), get_balance(&snip, account.clone())); + /// Query that airdrop is allowed { let msg = airdrop::QueryMsg::GetEligibility { @@ -103,12 +125,6 @@ fn run_airdrop() -> Result<()> { } } - /// Register airdrop as allowed minter - test_contract_handle(&snip20::HandleMsg::SetMinters { - minters: vec![HumanAddr::from(airdrop.address.clone())], padding: None }, - &snip, ACCOUNT_KEY, Some(GAS), - Some("test"), None)?; - print_warning("Claiming half of the airdrop"); /// Claim airdrop test_contract_handle(&airdrop::HandleMsg::Claim {}, @@ -168,6 +184,16 @@ fn run_airdrop() -> Result<()> { } } + /// Try to claim expired tokens + print_warning("Claiming expired tokens"); + thread::sleep(time::Duration::from_secs(duration)); + + test_contract_handle(&airdrop::HandleMsg::Decay {}, + &airdrop, ACCOUNT_KEY, Some(GAS), + Some("test"), None)?; + + assert_eq!(all_airdrop, get_balance(&snip, account.clone())); + Ok(()) } diff --git a/packages/shade_protocol/src/airdrop.rs b/packages/shade_protocol/src/airdrop.rs index e79573f8f..49f9d5301 100644 --- a/packages/shade_protocol/src/airdrop.rs +++ b/packages/shade_protocol/src/airdrop.rs @@ -21,8 +21,12 @@ pub struct Reward { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct Config { pub admin: HumanAddr, + // Where the decayed tokens will be dumped, if none then nothing happens + pub dump_address: Option, // The snip20 to be minted pub airdrop_snip20: Contract, + // Total claimable amount + pub airdrop_total: Uint128, // Required tasks pub task_claim: Vec, // Checks if airdrop has started / ended @@ -33,12 +37,14 @@ pub struct Config { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct InitMsg { pub admin: Option, + // Where the decayed tokens will be dumped, if none then nothing happens + pub dump_address: Option, pub airdrop_token: Contract, // The airdrop time limit pub start_time: Option, // Can be set to never end pub end_time: Option, - // Secret network delegators snapshot + // Delegators snapshot pub rewards: Vec, // Default gifted amount pub default_claim: Uint128, @@ -55,6 +61,7 @@ impl InitCallback for InitMsg { pub enum HandleMsg { UpdateConfig { admin: Option, + dump_address: Option, start_date: Option, end_date: Option, }, @@ -64,7 +71,8 @@ pub enum HandleMsg { CompleteTask { address: HumanAddr }, - Claim {} + Claim {}, + Decay {}, } impl HandleCallback for HandleMsg { @@ -78,7 +86,8 @@ pub enum HandleAnswer { UpdateConfig { status: ResponseStatus }, AddTask { status: ResponseStatus }, CompleteTask { status: ResponseStatus }, - Claim { status: ResponseStatus } + Claim { status: ResponseStatus }, + Decay { status: ResponseStatus }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -96,7 +105,7 @@ impl Query for QueryMsg { #[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryAnswer { - Config { config: Config }, + Config { config: Config, total_claimed: Uint128 }, Dates { start: u64, end: Option }, Eligibility { // Total eligible