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 }