moneymarket-liquidation-queue 0.1.0

A MoneyMarket liquidation contract - handles over liquidation model
Documentation
use cosmwasm_std::entry_point;

use crate::asserts::{assert_fees, assert_max_slot, assert_max_slot_premium};
use crate::bid::{activate_bids, claim_liquidations, execute_liquidation, retract_bid, submit_bid};
use crate::querier::query_collateral_whitelist_info;
use crate::query::{
    query_bid, query_bid_pool, query_bid_pools, query_bids_by_user, query_collateral_info,
    query_config, query_liquidation_amount,
};
use crate::state::{
    read_collateral_info, read_config, store_collateral_info, store_config, CollateralInfo, Config,
};

use cosmwasm_std::{
    from_json, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Decimal256, Uint256,
};
use cw20::Cw20ReceiveMsg;
use solid_moneymarket::liquidation_queue::{
    Cw20HookMsg, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg,
};

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    assert_fees(msg.liquidator_fee + msg.bid_fee)?;

    store_config(
        deps.storage,
        &Config {
            owner: deps.api.addr_validate(&msg.owner)?,
            oracle_contract: deps.api.addr_validate(&msg.oracle_contract)?,
            stable_contract: deps.api.addr_validate(&msg.stable_contract)?,
            safe_ratio: msg.safe_ratio,
            bid_fee: msg.bid_fee,
            liquidator_fee: msg.liquidator_fee,
            liquidation_threshold: msg.liquidation_threshold,
            price_timeframe: msg.price_timeframe,
            waiting_period: msg.waiting_period,
            overseer: deps.api.addr_validate(&msg.overseer)?,
        },
    )?;

    Ok(Response::new())
}

#[entry_point]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
    match msg {
        ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg),
        ExecuteMsg::UpdateConfig {
            owner,
            oracle_contract,
            safe_ratio,
            bid_fee,
            liquidator_fee,
            liquidation_threshold,
            price_timeframe,
            waiting_period,
            overseer,
        } => update_config(
            deps,
            info,
            owner,
            oracle_contract,
            safe_ratio,
            bid_fee,
            liquidator_fee,
            liquidation_threshold,
            price_timeframe,
            waiting_period,
            overseer,
        ),
        ExecuteMsg::WhitelistCollateral {
            collateral_token,
            bid_threshold,
            max_slot,
            premium_rate_per_slot,
        } => whitelist_collateral(
            deps,
            info,
            collateral_token,
            bid_threshold,
            max_slot,
            premium_rate_per_slot,
        ),
        ExecuteMsg::UpdateCollateralInfo {
            collateral_token,
            bid_threshold,
            max_slot,
        } => update_collateral_info(deps, info, collateral_token, bid_threshold, max_slot),
        ExecuteMsg::ActivateBids {
            collateral_token,
            bids_idx,
        } => activate_bids(deps, env, info, collateral_token, bids_idx),
        ExecuteMsg::RetractBid { bid_idx, amount } => retract_bid(deps, env, info, bid_idx, amount),
        ExecuteMsg::ClaimLiquidations {
            collateral_token,
            bids_idx,
        } => claim_liquidations(deps, env, info, collateral_token, bids_idx),
    }
}

pub fn receive_cw20(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    cw20_msg: Cw20ReceiveMsg,
) -> StdResult<Response> {
    let contract_addr = info.sender;
    match from_json(&cw20_msg.msg)? {
        Cw20HookMsg::ExecuteBid {
            liquidator,
            repay_address,
            fee_address,
            borrower_address,
        } => {
            let collateral_token = contract_addr.to_string();
            let repay_address = repay_address.unwrap_or_default();
            let fee_address = fee_address.unwrap_or_default();
            let borrower_address = borrower_address.unwrap_or_else(|| cw20_msg.sender.clone());

            if repay_address.is_empty() || fee_address.is_empty() {
                return Err(StdError::generic_err(
                    "Invalid message, specifiy all the field",
                ));
            }
            execute_liquidation(
                deps,
                env,
                cw20_msg.sender,
                liquidator,
                repay_address,
                fee_address,
                collateral_token,
                cw20_msg.amount.into(),
                borrower_address,
            )
        }

        Cw20HookMsg::SubmitBid {
            collateral_token,
            premium_slot,
        } => {
            // only asset contract can execute this message
            let config: Config = read_config(deps.storage)?;
            if deps.api.addr_validate(contract_addr.as_str())? != config.stable_contract {
                return Err(StdError::generic_err(
                    "unauthorized stable have been provided",
                ));
            }

            let bidder = cw20_msg.sender;

            let amount: Uint256 = cw20_msg.amount.into();
            submit_bid(deps, env, bidder, collateral_token, premium_slot, amount)
        }
    }
}

