#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
attr, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult,
SubMsg, Uint128,
};
use cw2::set_contract_version;
use cw20::{BalanceResponse, Cw20Coin, Cw20ReceiveMsg, MinterResponse, TokenInfoResponse};
use crate::allowances::{
execute_burn_from, execute_decrease_allowance, execute_increase_allowance, execute_send_from,
execute_transfer_from, query_allowance,
};
use crate::enumerable::{query_all_accounts, query_all_allowances};
use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
use crate::state::{MinterData, TokenInfo, BALANCES, TOKEN_INFO};
const CONTRACT_NAME: &str = "crates.io:cw20-base";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
mut deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
msg.validate()?;
let total_supply = create_accounts(&mut deps, &msg.initial_balances)?;
if let Some(limit) = msg.get_cap() {
if total_supply > limit {
return Err(StdError::generic_err("Initial supply greater than cap"));
}
}
let mint = match msg.mint {
Some(m) => Some(MinterData {
minter: deps.api.addr_validate(&m.minter)?,
cap: m.cap,
}),
None => None,
};
let data = TokenInfo {
name: msg.name,
symbol: msg.symbol,
decimals: msg.decimals,
total_supply,
mint,
};
TOKEN_INFO.save(deps.storage, &data)?;
Ok(Response::default())
}
pub fn create_accounts(deps: &mut DepsMut, accounts: &[Cw20Coin]) -> StdResult<Uint128> {
let mut total_supply = Uint128::zero();
for row in accounts {
let address = deps.api.addr_validate(&row.address)?;
BALANCES.save(deps.storage, &address, &row.amount)?;
total_supply += row.amount;
}
Ok(total_supply)
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Transfer { recipient, amount } => {
execute_transfer(deps, env, info, recipient, amount)
}
ExecuteMsg::Burn { amount } => execute_burn(deps, env, info, amount),
ExecuteMsg::Send {
contract,
amount,
msg,
} => execute_send(deps, env, info, contract, amount, msg),
ExecuteMsg::Mint { recipient, amount } => execute_mint(deps, env, info, recipient, amount),
ExecuteMsg::IncreaseAllowance {
spender,
amount,
expires,
} => execute_increase_allowance(deps, env, info, spender, amount, expires),
ExecuteMsg::DecreaseAllowance {
spender,
amount,
expires,
} => execute_decrease_allowance(deps, env, info, spender, amount, expires),
ExecuteMsg::TransferFrom {
owner,
recipient,
amount,
} => execute_transfer_from(deps, env, info, owner, recipient, amount),
ExecuteMsg::BurnFrom { owner, amount } => execute_burn_from(deps, env, info, owner, amount),
ExecuteMsg::SendFrom {
owner,
contract,
amount,
msg,
} => execute_send_from(deps, env, info, owner, contract, amount, msg),
}
}
pub fn execute_transfer(
deps: DepsMut,
_env: Env,
info: MessageInfo,
recipient: String,
amount: Uint128,
) -> Result<Response, ContractError> {
if amount == Uint128::zero() {
return Err(ContractError::InvalidZeroAmount {});
}
let rcpt_addr = deps.api.addr_validate(&recipient)?;
BALANCES.update(
deps.storage,
&info.sender,
|balance: Option<Uint128>| -> StdResult<_> {
Ok(balance.unwrap_or_default().checked_sub(amount)?)
},
)?;
BALANCES.update(
deps.storage,
&rcpt_addr,
|balance: Option<Uint128>| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) },
)?;
let res = Response {
messages: vec![],
attributes: vec![
attr("action", "transfer"),
attr("from", info.sender),
attr("to", recipient),
attr("amount", amount),
],
events: vec![],
data: None,
};
Ok(res)
}
pub fn execute_burn(
deps: DepsMut,
_env: Env,
info: MessageInfo,
amount: Uint128,
) -> Result<Response, ContractError> {
if amount == Uint128::zero() {
return Err(ContractError::InvalidZeroAmount {});
}
BALANCES.update(
deps.storage,
&info.sender,
|balance: Option<Uint128>| -> StdResult<_> {
Ok(balance.unwrap_or_default().checked_sub(amount)?)
},
)?;
TOKEN_INFO.update(deps.storage, |mut info| -> StdResult<_> {
info.total_supply = info.total_supply.checked_sub(amount)?;
Ok(info)
})?;
let res = Response {
messages: vec![],
attributes: vec![
attr("action", "burn"),
attr("from", info.sender),
attr("amount", amount),
],
events: vec![],
data: None,
};
Ok(res)
}
pub fn execute_mint(
deps: DepsMut,
_env: Env,
info: MessageInfo,
recipient: String,
amount: Uint128,
) -> Result<Response, ContractError> {
if amount == Uint128::zero() {
return Err(ContractError::InvalidZeroAmount {});
}
let mut config = TOKEN_INFO.load(deps.storage)?;
if config.mint.is_none() || config.mint.as_ref().unwrap().minter != info.sender {
return Err(ContractError::Unauthorized {});
}
config.total_supply += amount;
if let Some(limit) = config.get_cap() {
if config.total_supply > limit {
return Err(ContractError::CannotExceedCap {});
}
}
TOKEN_INFO.save(deps.storage, &config)?;
let rcpt_addr = deps.api.addr_validate(&recipient)?;
BALANCES.update(
deps.storage,
&rcpt_addr,
|balance: Option<Uint128>| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) },
)?;
let res = Response {
attributes: vec![
attr("action", "mint"),
attr("to", recipient),
attr("amount", amount),
],
..Response::default()
};
Ok(res)
}
pub fn execute_send(
deps: DepsMut,
_env: Env,
info: MessageInfo,
contract: String,
amount: Uint128,
msg: Binary,
) -> Result<Response, ContractError> {
if amount == Uint128::zero() {
return Err(ContractError::InvalidZeroAmount {});
}
let rcpt_addr = deps.api.addr_validate(&contract)?;
BALANCES.update(
deps.storage,
&info.sender,
|balance: Option<Uint128>| -> StdResult<_> {
Ok(balance.unwrap_or_default().checked_sub(amount)?)
},
)?;
BALANCES.update(
deps.storage,
&rcpt_addr,
|balance: Option<Uint128>| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) },
)?;
let attrs = vec![
attr("action", "send"),
attr("from", &info.sender),
attr("to", &contract),
attr("amount", amount),
];
let msg = SubMsg::new(
Cw20ReceiveMsg {
sender: info.sender.into(),
amount,
msg,
}
.into_cosmos_msg(contract)?,
);
let res = Response {
messages: vec![msg],
attributes: attrs,
..Response::default()
};
Ok(res)
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?),
QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?),
QueryMsg::Minter {} => to_binary(&query_minter(deps)?),
QueryMsg::Allowance { owner, spender } => {
to_binary(&query_allowance(deps, owner, spender)?)
}
QueryMsg::AllAllowances {
owner,
start_after,
limit,
} => to_binary(&query_all_allowances(deps, owner, start_after, limit)?),
QueryMsg::AllAccounts { start_after, limit } => {
to_binary(&query_all_accounts(deps, start_after, limit)?)
}
}
}
pub fn query_balance(deps: Deps, address: String) -> StdResult<BalanceResponse> {
let address = deps.api.addr_validate(&address)?;
let balance = BALANCES
.may_load(deps.storage, &address)?
.unwrap_or_default();
Ok(BalanceResponse { balance })
}
pub fn query_token_info(deps: Deps) -> StdResult<TokenInfoResponse> {
let info = TOKEN_INFO.load(deps.storage)?;
let res = TokenInfoResponse {
name: info.name,
symbol: info.symbol,
decimals: info.decimals,
total_supply: info.total_supply,
};
Ok(res)
}
pub fn query_minter(deps: Deps) -> StdResult<Option<MinterResponse>> {
let meta = TOKEN_INFO.load(deps.storage)?;
let minter = match meta.mint {
Some(m) => Some(MinterResponse {
minter: m.minter.into(),
cap: m.cap,
}),
None => None,
};
Ok(minter)
}
#[cfg(test)]
mod tests {
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{coins, from_binary, CosmosMsg, StdError, WasmMsg};
use super::*;
fn get_balance<T: Into<String>>(deps: Deps, address: T) -> Uint128 {
query_balance(deps, address.into()).unwrap().balance
}
fn do_instantiate_with_minter(
deps: DepsMut,
addr: &str,
amount: Uint128,
minter: &str,
cap: Option<Uint128>,
) -> TokenInfoResponse {
_do_instantiate(
deps,
addr,
amount,
Some(MinterResponse {
minter: minter.to_string(),
cap,
}),
)
}
fn do_instantiate(deps: DepsMut, addr: &str, amount: Uint128) -> TokenInfoResponse {
_do_instantiate(deps, addr, amount, None)
}
fn _do_instantiate(
mut deps: DepsMut,
addr: &str,
amount: Uint128,
mint: Option<MinterResponse>,
) -> TokenInfoResponse {
let instantiate_msg = InstantiateMsg {
name: "Auto Gen".to_string(),
symbol: "AUTO".to_string(),
decimals: 3,
initial_balances: vec![Cw20Coin {
address: addr.to_string(),
amount,
}],
mint: mint.clone(),
};
let info = mock_info("creator", &[]);
let env = mock_env();
let res = instantiate(deps.branch(), env, info, instantiate_msg).unwrap();
assert_eq!(0, res.messages.len());
let meta = query_token_info(deps.as_ref()).unwrap();
assert_eq!(
meta,
TokenInfoResponse {
name: "Auto Gen".to_string(),
symbol: "AUTO".to_string(),
decimals: 3,
total_supply: amount,
}
);
assert_eq!(get_balance(deps.as_ref(), addr), amount);
assert_eq!(query_minter(deps.as_ref()).unwrap(), mint,);
meta
}
#[test]
fn proper_instantiation() {
let mut deps = mock_dependencies(&[]);
let amount = Uint128::from(11223344u128);
let instantiate_msg = InstantiateMsg {
name: "Cash Token".to_string(),
symbol: "CASH".to_string(),
decimals: 9,
initial_balances: vec![Cw20Coin {
address: String::from("addr0000"),
amount,
}],
mint: None,
};
let info = mock_info("creator", &[]);
let env = mock_env();
let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap();
assert_eq!(0, res.messages.len());
assert_eq!(
query_token_info(deps.as_ref()).unwrap(),
TokenInfoResponse {
name: "Cash Token".to_string(),
symbol: "CASH".to_string(),
decimals: 9,
total_supply: amount,
}
);
assert_eq!(
get_balance(deps.as_ref(), "addr0000"),
Uint128::new(11223344)
);
}
#[test]
fn instantiate_mintable() {
let mut deps = mock_dependencies(&[]);
let amount = Uint128::new(11223344);
let minter = String::from("asmodat");
let limit = Uint128::new(511223344);
let instantiate_msg = InstantiateMsg {
name: "Cash Token".to_string(),
symbol: "CASH".to_string(),
decimals: 9,
initial_balances: vec![Cw20Coin {
address: "addr0000".into(),
amount,
}],
mint: Some(MinterResponse {
minter: minter.clone(),
cap: Some(limit),
}),
};
let info = mock_info("creator", &[]);
let env = mock_env();
let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap();
assert_eq!(0, res.messages.len());
assert_eq!(
query_token_info(deps.as_ref()).unwrap(),
TokenInfoResponse {
name: "Cash Token".to_string(),
symbol: "CASH".to_string(),
decimals: 9,
total_supply: amount,
}
);
assert_eq!(
get_balance(deps.as_ref(), "addr0000"),
Uint128::new(11223344)
);
assert_eq!(
query_minter(deps.as_ref()).unwrap(),
Some(MinterResponse {
minter,
cap: Some(limit),
}),
);
}
#[test]
fn instantiate_mintable_over_cap() {
let mut deps = mock_dependencies(&[]);
let amount = Uint128::new(11223344);
let minter = String::from("asmodat");
let limit = Uint128::new(11223300);
let instantiate_msg = InstantiateMsg {
name: "Cash Token".to_string(),
symbol: "CASH".to_string(),
decimals: 9,
initial_balances: vec![Cw20Coin {
address: String::from("addr0000"),
amount,
}],
mint: Some(MinterResponse {
minter,
cap: Some(limit),
}),
};
let info = mock_info("creator", &[]);
let env = mock_env();
let err = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap_err();
assert_eq!(
err,
StdError::generic_err("Initial supply greater than cap")
);
}
#[test]
fn can_mint_by_minter() {
let mut deps = mock_dependencies(&[]);
let genesis = String::from("genesis");
let amount = Uint128::new(11223344);
let minter = String::from("asmodat");
let limit = Uint128::new(511223344);
do_instantiate_with_minter(deps.as_mut(), &genesis, amount, &minter, Some(limit));
let winner = String::from("lucky");
let prize = Uint128::new(222_222_222);
let msg = ExecuteMsg::Mint {
recipient: winner.clone(),
amount: prize,
};
let info = mock_info(minter.as_ref(), &[]);
let env = mock_env();
let res = execute(deps.as_mut(), env, info, msg).unwrap();
assert_eq!(0, res.messages.len());
assert_eq!(get_balance(deps.as_ref(), genesis), amount);
assert_eq!(get_balance(deps.as_ref(), winner.clone()), prize);
let msg = ExecuteMsg::Mint {
recipient: winner.clone(),
amount: Uint128::zero(),
};
let info = mock_info(minter.as_ref(), &[]);
let env = mock_env();
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert_eq!(err, ContractError::InvalidZeroAmount {});
let msg = ExecuteMsg::Mint {
recipient: winner,
amount: Uint128::new(333_222_222),
};
let info = mock_info(minter.as_ref(), &[]);
let env = mock_env();
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert_eq!(err, ContractError::CannotExceedCap {});
}
#[test]
fn others_cannot_mint() {
let mut deps = mock_dependencies(&[]);
do_instantiate_with_minter(
deps.as_mut(),
&String::from("genesis"),
Uint128::new(1234),
&String::from("minter"),
None,
);
let msg = ExecuteMsg::Mint {
recipient: String::from("lucky"),
amount: Uint128::new(222),
};
let info = mock_info("anyone else", &[]);
let env = mock_env();
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert_eq!(err, ContractError::Unauthorized {});
}
#[test]
fn no_one_mints_if_minter_unset() {
let mut deps = mock_dependencies(&[]);
do_instantiate(deps.as_mut(), &String::from("genesis"), Uint128::new(1234));
let msg = ExecuteMsg::Mint {
recipient: String::from("lucky"),
amount: Uint128::new(222),
};
let info = mock_info("genesis", &[]);
let env = mock_env();
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert_eq!(err, ContractError::Unauthorized {});
}
#[test]
fn instantiate_multiple_accounts() {
let mut deps = mock_dependencies(&[]);
let amount1 = Uint128::from(11223344u128);
let addr1 = String::from("addr0001");
let amount2 = Uint128::from(7890987u128);
let addr2 = String::from("addr0002");
let instantiate_msg = InstantiateMsg {
name: "Bash Shell".to_string(),
symbol: "BASH".to_string(),
decimals: 6,
initial_balances: vec![
Cw20Coin {
address: addr1.clone(),
amount: amount1,
},
Cw20Coin {
address: addr2.clone(),
amount: amount2,
},
],
mint: None,
};
let info = mock_info("creator", &[]);
let env = mock_env();
let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap();
assert_eq!(0, res.messages.len());
assert_eq!(
query_token_info(deps.as_ref()).unwrap(),
TokenInfoResponse {
name: "Bash Shell".to_string(),
symbol: "BASH".to_string(),
decimals: 6,
total_supply: amount1 + amount2,
}
);
assert_eq!(get_balance(deps.as_ref(), addr1), amount1);
assert_eq!(get_balance(deps.as_ref(), addr2), amount2);
}
#[test]
fn queries_work() {
let mut deps = mock_dependencies(&coins(2, "token"));
let addr1 = String::from("addr0001");
let amount1 = Uint128::from(12340000u128);
let expected = do_instantiate(deps.as_mut(), &addr1, amount1);
let loaded = query_token_info(deps.as_ref()).unwrap();
assert_eq!(expected, loaded);
let _info = mock_info("test", &[]);
let env = mock_env();
let data = query(
deps.as_ref(),
env.clone(),
QueryMsg::Balance { address: addr1 },
)
.unwrap();
let loaded: BalanceResponse = from_binary(&data).unwrap();
assert_eq!(loaded.balance, amount1);
let data = query(
deps.as_ref(),
env,
QueryMsg::Balance {
address: String::from("addr0002"),
},
)
.unwrap();
let loaded: BalanceResponse = from_binary(&data).unwrap();
assert_eq!(loaded.balance, Uint128::zero());
}
#[test]
fn transfer() {
let mut deps = mock_dependencies(&coins(2, "token"));
let addr1 = String::from("addr0001");
let addr2 = String::from("addr0002");
let amount1 = Uint128::from(12340000u128);
let transfer = Uint128::from(76543u128);
let too_much = Uint128::from(12340321u128);
do_instantiate(deps.as_mut(), &addr1, amount1);
let info = mock_info(addr1.as_ref(), &[]);
let env = mock_env();
let msg = ExecuteMsg::Transfer {
recipient: addr2.clone(),
amount: Uint128::zero(),
};
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert_eq!(err, ContractError::InvalidZeroAmount {});
let info = mock_info(addr1.as_ref(), &[]);
let env = mock_env();
let msg = ExecuteMsg::Transfer {
recipient: addr2.clone(),
amount: too_much,
};
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert!(matches!(err, ContractError::Std(StdError::Overflow { .. })));
let info = mock_info(addr2.as_ref(), &[]);
let env = mock_env();
let msg = ExecuteMsg::Transfer {
recipient: addr1.clone(),
amount: transfer,
};
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert!(matches!(err, ContractError::Std(StdError::Overflow { .. })));
let info = mock_info(addr1.as_ref(), &[]);
let env = mock_env();
let msg = ExecuteMsg::Transfer {
recipient: addr2.clone(),
amount: transfer,
};
let res = execute(deps.as_mut(), env, info, msg).unwrap();
assert_eq!(res.messages.len(), 0);
let remainder = amount1.checked_sub(transfer).unwrap();
assert_eq!(get_balance(deps.as_ref(), addr1), remainder);
assert_eq!(get_balance(deps.as_ref(), addr2), transfer);
assert_eq!(
query_token_info(deps.as_ref()).unwrap().total_supply,
amount1
);
}
#[test]
fn burn() {
let mut deps = mock_dependencies(&coins(2, "token"));
let addr1 = String::from("addr0001");
let amount1 = Uint128::from(12340000u128);
let burn = Uint128::from(76543u128);
let too_much = Uint128::from(12340321u128);
do_instantiate(deps.as_mut(), &addr1, amount1);
let info = mock_info(addr1.as_ref(), &[]);
let env = mock_env();
let msg = ExecuteMsg::Burn {
amount: Uint128::zero(),
};
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert_eq!(err, ContractError::InvalidZeroAmount {});
assert_eq!(
query_token_info(deps.as_ref()).unwrap().total_supply,
amount1
);
let info = mock_info(addr1.as_ref(), &[]);
let env = mock_env();
let msg = ExecuteMsg::Burn { amount: too_much };
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert!(matches!(err, ContractError::Std(StdError::Overflow { .. })));
assert_eq!(
query_token_info(deps.as_ref()).unwrap().total_supply,
amount1
);
let info = mock_info(addr1.as_ref(), &[]);
let env = mock_env();
let msg = ExecuteMsg::Burn { amount: burn };
let res = execute(deps.as_mut(), env, info, msg).unwrap();
assert_eq!(res.messages.len(), 0);
let remainder = amount1.checked_sub(burn).unwrap();
assert_eq!(get_balance(deps.as_ref(), addr1), remainder);
assert_eq!(
query_token_info(deps.as_ref()).unwrap().total_supply,
remainder
);
}
#[test]
fn send() {
let mut deps = mock_dependencies(&coins(2, "token"));
let addr1 = String::from("addr0001");
let contract = String::from("addr0002");
let amount1 = Uint128::from(12340000u128);
let transfer = Uint128::from(76543u128);
let too_much = Uint128::from(12340321u128);
let send_msg = Binary::from(r#"{"some":123}"#.as_bytes());
do_instantiate(deps.as_mut(), &addr1, amount1);
let info = mock_info(addr1.as_ref(), &[]);
let env = mock_env();
let msg = ExecuteMsg::Send {
contract: contract.clone(),
amount: Uint128::zero(),
msg: send_msg.clone(),
};
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert_eq!(err, ContractError::InvalidZeroAmount {});
let info = mock_info(addr1.as_ref(), &[]);
let env = mock_env();
let msg = ExecuteMsg::Send {
contract: contract.clone(),
amount: too_much,
msg: send_msg.clone(),
};
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert!(matches!(err, ContractError::Std(StdError::Overflow { .. })));
let info = mock_info(addr1.as_ref(), &[]);
let env = mock_env();
let msg = ExecuteMsg::Send {
contract: contract.clone(),
amount: transfer,
msg: send_msg.clone(),
};
let res = execute(deps.as_mut(), env, info, msg).unwrap();
assert_eq!(res.messages.len(), 1);
let binary_msg = Cw20ReceiveMsg {
sender: addr1.clone(),
amount: transfer,
msg: send_msg,
}
.into_binary()
.unwrap();
assert_eq!(
res.messages[0],
SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: contract.clone(),
msg: binary_msg,
funds: vec![],
}))
);
let remainder = amount1.checked_sub(transfer).unwrap();
assert_eq!(get_balance(deps.as_ref(), addr1), remainder);
assert_eq!(get_balance(deps.as_ref(), contract), transfer);
assert_eq!(
query_token_info(deps.as_ref()).unwrap().total_supply,
amount1
);
}
}