Skip to content

Commit

Permalink
Merge pull request #219 from CosmWasm/payment-helpers
Browse files Browse the repository at this point in the history
Payment helpers
  • Loading branch information
ethanfrey authored Jan 11, 2021
2 parents 8b16df3 + 8acbab4 commit 24e8893
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 28 deletions.
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()));
}
}

0 comments on commit 24e8893

Please sign in to comment.