#[allow(clippy::too_many_arguments)]
pub fn update_config(
    deps: DepsMut,
    info: MessageInfo,
    owner: Option<String>,
    oracle_contract: Option<String>,
    safe_ratio: Option<Decimal256>,
    bid_fee: Option<Decimal256>,
    liquidator_fee: Option<Decimal256>,
    liquidation_threshold: Option<Uint256>,
    price_timeframe: Option<u64>,
    waiting_period: Option<u64>,
    overseer: Option<String>,
) -> StdResult<Response> {
    let mut config: Config = read_config(deps.storage)?;
    if info.sender != config.owner {
        return Err(StdError::generic_err("unauthorized"));
    }

    if let Some(owner) = owner {
        config.owner = deps.api.addr_validate(&owner)?;
    }

    if let Some(oracle_contract) = oracle_contract {
        config.oracle_contract = deps.api.addr_validate(&oracle_contract)?;
    }

    if let Some(safe_ratio) = safe_ratio {
        config.safe_ratio = safe_ratio;
    }

    if let Some(bid_fee) = bid_fee {
        assert_fees(bid_fee + config.liquidator_fee)?;
        config.bid_fee = bid_fee;
    }

    if let Some(liquidator_fee) = liquidator_fee {
        assert_fees(liquidator_fee + config.bid_fee)?;
        config.liquidator_fee = liquidator_fee;
    }

    if let Some(liquidation_threshold) = liquidation_threshold {
        config.liquidation_threshold = liquidation_threshold;
    }

    if let Some(price_timeframe) = price_timeframe {
        config.price_timeframe = price_timeframe;
    }

    if let Some(waiting_period) = waiting_period {
        config.waiting_period = waiting_period;
    }

    if let Some(overseer) = overseer {
        config.overseer = deps.api.addr_validate(&overseer)?;
    }

    store_config(deps.storage, &config)?;
    Ok(Response::new().add_attribute("action", "update_config"))
}

pub fn whitelist_collateral(
    deps: DepsMut,
    info: MessageInfo,
    collateral_token: String,
    bid_threshold: Uint256,
    max_slot: u8,
    premium_rate_per_slot: Decimal256,
) -> StdResult<Response> {
    let config: Config = read_config(deps.storage)?;
    let collateral_token_validated = deps.api.addr_validate(&collateral_token)?;
    if info.sender != config.owner {
        return Err(StdError::generic_err("unauthorized"));
    }

    // fail if the collateral is already whitelisted
    if read_collateral_info(deps.storage, &collateral_token_validated).is_ok() {
        return Err(StdError::generic_err("Collateral is already whitelisted"));
    }

    // check if the colalteral is whitelisted in overseer
    query_collateral_whitelist_info(&deps.querier, config.overseer.to_string(), collateral_token)
        .map_err(|_| StdError::generic_err("This collateral is not whitelisted in Capa overseer"))?;

    // assert max slot does not exceed cap and max premium rate does not exceed 1
    assert_max_slot(max_slot)?;
    assert_max_slot_premium(max_slot, premium_rate_per_slot)?;

    // save collateral info
    store_collateral_info(
        deps.storage,
        &collateral_token_validated,
        &CollateralInfo {
            collateral_token: collateral_token_validated.clone(),
            max_slot,
            bid_threshold,
            premium_rate_per_slot,
        },
    )?;

    Ok(Response::new().add_attribute("action", "whitelist_collateral"))
}

pub fn update_collateral_info(
    deps: DepsMut,
    info: MessageInfo,
    collateral_token: String,
    bid_threshold: Option<Uint256>,
    max_slot: Option<u8>,
) -> StdResult<Response> {
    let config: Config = read_config(deps.storage)?;
    let collateral_token_validated = deps.api.addr_validate(&collateral_token)?;
    if info.sender != config.owner {
        return Err(StdError::generic_err("unauthorized"));
    }

    // update collateral info
    let mut collateral_info: CollateralInfo =
        read_collateral_info(deps.storage, &collateral_token_validated)?;

    if let Some(bid_threshold) = bid_threshold {
        collateral_info.bid_threshold = bid_threshold;
    }

    if let Some(max_slot) = max_slot {
        // assert max slot does not exceed cap and max premium rate does not exceed 1
        assert_max_slot(max_slot)?;
        assert_max_slot_premium(max_slot, collateral_info.premium_rate_per_slot)?;
        collateral_info.max_slot = max_slot;
    }

    // save collateral info
    store_collateral_info(deps.storage, &collateral_token_validated, &collateral_info)?;

    Ok(Response::new().add_attribute("action", "update_collateral_info"))
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::Config {} => to_json_binary(&query_config(deps)?),
        QueryMsg::LiquidationAmount {
            borrow_amount,
            borrow_limit,
            collaterals,
            collateral_prices,
        } => to_json_binary(&query_liquidation_amount(
            deps,
            borrow_amount,
            borrow_limit,
            collaterals,
            collateral_prices,
        )?),
        QueryMsg::CollateralInfo { collateral_token } => {
            to_json_binary(&query_collateral_info(deps, collateral_token)?)
        }
        QueryMsg::Bid { bid_idx } => to_json_binary(&query_bid(deps, bid_idx)?),
        QueryMsg::BidsByUser {
            collateral_token,
            bidder,
            start_after,
            limit,
        } => to_json_binary(&query_bids_by_user(
            deps,
            collateral_token,
            bidder,
            start_after,
            limit,
        )?),
        QueryMsg::BidPool {
            collateral_token,
            bid_slot,
        } => to_json_binary(&query_bid_pool(deps, collateral_token, bid_slot)?),
        QueryMsg::BidPoolsByCollateral {
            collateral_token,
            start_after,
            limit,
        } => to_json_binary(&query_bid_pools(
            deps,
            collateral_token,
            start_after,
            limit,
        )?),
    }
}

#[entry_point]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
    Ok(Response::default())
}