dexter_multi_staking/
contract.rs

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
50/// Contract name that is used for migration.
51pub 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";
57/// Contract version that is used for migration.
58const 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    // validate keeper address
93    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            // only owner can create reward schedule
154            let config = CONFIG.load(deps.storage)?;
155            if info.sender != config.owner {
156                return Err(ContractError::Unauthorized);
157            }
158
159            // Verify that no more than one asset is sent with the message for reward distribution
160            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            // Transfer the lp token to the contract
188            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    // Verify that the message sender is the owner
266    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        // validate if unlock period is greater than the fee tier interval, then reset the fee tier interval to unlock period as well
279        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        // reset the current fee tier interval to unlock period if it is greater than unlock period
287        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        // validate max allowed instant unbond fee which is 10%
298        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        // validate min allowed instant unbond fee max value which is 10% and lesser than the instant unbond fee
313        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        // max allowed fee tier interval in equal to the unlock period.
331        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
347/// Claim unallocated reward for a reward schedule by the creator. This is useful when there was no tokens bonded for a certain
348/// time period during reward schedule and the reward schedule creator wants to claim the unallocated amount.
349fn 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    // Verify that the message sender is the reward schedule creator
361    if info.sender != reward_schedule.creator {
362        return Err(ContractError::Unauthorized);
363    }
364
365    // Verify that the reward schedule is not active
366    if reward_schedule.end_block_time > env.block.time.seconds() {
367        return Err(ContractError::RewardScheduleIsActive);
368    }
369
370    // Verify that the reward schedule is not already claimed
371    if creator_claimable_reward_state.claimed {
372        return Err(ContractError::UnallocatedRewardAlreadyClaimed);
373    }
374
375    // if no user activity happened after the last time rewards were computed for this reward schedule
376    // and before the reward schedule ended, then the creator claimable reward amount would be less
377    // than what it should be if there was nothing bonded for this LP token during that time.
378    compute_creator_claimable_reward(
379        deps.storage,
380        env,
381        &reward_schedule,
382        &mut creator_claimable_reward_state,
383    )?;
384
385    // Verify that the reward schedule has unclaimed reward
386    if creator_claimable_reward_state.amount.is_zero() {
387        return Err(ContractError::NoUnallocatedReward);
388    }
389
390    // Update the reward schedule to be claimed
391    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    // Send the unclaimed reward to the reward schedule creator
399    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        // this case is possible during the query
446        if start_time > current_block_time {
447            return Ok(());
448        }
449
450        // min(s.1, block_time) - max(s.0, last_distributed)
451        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    // validate if owner sent the message
476    let mut config = CONFIG.load(deps.storage)?;
477    if config.owner != info.sender {
478        return Err(ContractError::Unauthorized);
479    }
480
481    // To prevent out-of-gas issues in long run
482    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    // verify that lp token is not already allowed
489    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    // validate if owner sent the message
510    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    // validate block times
540    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    // still need to check as an LP token might have been removed after the reward schedule was proposed
555    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            // only owner can create reward schedule
643            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    // Current creator claimable rewards for the above reward schedule ids
676    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        // min(s.1, block_time) - max(s.0, last_distributed)
692        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        // This means between last distributed time and current block time, no one has bonded any assets
700        // This reward value must be claimable by the reward schedule creator
701        if total_bond_amount.is_zero() && state.last_distributed < current_block_time {
702            // Previous function ensures we can unwrap safely here
703            let current_creator_claimable_reward =
704                creator_claimable_reward.get(id).cloned().unwrap();
705            // don't update already claimed creator claimable rewards
706            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
749/// This function is called when a user wants to bond their LP tokens either directly or through the vault
750/// This function will update the user's bond amount and the total bond amount for the given LP token
751/// ### Params:
752/// **sender**: This is the address that sent the cw20 token.
753/// This is not necessarily the user address since vault can bond on behalf of the user
754/// **user**: This is the user address that owns the bonded tokens and will receive rewards
755/// This user is elligible to withdraw the tokens after unbonding and not the sender
756/// **lp_token**: The LP token address
757/// **amount**: The amount of LP tokens to bond
758pub 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    // Increase bond amount
797    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    // Increase user bond amount
803    USER_BONDED_LP_TOKENS.save(deps.storage, (&lp_token, &user), &user_updated_bond_amount)?;
804
805    // even though the msg sender might be a CW20 contract,
806    // in the event, we are only concerned with the actual human sender
807    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    // At each withdraw, we withdraw all earned rewards by the user.
966    // If we keep a track of the reward at the subgraph level, then that much data can really suffice.
967    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            // validate if token lock actually exists
990            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            // verify if we are running on V1 right now
1215            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            // validate input for upgrade
1225            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            // valiate fee tier interval
1242            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            // copy fields from v1 to v2
1250            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 version is v2 or v2.1, apply the changes.
1266            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 version if v2.2 apply the changes and return
1292            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 the contract version to v3
1307                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}