Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Payment helpers #219

Merged
merged 4 commits into from
Jan 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 10 additions & 19 deletions contracts/cw20-bonding/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::curves::DecimalPlaces;
use crate::error::ContractError;
use crate::msg::{CurveFn, CurveInfoResponse, HandleMsg, InitMsg, QueryMsg};
use crate::state::{CurveState, CURVE_STATE, CURVE_TYPE};
use cw0::{must_pay, nonpayable};

// version info for migration info
const CONTRACT_NAME: &str = "crates.io:cw20-bonding";
Expand All @@ -25,9 +26,10 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
pub fn init(
deps: DepsMut,
env: Env,
_info: MessageInfo,
info: MessageInfo,
msg: InitMsg,
) -> Result<InitResponse, ContractError> {
nonpayable(&info)?;
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

// store token info using cw20-base format
Expand Down Expand Up @@ -134,21 +136,7 @@ pub fn handle_buy(
) -> Result<HandleResponse, ContractError> {
let mut state = CURVE_STATE.load(deps.storage)?;

// ensure the sent denom was proper
let payment = match info.sent_funds.len() {
0 => Err(ContractError::NoFunds {}),
1 => {
if info.sent_funds[0].denom == state.reserve_denom {
Ok(info.sent_funds[0].amount)
} else {
Err(ContractError::MissingDenom(state.reserve_denom.clone()))
}
}
_ => Err(ContractError::ExtraDenoms(state.reserve_denom.clone())),
}?;
if payment.is_zero() {
return Err(ContractError::NoFunds {});
}
let payment = must_pay(&info, &state.reserve_denom)?;

// calculate how many tokens can be purchased with this and mint them
let curve = curve_fn(state.decimals);
Expand Down Expand Up @@ -186,6 +174,7 @@ pub fn handle_sell(
curve_fn: CurveFn,
amount: Uint128,
) -> Result<HandleResponse, ContractError> {
nonpayable(&info)?;
let receiver = info.sender.clone();
// do all the work
let mut res = do_sell(deps, env, info, curve_fn, receiver, amount)?;
Expand All @@ -203,6 +192,7 @@ pub fn handle_sell_from(
owner: HumanAddr,
amount: Uint128,
) -> Result<HandleResponse, ContractError> {
nonpayable(&info)?;
let owner_raw = deps.api.canonical_address(&owner)?;
let spender_raw = deps.api.canonical_address(&info.sender)?;

Expand Down Expand Up @@ -314,6 +304,7 @@ mod tests {
use crate::msg::CurveType;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MOCK_CONTRACT_ADDR};
use cosmwasm_std::{coin, Decimal};
use cw0::PaymentError;

const DENOM: &str = "satoshi";
const CREATOR: &str = "creator";
Expand Down Expand Up @@ -449,17 +440,17 @@ mod tests {
let info = mock_info(INVESTOR, &[]);
let buy = HandleMsg::Buy {};
let err = handle(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err();
assert_eq!(err, ContractError::NoFunds {});
assert_eq!(err, PaymentError::NoFunds {}.into());

// fails when wrong tokens sent
let info = mock_info(INVESTOR, &coins(1234567, "wei"));
let err = handle(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err();
assert_eq!(err, ContractError::MissingDenom(DENOM.into()));
assert_eq!(err, PaymentError::MissingDenom(DENOM.into()).into());

// fails when too many tokens sent
let info = mock_info(INVESTOR, &[coin(3400022, DENOM), coin(1234567, "wei")]);
let err = handle(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err();
assert_eq!(err, ContractError::ExtraDenoms(DENOM.into()));
assert_eq!(err, PaymentError::ExtraDenom("wei".to_string()).into());
}

#[test]
Expand Down
13 changes: 4 additions & 9 deletions contracts/cw20-bonding/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use cosmwasm_std::StdError;
use cw0::PaymentError;
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
Expand All @@ -9,15 +10,9 @@ pub enum ContractError {
#[error("{0}")]
Base(#[from] cw20_base::ContractError),

#[error("{0}")]
Payment(#[from] PaymentError),

#[error("Unauthorized")]
Unauthorized {},

#[error("Must send reserve token '{0}'")]
MissingDenom(String),

#[error("Sent unsupported token, must send reserve token '{0}'")]
ExtraDenoms(String),

#[error("No funds sent")]
NoFunds {},
}
2 changes: 2 additions & 0 deletions packages/cw0/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
mod balance;
mod expiration;
mod pagination;
mod payment;

pub use crate::balance::NativeBalance;
pub use crate::expiration::{Duration, Expiration, DAY, HOUR, WEEK};
pub use pagination::{
calc_range_end_human, calc_range_start_human, calc_range_start_string, maybe_canonical,
};
pub use payment::{may_pay, must_pay, nonpayable, PaymentError};
130 changes: 130 additions & 0 deletions packages/cw0/src/payment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use cosmwasm_std::{MessageInfo, Uint128};
use thiserror::Error;

/// returns an error if any coins were sent
pub fn nonpayable(info: &MessageInfo) -> Result<(), PaymentError> {
if info.sent_funds.is_empty() {
Ok(())
} else {
Err(PaymentError::NonPayable {})
}
}

/// Requires exactly one denom sent, which matches the requested denom.
/// Returns the amount if only one denom and non-zero amount. Errors otherwise.
pub fn must_pay(info: &MessageInfo, denom: &str) -> Result<Uint128, PaymentError> {
match info.sent_funds.len() {
0 => Err(PaymentError::NoFunds {}),
1 => {
if info.sent_funds[0].denom == denom {
let payment = info.sent_funds[0].amount;
if payment.is_zero() {
Err(PaymentError::NoFunds {})
} else {
Ok(payment)
}
} else {
Err(PaymentError::MissingDenom(denom.to_string()))
}
}
_ => {
// find first mis-match
let wrong = info.sent_funds.iter().find(|c| c.denom != denom).unwrap();
Err(PaymentError::ExtraDenom(wrong.denom.to_string()))
}
}
}

/// Similar to must_pay, but it any payment is optional. Returns an error if a different
/// denom was sent. Otherwise, returns the amount of `denom` sent, or 0 if nothing sent.
pub fn may_pay(info: &MessageInfo, denom: &str) -> Result<Uint128, PaymentError> {
if info.sent_funds.is_empty() {
Ok(Uint128(0))
} else if info.sent_funds.len() == 1 && info.sent_funds[0].denom == denom {
Ok(info.sent_funds[0].amount)
} else {
// find first mis-match
let wrong = info.sent_funds.iter().find(|c| c.denom != denom).unwrap();
Err(PaymentError::ExtraDenom(wrong.denom.to_string()))
}
}

#[derive(Error, Debug, PartialEq)]
pub enum PaymentError {
#[error("Must send reserve token '{0}'")]
MissingDenom(String),

#[error("Received unsupported denom '{0}'")]
ExtraDenom(String),

#[error("No funds sent")]
NoFunds {},

#[error("This message does no accept funds")]
NonPayable {},
}

#[cfg(test)]
mod test {
use super::*;
use cosmwasm_std::testing::mock_info;
use cosmwasm_std::{coin, coins};

const SENDER: &str = "sender";

#[test]
fn nonpayable_works() {
let no_payment = mock_info(SENDER, &[]);
nonpayable(&no_payment).unwrap();

let payment = mock_info(SENDER, &coins(100, "uatom"));
let res = nonpayable(&payment);
assert_eq!(res.unwrap_err(), PaymentError::NonPayable {});
}

#[test]
fn may_pay_works() {
let atom: &str = "uatom";
let no_payment = mock_info(SENDER, &[]);
let atom_payment = mock_info(SENDER, &coins(100, atom));
let eth_payment = mock_info(SENDER, &coins(100, "wei"));
let mixed_payment = mock_info(SENDER, &[coin(50, atom), coin(120, "wei")]);

let res = may_pay(&no_payment, atom).unwrap();
assert_eq!(res, Uint128(0));

let res = may_pay(&atom_payment, atom).unwrap();
assert_eq!(res, Uint128(100));

let err = may_pay(&eth_payment, atom).unwrap_err();
assert_eq!(err, PaymentError::ExtraDenom("wei".to_string()));

let err = may_pay(&mixed_payment, atom).unwrap_err();
assert_eq!(err, PaymentError::ExtraDenom("wei".to_string()));
}

#[test]
fn must_pay_works() {
let atom: &str = "uatom";
let no_payment = mock_info(SENDER, &[]);
let atom_payment = mock_info(SENDER, &coins(100, atom));
let zero_payment = mock_info(SENDER, &coins(0, atom));
let eth_payment = mock_info(SENDER, &coins(100, "wei"));
let mixed_payment = mock_info(SENDER, &[coin(50, atom), coin(120, "wei")]);

let res = must_pay(&atom_payment, atom).unwrap();
assert_eq!(res, Uint128(100));

let err = must_pay(&no_payment, atom).unwrap_err();
assert_eq!(err, PaymentError::NoFunds {});

let err = must_pay(&zero_payment, atom).unwrap_err();
assert_eq!(err, PaymentError::NoFunds {});

let err = must_pay(&eth_payment, atom).unwrap_err();
assert_eq!(err, PaymentError::MissingDenom(atom.to_string()));

let err = must_pay(&mixed_payment, atom).unwrap_err();
assert_eq!(err, PaymentError::ExtraDenom("wei".to_string()));
}
}