github.com/fibonacci-chain/fbc@v0.0.0-20231124064014-c7636198c1e9/dev/wasm/cw4-stake/src/contract.rs (about)

     1  #[cfg(not(feature = "library"))]
     2  use cosmwasm_std::entry_point;
     3  use cosmwasm_std::{
     4      coins, from_slice, to_binary, Addr, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Order,
     5      Response, StdResult, Storage, SubMsg, Uint128, WasmMsg,
     6  };
     7  
     8  use cw2::set_contract_version;
     9  use cw20::{Balance, Cw20CoinVerified, Cw20ExecuteMsg, Cw20ReceiveMsg, Denom};
    10  use cw4::{
    11      Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse,
    12      TotalWeightResponse,
    13  };
    14  use cw_storage_plus::Bound;
    15  use cw_utils::{maybe_addr, NativeBalance};
    16  
    17  use crate::error::ContractError;
    18  use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg, StakedResponse};
    19  use crate::state::{Config, ADMIN, CLAIMS, CONFIG, HOOKS, MEMBERS, STAKE, TOTAL};
    20  
    21  // version info for migration info
    22  const CONTRACT_NAME: &str = "crates.io:cw4-stake";
    23  const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
    24  
    25  // Note, you can use StdResult in some functions where you do not
    26  // make use of the custom errors
    27  #[cfg_attr(not(feature = "library"), entry_point)]
    28  pub fn instantiate(
    29      mut deps: DepsMut,
    30      _env: Env,
    31      _info: MessageInfo,
    32      msg: InstantiateMsg,
    33  ) -> Result<Response, ContractError> {
    34      set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    35      let api = deps.api;
    36      ADMIN.set(deps.branch(), maybe_addr(api, msg.admin)?)?;
    37  
    38      // min_bond is at least 1, so 0 stake -> non-membership
    39      let min_bond = std::cmp::max(msg.min_bond, Uint128::new(1));
    40  
    41      let config = Config {
    42          denom: msg.denom,
    43          tokens_per_weight: msg.tokens_per_weight,
    44          min_bond,
    45          unbonding_period: msg.unbonding_period,
    46      };
    47      CONFIG.save(deps.storage, &config)?;
    48      TOTAL.save(deps.storage, &0)?;
    49  
    50      Ok(Response::default())
    51  }
    52  
    53  // And declare a custom Error variant for the ones where you will want to make use of it
    54  #[cfg_attr(not(feature = "library"), entry_point)]
    55  pub fn execute(
    56      deps: DepsMut,
    57      env: Env,
    58      info: MessageInfo,
    59      msg: ExecuteMsg,
    60  ) -> Result<Response, ContractError> {
    61      let api = deps.api;
    62      match msg {
    63          ExecuteMsg::UpdateAdmin { admin } => {
    64              Ok(ADMIN.execute_update_admin(deps, info, maybe_addr(api, admin)?)?)
    65          }
    66          ExecuteMsg::AddHook { addr } => {
    67              Ok(HOOKS.execute_add_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?)
    68          }
    69          ExecuteMsg::RemoveHook { addr } => {
    70              Ok(HOOKS.execute_remove_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?)
    71          }
    72          ExecuteMsg::Bond {} => execute_bond(deps, env, Balance::from(info.funds), info.sender),
    73          ExecuteMsg::Unbond { tokens: amount } => execute_unbond(deps, env, info, amount),
    74          ExecuteMsg::Claim {} => execute_claim(deps, env, info),
    75          ExecuteMsg::Receive(msg) => execute_receive(deps, env, info, msg),
    76      }
    77  }
    78  
    79  pub fn execute_bond(
    80      deps: DepsMut,
    81      env: Env,
    82      amount: Balance,
    83      sender: Addr,
    84  ) -> Result<Response, ContractError> {
    85      let cfg = CONFIG.load(deps.storage)?;
    86  
    87      // ensure the sent denom was proper
    88      // NOTE: those clones are not needed (if we move denom, we return early),
    89      // but the compiler cannot see that (yet...)
    90      let amount = match (&cfg.denom, &amount) {
    91          (Denom::Native(want), Balance::Native(have)) => must_pay_funds(have, want),
    92          (Denom::Cw20(want), Balance::Cw20(have)) => {
    93              if want == &have.address {
    94                  Ok(have.amount)
    95              } else {
    96                  Err(ContractError::InvalidDenom(want.into()))
    97              }
    98          }
    99          _ => Err(ContractError::MixedNativeAndCw20(
   100              "Invalid address or denom".to_string(),
   101          )),
   102      }?;
   103  
   104      // update the sender's stake
   105      let new_stake = STAKE.update(deps.storage, &sender, |stake| -> StdResult<_> {
   106          Ok(stake.unwrap_or_default() + amount)
   107      })?;
   108  
   109      let messages = update_membership(
   110          deps.storage,
   111          sender.clone(),
   112          new_stake,
   113          &cfg,
   114          env.block.height,
   115      )?;
   116  
   117      Ok(Response::new()
   118          .add_submessages(messages)
   119          .add_attribute("action", "bond")
   120          .add_attribute("amount", amount)
   121          .add_attribute("sender", sender))
   122  }
   123  
   124  pub fn execute_receive(
   125      deps: DepsMut,
   126      env: Env,
   127      info: MessageInfo,
   128      wrapper: Cw20ReceiveMsg,
   129  ) -> Result<Response, ContractError> {
   130      // info.sender is the address of the cw20 contract (that re-sent this message).
   131      // wrapper.sender is the address of the user that requested the cw20 contract to send this.
   132      // This cannot be fully trusted (the cw20 contract can fake it), so only use it for actions
   133      // in the address's favor (like paying/bonding tokens, not withdrawls)
   134      let msg: ReceiveMsg = from_slice(&wrapper.msg)?;
   135      let balance = Balance::Cw20(Cw20CoinVerified {
   136          address: info.sender,
   137          amount: wrapper.amount,
   138      });
   139      let api = deps.api;
   140      match msg {
   141          ReceiveMsg::Bond {} => {
   142              execute_bond(deps, env, balance, api.addr_validate(&wrapper.sender)?)
   143          }
   144      }
   145  }
   146  
   147  pub fn execute_unbond(
   148      deps: DepsMut,
   149      env: Env,
   150      info: MessageInfo,
   151      amount: Uint128,
   152  ) -> Result<Response, ContractError> {
   153      // reduce the sender's stake - aborting if insufficient
   154      let new_stake = STAKE.update(deps.storage, &info.sender, |stake| -> StdResult<_> {
   155          Ok(stake.unwrap_or_default().checked_sub(amount)?)
   156      })?;
   157  
   158      // provide them a claim
   159      let cfg = CONFIG.load(deps.storage)?;
   160      CLAIMS.create_claim(
   161          deps.storage,
   162          &info.sender,
   163          amount,
   164          cfg.unbonding_period.after(&env.block),
   165      )?;
   166  
   167      let messages = update_membership(
   168          deps.storage,
   169          info.sender.clone(),
   170          new_stake,
   171          &cfg,
   172          env.block.height,
   173      )?;
   174  
   175      Ok(Response::new()
   176          .add_submessages(messages)
   177          .add_attribute("action", "unbond")
   178          .add_attribute("amount", amount)
   179          .add_attribute("sender", info.sender))
   180  }
   181  
   182  pub fn must_pay_funds(balance: &NativeBalance, denom: &str) -> Result<Uint128, ContractError> {
   183      match balance.0.len() {
   184          0 => Err(ContractError::NoFunds {}),
   185          1 => {
   186              let balance = &balance.0;
   187              let payment = balance[0].amount;
   188              if balance[0].denom == denom {
   189                  Ok(payment)
   190              } else {
   191                  Err(ContractError::MissingDenom(denom.to_string()))
   192              }
   193          }
   194          _ => Err(ContractError::ExtraDenoms(denom.to_string())),
   195      }
   196  }
   197  
   198  fn update_membership(
   199      storage: &mut dyn Storage,
   200      sender: Addr,
   201      new_stake: Uint128,
   202      cfg: &Config,
   203      height: u64,
   204  ) -> StdResult<Vec<SubMsg>> {
   205      // update their membership weight
   206      let new = calc_weight(new_stake, cfg);
   207      let old = MEMBERS.may_load(storage, &sender)?;
   208  
   209      // short-circuit if no change
   210      if new == old {
   211          return Ok(vec![]);
   212      }
   213      // otherwise, record change of weight
   214      match new.as_ref() {
   215          Some(w) => MEMBERS.save(storage, &sender, w, height),
   216          None => MEMBERS.remove(storage, &sender, height),
   217      }?;
   218  
   219      // update total
   220      TOTAL.update(storage, |total| -> StdResult<_> {
   221          Ok(total + new.unwrap_or_default() - old.unwrap_or_default())
   222      })?;
   223  
   224      // alert the hooks
   225      let diff = MemberDiff::new(sender, old, new);
   226      HOOKS.prepare_hooks(storage, |h| {
   227          MemberChangedHookMsg::one(diff.clone())
   228              .into_cosmos_msg(h)
   229              .map(SubMsg::new)
   230      })
   231  }
   232  
   233  fn calc_weight(stake: Uint128, cfg: &Config) -> Option<u64> {
   234      if stake < cfg.min_bond {
   235          None
   236      } else {
   237          let w = stake.u128() / (cfg.tokens_per_weight.u128());
   238          Some(w as u64)
   239      }
   240  }
   241  
   242  pub fn execute_claim(
   243      deps: DepsMut,
   244      env: Env,
   245      info: MessageInfo,
   246  ) -> Result<Response, ContractError> {
   247      let release = CLAIMS.claim_tokens(deps.storage, &info.sender, &env.block, None)?;
   248      if release.is_zero() {
   249          return Err(ContractError::NothingToClaim {});
   250      }
   251  
   252      let config = CONFIG.load(deps.storage)?;
   253      let (amount_str, message) = match &config.denom {
   254          Denom::Native(denom) => {
   255              let amount_str = coin_to_string(release, denom.as_str());
   256              let amount = coins(release.u128(), denom);
   257              let message = SubMsg::new(BankMsg::Send {
   258                  to_address: info.sender.to_string(),
   259                  amount,
   260              });
   261              (amount_str, message)
   262          }
   263          Denom::Cw20(addr) => {
   264              let amount_str = coin_to_string(release, addr.as_str());
   265              let transfer = Cw20ExecuteMsg::Transfer {
   266                  recipient: info.sender.clone().into(),
   267                  amount: release,
   268              };
   269              let message = SubMsg::new(WasmMsg::Execute {
   270                  contract_addr: addr.into(),
   271                  msg: to_binary(&transfer)?,
   272                  funds: vec![],
   273              });
   274              (amount_str, message)
   275          }
   276      };
   277  
   278      Ok(Response::new()
   279          .add_submessage(message)
   280          .add_attribute("action", "claim")
   281          .add_attribute("tokens", amount_str)
   282          .add_attribute("sender", info.sender))
   283  }
   284  
   285  #[inline]
   286  fn coin_to_string(amount: Uint128, denom: &str) -> String {
   287      format!("{} {}", amount, denom)
   288  }
   289  
   290  #[cfg_attr(not(feature = "library"), entry_point)]
   291  pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
   292      match msg {
   293          QueryMsg::Member {
   294              addr,
   295              at_height: height,
   296          } => to_binary(&query_member(deps, addr, height)?),
   297          QueryMsg::ListMembers { start_after, limit } => {
   298              to_binary(&list_members(deps, start_after, limit)?)
   299          }
   300          QueryMsg::TotalWeight {} => to_binary(&query_total_weight(deps)?),
   301          QueryMsg::Claims { address } => {
   302              to_binary(&CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?)?)
   303          }
   304          QueryMsg::Staked { address } => to_binary(&query_staked(deps, address)?),
   305          QueryMsg::Admin {} => to_binary(&ADMIN.query_admin(deps)?),
   306          QueryMsg::Hooks {} => to_binary(&HOOKS.query_hooks(deps)?),
   307      }
   308  }
   309  
   310  fn query_total_weight(deps: Deps) -> StdResult<TotalWeightResponse> {
   311      let weight = TOTAL.load(deps.storage)?;
   312      Ok(TotalWeightResponse { weight })
   313  }
   314  
   315  pub fn query_staked(deps: Deps, addr: String) -> StdResult<StakedResponse> {
   316      let addr = deps.api.addr_validate(&addr)?;
   317      let stake = STAKE.may_load(deps.storage, &addr)?.unwrap_or_default();
   318      let denom = CONFIG.load(deps.storage)?.denom;
   319      Ok(StakedResponse { stake, denom })
   320  }
   321  
   322  fn query_member(deps: Deps, addr: String, height: Option<u64>) -> StdResult<MemberResponse> {
   323      let addr = deps.api.addr_validate(&addr)?;
   324      let weight = match height {
   325          Some(h) => MEMBERS.may_load_at_height(deps.storage, &addr, h),
   326          None => MEMBERS.may_load(deps.storage, &addr),
   327      }?;
   328      Ok(MemberResponse { weight })
   329  }
   330  
   331  // settings for pagination
   332  const MAX_LIMIT: u32 = 30;
   333  const DEFAULT_LIMIT: u32 = 10;
   334  
   335  fn list_members(
   336      deps: Deps,
   337      start_after: Option<String>,
   338      limit: Option<u32>,
   339  ) -> StdResult<MemberListResponse> {
   340      let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
   341      let addr = maybe_addr(deps.api, start_after)?;
   342      let start = addr.as_ref().map(Bound::exclusive);
   343  
   344      let members = MEMBERS
   345          .range(deps.storage, start, None, Order::Ascending)
   346          .take(limit)
   347          .map(|item| {
   348              item.map(|(addr, weight)| Member {
   349                  addr: addr.into(),
   350                  weight,
   351              })
   352          })
   353          .collect::<StdResult<_>>()?;
   354  
   355      Ok(MemberListResponse { members })
   356  }
   357  
   358  #[cfg(test)]
   359  mod tests {
   360      use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
   361      use cosmwasm_std::{
   362          coin, from_slice, CosmosMsg, OverflowError, OverflowOperation, StdError, Storage,
   363      };
   364      use cw20::Denom;
   365      use cw4::{member_key, TOTAL_KEY};
   366      use cw_controllers::{AdminError, Claim, HookError};
   367      use cw_utils::Duration;
   368  
   369      use crate::error::ContractError;
   370  
   371      use super::*;
   372  
   373      const INIT_ADMIN: &str = "juan";
   374      const USER1: &str = "somebody";
   375      const USER2: &str = "else";
   376      const USER3: &str = "funny";
   377      const DENOM: &str = "stake";
   378      const TOKENS_PER_WEIGHT: Uint128 = Uint128::new(1_000);
   379      const MIN_BOND: Uint128 = Uint128::new(5_000);
   380      const UNBONDING_BLOCKS: u64 = 100;
   381      const CW20_ADDRESS: &str = "wasm1234567890";
   382  
   383      fn default_instantiate(deps: DepsMut) {
   384          do_instantiate(
   385              deps,
   386              TOKENS_PER_WEIGHT,
   387              MIN_BOND,
   388              Duration::Height(UNBONDING_BLOCKS),
   389          )
   390      }
   391  
   392      fn do_instantiate(
   393          deps: DepsMut,
   394          tokens_per_weight: Uint128,
   395          min_bond: Uint128,
   396          unbonding_period: Duration,
   397      ) {
   398          let msg = InstantiateMsg {
   399              denom: Denom::Native("stake".to_string()),
   400              tokens_per_weight,
   401              min_bond,
   402              unbonding_period,
   403              admin: Some(INIT_ADMIN.into()),
   404          };
   405          let info = mock_info("creator", &[]);
   406          instantiate(deps, mock_env(), info, msg).unwrap();
   407      }
   408  
   409      fn cw20_instantiate(deps: DepsMut, unbonding_period: Duration) {
   410          let msg = InstantiateMsg {
   411              denom: Denom::Cw20(Addr::unchecked(CW20_ADDRESS)),
   412              tokens_per_weight: TOKENS_PER_WEIGHT,
   413              min_bond: MIN_BOND,
   414              unbonding_period,
   415              admin: Some(INIT_ADMIN.into()),
   416          };
   417          let info = mock_info("creator", &[]);
   418          instantiate(deps, mock_env(), info, msg).unwrap();
   419      }
   420  
   421      fn bond(mut deps: DepsMut, user1: u128, user2: u128, user3: u128, height_delta: u64) {
   422          let mut env = mock_env();
   423          env.block.height += height_delta;
   424  
   425          for (addr, stake) in &[(USER1, user1), (USER2, user2), (USER3, user3)] {
   426              if *stake != 0 {
   427                  let msg = ExecuteMsg::Bond {};
   428                  let info = mock_info(addr, &coins(*stake, DENOM));
   429                  execute(deps.branch(), env.clone(), info, msg).unwrap();
   430              }
   431          }
   432      }
   433  
   434      fn bond_cw20(mut deps: DepsMut, user1: u128, user2: u128, user3: u128, height_delta: u64) {
   435          let mut env = mock_env();
   436          env.block.height += height_delta;
   437  
   438          for (addr, stake) in &[(USER1, user1), (USER2, user2), (USER3, user3)] {
   439              if *stake != 0 {
   440                  let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
   441                      sender: addr.to_string(),
   442                      amount: Uint128::new(*stake),
   443                      msg: to_binary(&ReceiveMsg::Bond {}).unwrap(),
   444                  });
   445                  let info = mock_info(CW20_ADDRESS, &[]);
   446                  execute(deps.branch(), env.clone(), info, msg).unwrap();
   447              }
   448          }
   449      }
   450  
   451      fn unbond(mut deps: DepsMut, user1: u128, user2: u128, user3: u128, height_delta: u64) {
   452          let mut env = mock_env();
   453          env.block.height += height_delta;
   454  
   455          for (addr, stake) in &[(USER1, user1), (USER2, user2), (USER3, user3)] {
   456              if *stake != 0 {
   457                  let msg = ExecuteMsg::Unbond {
   458                      tokens: Uint128::new(*stake),
   459                  };
   460                  let info = mock_info(addr, &[]);
   461                  execute(deps.branch(), env.clone(), info, msg).unwrap();
   462              }
   463          }
   464      }
   465  
   466      #[test]
   467      fn proper_instantiation() {
   468          let mut deps = mock_dependencies();
   469          default_instantiate(deps.as_mut());
   470  
   471          // it worked, let's query the state
   472          let res = ADMIN.query_admin(deps.as_ref()).unwrap();
   473          assert_eq!(Some(INIT_ADMIN.into()), res.admin);
   474  
   475          let res = query_total_weight(deps.as_ref()).unwrap();
   476          assert_eq!(0, res.weight);
   477      }
   478  
   479      fn get_member(deps: Deps, addr: String, at_height: Option<u64>) -> Option<u64> {
   480          let raw = query(deps, mock_env(), QueryMsg::Member { addr, at_height }).unwrap();
   481          let res: MemberResponse = from_slice(&raw).unwrap();
   482          res.weight
   483      }
   484  
   485      // this tests the member queries
   486      fn assert_users(
   487          deps: Deps,
   488          user1_weight: Option<u64>,
   489          user2_weight: Option<u64>,
   490          user3_weight: Option<u64>,
   491          height: Option<u64>,
   492      ) {
   493          let member1 = get_member(deps, USER1.into(), height);
   494          assert_eq!(member1, user1_weight);
   495  
   496          let member2 = get_member(deps, USER2.into(), height);
   497          assert_eq!(member2, user2_weight);
   498  
   499          let member3 = get_member(deps, USER3.into(), height);
   500          assert_eq!(member3, user3_weight);
   501  
   502          // this is only valid if we are not doing a historical query
   503          if height.is_none() {
   504              // compute expected metrics
   505              let weights = vec![user1_weight, user2_weight, user3_weight];
   506              let sum: u64 = weights.iter().map(|x| x.unwrap_or_default()).sum();
   507              let count = weights.iter().filter(|x| x.is_some()).count();
   508  
   509              // TODO: more detailed compare?
   510              let msg = QueryMsg::ListMembers {
   511                  start_after: None,
   512                  limit: None,
   513              };
   514              let raw = query(deps, mock_env(), msg).unwrap();
   515              let members: MemberListResponse = from_slice(&raw).unwrap();
   516              assert_eq!(count, members.members.len());
   517  
   518              let raw = query(deps, mock_env(), QueryMsg::TotalWeight {}).unwrap();
   519              let total: TotalWeightResponse = from_slice(&raw).unwrap();
   520              assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21
   521          }
   522      }
   523  
   524      // this tests the member queries
   525      fn assert_stake(deps: Deps, user1_stake: u128, user2_stake: u128, user3_stake: u128) {
   526          let stake1 = query_staked(deps, USER1.into()).unwrap();
   527          assert_eq!(stake1.stake, user1_stake.into());
   528  
   529          let stake2 = query_staked(deps, USER2.into()).unwrap();
   530          assert_eq!(stake2.stake, user2_stake.into());
   531  
   532          let stake3 = query_staked(deps, USER3.into()).unwrap();
   533          assert_eq!(stake3.stake, user3_stake.into());
   534      }
   535  
   536      #[test]
   537      fn bond_stake_adds_membership() {
   538          let mut deps = mock_dependencies();
   539          default_instantiate(deps.as_mut());
   540          let height = mock_env().block.height;
   541  
   542          // Assert original weights
   543          assert_users(deps.as_ref(), None, None, None, None);
   544  
   545          // ensure it rounds down, and respects cut-off
   546          bond(deps.as_mut(), 12_000, 7_500, 4_000, 1);
   547  
   548          // Assert updated weights
   549          assert_stake(deps.as_ref(), 12_000, 7_500, 4_000);
   550          assert_users(deps.as_ref(), Some(12), Some(7), None, None);
   551  
   552          // add some more, ensure the sum is properly respected (7.5 + 7.6 = 15 not 14)
   553          bond(deps.as_mut(), 0, 7_600, 1_200, 2);
   554  
   555          // Assert updated weights
   556          assert_stake(deps.as_ref(), 12_000, 15_100, 5_200);
   557          assert_users(deps.as_ref(), Some(12), Some(15), Some(5), None);
   558  
   559          // check historical queries all work
   560          assert_users(deps.as_ref(), None, None, None, Some(height + 1)); // before first stake
   561          assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); // after first stake
   562          assert_users(deps.as_ref(), Some(12), Some(15), Some(5), Some(height + 3));
   563          // after second stake
   564      }
   565  
   566      #[test]
   567      fn unbond_stake_update_membership() {
   568          let mut deps = mock_dependencies();
   569          default_instantiate(deps.as_mut());
   570          let height = mock_env().block.height;
   571  
   572          // ensure it rounds down, and respects cut-off
   573          bond(deps.as_mut(), 12_000, 7_500, 4_000, 1);
   574          unbond(deps.as_mut(), 4_500, 2_600, 1_111, 2);
   575  
   576          // Assert updated weights
   577          assert_stake(deps.as_ref(), 7_500, 4_900, 2_889);
   578          assert_users(deps.as_ref(), Some(7), None, None, None);
   579  
   580          // Adding a little more returns weight
   581          bond(deps.as_mut(), 600, 100, 2_222, 3);
   582  
   583          // Assert updated weights
   584          assert_users(deps.as_ref(), Some(8), Some(5), Some(5), None);
   585  
   586          // check historical queries all work
   587          assert_users(deps.as_ref(), None, None, None, Some(height + 1)); // before first stake
   588          assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); // after first bond
   589          assert_users(deps.as_ref(), Some(7), None, None, Some(height + 3)); // after first unbond
   590          assert_users(deps.as_ref(), Some(8), Some(5), Some(5), Some(height + 4)); // after second bond
   591  
   592          // error if try to unbond more than stake (USER2 has 5000 staked)
   593          let msg = ExecuteMsg::Unbond {
   594              tokens: Uint128::new(5100),
   595          };
   596          let mut env = mock_env();
   597          env.block.height += 5;
   598          let info = mock_info(USER2, &[]);
   599          let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
   600          assert_eq!(
   601              err,
   602              ContractError::Std(StdError::overflow(OverflowError::new(
   603                  OverflowOperation::Sub,
   604                  5000,
   605                  5100
   606              )))
   607          );
   608      }
   609  
   610      #[test]
   611      fn cw20_token_bond() {
   612          let mut deps = mock_dependencies();
   613          cw20_instantiate(deps.as_mut(), Duration::Height(2000));
   614  
   615          // Assert original weights
   616          assert_users(deps.as_ref(), None, None, None, None);
   617  
   618          // ensure it rounds down, and respects cut-off
   619          bond_cw20(deps.as_mut(), 12_000, 7_500, 4_000, 1);
   620  
   621          // Assert updated weights
   622          assert_stake(deps.as_ref(), 12_000, 7_500, 4_000);
   623          assert_users(deps.as_ref(), Some(12), Some(7), None, None);
   624      }
   625  
   626      #[test]
   627      fn cw20_token_claim() {
   628          let unbonding_period: u64 = 50;
   629          let unbond_height: u64 = 10;
   630  
   631          let mut deps = mock_dependencies();
   632          let unbonding = Duration::Height(unbonding_period);
   633          cw20_instantiate(deps.as_mut(), unbonding);
   634  
   635          // bond some tokens
   636          bond_cw20(deps.as_mut(), 20_000, 13_500, 500, 1);
   637  
   638          // unbond part
   639          unbond(deps.as_mut(), 7_900, 4_600, 0, unbond_height);
   640  
   641          // Assert updated weights
   642          assert_stake(deps.as_ref(), 12_100, 8_900, 500);
   643          assert_users(deps.as_ref(), Some(12), Some(8), None, None);
   644  
   645          // with proper claims
   646          let mut env = mock_env();
   647          env.block.height += unbond_height;
   648          let expires = unbonding.after(&env.block);
   649          assert_eq!(
   650              get_claims(deps.as_ref(), &Addr::unchecked(USER1)),
   651              vec![Claim::new(7_900, expires)]
   652          );
   653  
   654          // wait til they expire and get payout
   655          env.block.height += unbonding_period;
   656          let res = execute(
   657              deps.as_mut(),
   658              env,
   659              mock_info(USER1, &[]),
   660              ExecuteMsg::Claim {},
   661          )
   662          .unwrap();
   663          assert_eq!(res.messages.len(), 1);
   664          match &res.messages[0].msg {
   665              CosmosMsg::Wasm(WasmMsg::Execute {
   666                  contract_addr,
   667                  msg,
   668                  funds,
   669              }) => {
   670                  assert_eq!(contract_addr.as_str(), CW20_ADDRESS);
   671                  assert_eq!(funds.len(), 0);
   672                  let parsed: Cw20ExecuteMsg = from_slice(msg).unwrap();
   673                  assert_eq!(
   674                      parsed,
   675                      Cw20ExecuteMsg::Transfer {
   676                          recipient: USER1.into(),
   677                          amount: Uint128::new(7_900)
   678                      }
   679                  );
   680              }
   681              _ => panic!("Must initiate cw20 transfer"),
   682          }
   683      }
   684  
   685      #[test]
   686      fn raw_queries_work() {
   687          // add will over-write and remove have no effect
   688          let mut deps = mock_dependencies();
   689          default_instantiate(deps.as_mut());
   690          // Set values as (11, 6, None)
   691          bond(deps.as_mut(), 11_000, 6_000, 0, 1);
   692  
   693          // get total from raw key
   694          let total_raw = deps.storage.get(TOTAL_KEY.as_bytes()).unwrap();
   695          let total: u64 = from_slice(&total_raw).unwrap();
   696          assert_eq!(17, total);
   697  
   698          // get member votes from raw key
   699          let member2_raw = deps.storage.get(&member_key(USER2)).unwrap();
   700          let member2: u64 = from_slice(&member2_raw).unwrap();
   701          assert_eq!(6, member2);
   702  
   703          // and execute misses
   704          let member3_raw = deps.storage.get(&member_key(USER3));
   705          assert_eq!(None, member3_raw);
   706      }
   707  
   708      fn get_claims(deps: Deps, addr: &Addr) -> Vec<Claim> {
   709          CLAIMS.query_claims(deps, addr).unwrap().claims
   710      }
   711  
   712      #[test]
   713      fn unbond_claim_workflow() {
   714          let mut deps = mock_dependencies();
   715          default_instantiate(deps.as_mut());
   716  
   717          // create some data
   718          bond(deps.as_mut(), 12_000, 7_500, 4_000, 1);
   719          unbond(deps.as_mut(), 4_500, 2_600, 0, 2);
   720          let mut env = mock_env();
   721          env.block.height += 2;
   722  
   723          // check the claims for each user
   724          let expires = Duration::Height(UNBONDING_BLOCKS).after(&env.block);
   725          assert_eq!(
   726              get_claims(deps.as_ref(), &Addr::unchecked(USER1)),
   727              vec![Claim::new(4_500, expires)]
   728          );
   729          assert_eq!(
   730              get_claims(deps.as_ref(), &Addr::unchecked(USER2)),
   731              vec![Claim::new(2_600, expires)]
   732          );
   733          assert_eq!(get_claims(deps.as_ref(), &Addr::unchecked(USER3)), vec![]);
   734  
   735          // do another unbond later on
   736          let mut env2 = mock_env();
   737          env2.block.height += 22;
   738          unbond(deps.as_mut(), 0, 1_345, 1_500, 22);
   739  
   740          // with updated claims
   741          let expires2 = Duration::Height(UNBONDING_BLOCKS).after(&env2.block);
   742          assert_eq!(
   743              get_claims(deps.as_ref(), &Addr::unchecked(USER1)),
   744              vec![Claim::new(4_500, expires)]
   745          );
   746          assert_eq!(
   747              get_claims(deps.as_ref(), &Addr::unchecked(USER2)),
   748              vec![Claim::new(2_600, expires), Claim::new(1_345, expires2)]
   749          );
   750          assert_eq!(
   751              get_claims(deps.as_ref(), &Addr::unchecked(USER3)),
   752              vec![Claim::new(1_500, expires2)]
   753          );
   754  
   755          // nothing can be withdrawn yet
   756          let err = execute(
   757              deps.as_mut(),
   758              env2,
   759              mock_info(USER1, &[]),
   760              ExecuteMsg::Claim {},
   761          )
   762          .unwrap_err();
   763          assert_eq!(err, ContractError::NothingToClaim {});
   764  
   765          // now mature first section, withdraw that
   766          let mut env3 = mock_env();
   767          env3.block.height += 2 + UNBONDING_BLOCKS;
   768          // first one can now release
   769          let res = execute(
   770              deps.as_mut(),
   771              env3.clone(),
   772              mock_info(USER1, &[]),
   773              ExecuteMsg::Claim {},
   774          )
   775          .unwrap();
   776          assert_eq!(
   777              res.messages,
   778              vec![SubMsg::new(BankMsg::Send {
   779                  to_address: USER1.into(),
   780                  amount: coins(4_500, DENOM),
   781              })]
   782          );
   783  
   784          // second releases partially
   785          let res = execute(
   786              deps.as_mut(),
   787              env3.clone(),
   788              mock_info(USER2, &[]),
   789              ExecuteMsg::Claim {},
   790          )
   791          .unwrap();
   792          assert_eq!(
   793              res.messages,
   794              vec![SubMsg::new(BankMsg::Send {
   795                  to_address: USER2.into(),
   796                  amount: coins(2_600, DENOM),
   797              })]
   798          );
   799  
   800          // but the third one cannot release
   801          let err = execute(
   802              deps.as_mut(),
   803              env3,
   804              mock_info(USER3, &[]),
   805              ExecuteMsg::Claim {},
   806          )
   807          .unwrap_err();
   808          assert_eq!(err, ContractError::NothingToClaim {});
   809  
   810          // claims updated properly
   811          assert_eq!(get_claims(deps.as_ref(), &Addr::unchecked(USER1)), vec![]);
   812          assert_eq!(
   813              get_claims(deps.as_ref(), &Addr::unchecked(USER2)),
   814              vec![Claim::new(1_345, expires2)]
   815          );
   816          assert_eq!(
   817              get_claims(deps.as_ref(), &Addr::unchecked(USER3)),
   818              vec![Claim::new(1_500, expires2)]
   819          );
   820  
   821          // add another few claims for 2
   822          unbond(deps.as_mut(), 0, 600, 0, 30 + UNBONDING_BLOCKS);
   823          unbond(deps.as_mut(), 0, 1_005, 0, 50 + UNBONDING_BLOCKS);
   824  
   825          // ensure second can claim all tokens at once
   826          let mut env4 = mock_env();
   827          env4.block.height += 55 + UNBONDING_BLOCKS + UNBONDING_BLOCKS;
   828          let res = execute(
   829              deps.as_mut(),
   830              env4,
   831              mock_info(USER2, &[]),
   832              ExecuteMsg::Claim {},
   833          )
   834          .unwrap();
   835          assert_eq!(
   836              res.messages,
   837              vec![SubMsg::new(BankMsg::Send {
   838                  to_address: USER2.into(),
   839                  // 1_345 + 600 + 1_005
   840                  amount: coins(2_950, DENOM),
   841              })]
   842          );
   843          assert_eq!(get_claims(deps.as_ref(), &Addr::unchecked(USER2)), vec![]);
   844      }
   845  
   846      #[test]
   847      fn add_remove_hooks() {
   848          // add will over-write and remove have no effect
   849          let mut deps = mock_dependencies();
   850          default_instantiate(deps.as_mut());
   851  
   852          let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
   853          assert!(hooks.hooks.is_empty());
   854  
   855          let contract1 = String::from("hook1");
   856          let contract2 = String::from("hook2");
   857  
   858          let add_msg = ExecuteMsg::AddHook {
   859              addr: contract1.clone(),
   860          };
   861  
   862          // non-admin cannot add hook
   863          let user_info = mock_info(USER1, &[]);
   864          let err = execute(
   865              deps.as_mut(),
   866              mock_env(),
   867              user_info.clone(),
   868              add_msg.clone(),
   869          )
   870          .unwrap_err();
   871          assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into());
   872  
   873          // admin can add it, and it appears in the query
   874          let admin_info = mock_info(INIT_ADMIN, &[]);
   875          let _ = execute(
   876              deps.as_mut(),
   877              mock_env(),
   878              admin_info.clone(),
   879              add_msg.clone(),
   880          )
   881          .unwrap();
   882          let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
   883          assert_eq!(hooks.hooks, vec![contract1.clone()]);
   884  
   885          // cannot remove a non-registered contract
   886          let remove_msg = ExecuteMsg::RemoveHook {
   887              addr: contract2.clone(),
   888          };
   889          let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), remove_msg).unwrap_err();
   890          assert_eq!(err, HookError::HookNotRegistered {}.into());
   891  
   892          // add second contract
   893          let add_msg2 = ExecuteMsg::AddHook {
   894              addr: contract2.clone(),
   895          };
   896          let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap();
   897          let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
   898          assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]);
   899  
   900          // cannot re-add an existing contract
   901          let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg).unwrap_err();
   902          assert_eq!(err, HookError::HookAlreadyRegistered {}.into());
   903  
   904          // non-admin cannot remove
   905          let remove_msg = ExecuteMsg::RemoveHook { addr: contract1 };
   906          let err = execute(deps.as_mut(), mock_env(), user_info, remove_msg.clone()).unwrap_err();
   907          assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into());
   908  
   909          // remove the original
   910          let _ = execute(deps.as_mut(), mock_env(), admin_info, remove_msg).unwrap();
   911          let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
   912          assert_eq!(hooks.hooks, vec![contract2]);
   913      }
   914  
   915      #[test]
   916      fn hooks_fire() {
   917          let mut deps = mock_dependencies();
   918          default_instantiate(deps.as_mut());
   919  
   920          let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
   921          assert!(hooks.hooks.is_empty());
   922  
   923          let contract1 = String::from("hook1");
   924          let contract2 = String::from("hook2");
   925  
   926          // register 2 hooks
   927          let admin_info = mock_info(INIT_ADMIN, &[]);
   928          let add_msg = ExecuteMsg::AddHook {
   929              addr: contract1.clone(),
   930          };
   931          let add_msg2 = ExecuteMsg::AddHook {
   932              addr: contract2.clone(),
   933          };
   934          for msg in vec![add_msg, add_msg2] {
   935              let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap();
   936          }
   937  
   938          // check firing on bond
   939          assert_users(deps.as_ref(), None, None, None, None);
   940          let info = mock_info(USER1, &coins(13_800, DENOM));
   941          let res = execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Bond {}).unwrap();
   942          assert_users(deps.as_ref(), Some(13), None, None, None);
   943  
   944          // ensure messages for each of the 2 hooks
   945          assert_eq!(res.messages.len(), 2);
   946          let diff = MemberDiff::new(USER1, None, Some(13));
   947          let hook_msg = MemberChangedHookMsg::one(diff);
   948          let msg1 = SubMsg::new(hook_msg.clone().into_cosmos_msg(contract1.clone()).unwrap());
   949          let msg2 = SubMsg::new(hook_msg.into_cosmos_msg(contract2.clone()).unwrap());
   950          assert_eq!(res.messages, vec![msg1, msg2]);
   951  
   952          // check firing on unbond
   953          let msg = ExecuteMsg::Unbond {
   954              tokens: Uint128::new(7_300),
   955          };
   956          let info = mock_info(USER1, &[]);
   957          let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap();
   958          assert_users(deps.as_ref(), Some(6), None, None, None);
   959  
   960          // ensure messages for each of the 2 hooks
   961          assert_eq!(res.messages.len(), 2);
   962          let diff = MemberDiff::new(USER1, Some(13), Some(6));
   963          let hook_msg = MemberChangedHookMsg::one(diff);
   964          let msg1 = SubMsg::new(hook_msg.clone().into_cosmos_msg(contract1).unwrap());
   965          let msg2 = SubMsg::new(hook_msg.into_cosmos_msg(contract2).unwrap());
   966          assert_eq!(res.messages, vec![msg1, msg2]);
   967      }
   968  
   969      #[test]
   970      fn only_bond_valid_coins() {
   971          let mut deps = mock_dependencies();
   972          default_instantiate(deps.as_mut());
   973  
   974          // cannot bond with 0 coins
   975          let info = mock_info(USER1, &[]);
   976          let err = execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Bond {}).unwrap_err();
   977          assert_eq!(err, ContractError::NoFunds {});
   978  
   979          // cannot bond with incorrect denom
   980          let info = mock_info(USER1, &[coin(500, "FOO")]);
   981          let err = execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Bond {}).unwrap_err();
   982          assert_eq!(err, ContractError::MissingDenom(DENOM.to_string()));
   983  
   984          // cannot bond with 2 coins (even if one is correct)
   985          let info = mock_info(USER1, &[coin(1234, DENOM), coin(5000, "BAR")]);
   986          let err = execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Bond {}).unwrap_err();
   987          assert_eq!(err, ContractError::ExtraDenoms(DENOM.to_string()));
   988  
   989          // can bond with just the proper denom
   990          // cannot bond with incorrect denom
   991          let info = mock_info(USER1, &[coin(500, DENOM)]);
   992          execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Bond {}).unwrap();
   993      }
   994  
   995      #[test]
   996      fn ensure_bonding_edge_cases() {
   997          // use min_bond 0, tokens_per_weight 500
   998          let mut deps = mock_dependencies();
   999          do_instantiate(
  1000              deps.as_mut(),
  1001              Uint128::new(100),
  1002              Uint128::zero(),
  1003              Duration::Height(5),
  1004          );
  1005  
  1006          // setting 50 tokens, gives us Some(0) weight
  1007          // even setting to 1 token
  1008          bond(deps.as_mut(), 50, 1, 102, 1);
  1009          assert_users(deps.as_ref(), Some(0), Some(0), Some(1), None);
  1010  
  1011          // reducing to 0 token makes us None even with min_bond 0
  1012          unbond(deps.as_mut(), 49, 1, 102, 2);
  1013          assert_users(deps.as_ref(), Some(0), None, None, None);
  1014      }
  1015  }