#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
attr, coins, to_binary, Addr, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Response,
StdError, StdResult, Uint128,
};
use cw2::set_contract_version;
use cw20_base::allowances::{
deduct_allowance, execute_decrease_allowance, execute_increase_allowance, execute_send_from,
execute_transfer_from, query_allowance,
};
use cw20_base::contract::{
execute_burn, execute_mint, execute_send, execute_transfer, query_balance, query_token_info,
};
use cw20_base::state::{MinterData, TokenInfo, TOKEN_INFO};
use crate::curves::DecimalPlaces;
use crate::error::ContractError;
use crate::msg::{CurveFn, CurveInfoResponse, ExecuteMsg, InstantiateMsg, QueryMsg};
use crate::state::{CurveState, CURVE_STATE, CURVE_TYPE};
use cw0::{must_pay, nonpayable};
const CONTRACT_NAME: &str = "crates.io:cw20-bonding";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
nonpayable(&info)?;
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
let data = TokenInfo {
name: msg.name,
symbol: msg.symbol,
decimals: msg.decimals,
total_supply: Uint128::zero(),
mint: Some(MinterData {
minter: env.contract.address,
cap: None,
}),
};
TOKEN_INFO.save(deps.storage, &data)?;
let places = DecimalPlaces::new(msg.decimals, msg.reserve_decimals);
let supply = CurveState::new(msg.reserve_denom, places);
CURVE_STATE.save(deps.storage, &supply)?;
CURVE_TYPE.save(deps.storage, &msg.curve_type)?;
Ok(Response::default())
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
let curve_type = CURVE_TYPE.load(deps.storage)?;
let curve_fn = curve_type.to_curve_fn();
do_execute(deps, env, info, msg, curve_fn)
}
pub fn do_execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
curve_fn: CurveFn,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Buy {} => execute_buy(deps, env, info, curve_fn),
ExecuteMsg::Burn { amount } => Ok(execute_sell(deps, env, info, curve_fn, amount)?),
ExecuteMsg::BurnFrom { owner, amount } => {
Ok(execute_sell_from(deps, env, info, curve_fn, owner, amount)?)
}
ExecuteMsg::Transfer { recipient, amount } => {
Ok(execute_transfer(deps, env, info, recipient, amount)?)
}
ExecuteMsg::Send {
contract,
amount,
msg,
} => Ok(execute_send(deps, env, info, contract, amount, msg)?),
ExecuteMsg::IncreaseAllowance {
spender,
amount,
expires,
} => Ok(execute_increase_allowance(
deps, env, info, spender, amount, expires,
)?),
ExecuteMsg::DecreaseAllowance {
spender,
amount,
expires,
} => Ok(execute_decrease_allowance(
deps, env, info, spender, amount, expires,
)?),
ExecuteMsg::TransferFrom {
owner,
recipient,
amount,
} => Ok(execute_transfer_from(
deps, env, info, owner, recipient, amount,
)?),
ExecuteMsg::SendFrom {
owner,
contract,
amount,
msg,
} => Ok(execute_send_from(
deps, env, info, owner, contract, amount, msg,
)?),
}
}
pub fn execute_buy(
deps: DepsMut,
env: Env,
info: MessageInfo,
curve_fn: CurveFn,
) -> Result<Response, ContractError> {
let mut state = CURVE_STATE.load(deps.storage)?;
let payment = must_pay(&info, &state.reserve_denom)?;
let curve = curve_fn(state.decimals);
state.reserve += payment;
let new_supply = curve.supply(state.reserve);
let minted = new_supply
.checked_sub(state.supply)
.map_err(StdError::overflow)?;
state.supply = new_supply;
CURVE_STATE.save(deps.storage, &state)?;
let sub_info = MessageInfo {
sender: env.contract.address.clone(),
funds: vec![],
};
execute_mint(deps, env, sub_info, info.sender.to_string(), minted)?;
let res = Response::new()
.add_attribute("action", "buy")
.add_attribute("from", info.sender)
.add_attribute("reserve", payment)
.add_attribute("supply", minted);
Ok(res)
}
pub fn execute_sell(
deps: DepsMut,
env: Env,
info: MessageInfo,
curve_fn: CurveFn,
amount: Uint128,
) -> Result<Response, ContractError> {
nonpayable(&info)?;
let receiver = info.sender.clone();
let mut res = do_sell(deps, env, info, curve_fn, receiver, amount)?;
res.attributes.push(attr("action", "burn"));
Ok(res)
}
pub fn execute_sell_from(
deps: DepsMut,
env: Env,
info: MessageInfo,
curve_fn: CurveFn,
owner: String,
amount: Uint128,
) -> Result<Response, ContractError> {
nonpayable(&info)?;
let owner_addr = deps.api.addr_validate(&owner)?;
let spender_addr = info.sender.clone();
deduct_allowance(deps.storage, &owner_addr, &spender_addr, &env.block, amount)?;
let receiver_addr = info.sender;
let owner_info = MessageInfo {
sender: owner_addr,
funds: info.funds,
};
let mut res = do_sell(
deps,
env,
owner_info,
curve_fn,
receiver_addr.clone(),
amount,
)?;
res.attributes.push(attr("action", "burn_from"));
res.attributes.push(attr("by", receiver_addr));
Ok(res)
}
fn do_sell(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
curve_fn: CurveFn,
receiver: Addr,
amount: Uint128,
) -> Result<Response, ContractError> {
execute_burn(deps.branch(), env, info.clone(), amount)?;
let mut state = CURVE_STATE.load(deps.storage)?;
let curve = curve_fn(state.decimals);
state.supply = state
.supply
.checked_sub(amount)
.map_err(StdError::overflow)?;
let new_reserve = curve.reserve(state.supply);
let released = state
.reserve
.checked_sub(new_reserve)
.map_err(StdError::overflow)?;
state.reserve = new_reserve;
CURVE_STATE.save(deps.storage, &state)?;
let msg = BankMsg::Send {
to_address: receiver.to_string(),
amount: coins(released.u128(), state.reserve_denom),
};
let res = Response::new()
.add_message(msg)
.add_attribute("from", info.sender)
.add_attribute("supply", amount)
.add_attribute("reserve", released);
Ok(res)
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
let curve_type = CURVE_TYPE.load(deps.storage)?;
let curve_fn = curve_type.to_curve_fn();
do_query(deps, env, msg, curve_fn)
}
pub fn do_query(deps: Deps, _env: Env, msg: QueryMsg, curve_fn: CurveFn) -> StdResult<Binary> {
match msg {
QueryMsg::CurveInfo {} => to_binary(&query_curve_info(deps, curve_fn)?),
QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?),
QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?),
QueryMsg::Allowance { owner, spender } => {
to_binary(&query_allowance(deps, owner, spender)?)
}
}
}
pub fn query_curve_info(deps: Deps, curve_fn: CurveFn) -> StdResult<CurveInfoResponse> {
let CurveState {
reserve,
supply,
reserve_denom,
decimals,
} = CURVE_STATE.load(deps.storage)?;
let curve = curve_fn(decimals);
let spot_price = curve.spot_price(supply);
Ok(CurveInfoResponse {
reserve,
supply,
spot_price,
reserve_denom,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::msg::CurveType;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{coin, Decimal, OverflowError, OverflowOperation, StdError, SubMsg};
use cw0::PaymentError;
const DENOM: &str = "satoshi";
const CREATOR: &str = "creator";
const INVESTOR: &str = "investor";
const BUYER: &str = "buyer";
fn default_instantiate(
decimals: u8,
reserve_decimals: u8,
curve_type: CurveType,
) -> InstantiateMsg {
InstantiateMsg {
name: "Bonded".to_string(),
symbol: "EPOXY".to_string(),
decimals,
reserve_denom: DENOM.to_string(),
reserve_decimals,
curve_type,
}
}
fn get_balance<U: Into<String>>(deps: Deps, addr: U) -> Uint128 {
query_balance(deps, addr.into()).unwrap().balance
}
fn setup_test(deps: DepsMut, decimals: u8, reserve_decimals: u8, curve_type: CurveType) {
let creator = String::from(CREATOR);
let msg = default_instantiate(decimals, reserve_decimals, curve_type);
let info = mock_info(&creator, &[]);
let res = instantiate(deps, mock_env(), info, msg).unwrap();
assert_eq!(0, res.messages.len());
}
#[test]
fn proper_instantiation() {
let mut deps = mock_dependencies(&[]);
let creator = String::from("creator");
let curve_type = CurveType::SquareRoot {
slope: Uint128::new(1),
scale: 1,
};
let msg = default_instantiate(2, 8, curve_type.clone());
let info = mock_info(&creator, &[]);
let res = instantiate(deps.as_mut(), mock_env(), info, msg.clone()).unwrap();
assert_eq!(0, res.messages.len());
let token = query_token_info(deps.as_ref()).unwrap();
assert_eq!(&token.name, &msg.name);
assert_eq!(&token.symbol, &msg.symbol);
assert_eq!(token.decimals, 2);
assert_eq!(token.total_supply, Uint128::zero());
let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap();
assert_eq!(state.reserve, Uint128::zero());
assert_eq!(state.supply, Uint128::zero());
assert_eq!(state.reserve_denom.as_str(), DENOM);
assert_eq!(state.spot_price, Decimal::zero());
let curve = CURVE_TYPE.load(&deps.storage).unwrap();
assert_eq!(curve_type, curve);
assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero());
}
#[test]
fn buy_issues_tokens() {
let mut deps = mock_dependencies(&[]);
let curve_type = CurveType::Linear {
slope: Uint128::new(1),
scale: 1,
};
setup_test(deps.as_mut(), 2, 8, curve_type.clone());
let info = mock_info(INVESTOR, &coins(500_000_000, DENOM));
let buy = ExecuteMsg::Buy {};
execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap();
assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000));
assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::zero());
let info = mock_info(INVESTOR, &[]);
let send = ExecuteMsg::Transfer {
recipient: BUYER.into(),
amount: Uint128::new(1000),
};
execute(deps.as_mut(), mock_env(), info, send).unwrap();
assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::zero());
assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000));
let info = mock_info(INVESTOR, &coins(1_500_000_000, DENOM));
execute(deps.as_mut(), mock_env(), info, buy).unwrap();
assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000));
assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000));
let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap();
assert_eq!(curve.reserve, Uint128::new(2_000_000_000));
assert_eq!(curve.supply, Uint128::new(2000));
assert_eq!(curve.spot_price, Decimal::percent(200));
let token = query_token_info(deps.as_ref()).unwrap();
assert_eq!(token.decimals, 2);
assert_eq!(token.total_supply, Uint128::new(2000));
}
#[test]
fn bonding_fails_with_wrong_denom() {
let mut deps = mock_dependencies(&[]);
let curve_type = CurveType::Linear {
slope: Uint128::new(1),
scale: 1,
};
setup_test(deps.as_mut(), 2, 8, curve_type);
let info = mock_info(INVESTOR, &[]);
let buy = ExecuteMsg::Buy {};
let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err();
assert_eq!(err, PaymentError::NoFunds {}.into());
let info = mock_info(INVESTOR, &coins(1234567, "wei"));
let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err();
assert_eq!(err, PaymentError::MissingDenom(DENOM.into()).into());
let info = mock_info(INVESTOR, &[coin(3400022, DENOM), coin(1234567, "wei")]);
let err = execute(deps.as_mut(), mock_env(), info, buy).unwrap_err();
assert_eq!(err, PaymentError::MultipleDenoms {}.into());
}
#[test]
fn burning_sends_reserve() {
let mut deps = mock_dependencies(&[]);
let curve_type = CurveType::Linear {
slope: Uint128::new(1),
scale: 1,
};
setup_test(deps.as_mut(), 2, 8, curve_type.clone());
let info = mock_info(INVESTOR, &coins(2_000_000_000, DENOM));
let buy = ExecuteMsg::Buy {};
execute(deps.as_mut(), mock_env(), info, buy).unwrap();
assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(2000));
let info = mock_info(INVESTOR, &[]);
let burn = ExecuteMsg::Burn {
amount: Uint128::new(3000),
};
let err = execute(deps.as_mut(), mock_env(), info, burn).unwrap_err();
assert_eq!(
err,
ContractError::Base(cw20_base::ContractError::Std(StdError::overflow(
OverflowError::new(OverflowOperation::Sub, 2000, 3000)
)))
);
let info = mock_info(INVESTOR, &[]);
let burn = ExecuteMsg::Burn {
amount: Uint128::new(1000),
};
let res = execute(deps.as_mut(), mock_env(), info, burn).unwrap();
assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000));
assert_eq!(1, res.messages.len());
assert_eq!(
&res.messages[0],
&SubMsg::new(BankMsg::Send {
to_address: INVESTOR.into(),
amount: coins(1_500_000_000, DENOM),
})
);
let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap();
assert_eq!(curve.reserve, Uint128::new(500_000_000));
assert_eq!(curve.supply, Uint128::new(1000));
assert_eq!(curve.spot_price, Decimal::percent(100));
let token = query_token_info(deps.as_ref()).unwrap();
assert_eq!(token.decimals, 2);
assert_eq!(token.total_supply, Uint128::new(1000));
}
#[test]
fn cw20_imports_work() {
let mut deps = mock_dependencies(&[]);
let curve_type = CurveType::Constant {
value: Uint128::new(15),
scale: 1,
};
setup_test(deps.as_mut(), 9, 6, curve_type);
let alice: &str = "alice";
let bob: &str = "bobby";
let carl: &str = "carl";
let info = mock_info(bob, &coins(45_000, DENOM));
let buy = ExecuteMsg::Buy {};
execute(deps.as_mut(), mock_env(), info, buy).unwrap();
assert_eq!(get_balance(deps.as_ref(), bob), Uint128::new(30_000_000));
assert_eq!(get_balance(deps.as_ref(), carl), Uint128::zero());
let bob_info = mock_info(bob, &[]);
let transfer = ExecuteMsg::Transfer {
recipient: carl.into(),
amount: Uint128::new(2_000_000),
};
execute(deps.as_mut(), mock_env(), bob_info.clone(), transfer).unwrap();
assert_eq!(get_balance(deps.as_ref(), bob), Uint128::new(28_000_000));
assert_eq!(get_balance(deps.as_ref(), carl), Uint128::new(2_000_000));
let allow = ExecuteMsg::IncreaseAllowance {
spender: alice.into(),
amount: Uint128::new(35_000_000),
expires: None,
};
execute(deps.as_mut(), mock_env(), bob_info, allow).unwrap();
assert_eq!(get_balance(deps.as_ref(), bob), Uint128::new(28_000_000));
assert_eq!(get_balance(deps.as_ref(), alice), Uint128::zero());
assert_eq!(
query_allowance(deps.as_ref(), bob.into(), alice.into())
.unwrap()
.allowance,
Uint128::new(35_000_000)
);
let self_pay = ExecuteMsg::TransferFrom {
owner: bob.into(),
recipient: alice.into(),
amount: Uint128::new(25_000_000),
};
let alice_info = mock_info(alice, &[]);
execute(deps.as_mut(), mock_env(), alice_info, self_pay).unwrap();
assert_eq!(get_balance(deps.as_ref(), bob), Uint128::new(3_000_000));
assert_eq!(get_balance(deps.as_ref(), alice), Uint128::new(25_000_000));
assert_eq!(get_balance(deps.as_ref(), carl), Uint128::new(2_000_000));
assert_eq!(
query_allowance(deps.as_ref(), bob.into(), alice.into())
.unwrap()
.allowance,
Uint128::new(10_000_000)
);
let info = mock_info(alice, &[]);
let burn_from = ExecuteMsg::BurnFrom {
owner: bob.into(),
amount: Uint128::new(3_300_000),
};
let err = execute(deps.as_mut(), mock_env(), info, burn_from).unwrap_err();
assert_eq!(
err,
ContractError::Base(cw20_base::ContractError::Std(StdError::overflow(
OverflowError::new(OverflowOperation::Sub, 3000000, 3300000)
)))
);
let info = mock_info(alice, &[]);
let burn_from = ExecuteMsg::BurnFrom {
owner: bob.into(),
amount: Uint128::new(1_000_000),
};
let res = execute(deps.as_mut(), mock_env(), info, burn_from).unwrap();
assert_eq!(get_balance(deps.as_ref(), alice), Uint128::new(25_000_000));
assert_eq!(get_balance(deps.as_ref(), bob), Uint128::new(2_000_000));
assert_eq!(1, res.messages.len());
assert_eq!(
&res.messages[0],
&SubMsg::new(BankMsg::Send {
to_address: alice.into(),
amount: coins(1_500, DENOM),
})
);
}
}