1#[cfg(not(feature = "library"))]
2use cosmwasm_std::entry_point;
3
4use const_format::concatcp;
5use cosmwasm_std::{
6 from_json, to_json_binary, Addr, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, Event,
7 MessageInfo, Response, StdError, StdResult, Storage, Uint128, WasmMsg,
8};
9use std::{cmp::min, collections::HashMap};
10
11use dexter::{
12 asset::AssetInfo,
13 helper::{
14 build_transfer_token_to_user_msg, claim_ownership, drop_ownership_proposal,
15 propose_new_owner,
16 },
17 multi_staking::{
18 AssetRewardState, AssetStakerInfo, Config, ConfigV1,
19 CreatorClaimableRewardState, Cw20HookMsg, ExecuteMsg, InstantLpUnlockFee, InstantiateMsg,
20 MigrateMsg, QueryMsg, RewardSchedule, TokenLockInfo, UnclaimedReward, ConfigV2_1, ConfigV2_2,
21 },
22};
23
24use cw2::{get_contract_version, set_contract_version};
25use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg};
26use cw_storage_plus::Item;
27use dexter::asset::Asset;
28use dexter::helper::EventExt;
29use dexter::multi_staking::{
30 RewardScheduleResponse, MAX_ALLOWED_LP_TOKENS, MAX_INSTANT_UNBOND_FEE_BP,
31};
32
33use crate::{
34 error::ContractError,
35 state::{
36 next_reward_schedule_id, ASSET_LP_REWARD_STATE, ASSET_STAKER_INFO, CONFIG,
37 CREATOR_CLAIMABLE_REWARD, LP_GLOBAL_STATE, LP_TOKEN_ASSET_REWARD_SCHEDULE,
38 OWNERSHIP_PROPOSAL, REWARD_SCHEDULES, USER_BONDED_LP_TOKENS, USER_LP_TOKEN_LOCKS,
39 },
40};
41use crate::{
42 execute::{
43 unbond::{instant_unbond, unbond},
44 unlock::{instant_unlock, unlock},
45 },
46 query::query_instant_unlock_fee_tiers,
47 utils::calculate_unlock_fee,
48};
49
50pub const CONTRACT_NAME: &str = "dexter-multi-staking";
52
53const CONTRACT_VERSION_V1: &str = "1.0.0";
54const CONTRACT_VERSION_V2: &str = "2.0.0";
55const CONTRACT_VERSION_V2_1: &str = "2.1.0";
56const CONTRACT_VERSION_V2_2: &str = "2.2.0";
57const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
59
60pub type ContractResult<T> = Result<T, ContractError>;
61
62#[cfg_attr(not(feature = "library"), entry_point)]
63pub fn instantiate(
64 deps: DepsMut,
65 _env: Env,
66 info: MessageInfo,
67 msg: InstantiateMsg,
68) -> ContractResult<Response> {
69 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
70
71 if msg.instant_unbond_fee_bp > MAX_INSTANT_UNBOND_FEE_BP {
72 return Err(ContractError::InvalidInstantUnbondFee {
73 max_allowed: MAX_INSTANT_UNBOND_FEE_BP,
74 received: msg.instant_unbond_fee_bp,
75 });
76 }
77
78 if msg.instant_unbond_min_fee_bp > msg.instant_unbond_fee_bp {
79 return Err(ContractError::InvalidInstantUnbondMinFee {
80 max_allowed: msg.instant_unbond_fee_bp,
81 received: msg.instant_unbond_min_fee_bp,
82 });
83 }
84
85 if msg.fee_tier_interval > msg.unlock_period {
86 return Err(ContractError::InvalidFeeTierInterval {
87 max_allowed: msg.unlock_period,
88 received: msg.fee_tier_interval,
89 });
90 }
91
92 deps.api.addr_validate(&msg.keeper_addr.to_string())?;
94
95 CONFIG.save(
96 deps.storage,
97 &Config {
98 keeper: msg.keeper_addr,
99 unlock_period: msg.unlock_period,
100 owner: deps.api.addr_validate(msg.owner.as_str())?,
101 allowed_lp_tokens: vec![],
102 instant_unbond_fee_bp: msg.instant_unbond_fee_bp,
103 instant_unbond_min_fee_bp: msg.instant_unbond_min_fee_bp,
104 fee_tier_interval: msg.fee_tier_interval,
105 },
106 )?;
107
108 Ok(Response::new().add_event(
109 Event::from_info(concatcp!(CONTRACT_NAME, "::instantiate"), &info)
110 .add_attribute("owner", msg.owner.to_string())
111 .add_attribute("unlock_period", msg.unlock_period.to_string())
112 .add_attribute(
113 "minimum_reward_schedule_proposal_start_delay",
114 msg.minimum_reward_schedule_proposal_start_delay.to_string(),
115 ),
116 ))
117}
118
119#[cfg_attr(not(feature = "library"), entry_point)]
120pub fn execute(
121 deps: DepsMut,
122 env: Env,
123 info: MessageInfo,
124 msg: ExecuteMsg,
125) -> ContractResult<Response> {
126 match msg {
127 ExecuteMsg::UpdateConfig {
128 keeper_addr,
129 unlock_period,
130 instant_unbond_fee_bp,
131 instant_unbond_min_fee_bp,
132 fee_tier_interval,
133 } => update_config(
134 deps,
135 env,
136 info,
137 keeper_addr,
138 unlock_period,
139 instant_unbond_fee_bp,
140 instant_unbond_min_fee_bp,
141 fee_tier_interval,
142 ),
143 ExecuteMsg::AllowLpToken { lp_token } => allow_lp_token(deps, env, info, lp_token),
144 ExecuteMsg::RemoveLpToken { lp_token } => remove_lp_token(deps, info, &lp_token),
145 ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg),
146 ExecuteMsg::CreateRewardSchedule {
147 lp_token,
148 title,
149 actual_creator,
150 start_block_time,
151 end_block_time,
152 } => {
153 let config = CONFIG.load(deps.storage)?;
155 if info.sender != config.owner {
156 return Err(ContractError::Unauthorized);
157 }
158
159 if info.funds.len() != 1 {
161 return Err(ContractError::InvalidNumberOfAssets {
162 correct_number: 1,
163 received_number: info.funds.len() as u8,
164 });
165 }
166
167 let sent_asset = info.funds[0].clone();
168 let creator = match actual_creator {
169 Some(creator) => deps.api.addr_validate(&creator.to_string())?,
170 None => info.sender.clone(),
171 };
172
173 create_reward_schedule(
174 deps,
175 env,
176 info,
177 lp_token,
178 title,
179 start_block_time,
180 end_block_time,
181 creator,
182 Asset::new_native(sent_asset.denom, sent_asset.amount),
183 )
184 }
185 ExecuteMsg::Bond { lp_token, amount } => {
186 let sender = info.sender;
187 let transfer_msg = CosmosMsg::Wasm(WasmMsg::Execute {
189 contract_addr: lp_token.to_string(),
190 funds: vec![],
191 msg: to_json_binary(&Cw20ExecuteMsg::TransferFrom {
192 owner: sender.to_string(),
193 recipient: env.contract.address.to_string(),
194 amount,
195 })?,
196 });
197
198 let response = bond(deps, env, sender.clone(), sender, lp_token, amount)?;
199 Ok(response.add_message(transfer_msg))
200 }
201 ExecuteMsg::Unbond { lp_token, amount } => unbond(deps, env, info, lp_token, amount),
202 ExecuteMsg::InstantUnbond { lp_token, amount } => {
203 instant_unbond(deps, env, info, lp_token, amount)
204 }
205 ExecuteMsg::Unlock { lp_token } => unlock(deps, env, info, lp_token),
206 ExecuteMsg::InstantUnlock {
207 lp_token,
208 token_locks,
209 } => instant_unlock(deps, &env, &info, &lp_token, token_locks),
210 ExecuteMsg::Withdraw { lp_token } => withdraw(deps, env, info, lp_token),
211 ExecuteMsg::ClaimUnallocatedReward { reward_schedule_id } => {
212 claim_unallocated_reward(deps, env, info, reward_schedule_id)
213 }
214 ExecuteMsg::ProposeNewOwner { owner, expires_in } => {
215 let config = CONFIG.load(deps.storage)?;
216 let response = propose_new_owner(
217 deps,
218 info,
219 env,
220 owner.to_string(),
221 expires_in,
222 config.owner,
223 OWNERSHIP_PROPOSAL,
224 CONTRACT_NAME,
225 )?;
226 Ok(response)
227 }
228 ExecuteMsg::DropOwnershipProposal {} => {
229 let config: Config = CONFIG.load(deps.storage)?;
230
231 drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL, CONTRACT_NAME)
232 .map_err(|e| e.into())
233 }
234 ExecuteMsg::ClaimOwnership {} => claim_ownership(
235 deps,
236 info,
237 env,
238 OWNERSHIP_PROPOSAL,
239 |deps, new_owner| {
240 CONFIG.update::<_, StdError>(deps.storage, |mut v| {
241 v.owner = new_owner;
242 Ok(v)
243 })?;
244
245 Ok(())
246 },
247 CONTRACT_NAME,
248 )
249 .map_err(|e| e.into()),
250 }
251}
252
253fn update_config(
254 deps: DepsMut,
255 _env: Env,
256 info: MessageInfo,
257 keeper_addr: Option<Addr>,
258 unlock_period: Option<u64>,
259 instant_unbond_fee_bp: Option<u64>,
260 instant_unbond_min_fee_bp: Option<u64>,
261 fee_tier_interval: Option<u64>,
262) -> ContractResult<Response> {
263 let mut config: Config = CONFIG.load(deps.storage)?;
264
265 if info.sender != config.owner {
267 return Err(ContractError::Unauthorized);
268 }
269
270 let mut event = Event::from_info(concatcp!(CONTRACT_NAME, "::update_config"), &info);
271
272 if let Some(keeper_addr) = keeper_addr {
273 config.keeper = keeper_addr.clone();
274 event = event.add_attribute("keeper_addr", keeper_addr.to_string());
275 }
276
277 if let Some(unlock_period) = unlock_period {
278 if fee_tier_interval.is_some() && fee_tier_interval.unwrap() > unlock_period {
280 return Err(ContractError::InvalidFeeTierInterval {
281 max_allowed: unlock_period,
282 received: fee_tier_interval.unwrap(),
283 });
284 }
285
286 if config.fee_tier_interval > unlock_period {
288 config.fee_tier_interval = unlock_period;
289 event = event.add_attribute("fee_tier_interval", config.fee_tier_interval.to_string());
290 }
291
292 config.unlock_period = unlock_period;
293 event = event.add_attribute("unlock_period", config.unlock_period.to_string());
294 }
295
296 if let Some(instant_unbond_fee_bp) = instant_unbond_fee_bp {
297 if instant_unbond_fee_bp > MAX_INSTANT_UNBOND_FEE_BP {
299 return Err(ContractError::InvalidInstantUnbondFee {
300 max_allowed: MAX_INSTANT_UNBOND_FEE_BP,
301 received: instant_unbond_fee_bp,
302 });
303 }
304 config.instant_unbond_fee_bp = instant_unbond_fee_bp;
305 event = event.add_attribute(
306 "instant_unbond_fee_bp",
307 config.instant_unbond_fee_bp.to_string(),
308 );
309 }
310
311 if let Some(instant_unbond_min_fee_bp) = instant_unbond_min_fee_bp {
312 if instant_unbond_min_fee_bp > MAX_INSTANT_UNBOND_FEE_BP
314 || instant_unbond_min_fee_bp > config.instant_unbond_fee_bp
315 {
316 return Err(ContractError::InvalidInstantUnbondMinFee {
317 max_allowed: min(config.instant_unbond_fee_bp, MAX_INSTANT_UNBOND_FEE_BP),
318 received: instant_unbond_min_fee_bp,
319 });
320 }
321
322 config.instant_unbond_min_fee_bp = instant_unbond_min_fee_bp;
323 event = event.add_attribute(
324 "instant_unbond_min_fee_bp",
325 config.instant_unbond_min_fee_bp.to_string(),
326 );
327 }
328
329 if let Some(fee_tier_interval) = fee_tier_interval {
330 if fee_tier_interval > config.unlock_period {
332 return Err(ContractError::InvalidFeeTierInterval {
333 max_allowed: config.unlock_period,
334 received: fee_tier_interval,
335 });
336 }
337
338 config.fee_tier_interval = fee_tier_interval;
339 event = event.add_attribute("fee_tier_interval", config.fee_tier_interval.to_string());
340 }
341
342 CONFIG.save(deps.storage, &config)?;
343
344 Ok(Response::new().add_event(event))
345}
346
347fn claim_unallocated_reward(
350 deps: DepsMut,
351 env: Env,
352 info: MessageInfo,
353 reward_schedule_id: u64,
354) -> ContractResult<Response> {
355 let reward_schedule = REWARD_SCHEDULES.load(deps.storage, reward_schedule_id)?;
356 let mut creator_claimable_reward_state = CREATOR_CLAIMABLE_REWARD
357 .may_load(deps.storage, reward_schedule_id)?
358 .unwrap_or_default();
359
360 if info.sender != reward_schedule.creator {
362 return Err(ContractError::Unauthorized);
363 }
364
365 if reward_schedule.end_block_time > env.block.time.seconds() {
367 return Err(ContractError::RewardScheduleIsActive);
368 }
369
370 if creator_claimable_reward_state.claimed {
372 return Err(ContractError::UnallocatedRewardAlreadyClaimed);
373 }
374
375 compute_creator_claimable_reward(
379 deps.storage,
380 env,
381 &reward_schedule,
382 &mut creator_claimable_reward_state,
383 )?;
384
385 if creator_claimable_reward_state.amount.is_zero() {
387 return Err(ContractError::NoUnallocatedReward);
388 }
389
390 creator_claimable_reward_state.claimed = true;
392 CREATOR_CLAIMABLE_REWARD.save(
393 deps.storage,
394 reward_schedule_id,
395 &creator_claimable_reward_state,
396 )?;
397
398 let msg = build_transfer_token_to_user_msg(
400 reward_schedule.asset.clone(),
401 reward_schedule.creator,
402 creator_claimable_reward_state.amount,
403 )?;
404
405 let event = Event::from_info(
406 concatcp!(CONTRACT_NAME, "::claim_unallocated_reward"),
407 &info,
408 )
409 .add_attribute("reward_schedule_id", reward_schedule_id.to_string())
410 .add_attribute("asset", reward_schedule.asset.as_string())
411 .add_attribute("amount", creator_claimable_reward_state.amount.to_string());
412
413 Ok(Response::new().add_event(event).add_message(msg))
414}
415
416fn compute_creator_claimable_reward(
417 store: &dyn Storage,
418 env: Env,
419 reward_schedule: &RewardSchedule,
420 creator_claimable_reward_state: &mut CreatorClaimableRewardState,
421) -> ContractResult<()> {
422 let lp_global_state = LP_GLOBAL_STATE
423 .may_load(store, &reward_schedule.staking_lp_token)?
424 .unwrap_or_default();
425 let asset_state = ASSET_LP_REWARD_STATE
426 .may_load(
427 store,
428 (
429 &reward_schedule.asset.to_string(),
430 &reward_schedule.staking_lp_token,
431 ),
432 )?
433 .unwrap_or(AssetRewardState {
434 reward_index: Decimal::zero(),
435 last_distributed: 0,
436 });
437 let current_block_time = env.block.time.seconds();
438
439 if lp_global_state.total_bond_amount.is_zero()
440 && asset_state.last_distributed < reward_schedule.end_block_time
441 {
442 let start_time = reward_schedule.start_block_time;
443 let end_time = reward_schedule.end_block_time;
444
445 if start_time > current_block_time {
447 return Ok(());
448 }
449
450 let passed_time = std::cmp::min(end_time, current_block_time)
452 - std::cmp::max(start_time, asset_state.last_distributed);
453
454 let time = end_time - start_time;
455 let distribution_amount_per_second: Decimal =
456 Decimal::from_ratio(reward_schedule.amount, time);
457 let distributed_amount =
458 distribution_amount_per_second * Uint128::from(passed_time as u128);
459
460 creator_claimable_reward_state.amount = creator_claimable_reward_state
461 .amount
462 .checked_add(distributed_amount)?;
463 creator_claimable_reward_state.last_update = env.block.time.seconds();
464 }
465
466 Ok(())
467}
468
469fn allow_lp_token(
470 deps: DepsMut,
471 _env: Env,
472 info: MessageInfo,
473 lp_token: Addr,
474) -> Result<Response, ContractError> {
475 let mut config = CONFIG.load(deps.storage)?;
477 if config.owner != info.sender {
478 return Err(ContractError::Unauthorized);
479 }
480
481 if config.allowed_lp_tokens.len() == MAX_ALLOWED_LP_TOKENS {
483 return Err(ContractError::CantAllowAnyMoreLpTokens);
484 }
485
486 let lp_token = deps.api.addr_validate(lp_token.as_str())?;
487
488 if config.allowed_lp_tokens.contains(&lp_token) {
490 return Err(ContractError::LpTokenAlreadyAllowed);
491 }
492
493 config.allowed_lp_tokens.push(lp_token.clone());
494 CONFIG.save(deps.storage, &config)?;
495
496 let response = Response::new().add_event(
497 Event::from_info(concatcp!(CONTRACT_NAME, "::allow_lp_token"), &info)
498 .add_attribute("lp_token", lp_token.to_string()),
499 );
500 Ok(response)
501}
502
503fn remove_lp_token(
504 deps: DepsMut,
505 info: MessageInfo,
506 lp_token: &Addr,
507) -> Result<Response, ContractError> {
508 let mut config = CONFIG.load(deps.storage)?;
509 if config.owner != info.sender {
511 return Err(ContractError::Unauthorized);
512 }
513
514 config.allowed_lp_tokens.retain(|x| x != lp_token);
515 CONFIG.save(deps.storage, &config)?;
516
517 let response = Response::new().add_event(
518 Event::from_info(concatcp!(CONTRACT_NAME, "::remove_lp_token"), &info)
519 .add_attribute("lp_token", lp_token.to_string()),
520 );
521
522 Ok(response)
523}
524
525pub fn create_reward_schedule(
526 deps: DepsMut,
527 env: Env,
528 info: MessageInfo,
529 lp_token: Addr,
530 title: String,
531 start_block_time: u64,
532 end_block_time: u64,
533 creator: Addr,
534 asset: Asset,
535) -> ContractResult<Response> {
536 let config = CONFIG.load(deps.storage)?;
537 check_if_lp_token_allowed(&config, &lp_token)?;
538
539 if start_block_time >= end_block_time {
541 return Err(ContractError::InvalidBlockTimes {
542 start_block_time,
543 end_block_time,
544 });
545 }
546 if start_block_time <= env.block.time.seconds()
547 {
548 return Err(ContractError::InvalidStartBlockTime {
549 start_block_time,
550 current_block_time: env.block.time.seconds(),
551 });
552 }
553
554 check_if_lp_token_allowed(&config, &lp_token)?;
556
557 let mut lp_global_state = LP_GLOBAL_STATE
558 .may_load(deps.storage, &lp_token)?
559 .unwrap_or_default();
560
561 if !lp_global_state.active_reward_assets.contains(&asset.info) {
562 lp_global_state
563 .active_reward_assets
564 .push(asset.info.clone());
565 }
566
567 LP_GLOBAL_STATE.save(deps.storage, &lp_token, &lp_global_state)?;
568
569 let reward_schedule_id = next_reward_schedule_id(deps.storage)?;
570
571 let reward_schedule = RewardSchedule {
572 title: title.clone(),
573 creator: creator.clone(),
574 asset: asset.info.clone(),
575 amount: asset.amount,
576 staking_lp_token: lp_token.clone(),
577 start_block_time,
578 end_block_time,
579 };
580
581 REWARD_SCHEDULES.save(deps.storage, reward_schedule_id, &reward_schedule)?;
582
583 let mut reward_schedules_ids = LP_TOKEN_ASSET_REWARD_SCHEDULE
584 .may_load(deps.storage, (&lp_token, &asset.info.to_string()))?
585 .unwrap_or_default();
586
587 reward_schedules_ids.push(reward_schedule_id);
588 LP_TOKEN_ASSET_REWARD_SCHEDULE.save(
589 deps.storage,
590 (&lp_token, &asset.info.to_string()),
591 &reward_schedules_ids,
592 )?;
593
594 Ok(Response::new().add_event(
595 Event::from_sender(
596 concatcp!(CONTRACT_NAME, "::create_reward_schedule"),
597 &info.sender,
598 )
599 .add_attribute("creator", creator.to_string())
600 .add_attribute("lp_token", lp_token.to_string())
601 .add_attribute("title", title)
602 .add_attribute("start_block_time", start_block_time.to_string())
603 .add_attribute("end_block_time", end_block_time.to_string())
604 .add_attribute("asset", serde_json_wasm::to_string(&asset).unwrap())
605 .add_attribute("reward_schedule_id", reward_schedule_id.to_string()),
606 ))
607}
608
609pub fn receive_cw20(
610 deps: DepsMut,
611 env: Env,
612 info: MessageInfo,
613 cw20_msg: Cw20ReceiveMsg,
614) -> Result<Response, ContractError> {
615 match from_json(&cw20_msg.msg)? {
616 Cw20HookMsg::Bond { beneficiary_user } => {
617 let token_address = info.sender;
618 let cw20_sender = deps.api.addr_validate(&cw20_msg.sender)?;
619
620 let user = if let Some(beneficiary_user) = beneficiary_user {
621 deps.api.addr_validate(beneficiary_user.as_str())?
622 } else {
623 cw20_sender.clone()
624 };
625
626 bond(
627 deps,
628 env,
629 cw20_sender.clone(),
630 user,
631 token_address,
632 cw20_msg.amount,
633 )
634 }
635 Cw20HookMsg::CreateRewardSchedule {
636 lp_token,
637 title,
638 actual_creator,
639 start_block_time,
640 end_block_time,
641 } => {
642 let config = CONFIG.load(deps.storage)?;
644 if cw20_msg.sender != config.owner {
645 return Err(ContractError::Unauthorized);
646 }
647
648 let token_addr = info.sender.clone();
649
650 let creator = match actual_creator {
651 Some(creator) => deps.api.addr_validate(&creator.to_string())?,
652 None => deps.api.addr_validate(&cw20_msg.sender)?,
653 };
654
655 create_reward_schedule(
656 deps,
657 env,
658 info,
659 lp_token,
660 title,
661 start_block_time,
662 end_block_time,
663 creator,
664 Asset::new_token(token_addr, cw20_msg.amount),
665 )
666 }
667 }
668}
669
670pub fn compute_reward(
671 current_block_time: u64,
672 total_bond_amount: Uint128,
673 state: &mut AssetRewardState,
674 reward_schedules: Vec<(u64, RewardSchedule)>,
675 creator_claimable_reward: &mut HashMap<u64, CreatorClaimableRewardState>,
677) {
678 if state.last_distributed == current_block_time {
679 return;
680 }
681
682 let mut distributed_amount: Uint128 = Uint128::zero();
683 for (id, s) in reward_schedules.iter() {
684 let start_time = s.start_block_time;
685 let end_time = s.end_block_time;
686
687 if start_time > current_block_time || end_time <= state.last_distributed {
688 continue;
689 }
690
691 let passed_time = std::cmp::min(end_time, current_block_time)
693 - std::cmp::max(start_time, state.last_distributed);
694
695 let time = end_time - start_time;
696 let distribution_amount_per_second: Decimal = Decimal::from_ratio(s.amount, time);
697 distributed_amount += distribution_amount_per_second * Uint128::from(passed_time as u128);
698
699 if total_bond_amount.is_zero() && state.last_distributed < current_block_time {
702 let current_creator_claimable_reward =
704 creator_claimable_reward.get(id).cloned().unwrap();
705 if !current_creator_claimable_reward.claimed {
707 let amount = current_creator_claimable_reward.amount;
708 let new_amount = amount.checked_add(distributed_amount).unwrap();
709 creator_claimable_reward.insert(
710 *id,
711 CreatorClaimableRewardState {
712 claimed: false,
713 amount: new_amount,
714 last_update: current_block_time,
715 },
716 );
717 }
718 }
719 }
720
721 state.last_distributed = current_block_time;
722
723 if total_bond_amount.is_zero() {
724 return;
725 }
726 state.reward_index =
727 state.reward_index + Decimal::from_ratio(distributed_amount, total_bond_amount);
728}
729
730pub fn compute_staker_reward(
731 bond_amount: Uint128,
732 state: &AssetRewardState,
733 staker_info: &mut AssetStakerInfo,
734) -> StdResult<()> {
735 let pending_reward =
736 bond_amount * (state.reward_index.checked_sub(staker_info.reward_index)?);
737 staker_info.reward_index = state.reward_index;
738 staker_info.pending_reward = staker_info.pending_reward.checked_add(pending_reward)?;
739 Ok(())
740}
741
742fn check_if_lp_token_allowed(config: &Config, lp_token: &Addr) -> ContractResult<()> {
743 if !config.allowed_lp_tokens.contains(lp_token) {
744 return Err(ContractError::LpTokenNotAllowed);
745 }
746 Ok(())
747}
748
749pub fn bond(
759 mut deps: DepsMut,
760 env: Env,
761 sender: Addr,
762 user: Addr,
763 lp_token: Addr,
764 amount: Uint128,
765) -> Result<Response, ContractError> {
766 if amount.is_zero() {
767 return Err(ContractError::ZeroAmount);
768 }
769
770 let config = CONFIG.load(deps.storage)?;
771 check_if_lp_token_allowed(&config, &lp_token)?;
772
773 let current_bond_amount = USER_BONDED_LP_TOKENS
774 .may_load(deps.storage, (&lp_token, &user))?
775 .unwrap_or_default();
776
777 let mut lp_global_state = LP_GLOBAL_STATE
778 .may_load(deps.storage, &lp_token)?
779 .unwrap_or_default();
780 let mut response = Response::default();
781
782 for asset in &lp_global_state.active_reward_assets {
783 update_staking_rewards(
784 asset,
785 &lp_token,
786 &user,
787 lp_global_state.total_bond_amount,
788 current_bond_amount,
789 env.block.time.seconds(),
790 &mut deps,
791 &mut response,
792 None,
793 )?;
794 }
795
796 lp_global_state.total_bond_amount = lp_global_state.total_bond_amount.checked_add(amount)?;
798 LP_GLOBAL_STATE.save(deps.storage, &lp_token, &lp_global_state)?;
799
800 let user_updated_bond_amount = current_bond_amount.checked_add(amount)?;
801
802 USER_BONDED_LP_TOKENS.save(deps.storage, (&lp_token, &user), &user_updated_bond_amount)?;
804
805 let event = Event::from_sender(concatcp!(CONTRACT_NAME, "::bond"), sender)
808 .add_attribute("user", user)
809 .add_attribute("lp_token", lp_token)
810 .add_attribute("amount", amount)
811 .add_attribute("total_bond_amount", lp_global_state.total_bond_amount)
812 .add_attribute("user_updated_bond_amount", user_updated_bond_amount);
813
814 response = response.add_event(event);
815 Ok(response)
816}
817
818pub fn update_staking_rewards(
819 asset: &AssetInfo,
820 lp_token: &Addr,
821 user: &Addr,
822 total_bond_amount: Uint128,
823 current_bond_amount: Uint128,
824 current_block_time: u64,
825 deps: &mut DepsMut,
826 response: &mut Response,
827 operation_post_update: Option<
828 fn(
829 &Addr,
830 &Addr,
831 &mut AssetRewardState,
832 &mut AssetStakerInfo,
833 &mut Response,
834 ) -> ContractResult<()>,
835 >,
836) -> ContractResult<()> {
837 let mut asset_staker_info = ASSET_STAKER_INFO
838 .may_load(deps.storage, (&lp_token, &user, &asset.to_string()))?
839 .unwrap_or(AssetStakerInfo {
840 asset: asset.clone(),
841 pending_reward: Uint128::zero(),
842 reward_index: Decimal::zero(),
843 });
844
845 let mut asset_state = ASSET_LP_REWARD_STATE
846 .may_load(deps.storage, (&asset.to_string(), &lp_token))?
847 .unwrap_or(AssetRewardState {
848 reward_index: Decimal::zero(),
849 last_distributed: 0,
850 });
851
852 let reward_schedule_ids = LP_TOKEN_ASSET_REWARD_SCHEDULE
853 .may_load(deps.storage, (&lp_token, &asset.to_string()))?
854 .unwrap_or_default();
855
856 let mut reward_schedules = vec![];
857 for id in &reward_schedule_ids {
858 reward_schedules.push((*id, REWARD_SCHEDULES.load(deps.storage, *id)?.clone()));
859 }
860
861 let mut current_creator_claimable_rewards = HashMap::new();
862 for id in &reward_schedule_ids {
863 let reward = CREATOR_CLAIMABLE_REWARD
864 .may_load(deps.storage, *id)?
865 .unwrap_or_default();
866 current_creator_claimable_rewards.insert(*id, reward);
867 }
868
869 compute_reward(
870 current_block_time,
871 total_bond_amount,
872 &mut asset_state,
873 reward_schedules,
874 &mut current_creator_claimable_rewards,
875 );
876 compute_staker_reward(
877 current_bond_amount,
878 &mut asset_state,
879 &mut asset_staker_info,
880 )?;
881
882 if let Some(operation) = operation_post_update {
883 operation(
884 user,
885 lp_token,
886 &mut asset_state,
887 &mut asset_staker_info,
888 response,
889 )?;
890 }
891
892 ASSET_LP_REWARD_STATE.save(deps.storage, (&asset.to_string(), &lp_token), &asset_state)?;
893
894 ASSET_STAKER_INFO.save(
895 deps.storage,
896 (&lp_token, &user, &asset.to_string()),
897 &asset_staker_info,
898 )?;
899
900 for (id, reward) in current_creator_claimable_rewards {
901 CREATOR_CLAIMABLE_REWARD.save(deps.storage, id, &reward)?;
902 }
903
904 Ok(())
905}
906
907fn withdraw_pending_reward(
908 user: &Addr,
909 lp_token: &Addr,
910 _asset_reward_state: &mut AssetRewardState,
911 asset_staker_info: &mut AssetStakerInfo,
912 response: &mut Response,
913) -> ContractResult<()> {
914 let pending_reward = asset_staker_info.pending_reward;
915
916 if pending_reward > Uint128::zero() {
917 let event = Event::from_sender(concatcp!(CONTRACT_NAME, "::withdraw_reward"), user)
918 .add_attribute("lp_token", lp_token)
919 .add_attribute("asset", asset_staker_info.asset.to_string())
920 .add_attribute("amount", pending_reward);
921
922 let res = response
923 .clone()
924 .add_message(build_transfer_token_to_user_msg(
925 asset_staker_info.asset.clone(),
926 user.clone(),
927 pending_reward,
928 )?)
929 .add_event(event);
930 *response = res;
931 }
932
933 asset_staker_info.pending_reward = Uint128::zero();
934
935 Ok(())
936}
937
938pub fn withdraw(
939 mut deps: DepsMut,
940 env: Env,
941 info: MessageInfo,
942 lp_token: Addr,
943) -> ContractResult<Response> {
944 let mut response = Response::new();
945 let current_bonded_amount = USER_BONDED_LP_TOKENS
946 .may_load(deps.storage, (&lp_token, &info.sender))?
947 .unwrap_or_default();
948
949 let lp_global_state = LP_GLOBAL_STATE.load(deps.storage, &lp_token)?;
950
951 for asset in &lp_global_state.active_reward_assets {
952 update_staking_rewards(
953 asset,
954 &lp_token,
955 &info.sender,
956 lp_global_state.total_bond_amount,
957 current_bonded_amount,
958 env.block.time.seconds(),
959 &mut deps,
960 &mut response,
961 Some(withdraw_pending_reward),
962 )?;
963 }
964
965 response = response.add_event(
968 Event::from_info(concatcp!(CONTRACT_NAME, "::withdraw"), &info)
969 .add_attribute("lp_token", lp_token.clone()),
970 );
971 Ok(response)
972}
973
974#[cfg_attr(not(feature = "library"), entry_point)]
975pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult<Binary> {
976 match msg {
977 QueryMsg::BondedLpTokens { lp_token, user } => {
978 let bonded_amount = USER_BONDED_LP_TOKENS
979 .may_load(deps.storage, (&lp_token, &user))?
980 .unwrap_or_default();
981 to_json_binary(&bonded_amount).map_err(ContractError::from)
982 }
983 QueryMsg::InstantUnlockFee {
984 user,
985 lp_token,
986 token_lock,
987 } => {
988 let config = CONFIG.load(deps.storage)?;
989 let token_locks = USER_LP_TOKEN_LOCKS
991 .may_load(deps.storage, (&lp_token, &user))?
992 .unwrap_or_default();
993
994 let exists = token_locks.iter().any(|lock| *lock == token_lock.clone());
995 if !exists {
996 return Err(ContractError::TokenLockNotFound);
997 }
998
999 let (fee_bp, unlock_fee) =
1000 calculate_unlock_fee(&token_lock, env.block.time.seconds(), &config);
1001
1002 let instant_lp_unlock_fee = InstantLpUnlockFee {
1003 time_until_lock_expiry: token_lock
1004 .unlock_time
1005 .checked_sub(env.block.time.seconds())
1006 .unwrap_or_default(),
1007 unlock_amount: token_lock.amount,
1008 unlock_fee_bp: fee_bp,
1009 unlock_fee,
1010 };
1011
1012 to_json_binary(&instant_lp_unlock_fee).map_err(ContractError::from)
1013 }
1014 QueryMsg::InstantUnlockFeeTiers {} => {
1015 let config = CONFIG.load(deps.storage)?;
1016 let min_fee = config.instant_unbond_min_fee_bp;
1017 let max_fee = config.instant_unbond_fee_bp;
1018
1019 let unlock_period = config.unlock_period;
1020 let fee_tiers = query_instant_unlock_fee_tiers(
1021 config.fee_tier_interval,
1022 unlock_period,
1023 min_fee,
1024 max_fee,
1025 );
1026
1027 to_json_binary(&fee_tiers).map_err(ContractError::from)
1028 }
1029 QueryMsg::UnclaimedRewards {
1030 lp_token,
1031 user,
1032 block_time,
1033 } => {
1034 let current_bonded_amount = USER_BONDED_LP_TOKENS
1035 .may_load(deps.storage, (&lp_token, &user))?
1036 .unwrap_or_default();
1037
1038 let lp_global_state = LP_GLOBAL_STATE.load(deps.storage, &lp_token)?;
1039
1040 let mut reward_info = vec![];
1041 let block_time = block_time.unwrap_or(env.block.time.seconds());
1042
1043 if block_time < env.block.time.seconds() {
1044 return Err(ContractError::BlockTimeInPast);
1045 }
1046
1047 for asset in lp_global_state.active_reward_assets {
1048 let mut asset_staker_info = ASSET_STAKER_INFO
1049 .may_load(deps.storage, (&lp_token, &user, &asset.to_string()))?
1050 .unwrap_or(AssetStakerInfo {
1051 asset: asset.clone(),
1052 pending_reward: Uint128::zero(),
1053 reward_index: Decimal::zero(),
1054 });
1055
1056 let mut asset_state = ASSET_LP_REWARD_STATE
1057 .may_load(deps.storage, (&asset.to_string(), &lp_token))?
1058 .unwrap_or(AssetRewardState {
1059 reward_index: Decimal::zero(),
1060 last_distributed: 0,
1061 });
1062
1063 let reward_schedule_ids = LP_TOKEN_ASSET_REWARD_SCHEDULE
1064 .may_load(deps.storage, (&lp_token, &asset.to_string()))?
1065 .unwrap_or_default();
1066
1067 let mut reward_schedules = vec![];
1068 for id in &reward_schedule_ids {
1069 reward_schedules.push((*id, REWARD_SCHEDULES.load(deps.storage, *id)?.clone()));
1070 }
1071
1072 let mut current_creator_claimable_rewards = HashMap::new();
1073 for id in &reward_schedule_ids {
1074 let reward = CREATOR_CLAIMABLE_REWARD
1075 .may_load(deps.storage, *id)?
1076 .unwrap_or_default();
1077 current_creator_claimable_rewards.insert(*id, reward);
1078 }
1079
1080 compute_reward(
1081 block_time,
1082 lp_global_state.total_bond_amount,
1083 &mut asset_state,
1084 reward_schedules,
1085 &mut current_creator_claimable_rewards,
1086 );
1087 compute_staker_reward(
1088 current_bonded_amount,
1089 &mut asset_state,
1090 &mut asset_staker_info,
1091 )?;
1092
1093 if asset_staker_info.pending_reward > Uint128::zero() {
1094 reward_info.push(UnclaimedReward {
1095 asset: asset.clone(),
1096 amount: asset_staker_info.pending_reward,
1097 });
1098 }
1099 }
1100
1101 to_json_binary(&reward_info).map_err(ContractError::from)
1102 }
1103 QueryMsg::AllowedLPTokensForReward {} => {
1104 let config = CONFIG.load(deps.storage)?;
1105 let allowed_lp_tokens = config.allowed_lp_tokens;
1106 to_json_binary(&allowed_lp_tokens).map_err(ContractError::from)
1107 }
1108 QueryMsg::Owner {} => {
1109 let config = CONFIG.load(deps.storage)?;
1110 to_json_binary(&config.owner).map_err(ContractError::from)
1111 }
1112 QueryMsg::RewardSchedules { lp_token, asset } => {
1113 let reward_schedule_ids = LP_TOKEN_ASSET_REWARD_SCHEDULE
1114 .may_load(deps.storage, (&lp_token, &asset.to_string()))?
1115 .unwrap_or_default();
1116
1117 let mut reward_schedules = vec![];
1118 for id in &reward_schedule_ids {
1119 reward_schedules.push(RewardScheduleResponse {
1120 id: *id,
1121 reward_schedule: REWARD_SCHEDULES.load(deps.storage, *id)?.clone(),
1122 });
1123 }
1124 to_json_binary(&reward_schedules).map_err(ContractError::from)
1125 }
1126 QueryMsg::TokenLocks {
1127 lp_token,
1128 user,
1129 block_time,
1130 } => {
1131 let mut locks = USER_LP_TOKEN_LOCKS
1132 .may_load(deps.storage, (&lp_token, &user))?
1133 .unwrap_or_default();
1134
1135 let mut unlocked_amount = Uint128::zero();
1136 let mut filtered_locks = vec![];
1137
1138 let block_time = block_time.unwrap_or(env.block.time.seconds());
1139 for lock in locks.iter_mut() {
1140 if lock.unlock_time < block_time {
1141 unlocked_amount += lock.amount;
1142 lock.amount = Uint128::zero();
1143 } else {
1144 filtered_locks.push(lock.clone());
1145 }
1146 }
1147
1148 to_json_binary(&TokenLockInfo {
1149 unlocked_amount,
1150 locks: filtered_locks,
1151 })
1152 .map_err(ContractError::from)
1153 }
1154 QueryMsg::RawTokenLocks { lp_token, user } => {
1155 let locks = USER_LP_TOKEN_LOCKS
1156 .may_load(deps.storage, (&lp_token, &user))?
1157 .unwrap_or_default();
1158
1159 to_json_binary(&locks).map_err(ContractError::from)
1160 }
1161 QueryMsg::RewardState { lp_token, asset } => {
1162 let reward_state =
1163 ASSET_LP_REWARD_STATE.may_load(deps.storage, (&asset.to_string(), &lp_token))?;
1164
1165 match reward_state {
1166 Some(reward_state) => to_json_binary(&reward_state).map_err(ContractError::from),
1167 None => Err(ContractError::NoRewardState),
1168 }
1169 }
1170 QueryMsg::StakerInfo {
1171 lp_token,
1172 asset,
1173 user,
1174 } => {
1175 let reward_state =
1176 ASSET_STAKER_INFO.may_load(deps.storage, (&lp_token, &user, &asset.to_string()))?;
1177
1178 match reward_state {
1179 Some(reward_state) => to_json_binary(&reward_state).map_err(ContractError::from),
1180 None => Err(ContractError::NoUserRewardState),
1181 }
1182 }
1183 QueryMsg::CreatorClaimableReward { reward_schedule_id } => {
1184 let reward_schedule = REWARD_SCHEDULES.load(deps.storage, reward_schedule_id)?;
1185 let mut creator_claimable_reward = CREATOR_CLAIMABLE_REWARD
1186 .may_load(deps.storage, reward_schedule_id)?
1187 .unwrap_or_default();
1188
1189 compute_creator_claimable_reward(
1190 deps.storage,
1191 env,
1192 &reward_schedule,
1193 &mut creator_claimable_reward,
1194 )?;
1195
1196 to_json_binary(&creator_claimable_reward).map_err(ContractError::from)
1197 }
1198 QueryMsg::Config {} => {
1199 let config = CONFIG.load(deps.storage)?;
1200 to_json_binary(&config).map_err(ContractError::from)
1201 }
1202 }
1203}
1204
1205#[cfg_attr(not(feature = "library"), entry_point)]
1206pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> ContractResult<Response> {
1207 match msg {
1208 MigrateMsg::V3FromV1 {
1209 keeper_addr,
1210 instant_unbond_fee_bp,
1211 instant_unbond_min_fee_bp,
1212 fee_tier_interval
1213 } => {
1214 let contract_version = get_contract_version(deps.storage)?;
1216 if contract_version.version != CONTRACT_VERSION_V1 {
1217 return Err(ContractError::InvalidContractVersionForUpgrade {
1218 upgrade_version: CONTRACT_VERSION.to_string(),
1219 expected: CONTRACT_VERSION_V1.to_string(),
1220 actual: contract_version.version,
1221 });
1222 }
1223
1224 if instant_unbond_fee_bp > MAX_INSTANT_UNBOND_FEE_BP {
1226 return Err(ContractError::InvalidInstantUnbondFee {
1227 max_allowed: MAX_INSTANT_UNBOND_FEE_BP,
1228 received: instant_unbond_fee_bp,
1229 });
1230 }
1231
1232 if instant_unbond_min_fee_bp > instant_unbond_fee_bp {
1233 return Err(ContractError::InvalidInstantUnbondMinFee {
1234 max_allowed: instant_unbond_fee_bp,
1235 received: instant_unbond_min_fee_bp,
1236 });
1237 }
1238
1239 let config_v1: ConfigV1 = Item::new("config").load(deps.storage)?;
1240
1241 if fee_tier_interval > config_v1.unlock_period {
1243 return Err(ContractError::InvalidFeeTierInterval {
1244 max_allowed: config_v1.unlock_period,
1245 received: fee_tier_interval,
1246 });
1247 }
1248
1249 let config = Config {
1251 owner: config_v1.owner,
1252 allowed_lp_tokens: config_v1.allowed_lp_tokens,
1253 unlock_period: config_v1.unlock_period,
1254 keeper: deps.api.addr_validate(&keeper_addr.to_string())?,
1255 instant_unbond_fee_bp,
1256 instant_unbond_min_fee_bp,
1257 fee_tier_interval,
1258 };
1259
1260 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
1261 CONFIG.save(deps.storage, &config)?;
1262 },
1263 MigrateMsg::V3FromV2 { keeper_addr } => {
1264 let contract_version = get_contract_version(deps.storage)?;
1265 if contract_version.version == CONTRACT_VERSION_V2 || contract_version.version == CONTRACT_VERSION_V2_1 {
1267 let config_v2: ConfigV2_1 = Item::new("config").load(deps.storage)?;
1268 let config = Config {
1269 owner: config_v2.owner,
1270 allowed_lp_tokens: config_v2.allowed_lp_tokens,
1271 unlock_period: config_v2.unlock_period,
1272 keeper: keeper_addr,
1273 instant_unbond_fee_bp: config_v2.instant_unbond_fee_bp,
1274 instant_unbond_min_fee_bp: config_v2.instant_unbond_min_fee_bp,
1275 fee_tier_interval: config_v2.fee_tier_interval,
1276 };
1277
1278 CONFIG.save(deps.storage, &config)?;
1279 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
1280 } else {
1281 return Err(ContractError::InvalidContractVersionForUpgrade {
1282 upgrade_version: CONTRACT_VERSION.to_string(),
1283 expected: CONTRACT_VERSION_V2.to_string(),
1284 actual: contract_version.version,
1285 });
1286 }
1287 }
1288
1289 MigrateMsg::V3FromV2_2 {} => {
1290 let contract_version = get_contract_version(deps.storage)?;
1291 if contract_version.version == CONTRACT_VERSION_V2_2 {
1293 let config_v2: ConfigV2_2 = Item::new("config").load(deps.storage)?;
1294 let config = Config {
1295 owner: config_v2.owner,
1296 allowed_lp_tokens: config_v2.allowed_lp_tokens,
1297 unlock_period: config_v2.unlock_period,
1298 keeper: config_v2.keeper,
1299 instant_unbond_fee_bp: config_v2.instant_unbond_fee_bp,
1300 instant_unbond_min_fee_bp: config_v2.instant_unbond_min_fee_bp,
1301 fee_tier_interval: config_v2.fee_tier_interval,
1302 };
1303
1304 CONFIG.save(deps.storage, &config)?;
1305
1306 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
1308
1309 } else {
1310 return Err(ContractError::InvalidContractVersionForUpgrade {
1311 upgrade_version: CONTRACT_VERSION.to_string(),
1312 expected: CONTRACT_VERSION_V2_2.to_string(),
1313 actual: contract_version.version,
1314 });
1315 }
1316 }
1317 }
1318
1319 Ok(Response::default())
1320}