├── .github ├── pull_request_template.md └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml └── src ├── contract.rs ├── data_structure.rs ├── error.rs ├── lib.rs ├── msg.rs ├── permission.rs ├── staking.rs ├── state.rs └── vesting.rs /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes and provide context 2 | 3 | ## Testing performed to validate your change -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup 20 | run: rustup update && rustup install 1.65.0 && rustup default 1.65.0 21 | - name: Build 22 | run: cargo build --verbose 23 | - name: Run tests 24 | run: cargo test --verbose 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/artifacts 3 | 4 | **/Cargo.lock 5 | 6 | # Build artifacts 7 | *.wasm 8 | hash.txt 9 | contracts.txt 10 | .idea/ 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #0.1.7 2 | - Add msgs for updating unlocked distribution address and staking rewards distribution address 3 | # 0.1.6 4 | - withdraw already-withdrawn rewards (as a result of re/undelegation) in the withdraw staking reward execute endpoint 5 | 6 | # 0.1.5 7 | - Add validations for timestamp in tranche: earliest must be later than current block time and latest must be within 100 years 8 | - Fix validation that enforces equal timestamp and amount size 9 | - Migrations to fix vesting timestamps/amounts due to bad input 10 | 11 | # 0.1.4 12 | Pacific-1 code ID: 375 13 | - Add `total_vested` query endpoint that returns the amount of vested tokens ready for withdraw 14 | 15 | # 0.1.3 16 | Pacific-1 code ID: 317 17 | - Add new proposal type to vote `gov` module proposals 18 | 19 | # 0.1.2 20 | Pacific-1 code ID: 62 21 | - Add migration validation logic 22 | - Add new proposal type to initiate emergency withdraw 23 | 24 | # 0.1.0 25 | Pacific-1 code ID: 59 26 | - initial implementation -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gringotts" 3 | version = "0.1.9" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | doctest = false 9 | 10 | [features] 11 | backtraces = ["cosmwasm-std/backtraces"] 12 | # use library feature to disable all instantiate/execute/query exports 13 | library = [] 14 | 15 | [dependencies] 16 | cosmwasm-schema = { version = "1.3.1" } 17 | cw-utils = "1.0.1" 18 | cw2 = { version = "1.0.1" } 19 | cw3 = { version = "1.0.1" } 20 | cw-storage-plus = "1.0.1" 21 | cosmwasm-std = { version = "1.3.1", features = ["staking", "stargate"] } 22 | schemars = "0.8.1" 23 | serde = { version = "1.0.103", default-features = false, features = ["derive"] } 24 | thiserror = { version = "1.0.23" } 25 | semver = "1" 26 | 27 | [dev-dependencies] 28 | cw20 = { version = "1.0.1" } 29 | cw20-base = { version = "1.0.1", features = ["library"] } 30 | cw-multi-test = "0.16.1" -------------------------------------------------------------------------------- /src/contract.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "library"))] 2 | use cosmwasm_std::entry_point; 3 | use cosmwasm_std::{ 4 | coins, to_binary, Addr, BankMsg, Binary, BlockInfo, CosmosMsg, Decimal, Delegation, Deps, 5 | DepsMut, Empty, Env, GovMsg, MessageInfo, Order, Response, StdError, StdResult, Timestamp, 6 | VoteOption, WasmMsg, 7 | }; 8 | use cw2::set_contract_version; 9 | use cw3::{ 10 | Ballot, Proposal, ProposalListResponse, ProposalResponse, Status, Vote, VoteInfo, 11 | VoteListResponse, Votes, 12 | }; 13 | use cw_utils::{Threshold, ThresholdError}; 14 | 15 | use crate::data_structure::EmptyStruct; 16 | use crate::error::ContractError; 17 | use crate::msg::{ 18 | AdminListResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, OpListResponse, QueryMsg, 19 | SeiQueryWrapper, ShowConfigResponse, ShowInfoResponse, ShowTotalVestedResponse, 20 | }; 21 | use crate::permission::{authorize_admin, authorize_op, authorize_self_call}; 22 | use crate::staking::{ 23 | delegate, get_all_delegated_validators, get_delegation_rewards, get_unbonding_balance, 24 | redelegate, undelegate, withdraw_delegation_rewards, 25 | }; 26 | use crate::state::{ 27 | get_number_of_admins, next_proposal_id, ADMINS, ADMIN_VOTING_THRESHOLD, BALLOTS, DENOM, 28 | MAX_VOTING_PERIOD, OPS, PROPOSALS, STAKING_REWARD_ADDRESS, TOTAL_AMOUNT, 29 | UNLOCK_DISTRIBUTION_ADDRESS, VESTING_AMOUNTS, VESTING_TIMESTAMPS, WITHDRAWN_LOCKED, 30 | WITHDRAWN_STAKING_REWARDS, WITHDRAWN_UNLOCKED, 31 | }; 32 | use crate::vesting::{collect_vested, distribute_vested, total_vested_amount}; 33 | use semver::Version; 34 | 35 | // version info for migration info 36 | const CONTRACT_NAME: &str = "crates.io:sei-gringotts"; 37 | const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); 38 | 39 | pub fn validate_migration( 40 | deps: Deps, 41 | contract_name: &str, 42 | contract_version: &str, 43 | ) -> Result<(), ContractError> { 44 | let ver = cw2::get_contract_version(deps.storage)?; 45 | // ensure we are migrating from an allowed contract 46 | if ver.contract != contract_name { 47 | return Err(StdError::generic_err("Can only upgrade from same type").into()); 48 | } 49 | 50 | let storage_version: Version = ver.version.parse()?; 51 | let version: Version = contract_version.parse()?; 52 | if storage_version >= version { 53 | return Err(StdError::generic_err("Cannot upgrade from a newer version").into()); 54 | } 55 | Ok(()) 56 | } 57 | 58 | // NOTE: New migrations may need store migrations if store changes are being made 59 | #[cfg_attr(not(feature = "library"), entry_point)] 60 | pub fn migrate( 61 | deps: DepsMut, 62 | env: Env, 63 | _msg: MigrateMsg, 64 | ) -> Result { 65 | validate_migration(deps.as_ref(), CONTRACT_NAME, CONTRACT_VERSION)?; 66 | 67 | // set the new version 68 | cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; 69 | 70 | if CONTRACT_VERSION == "0.1.5" { 71 | return migrate_105_handler(deps, env); 72 | } 73 | 74 | if CONTRACT_VERSION == "0.1.9" { 75 | return migrate_109_handler(deps, env); 76 | } 77 | 78 | Ok(Response::default()) 79 | } 80 | 81 | fn migrate_105_handler( 82 | deps: DepsMut, 83 | env: Env, 84 | ) -> Result { 85 | if env.contract.address.as_str() 86 | == "sei1w0fvamykx7v2e6n5x0e2s39m0jz3krejjkpmgc3tmnqdf8p9fy5syg05yv" 87 | { 88 | let timestamps: Vec = vec![ 89 | Timestamp::from_nanos(1726358400000000000), 90 | Timestamp::from_nanos(1728950400000000000), 91 | Timestamp::from_nanos(1731628800000000000), 92 | Timestamp::from_nanos(1734220800000000000), 93 | Timestamp::from_nanos(1736899200000000000), 94 | Timestamp::from_nanos(1739577600000000000), 95 | Timestamp::from_nanos(1741996800000000000), 96 | Timestamp::from_nanos(1744675200000000000), 97 | Timestamp::from_nanos(1747267200000000000), 98 | Timestamp::from_nanos(1749945600000000000), 99 | Timestamp::from_nanos(1752537600000000000), 100 | Timestamp::from_nanos(1755216000000000000), 101 | Timestamp::from_nanos(1757894400000000000), 102 | Timestamp::from_nanos(1760486400000000000), 103 | Timestamp::from_nanos(1763164800000000000), 104 | Timestamp::from_nanos(1765756800000000000), 105 | Timestamp::from_nanos(1768435200000000000), 106 | Timestamp::from_nanos(1771113600000000000), 107 | Timestamp::from_nanos(1773532800000000000), 108 | Timestamp::from_nanos(1776211200000000000), 109 | Timestamp::from_nanos(1778803200000000000), 110 | Timestamp::from_nanos(1781481600000000000), 111 | Timestamp::from_nanos(1784073600000000000), 112 | Timestamp::from_nanos(1786752000000000000), 113 | Timestamp::from_nanos(1789430400000000000), 114 | Timestamp::from_nanos(1792022400000000000), 115 | Timestamp::from_nanos(1794700800000000000), 116 | Timestamp::from_nanos(1797292800000000000), 117 | Timestamp::from_nanos(1799971200000000000), 118 | Timestamp::from_nanos(1802649600000000000), 119 | Timestamp::from_nanos(1805068800000000000), 120 | Timestamp::from_nanos(1807747200000000000), 121 | Timestamp::from_nanos(1810339200000000000), 122 | Timestamp::from_nanos(1813017600000000000), 123 | Timestamp::from_nanos(1815609600000000000), 124 | Timestamp::from_nanos(1818288000000000000), 125 | ]; 126 | VESTING_TIMESTAMPS.save(deps.storage, ×tamps)?; 127 | return Ok(Response::default()); 128 | } 129 | 130 | if env.contract.address.as_str() 131 | == "sei1letzrrlgdlrpxj6z279fx85hn5u34mm9nrc9hq4e6wxz5c79je2swt6x4a" 132 | { 133 | let timestamps: Vec = vec![ 134 | Timestamp::from_nanos(1726358400000000000), 135 | Timestamp::from_nanos(1728950400000000000), 136 | Timestamp::from_nanos(1731628800000000000), 137 | Timestamp::from_nanos(1734220800000000000), 138 | Timestamp::from_nanos(1736899200000000000), 139 | Timestamp::from_nanos(1739577600000000000), 140 | Timestamp::from_nanos(1741996800000000000), 141 | Timestamp::from_nanos(1744675200000000000), 142 | Timestamp::from_nanos(1747267200000000000), 143 | Timestamp::from_nanos(1749945600000000000), 144 | Timestamp::from_nanos(1752537600000000000), 145 | Timestamp::from_nanos(1755216000000000000), 146 | Timestamp::from_nanos(1757894400000000000), 147 | Timestamp::from_nanos(1760486400000000000), 148 | Timestamp::from_nanos(1763164800000000000), 149 | Timestamp::from_nanos(1765756800000000000), 150 | Timestamp::from_nanos(1768435200000000000), 151 | Timestamp::from_nanos(1771113600000000000), 152 | Timestamp::from_nanos(1773532800000000000), 153 | Timestamp::from_nanos(1776211200000000000), 154 | Timestamp::from_nanos(1778803200000000000), 155 | Timestamp::from_nanos(1781481600000000000), 156 | Timestamp::from_nanos(1784073600000000000), 157 | Timestamp::from_nanos(1786752000000000000), 158 | Timestamp::from_nanos(1789430400000000000), 159 | Timestamp::from_nanos(1792022400000000000), 160 | Timestamp::from_nanos(1794700800000000000), 161 | Timestamp::from_nanos(1797292800000000000), 162 | Timestamp::from_nanos(1799971200000000000), 163 | Timestamp::from_nanos(1802649600000000000), 164 | Timestamp::from_nanos(1805068800000000000), 165 | Timestamp::from_nanos(1807747200000000000), 166 | Timestamp::from_nanos(1810339200000000000), 167 | Timestamp::from_nanos(1813017600000000000), 168 | Timestamp::from_nanos(1815609600000000000), 169 | Timestamp::from_nanos(1818288000000000000), 170 | Timestamp::from_nanos(1820966400000000000), 171 | Timestamp::from_nanos(1823558400000000000), 172 | Timestamp::from_nanos(1826236800000000000), 173 | Timestamp::from_nanos(1828828800000000000), 174 | Timestamp::from_nanos(1831507200000000000), 175 | Timestamp::from_nanos(1834185600000000000), 176 | Timestamp::from_nanos(1836691200000000000), 177 | Timestamp::from_nanos(1839369600000000000), 178 | Timestamp::from_nanos(1841961600000000000), 179 | Timestamp::from_nanos(1844640000000000000), 180 | Timestamp::from_nanos(1847232000000000000), 181 | Timestamp::from_nanos(1849910400000000000), 182 | Timestamp::from_nanos(1852588800000000000), 183 | Timestamp::from_nanos(1855180800000000000), 184 | Timestamp::from_nanos(1857859200000000000), 185 | Timestamp::from_nanos(1860451200000000000), 186 | Timestamp::from_nanos(1863129600000000000), 187 | Timestamp::from_nanos(1865808000000000000), 188 | Timestamp::from_nanos(1868227200000000000), 189 | Timestamp::from_nanos(1870905600000000000), 190 | Timestamp::from_nanos(1873497600000000000), 191 | Timestamp::from_nanos(1876176000000000000), 192 | Timestamp::from_nanos(1878768000000000000), 193 | Timestamp::from_nanos(1881446400000000000), 194 | ]; 195 | let amounts: Vec = vec![20000000000000; 60]; 196 | VESTING_TIMESTAMPS.save(deps.storage, ×tamps)?; 197 | VESTING_AMOUNTS.save(deps.storage, &amounts)?; 198 | return Ok(Response::default()); 199 | } 200 | 201 | Ok(Response::default()) 202 | } 203 | 204 | fn migrate_109_handler( 205 | deps: DepsMut, 206 | env: Env, 207 | ) -> Result { 208 | Ok(Response::new()) 209 | } 210 | 211 | #[cfg_attr(not(feature = "library"), entry_point)] 212 | pub fn instantiate( 213 | deps: DepsMut, 214 | env: Env, 215 | info: MessageInfo, 216 | msg: InstantiateMsg, 217 | ) -> Result { 218 | if msg.admins.is_empty() { 219 | return Err(ContractError::NoAdmins {}); 220 | } 221 | if msg.ops.is_empty() { 222 | return Err(ContractError::NoOps {}); 223 | } 224 | if msg.admin_voting_threshold_percentage > 100 { 225 | return Err(ContractError::Threshold( 226 | ThresholdError::InvalidThreshold {}, 227 | )); 228 | } 229 | msg.tranche.validate(env, info.funds)?; 230 | 231 | set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; 232 | for admin in msg.admins.iter() { 233 | ADMINS.save(deps.storage, admin, &EmptyStruct {})?; 234 | } 235 | for op in msg.ops.iter() { 236 | OPS.save(deps.storage, op, &EmptyStruct {})?; 237 | } 238 | DENOM.save(deps.storage, &msg.tranche.denom)?; 239 | VESTING_TIMESTAMPS.save(deps.storage, &msg.tranche.vesting_timestamps)?; 240 | VESTING_AMOUNTS.save(deps.storage, &msg.tranche.vesting_amounts)?; 241 | let total: u128 = msg.tranche.vesting_amounts.iter().sum(); 242 | TOTAL_AMOUNT.save(deps.storage, &total)?; 243 | UNLOCK_DISTRIBUTION_ADDRESS.save( 244 | deps.storage, 245 | &msg.tranche.unlocked_token_distribution_address, 246 | )?; 247 | STAKING_REWARD_ADDRESS.save( 248 | deps.storage, 249 | &msg.tranche.staking_reward_distribution_address, 250 | )?; 251 | MAX_VOTING_PERIOD.save(deps.storage, &msg.max_voting_period)?; 252 | ADMIN_VOTING_THRESHOLD.save( 253 | deps.storage, 254 | &Threshold::AbsolutePercentage { 255 | percentage: Decimal::percent(msg.admin_voting_threshold_percentage as u64), 256 | }, 257 | )?; 258 | WITHDRAWN_STAKING_REWARDS.save(deps.storage, &0)?; 259 | WITHDRAWN_UNLOCKED.save(deps.storage, &0)?; 260 | WITHDRAWN_LOCKED.save(deps.storage, &0)?; 261 | Ok(Response::default()) 262 | } 263 | 264 | #[cfg_attr(not(feature = "library"), entry_point)] 265 | pub fn execute( 266 | deps: DepsMut, 267 | env: Env, 268 | info: MessageInfo, 269 | msg: ExecuteMsg, 270 | ) -> Result, ContractError> { 271 | match msg { 272 | ExecuteMsg::Delegate { validator, amount } => { 273 | execute_delegate(deps.as_ref(), info, validator, amount) 274 | } 275 | ExecuteMsg::Redelegate { 276 | src_validator, 277 | dst_validator, 278 | amount, 279 | } => execute_redelegate(deps.as_ref(), info, src_validator, dst_validator, amount), 280 | ExecuteMsg::Undelegate { validator, amount } => { 281 | execute_undelegate(deps.as_ref(), info, validator, amount) 282 | } 283 | ExecuteMsg::InitiateWithdrawUnlocked { amount } => { 284 | execute_initiate_withdraw_unlocked(deps, env, info, amount) 285 | } 286 | ExecuteMsg::UpdateOp { op, remove } => execute_update_op(deps, info, op, remove), 287 | ExecuteMsg::InitiateWithdrawReward {} => execute_initiate_withdraw_reward(deps, env, info), 288 | ExecuteMsg::ProposeUpdateAdmin { admin, remove } => { 289 | execute_propose_update_admin(deps, env, info, admin, remove) 290 | } 291 | ExecuteMsg::ProposeUpdateUnlockedDistributionAddress { 292 | unlocked_distribution_address, 293 | } => execute_propose_update_unlocked_distribution_address( 294 | deps, 295 | env, 296 | info, 297 | unlocked_distribution_address, 298 | ), 299 | ExecuteMsg::ProposeUpdateStakingRewardDistributionAddress { 300 | staking_reward_distribution_address, 301 | } => execute_propose_update_staking_reward_distribution_address( 302 | deps, 303 | env, 304 | info, 305 | staking_reward_distribution_address, 306 | ), 307 | ExecuteMsg::ProposeEmergencyWithdraw { dst } => { 308 | execute_propose_emergency_withdraw(deps, env, info, dst) 309 | } 310 | ExecuteMsg::ProposeGovVote { 311 | gov_proposal_id, 312 | gov_vote, 313 | } => execute_propose_gov_vote(deps, env, info, gov_proposal_id, gov_vote), 314 | ExecuteMsg::VoteProposal { proposal_id } => execute_vote(deps, env, info, proposal_id), 315 | ExecuteMsg::ProcessProposal { proposal_id } => { 316 | execute_process_proposal(deps, env, info, proposal_id) 317 | } 318 | ExecuteMsg::InternalUpdateAdmin { admin, remove } => { 319 | execute_internal_update_admin(deps, env, info, admin, remove) 320 | } 321 | ExecuteMsg::InternalUpdateUnlockedDistributionAddress { 322 | unlocked_distribution_address, 323 | } => execute_internal_update_unlocked_distribution_address( 324 | deps, 325 | env, 326 | info, 327 | unlocked_distribution_address, 328 | ), 329 | ExecuteMsg::InternalUpdateStakingRewardDistributionAddress { 330 | staking_reward_distribution_address, 331 | } => execute_internal_update_staking_reward_distribution_address( 332 | deps, 333 | env, 334 | info, 335 | staking_reward_distribution_address, 336 | ), 337 | ExecuteMsg::InternalWithdrawLocked { dst } => { 338 | execute_internal_withdraw_locked(deps, env, info, dst) 339 | } 340 | } 341 | } 342 | 343 | fn execute_delegate( 344 | deps: Deps, 345 | info: MessageInfo, 346 | validator: String, 347 | amount: u128, 348 | ) -> Result, ContractError> { 349 | authorize_op(deps.storage, info.sender)?; 350 | let denom = DENOM.load(deps.storage)?; 351 | let mut response = Response::new(); 352 | response = delegate(response, validator, amount, denom); 353 | Ok(response) 354 | } 355 | 356 | fn execute_redelegate( 357 | deps: Deps, 358 | info: MessageInfo, 359 | src_validator: String, 360 | dst_validator: String, 361 | amount: u128, 362 | ) -> Result, ContractError> { 363 | authorize_op(deps.storage, info.sender)?; 364 | let denom = DENOM.load(deps.storage)?; 365 | let mut response = Response::new(); 366 | response = redelegate(response, src_validator, dst_validator, amount, denom); 367 | Ok(response) 368 | } 369 | 370 | fn execute_undelegate( 371 | deps: Deps, 372 | info: MessageInfo, 373 | validator: String, 374 | amount: u128, 375 | ) -> Result, ContractError> { 376 | authorize_op(deps.storage, info.sender)?; 377 | let denom = DENOM.load(deps.storage)?; 378 | let mut response = Response::new(); 379 | response = undelegate(response, validator, amount, denom); 380 | Ok(response) 381 | } 382 | 383 | fn execute_initiate_withdraw_unlocked( 384 | deps: DepsMut, 385 | env: Env, 386 | info: MessageInfo, 387 | amount: u128, 388 | ) -> Result, ContractError> { 389 | authorize_op(deps.storage, info.sender)?; 390 | let vested_amount = collect_vested(deps.storage, env.block.time, amount)?; 391 | WITHDRAWN_UNLOCKED.update(deps.storage, |old| -> Result { 392 | Ok(old + vested_amount) 393 | })?; 394 | distribute_vested(deps.storage, vested_amount, Response::new()) 395 | } 396 | 397 | fn execute_initiate_withdraw_reward( 398 | deps: DepsMut, 399 | env: Env, 400 | info: MessageInfo, 401 | ) -> Result, ContractError> { 402 | authorize_op(deps.storage, info.sender)?; 403 | let mut response = Response::new(); 404 | let mut total = calculate_withdrawn_rewards(deps.as_ref(), env.clone())?; 405 | if total > 0 { 406 | response = response.add_message(BankMsg::Send { 407 | to_address: STAKING_REWARD_ADDRESS.load(deps.storage)?.to_string(), 408 | amount: coins(total, DENOM.load(deps.storage)?), 409 | }); 410 | } 411 | for validator in get_all_delegated_validators(deps.as_ref(), env.clone())? { 412 | let withdrawable_amount = 413 | get_delegation_rewards(deps.as_ref(), env.clone(), validator.clone())?; 414 | response = 415 | withdraw_delegation_rewards(deps.as_ref(), response, validator, withdrawable_amount)?; 416 | total += withdrawable_amount; 417 | } 418 | WITHDRAWN_STAKING_REWARDS.update(deps.storage, |old| -> Result { 419 | Ok(old + total) 420 | })?; 421 | Ok(response) 422 | } 423 | 424 | // rewards may be automatically withdrawn to contract's bank balance during redelegation/undelegation/delegating 425 | // more to the same validator. The amount of such withdrawn rewards, assuming no external deposit to the contract 426 | // is present, can be calculated as: bank balance - (total - withdrawn principal - staked - unbonding). 427 | // Note that because CW currently doesn't support querying unbonding amount, we will ignore unbonding amount in the 428 | // calculationg for now. This would make under-withdraw possible but still impossible to over-withdraw (which is bad). 429 | // To avoid under-withdraw, the operator can wait till there is no unbonding amount for the contract when executing 430 | // rewards withdrawal. 431 | fn calculate_withdrawn_rewards( 432 | deps: Deps, 433 | env: Env, 434 | ) -> Result { 435 | let bank_balance = deps 436 | .querier 437 | .query_balance(env.contract.address.clone(), DENOM.load(deps.storage)?)? 438 | .amount 439 | .u128(); 440 | let total_locked: u128 = TOTAL_AMOUNT.load(deps.storage)?; 441 | let withdrawn_principal = 442 | WITHDRAWN_LOCKED.load(deps.storage)? + WITHDRAWN_UNLOCKED.load(deps.storage)?; 443 | let staked: u128 = deps 444 | .querier 445 | .query_all_delegations(env.contract.address.clone())? 446 | .iter() 447 | .map(|del: &Delegation| -> u128 { 448 | if del.amount.clone().denom != DENOM.load(deps.storage).unwrap() { 449 | return 0; 450 | } 451 | del.amount.amount.u128() 452 | }) 453 | .sum(); 454 | let unbonding: u128 = get_unbonding_balance(deps, env.clone())?; 455 | let mut principal_in_bank: u128 = 0; 456 | if withdrawn_principal + staked + unbonding < total_locked { 457 | principal_in_bank = total_locked - withdrawn_principal - staked - unbonding; 458 | } 459 | if principal_in_bank < bank_balance { 460 | return Ok(bank_balance - principal_in_bank); 461 | } 462 | Ok(0) 463 | } 464 | 465 | fn execute_update_op( 466 | deps: DepsMut, 467 | info: MessageInfo, 468 | op: Addr, 469 | remove: bool, 470 | ) -> Result, ContractError> { 471 | authorize_admin(deps.storage, info.sender)?; 472 | if remove { 473 | OPS.remove(deps.storage, &op); 474 | } else { 475 | OPS.save(deps.storage, &op, &EmptyStruct {})?; 476 | } 477 | Ok(Response::new()) 478 | } 479 | 480 | fn execute_propose_update_admin( 481 | deps: DepsMut, 482 | env: Env, 483 | info: MessageInfo, 484 | admin: Addr, 485 | remove: bool, 486 | ) -> Result, ContractError> { 487 | let msg = ExecuteMsg::InternalUpdateAdmin { 488 | admin: admin.clone(), 489 | remove: remove, 490 | }; 491 | let title: String; 492 | if remove { 493 | title = format!("remove {}", admin.to_string()); 494 | } else { 495 | title = format!("add {}", admin.to_string()); 496 | } 497 | execute_propose( 498 | deps, 499 | env.clone(), 500 | info.clone(), 501 | title.clone(), 502 | vec![CosmosMsg::Wasm(WasmMsg::Execute { 503 | contract_addr: env.contract.address.to_string(), 504 | msg: to_binary(&msg)?, 505 | funds: vec![], 506 | })], 507 | ) 508 | } 509 | 510 | fn execute_propose_update_unlocked_distribution_address( 511 | deps: DepsMut, 512 | env: Env, 513 | info: MessageInfo, 514 | unlocked_distribution_address: Addr, 515 | ) -> Result, ContractError> { 516 | let msg = ExecuteMsg::InternalUpdateUnlockedDistributionAddress { 517 | unlocked_distribution_address: unlocked_distribution_address.clone(), 518 | }; 519 | let title = format!( 520 | "updating unlocked distribution address {}", 521 | unlocked_distribution_address.to_string() 522 | ); 523 | execute_propose( 524 | deps, 525 | env.clone(), 526 | info.clone(), 527 | title.clone(), 528 | vec![CosmosMsg::Wasm(WasmMsg::Execute { 529 | contract_addr: env.contract.address.to_string(), 530 | msg: to_binary(&msg)?, 531 | funds: vec![], 532 | })], 533 | ) 534 | } 535 | 536 | fn execute_propose_update_staking_reward_distribution_address( 537 | deps: DepsMut, 538 | env: Env, 539 | info: MessageInfo, 540 | staking_reward_distribution_address: Addr, 541 | ) -> Result, ContractError> { 542 | let msg = ExecuteMsg::InternalUpdateStakingRewardDistributionAddress { 543 | staking_reward_distribution_address: staking_reward_distribution_address.clone(), 544 | }; 545 | let title = format!( 546 | "updating staking reward distribution address {}", 547 | staking_reward_distribution_address.to_string() 548 | ); 549 | execute_propose( 550 | deps, 551 | env.clone(), 552 | info.clone(), 553 | title.clone(), 554 | vec![CosmosMsg::Wasm(WasmMsg::Execute { 555 | contract_addr: env.contract.address.to_string(), 556 | msg: to_binary(&msg)?, 557 | funds: vec![], 558 | })], 559 | ) 560 | } 561 | 562 | fn execute_propose_emergency_withdraw( 563 | deps: DepsMut, 564 | env: Env, 565 | info: MessageInfo, 566 | dst: Addr, 567 | ) -> Result, ContractError> { 568 | let title = format!("emergecy withdraw to {}", dst.to_string()); 569 | let msg = ExecuteMsg::InternalWithdrawLocked { dst }; 570 | execute_propose( 571 | deps, 572 | env.clone(), 573 | info.clone(), 574 | title.clone(), 575 | vec![CosmosMsg::Wasm(WasmMsg::Execute { 576 | contract_addr: env.contract.address.to_string(), 577 | msg: to_binary(&msg)?, 578 | funds: vec![], 579 | })], 580 | ) 581 | } 582 | 583 | fn execute_propose_gov_vote( 584 | deps: DepsMut, 585 | env: Env, 586 | info: MessageInfo, 587 | gov_proposal_id: u64, 588 | gov_vote: VoteOption, 589 | ) -> Result, ContractError> { 590 | let title = format!("voting {:?} for {}", gov_vote, gov_proposal_id); 591 | let msg = GovMsg::Vote { 592 | proposal_id: gov_proposal_id, 593 | vote: gov_vote, 594 | }; 595 | execute_propose( 596 | deps, 597 | env.clone(), 598 | info.clone(), 599 | title.clone(), 600 | vec![CosmosMsg::Gov(msg)], 601 | ) 602 | } 603 | 604 | fn execute_propose( 605 | deps: DepsMut, 606 | env: Env, 607 | info: MessageInfo, 608 | title: String, 609 | msgs: Vec, 610 | ) -> Result, ContractError> { 611 | authorize_admin(deps.storage, info.sender.clone())?; 612 | 613 | let expires = MAX_VOTING_PERIOD.load(deps.storage)?.after(&env.block); 614 | let mut prop = Proposal { 615 | title: title, 616 | description: "".to_string(), 617 | start_height: env.block.height, 618 | expires, 619 | msgs: msgs, 620 | status: Status::Open, 621 | votes: Votes::yes(1), // every admin has equal voting power, and the proposer automatically votes 622 | threshold: ADMIN_VOTING_THRESHOLD.load(deps.storage)?, 623 | total_weight: get_number_of_admins(deps.storage) as u64, 624 | proposer: info.sender.clone(), 625 | deposit: None, 626 | }; 627 | prop.update_status(&env.block); 628 | let id = next_proposal_id(deps.storage)?; 629 | PROPOSALS.save(deps.storage, id, &prop)?; 630 | 631 | let ballot = Ballot { 632 | weight: 1, 633 | vote: Vote::Yes, 634 | }; 635 | BALLOTS.save(deps.storage, (id, &info.sender), &ballot)?; 636 | 637 | Ok(Response::new() 638 | .add_attribute("action", "propose") 639 | .add_attribute("sender", info.sender) 640 | .add_attribute("proposal_id", id.to_string()) 641 | .add_attribute("status", format!("{:?}", prop.status))) 642 | } 643 | 644 | fn execute_vote( 645 | deps: DepsMut, 646 | env: Env, 647 | info: MessageInfo, 648 | proposal_id: u64, 649 | ) -> Result, ContractError> { 650 | authorize_admin(deps.storage, info.sender.clone())?; 651 | 652 | let mut prop = PROPOSALS.load(deps.storage, proposal_id)?; 653 | if prop.status != Status::Open { 654 | return Err(ContractError::NotOpen {}); 655 | } 656 | if prop.expires.is_expired(&env.block) { 657 | return Err(ContractError::Expired {}); 658 | } 659 | 660 | // cast vote if no vote previously cast 661 | BALLOTS.update(deps.storage, (proposal_id, &info.sender), |bal| match bal { 662 | Some(_) => Err(ContractError::AlreadyVoted {}), 663 | None => Ok(Ballot { 664 | weight: 1, 665 | vote: Vote::Yes, 666 | }), 667 | })?; 668 | 669 | // update vote tally 670 | prop.votes.add_vote(Vote::Yes, 1); 671 | prop.update_status(&env.block); 672 | PROPOSALS.save(deps.storage, proposal_id, &prop)?; 673 | 674 | Ok(Response::new() 675 | .add_attribute("action", "vote") 676 | .add_attribute("sender", info.sender) 677 | .add_attribute("proposal_id", proposal_id.to_string()) 678 | .add_attribute("status", format!("{:?}", prop.status))) 679 | } 680 | 681 | fn execute_process_proposal( 682 | deps: DepsMut, 683 | env: Env, 684 | info: MessageInfo, 685 | proposal_id: u64, 686 | ) -> Result, ContractError> { 687 | authorize_admin(deps.storage, info.sender.clone())?; 688 | 689 | let mut prop = PROPOSALS.load(deps.storage, proposal_id)?; 690 | // we allow execution even after the proposal "expiration" as long as all vote come in before 691 | // that point. If it was approved on time, it can be executed any time. 692 | prop.update_status(&env.block); 693 | if prop.status != Status::Passed { 694 | return Err(ContractError::WrongExecuteStatus {}); 695 | } 696 | 697 | // set it to executed 698 | prop.status = Status::Executed; 699 | PROPOSALS.save(deps.storage, proposal_id, &prop)?; 700 | 701 | // dispatch all proposed messages 702 | Ok(Response::new() 703 | .add_messages(prop.msgs) 704 | .add_attribute("action", "execute") 705 | .add_attribute("sender", info.sender) 706 | .add_attribute("proposal_id", proposal_id.to_string())) 707 | } 708 | 709 | fn execute_internal_update_admin( 710 | deps: DepsMut, 711 | env: Env, 712 | info: MessageInfo, 713 | admin: Addr, 714 | remove: bool, 715 | ) -> Result, ContractError> { 716 | authorize_self_call(env, info)?; 717 | if remove { 718 | ADMINS.remove(deps.storage, &admin); 719 | } else { 720 | ADMINS.save(deps.storage, &admin, &EmptyStruct {})?; 721 | } 722 | Ok(Response::new()) 723 | } 724 | 725 | fn execute_internal_update_unlocked_distribution_address( 726 | deps: DepsMut, 727 | env: Env, 728 | info: MessageInfo, 729 | unlocked_distribution_address: Addr, 730 | ) -> Result, ContractError> { 731 | authorize_self_call(env, info)?; 732 | UNLOCK_DISTRIBUTION_ADDRESS.save(deps.storage, &unlocked_distribution_address)?; 733 | Ok(Response::new()) 734 | } 735 | 736 | fn execute_internal_update_staking_reward_distribution_address( 737 | deps: DepsMut, 738 | env: Env, 739 | info: MessageInfo, 740 | staking_reward_distribution_address: Addr, 741 | ) -> Result, ContractError> { 742 | authorize_self_call(env, info)?; 743 | STAKING_REWARD_ADDRESS.save(deps.storage, &staking_reward_distribution_address)?; 744 | Ok(Response::new()) 745 | } 746 | 747 | fn execute_internal_withdraw_locked( 748 | deps: DepsMut, 749 | env: Env, 750 | info: MessageInfo, 751 | dst: Addr, 752 | ) -> Result, ContractError> { 753 | authorize_self_call(env, info)?; 754 | let amount = VESTING_AMOUNTS.load(deps.storage)?.iter().sum(); 755 | VESTING_AMOUNTS.save(deps.storage, &vec![])?; 756 | VESTING_TIMESTAMPS.save(deps.storage, &vec![])?; 757 | WITHDRAWN_LOCKED.update(deps.storage, |old| -> Result { 758 | Ok(old + amount) 759 | })?; 760 | Ok(Response::new().add_message(BankMsg::Send { 761 | to_address: dst.to_string(), 762 | amount: coins(amount, DENOM.load(deps.storage)?), 763 | })) 764 | } 765 | 766 | #[cfg_attr(not(feature = "library"), entry_point)] 767 | pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { 768 | match msg { 769 | QueryMsg::ListProposals {} => to_binary(&query_proposals(deps, env)?), 770 | QueryMsg::ListVotes { proposal_id } => to_binary(&query_votes(deps, proposal_id)?), 771 | QueryMsg::ListAdmins {} => to_binary(&query_admins(deps)?), 772 | QueryMsg::ListOps {} => to_binary(&query_ops(deps)?), 773 | QueryMsg::Info {} => to_binary(&query_info(deps)?), 774 | QueryMsg::Config {} => to_binary(&query_config(deps)?), 775 | QueryMsg::TotalVested {} => to_binary(&query_total_vested(deps, env)?), 776 | } 777 | } 778 | 779 | fn query_proposals(deps: Deps, env: Env) -> StdResult { 780 | let proposals: Vec = PROPOSALS 781 | .range(deps.storage, None, None, Order::Descending) 782 | .map(|p| map_proposal(&env.block, p)) 783 | .collect::>()?; 784 | Ok(ProposalListResponse { proposals }) 785 | } 786 | 787 | fn map_proposal( 788 | block: &BlockInfo, 789 | item: StdResult<(u64, Proposal)>, 790 | ) -> StdResult { 791 | item.map(|(id, prop)| { 792 | let status = prop.current_status(block); 793 | let threshold = prop.threshold.to_response(prop.total_weight); 794 | ProposalResponse { 795 | id, 796 | title: prop.title, 797 | description: prop.description, 798 | msgs: prop.msgs, 799 | status, 800 | deposit: prop.deposit, 801 | proposer: prop.proposer, 802 | expires: prop.expires, 803 | threshold, 804 | } 805 | }) 806 | } 807 | 808 | fn query_votes(deps: Deps, proposal_id: u64) -> StdResult { 809 | let votes = BALLOTS 810 | .prefix(proposal_id) 811 | .range(deps.storage, None, None, Order::Ascending) 812 | .map(|item| { 813 | item.map(|(addr, ballot)| VoteInfo { 814 | proposal_id, 815 | voter: addr.into(), 816 | vote: ballot.vote, 817 | weight: ballot.weight, 818 | }) 819 | }) 820 | .collect::>()?; 821 | 822 | Ok(VoteListResponse { votes }) 823 | } 824 | 825 | fn query_admins(deps: Deps) -> StdResult { 826 | let admins: Vec = ADMINS 827 | .range(deps.storage, None, None, Order::Ascending) 828 | .map(|admin| admin.map(|(admin, _)| -> Addr { admin })) 829 | .collect::>()?; 830 | Ok(AdminListResponse { admins }) 831 | } 832 | 833 | fn query_ops(deps: Deps) -> StdResult { 834 | let ops: Vec = OPS 835 | .range(deps.storage, None, None, Order::Ascending) 836 | .map(|op| op.map(|(op, _)| -> Addr { op })) 837 | .collect::>()?; 838 | Ok(OpListResponse { ops }) 839 | } 840 | 841 | fn query_info(deps: Deps) -> StdResult { 842 | Ok(ShowInfoResponse { 843 | denom: DENOM.load(deps.storage)?, 844 | vesting_timestamps: VESTING_TIMESTAMPS.load(deps.storage)?, 845 | vesting_amounts: VESTING_AMOUNTS.load(deps.storage)?, 846 | unlock_distribution_address: UNLOCK_DISTRIBUTION_ADDRESS.load(deps.storage)?, 847 | staking_reward_address: STAKING_REWARD_ADDRESS.load(deps.storage)?, 848 | withdrawn_staking_rewards: WITHDRAWN_STAKING_REWARDS.load(deps.storage)?, 849 | withdrawn_unlocked: WITHDRAWN_UNLOCKED.load(deps.storage)?, 850 | withdrawn_locked: WITHDRAWN_LOCKED.load(deps.storage)?, 851 | }) 852 | } 853 | 854 | fn query_config(deps: Deps) -> StdResult { 855 | Ok(ShowConfigResponse { 856 | max_voting_period: MAX_VOTING_PERIOD.load(deps.storage)?, 857 | admin_voting_threshold: ADMIN_VOTING_THRESHOLD.load(deps.storage)?, 858 | }) 859 | } 860 | 861 | fn query_total_vested(deps: Deps, env: Env) -> StdResult { 862 | let vested_amount = total_vested_amount(deps.storage, env.block.time)?; 863 | Ok(ShowTotalVestedResponse { 864 | vested_amount: vested_amount, 865 | }) 866 | } 867 | 868 | #[cfg(test)] 869 | mod tests { 870 | use core::marker::PhantomData; 871 | use cosmwasm_std::testing::{ 872 | mock_env, mock_info, MockApi, MockQuerier, MockQuerierCustomHandlerResult, MockStorage, 873 | MOCK_CONTRACT_ADDR, 874 | }; 875 | use cosmwasm_std::{ 876 | from_binary, to_json_binary, Addr, Coin, ContractResult, Decimal, FullDelegation, 877 | OwnedDeps, SystemResult, Timestamp, Uint128, Validator, 878 | }; 879 | 880 | use cw2::{get_contract_version, ContractVersion}; 881 | use cw_utils::{Duration, Expiration, ThresholdResponse}; 882 | 883 | use crate::data_structure::Tranche; 884 | use crate::msg::{SeiQueryWrapper, UnbondingDelegationEntry, UnbondingDelegationsResponse}; 885 | use crate::state::get_number_of_ops; 886 | 887 | use super::*; 888 | 889 | const OWNER: &str = "admin0001"; 890 | const VOTER1: &str = "voter0001"; 891 | const VOTER2: &str = "voter0002"; 892 | const VOTER3: &str = "voter0003"; 893 | const VOTER4: &str = "voter0004"; 894 | const VOTER5: &str = "voter0005"; 895 | const VOTER6: &str = "voter0006"; 896 | 897 | const UNLOCK_ADDR1: &str = "unlock0001"; 898 | const REWARD_ADDR1: &str = "reward0001"; 899 | 900 | fn mock_dependencies( 901 | ) -> OwnedDeps, SeiQueryWrapper> { 902 | OwnedDeps { 903 | storage: MockStorage::default(), 904 | api: MockApi::default(), 905 | querier: MockQuerier::new(&[]), 906 | custom_query_type: PhantomData::default(), 907 | } 908 | } 909 | 910 | // this will set up the instantiation for other tests 911 | #[track_caller] 912 | fn setup_test_case( 913 | deps: DepsMut, 914 | info: MessageInfo, 915 | ) -> Result, ContractError> { 916 | let env = mock_env(); 917 | let mut vesting_amounts = vec![12000000u128]; 918 | let mut vesting_timestamps = vec![env.block.time.plus_seconds(31536000)]; 919 | for _ in 1..37 { 920 | vesting_amounts.push(1000000u128); 921 | vesting_timestamps.push(vesting_timestamps.last().unwrap().plus_seconds(2592000)); 922 | } 923 | let instantiate_msg = InstantiateMsg { 924 | admins: vec![ 925 | Addr::unchecked(VOTER1), 926 | Addr::unchecked(VOTER2), 927 | Addr::unchecked(VOTER3), 928 | Addr::unchecked(VOTER4), 929 | ], 930 | ops: vec![Addr::unchecked(VOTER5), Addr::unchecked(VOTER6)], 931 | tranche: Tranche { 932 | denom: "usei".to_string(), 933 | vesting_amounts, 934 | vesting_timestamps, 935 | unlocked_token_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 936 | staking_reward_distribution_address: Addr::unchecked(REWARD_ADDR1), 937 | }, 938 | max_voting_period: Duration::Time(3600), 939 | admin_voting_threshold_percentage: 75, 940 | }; 941 | instantiate(deps, mock_env(), info, instantiate_msg) 942 | } 943 | 944 | #[test] 945 | fn test_instantiate_works() { 946 | let mut deps = mock_dependencies(); 947 | let info = mock_info(OWNER, &[]); 948 | 949 | let _max_voting_period = Duration::Time(1234567); 950 | 951 | // No admins fails 952 | let instantiate_msg = InstantiateMsg { 953 | admins: vec![], 954 | ops: vec![Addr::unchecked(VOTER5)], 955 | tranche: Tranche { 956 | denom: "usei".to_string(), 957 | vesting_amounts: vec![1], 958 | vesting_timestamps: vec![mock_env().block.time], 959 | unlocked_token_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 960 | staking_reward_distribution_address: Addr::unchecked(REWARD_ADDR1), 961 | }, 962 | max_voting_period: Duration::Time(3600), 963 | admin_voting_threshold_percentage: 75, 964 | }; 965 | let err = 966 | instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap_err(); 967 | assert_eq!(err, ContractError::NoAdmins {}); 968 | 969 | // Zero ops fails 970 | let instantiate_msg = InstantiateMsg { 971 | admins: vec![Addr::unchecked(VOTER1)], 972 | ops: vec![], 973 | tranche: Tranche { 974 | denom: "usei".to_string(), 975 | vesting_amounts: vec![1], 976 | vesting_timestamps: vec![mock_env().block.time], 977 | unlocked_token_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 978 | staking_reward_distribution_address: Addr::unchecked(REWARD_ADDR1), 979 | }, 980 | max_voting_period: Duration::Time(3600), 981 | admin_voting_threshold_percentage: 75, 982 | }; 983 | let err = 984 | instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap_err(); 985 | assert_eq!(err, ContractError::NoOps {},); 986 | 987 | // Invalid vesting schedule 988 | let instantiate_msg = InstantiateMsg { 989 | admins: vec![Addr::unchecked(VOTER1)], 990 | ops: vec![Addr::unchecked(VOTER5)], 991 | tranche: Tranche { 992 | denom: "usei".to_string(), 993 | vesting_amounts: vec![], 994 | vesting_timestamps: vec![mock_env().block.time], 995 | unlocked_token_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 996 | staking_reward_distribution_address: Addr::unchecked(REWARD_ADDR1), 997 | }, 998 | max_voting_period: Duration::Time(3600), 999 | admin_voting_threshold_percentage: 75, 1000 | }; 1001 | let err = 1002 | instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap_err(); 1003 | assert_eq!( 1004 | err, 1005 | ContractError::InvalidTranche("mismatched vesting amounts and schedule".to_string()), 1006 | ); 1007 | 1008 | // insufficient funds 1009 | let err = setup_test_case(deps.as_mut(), info).unwrap_err(); 1010 | assert_eq!( 1011 | err, 1012 | ContractError::InvalidTranche("insufficient deposit for the vesting plan".to_string()), 1013 | ); 1014 | 1015 | // happy path 1016 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1017 | setup_test_case(deps.as_mut(), info).unwrap(); 1018 | 1019 | // Verify 1020 | assert_eq!( 1021 | ContractVersion { 1022 | contract: CONTRACT_NAME.to_string(), 1023 | version: CONTRACT_VERSION.to_string(), 1024 | }, 1025 | get_contract_version(&deps.storage).unwrap() 1026 | ) 1027 | } 1028 | 1029 | #[test] 1030 | fn delegate_work() { 1031 | let mut deps = mock_dependencies(); 1032 | 1033 | let info = mock_info(VOTER5, &[Coin::new(48000000, "usei".to_string())]); 1034 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1035 | 1036 | let msg = ExecuteMsg::Delegate { 1037 | validator: "val".to_string(), 1038 | amount: 100, 1039 | }; 1040 | let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); 1041 | assert_eq!(1, res.messages.len()); 1042 | } 1043 | 1044 | #[test] 1045 | fn delegate_unauthorized() { 1046 | let mut deps = mock_dependencies(); 1047 | 1048 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1049 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1050 | 1051 | let msg = ExecuteMsg::Delegate { 1052 | validator: "val".to_string(), 1053 | amount: 100, 1054 | }; 1055 | execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); 1056 | } 1057 | 1058 | #[test] 1059 | fn redelegate_work() { 1060 | let mut deps = mock_dependencies(); 1061 | 1062 | let info = mock_info(VOTER5, &[Coin::new(48000000, "usei".to_string())]); 1063 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1064 | 1065 | let msg = ExecuteMsg::Redelegate { 1066 | src_validator: "val1".to_string(), 1067 | dst_validator: "val2".to_string(), 1068 | amount: 100, 1069 | }; 1070 | let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); 1071 | assert_eq!(1, res.messages.len()); 1072 | } 1073 | 1074 | #[test] 1075 | fn redelegate_unauthorized() { 1076 | let mut deps = mock_dependencies(); 1077 | 1078 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1079 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1080 | 1081 | let msg = ExecuteMsg::Redelegate { 1082 | src_validator: "val1".to_string(), 1083 | dst_validator: "val2".to_string(), 1084 | amount: 100, 1085 | }; 1086 | execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); 1087 | } 1088 | 1089 | #[test] 1090 | fn undelegate_work() { 1091 | let mut deps = mock_dependencies(); 1092 | 1093 | let info = mock_info(VOTER5, &[Coin::new(48000000, "usei".to_string())]); 1094 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1095 | 1096 | let msg = ExecuteMsg::Undelegate { 1097 | validator: "val".to_string(), 1098 | amount: 100, 1099 | }; 1100 | let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); 1101 | assert_eq!(1, res.messages.len()); 1102 | } 1103 | 1104 | #[test] 1105 | fn undelegate_unauthorized() { 1106 | let mut deps = mock_dependencies(); 1107 | 1108 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1109 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1110 | 1111 | let msg = ExecuteMsg::Undelegate { 1112 | validator: "val".to_string(), 1113 | amount: 100, 1114 | }; 1115 | execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); 1116 | } 1117 | 1118 | #[test] 1119 | fn initiate_withdraw_unlocked_work() { 1120 | let mut deps = mock_dependencies(); 1121 | 1122 | let info = mock_info(VOTER5, &[Coin::new(48000000, "usei".to_string())]); 1123 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1124 | 1125 | let msg = ExecuteMsg::InitiateWithdrawUnlocked { amount: 12000000 }; 1126 | let mut env = mock_env(); 1127 | let mut block = env.block; 1128 | block.time = block.time.plus_seconds(31536000); 1129 | env.block = block; 1130 | let res = execute(deps.as_mut(), env, info, msg).unwrap(); 1131 | assert_eq!(1, res.messages.len()); 1132 | assert_eq!( 1133 | 12000000, 1134 | WITHDRAWN_UNLOCKED.load(deps.as_ref().storage).unwrap() 1135 | ); 1136 | } 1137 | 1138 | #[test] 1139 | fn initiate_withdraw_unlocked_unauthorized() { 1140 | let mut deps = mock_dependencies(); 1141 | 1142 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1143 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1144 | 1145 | let msg = ExecuteMsg::InitiateWithdrawUnlocked { amount: 12000000 }; 1146 | let mut env = mock_env(); 1147 | let mut block = env.block; 1148 | block.time = block.time.plus_seconds(31536000); 1149 | env.block = block; 1150 | let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); 1151 | assert_eq!(err, ContractError::Unauthorized {}); 1152 | } 1153 | 1154 | #[test] 1155 | fn initiate_withdraw_reward_work() { 1156 | let validator1 = "val1"; 1157 | let validator2 = "val2"; 1158 | let mut deps = mock_dependencies(); 1159 | deps.querier.update_staking( 1160 | "usei", 1161 | &[ 1162 | Validator { 1163 | address: validator1.to_string(), 1164 | commission: Decimal::zero(), 1165 | max_commission: Decimal::zero(), 1166 | max_change_rate: Decimal::zero(), 1167 | }, 1168 | Validator { 1169 | address: validator2.to_string(), 1170 | commission: Decimal::zero(), 1171 | max_commission: Decimal::zero(), 1172 | max_change_rate: Decimal::zero(), 1173 | }, 1174 | ], 1175 | &[ 1176 | FullDelegation { 1177 | delegator: Addr::unchecked(mock_env().contract.address), 1178 | validator: validator1.to_string(), 1179 | amount: Coin::new(1000000, "usei"), 1180 | can_redelegate: Coin::new(0, "usei"), 1181 | accumulated_rewards: vec![Coin::new(10, "usei"), Coin::new(20, "usei")], 1182 | }, 1183 | FullDelegation { 1184 | delegator: Addr::unchecked(mock_env().contract.address), 1185 | validator: validator2.to_string(), 1186 | amount: Coin::new(500000, "usei"), 1187 | can_redelegate: Coin::new(0, "usei"), 1188 | accumulated_rewards: vec![Coin::new(5, "usei")], 1189 | }, 1190 | ], 1191 | ); 1192 | deps.querier.update_balance( 1193 | mock_env().contract.address.clone(), 1194 | vec![Coin::new(48000000 - 1500000 + 100, "usei")], 1195 | ); 1196 | // principal: 48000000 - 1500000 (delegations). 1197 | // Withdrawn rewards: principal - balance (100) + 10 = 110 1198 | deps.querier = deps.querier.with_custom_handler( 1199 | |_: &SeiQueryWrapper| -> MockQuerierCustomHandlerResult { 1200 | let res = UnbondingDelegationsResponse { 1201 | entries: vec![UnbondingDelegationEntry { 1202 | creation_height: 1, 1203 | completion_time: "".to_string(), 1204 | initial_balance: Uint128::new(10), 1205 | balance: Uint128::new(10), 1206 | }], 1207 | }; 1208 | return MockQuerierCustomHandlerResult::Ok(ContractResult::Ok( 1209 | to_json_binary(&res).unwrap(), 1210 | )); 1211 | }, 1212 | ); 1213 | 1214 | let info = mock_info(VOTER5, &[Coin::new(48000000, "usei".to_string())]); 1215 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1216 | 1217 | let msg = ExecuteMsg::InitiateWithdrawReward {}; 1218 | let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); 1219 | assert_eq!(5, res.messages.len()); 1220 | assert_eq!( 1221 | 35 + 110, 1222 | WITHDRAWN_STAKING_REWARDS 1223 | .load(deps.as_ref().storage) 1224 | .unwrap() 1225 | ); 1226 | } 1227 | 1228 | #[test] 1229 | fn initiate_withdraw_reward_unauthorized() { 1230 | let mut deps = mock_dependencies(); 1231 | 1232 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1233 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1234 | 1235 | let msg = ExecuteMsg::InitiateWithdrawReward {}; 1236 | execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); 1237 | } 1238 | 1239 | #[test] 1240 | fn test_propose_update_admin_works() { 1241 | let mut deps = mock_dependencies(); 1242 | 1243 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1244 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1245 | 1246 | let info = mock_info(VOTER1, &[]); 1247 | let new_admin = Addr::unchecked("new_admin1"); 1248 | let proposal = ExecuteMsg::ProposeUpdateAdmin { 1249 | admin: new_admin.clone(), 1250 | remove: false, 1251 | }; 1252 | let res = execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap(); 1253 | 1254 | // Verify 1255 | assert_eq!( 1256 | res, 1257 | Response::new() 1258 | .add_attribute("action", "propose") 1259 | .add_attribute("sender", VOTER1) 1260 | .add_attribute("proposal_id", 1.to_string()) 1261 | .add_attribute("status", "Open") 1262 | ); 1263 | 1264 | // Verify admin has updated after internal call 1265 | let internal_update = ExecuteMsg::InternalUpdateAdmin { 1266 | admin: new_admin.clone(), 1267 | remove: false, 1268 | }; 1269 | let internal_info = mock_info(MOCK_CONTRACT_ADDR, &[]); 1270 | execute( 1271 | deps.as_mut(), 1272 | mock_env(), 1273 | internal_info, 1274 | internal_update.clone(), 1275 | ) 1276 | .unwrap(); 1277 | let result = match ADMINS.load(deps.as_ref().storage, &Addr::unchecked(new_admin.clone())) { 1278 | Ok(_) => Ok(()), 1279 | Err(_) => Err(ContractError::Unauthorized {}), 1280 | }; 1281 | assert_eq!(result, Ok(())) 1282 | } 1283 | 1284 | #[test] 1285 | fn test_propose_update_admin_unauthorized() { 1286 | let mut deps = mock_dependencies(); 1287 | 1288 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1289 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1290 | 1291 | let info = mock_info(OWNER, &[]); 1292 | let proposal = ExecuteMsg::ProposeUpdateAdmin { 1293 | admin: Addr::unchecked("new_admin1"), 1294 | remove: false, 1295 | }; 1296 | let err = execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap_err(); 1297 | assert_eq!(err, ContractError::Unauthorized {}); 1298 | } 1299 | 1300 | #[test] 1301 | fn test_propose_update_unlocked_distribution_address_works() { 1302 | let mut deps = mock_dependencies(); 1303 | 1304 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1305 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1306 | 1307 | let info = mock_info(VOTER1, &[]); 1308 | let new_addr = Addr::unchecked("new_unlock1"); 1309 | let proposal = ExecuteMsg::ProposeUpdateUnlockedDistributionAddress { 1310 | unlocked_distribution_address: new_addr.clone(), 1311 | }; 1312 | let res = execute(deps.as_mut(), mock_env(), info.clone(), proposal.clone()).unwrap(); 1313 | 1314 | // Verify 1315 | assert_eq!( 1316 | res, 1317 | Response::new() 1318 | .add_attribute("action", "propose") 1319 | .add_attribute("sender", VOTER1) 1320 | .add_attribute("proposal_id", 1.to_string()) 1321 | .add_attribute("status", "Open") 1322 | ); 1323 | 1324 | // Verify address has updated after internal call 1325 | let internal_update = ExecuteMsg::InternalUpdateUnlockedDistributionAddress { 1326 | unlocked_distribution_address: new_addr.clone(), 1327 | }; 1328 | let internal_info = mock_info(MOCK_CONTRACT_ADDR, &[]); 1329 | execute( 1330 | deps.as_mut(), 1331 | mock_env(), 1332 | internal_info, 1333 | internal_update.clone(), 1334 | ) 1335 | .unwrap(); 1336 | assert_eq!( 1337 | UNLOCK_DISTRIBUTION_ADDRESS 1338 | .load(deps.as_ref().storage) 1339 | .unwrap(), 1340 | new_addr 1341 | ); 1342 | } 1343 | 1344 | #[test] 1345 | fn test_propose_update_stakign_reward_distribution_address_works() { 1346 | let mut deps = mock_dependencies(); 1347 | 1348 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1349 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1350 | 1351 | let info = mock_info(VOTER1, &[]); 1352 | let new_addr = Addr::unchecked("new_staking1"); 1353 | let proposal = ExecuteMsg::ProposeUpdateStakingRewardDistributionAddress { 1354 | staking_reward_distribution_address: new_addr.clone(), 1355 | }; 1356 | let res = execute(deps.as_mut(), mock_env(), info.clone(), proposal.clone()).unwrap(); 1357 | 1358 | // Verify 1359 | assert_eq!( 1360 | res, 1361 | Response::new() 1362 | .add_attribute("action", "propose") 1363 | .add_attribute("sender", VOTER1) 1364 | .add_attribute("proposal_id", 1.to_string()) 1365 | .add_attribute("status", "Open") 1366 | ); 1367 | 1368 | // Verify address has updated after internal call 1369 | let internal_update = ExecuteMsg::InternalUpdateStakingRewardDistributionAddress { 1370 | staking_reward_distribution_address: new_addr.clone(), 1371 | }; 1372 | let internal_info = mock_info(MOCK_CONTRACT_ADDR, &[]); 1373 | execute( 1374 | deps.as_mut(), 1375 | mock_env(), 1376 | internal_info, 1377 | internal_update.clone(), 1378 | ) 1379 | .unwrap(); 1380 | assert_eq!( 1381 | STAKING_REWARD_ADDRESS.load(deps.as_ref().storage).unwrap(), 1382 | new_addr 1383 | ); 1384 | } 1385 | 1386 | #[test] 1387 | fn test_propose_emergency_withdraw_works() { 1388 | let mut deps = mock_dependencies(); 1389 | 1390 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1391 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1392 | 1393 | let info = mock_info(VOTER1, &[]); 1394 | let proposal = ExecuteMsg::ProposeEmergencyWithdraw { 1395 | dst: Addr::unchecked("destination"), 1396 | }; 1397 | let res = execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap(); 1398 | 1399 | // Verify 1400 | assert_eq!( 1401 | res, 1402 | Response::new() 1403 | .add_attribute("action", "propose") 1404 | .add_attribute("sender", VOTER1) 1405 | .add_attribute("proposal_id", 1.to_string()) 1406 | .add_attribute("status", "Open") 1407 | ); 1408 | } 1409 | 1410 | #[test] 1411 | fn test_propose_emergency_withdraw_unauthorized() { 1412 | let mut deps = mock_dependencies(); 1413 | 1414 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1415 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1416 | 1417 | let info = mock_info(OWNER, &[]); 1418 | let proposal = ExecuteMsg::ProposeEmergencyWithdraw { 1419 | dst: Addr::unchecked("destination"), 1420 | }; 1421 | let err = execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap_err(); 1422 | assert_eq!(err, ContractError::Unauthorized {}); 1423 | } 1424 | 1425 | #[test] 1426 | fn test_propose_gov_vote_works() { 1427 | let mut deps = mock_dependencies(); 1428 | 1429 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1430 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1431 | 1432 | let info = mock_info(VOTER1, &[]); 1433 | let proposal = ExecuteMsg::ProposeGovVote { 1434 | gov_proposal_id: 1, 1435 | gov_vote: VoteOption::Yes, 1436 | }; 1437 | let res = execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap(); 1438 | 1439 | // Verify 1440 | assert_eq!( 1441 | res, 1442 | Response::new() 1443 | .add_attribute("action", "propose") 1444 | .add_attribute("sender", VOTER1) 1445 | .add_attribute("proposal_id", 1.to_string()) 1446 | .add_attribute("status", "Open") 1447 | ); 1448 | } 1449 | 1450 | #[test] 1451 | fn test_vote_works() { 1452 | let mut deps = mock_dependencies(); 1453 | 1454 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1455 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1456 | 1457 | let info = mock_info(VOTER1, &[]); 1458 | let proposal = ExecuteMsg::ProposeUpdateAdmin { 1459 | admin: Addr::unchecked("new_admin1"), 1460 | remove: false, 1461 | }; 1462 | execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap(); 1463 | 1464 | let info = mock_info(VOTER2, &[]); 1465 | let vote2 = ExecuteMsg::VoteProposal { proposal_id: 1 }; 1466 | execute(deps.as_mut(), mock_env(), info, vote2.clone()).unwrap(); 1467 | 1468 | let info = mock_info(VOTER3, &[]); 1469 | let vote3 = ExecuteMsg::VoteProposal { proposal_id: 1 }; 1470 | execute(deps.as_mut(), mock_env(), info, vote3.clone()).unwrap(); 1471 | } 1472 | 1473 | #[test] 1474 | fn test_vote_expired() { 1475 | let mut deps = mock_dependencies(); 1476 | 1477 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1478 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1479 | 1480 | let info = mock_info(VOTER1, &[]); 1481 | let proposal = ExecuteMsg::ProposeUpdateAdmin { 1482 | admin: Addr::unchecked("new_admin1"), 1483 | remove: false, 1484 | }; 1485 | execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap(); 1486 | 1487 | let info = mock_info(VOTER2, &[]); 1488 | let vote2 = ExecuteMsg::VoteProposal { proposal_id: 1 }; 1489 | execute(deps.as_mut(), mock_env(), info, vote2.clone()).unwrap(); 1490 | 1491 | let info = mock_info(VOTER3, &[]); 1492 | let vote3 = ExecuteMsg::VoteProposal { proposal_id: 1 }; 1493 | let mut env = mock_env(); 1494 | env.block.time = env.block.time.plus_seconds(3601); 1495 | let err = execute(deps.as_mut(), env, info, vote3.clone()).unwrap_err(); 1496 | assert_eq!(err, ContractError::Expired {}); 1497 | } 1498 | 1499 | #[test] 1500 | fn test_process_update_admin_works() { 1501 | let mut deps = mock_dependencies(); 1502 | 1503 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1504 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1505 | 1506 | let info = mock_info(VOTER1, &[]); 1507 | let proposal = ExecuteMsg::ProposeUpdateAdmin { 1508 | admin: Addr::unchecked("new_admin1"), 1509 | remove: false, 1510 | }; 1511 | execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap(); 1512 | 1513 | let info = mock_info(VOTER2, &[]); 1514 | let vote2 = ExecuteMsg::VoteProposal { proposal_id: 1 }; 1515 | execute(deps.as_mut(), mock_env(), info, vote2.clone()).unwrap(); 1516 | 1517 | let info = mock_info(VOTER3, &[]); 1518 | let vote3 = ExecuteMsg::VoteProposal { proposal_id: 1 }; 1519 | execute(deps.as_mut(), mock_env(), info, vote3.clone()).unwrap(); 1520 | 1521 | let info = mock_info(VOTER3, &[]); 1522 | let process = ExecuteMsg::ProcessProposal { proposal_id: 1 }; 1523 | let res = execute(deps.as_mut(), mock_env(), info, process.clone()).unwrap(); 1524 | 1525 | assert_eq!(1, res.messages.len()); 1526 | } 1527 | 1528 | #[test] 1529 | fn test_process_proposal_premature() { 1530 | let mut deps = mock_dependencies(); 1531 | 1532 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1533 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1534 | 1535 | let info = mock_info(VOTER1, &[]); 1536 | let proposal = ExecuteMsg::ProposeUpdateAdmin { 1537 | admin: Addr::unchecked("new_admin1"), 1538 | remove: false, 1539 | }; 1540 | execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap(); 1541 | 1542 | let info = mock_info(VOTER2, &[]); 1543 | let vote2 = ExecuteMsg::VoteProposal { proposal_id: 1 }; 1544 | execute(deps.as_mut(), mock_env(), info, vote2.clone()).unwrap(); 1545 | 1546 | let info = mock_info(VOTER3, &[]); 1547 | let process = ExecuteMsg::ProcessProposal { proposal_id: 1 }; 1548 | let err = execute(deps.as_mut(), mock_env(), info, process.clone()).unwrap_err(); 1549 | 1550 | assert_eq!(err, ContractError::WrongExecuteStatus {}); 1551 | } 1552 | 1553 | #[test] 1554 | fn test_process_update_admin_double_process() { 1555 | let mut deps = mock_dependencies(); 1556 | 1557 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1558 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1559 | 1560 | let info = mock_info(VOTER1, &[]); 1561 | let proposal = ExecuteMsg::ProposeUpdateAdmin { 1562 | admin: Addr::unchecked("new_admin1"), 1563 | remove: false, 1564 | }; 1565 | execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap(); 1566 | 1567 | let info = mock_info(VOTER2, &[]); 1568 | let vote2 = ExecuteMsg::VoteProposal { proposal_id: 1 }; 1569 | execute(deps.as_mut(), mock_env(), info, vote2.clone()).unwrap(); 1570 | 1571 | let info = mock_info(VOTER3, &[]); 1572 | let vote3 = ExecuteMsg::VoteProposal { proposal_id: 1 }; 1573 | execute(deps.as_mut(), mock_env(), info, vote3.clone()).unwrap(); 1574 | 1575 | let info = mock_info(VOTER3, &[]); 1576 | let process = ExecuteMsg::ProcessProposal { proposal_id: 1 }; 1577 | execute(deps.as_mut(), mock_env(), info, process.clone()).unwrap(); 1578 | let info = mock_info(VOTER3, &[]); 1579 | let err = execute(deps.as_mut(), mock_env(), info, process.clone()).unwrap_err(); 1580 | 1581 | assert_eq!(err, ContractError::WrongExecuteStatus {}); 1582 | } 1583 | 1584 | #[test] 1585 | fn test_process_gov_vote_works() { 1586 | let mut deps = mock_dependencies(); 1587 | 1588 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1589 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1590 | 1591 | let info = mock_info(VOTER1, &[]); 1592 | let proposal = ExecuteMsg::ProposeGovVote { 1593 | gov_proposal_id: 1, 1594 | gov_vote: VoteOption::Yes, 1595 | }; 1596 | execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap(); 1597 | 1598 | let info = mock_info(VOTER2, &[]); 1599 | let vote2 = ExecuteMsg::VoteProposal { proposal_id: 1 }; 1600 | execute(deps.as_mut(), mock_env(), info, vote2.clone()).unwrap(); 1601 | 1602 | let info = mock_info(VOTER3, &[]); 1603 | let vote3 = ExecuteMsg::VoteProposal { proposal_id: 1 }; 1604 | execute(deps.as_mut(), mock_env(), info, vote3.clone()).unwrap(); 1605 | 1606 | let info = mock_info(VOTER3, &[]); 1607 | let process = ExecuteMsg::ProcessProposal { proposal_id: 1 }; 1608 | let res = execute(deps.as_mut(), mock_env(), info, process.clone()).unwrap(); 1609 | 1610 | assert_eq!(1, res.messages.len()); 1611 | } 1612 | 1613 | #[test] 1614 | fn test_execute_internal_update_admin_works() { 1615 | let mut deps = mock_dependencies(); 1616 | 1617 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1618 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1619 | 1620 | let info = mock_info(mock_env().contract.address.as_str(), &[]); 1621 | let msg = ExecuteMsg::InternalUpdateAdmin { 1622 | admin: Addr::unchecked("new_admin1"), 1623 | remove: false, 1624 | }; 1625 | execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); 1626 | ADMINS 1627 | .load(deps.as_ref().storage, &Addr::unchecked("new_admin1")) 1628 | .unwrap(); 1629 | assert_eq!(5, get_number_of_admins(deps.as_ref().storage)); 1630 | } 1631 | 1632 | #[test] 1633 | fn test_execute_internal_update_admin_remove_works() { 1634 | let mut deps = mock_dependencies(); 1635 | 1636 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1637 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1638 | 1639 | let info = mock_info(mock_env().contract.address.as_str(), &[]); 1640 | let msg = ExecuteMsg::InternalUpdateAdmin { 1641 | admin: Addr::unchecked(VOTER1), 1642 | remove: true, 1643 | }; 1644 | execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); 1645 | ADMINS 1646 | .load(deps.as_ref().storage, &Addr::unchecked(VOTER1)) 1647 | .unwrap_err(); 1648 | assert_eq!(3, get_number_of_admins(deps.as_ref().storage)); 1649 | } 1650 | 1651 | #[test] 1652 | fn test_execute_internal_update_admin_unauthorized() { 1653 | let mut deps = mock_dependencies(); 1654 | 1655 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1656 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1657 | 1658 | let info = mock_info(VOTER1, &[]); 1659 | let msg = ExecuteMsg::InternalUpdateAdmin { 1660 | admin: Addr::unchecked("new_admin1"), 1661 | remove: false, 1662 | }; 1663 | let err = execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap_err(); 1664 | assert_eq!(err, ContractError::Unauthorized {}); 1665 | ADMINS 1666 | .load(deps.as_ref().storage, &Addr::unchecked("new_admin1")) 1667 | .unwrap_err(); 1668 | assert_eq!(4, get_number_of_admins(deps.as_ref().storage)); 1669 | } 1670 | 1671 | #[test] 1672 | fn test_execute_update_op_works() { 1673 | let mut deps = mock_dependencies(); 1674 | 1675 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1676 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1677 | 1678 | let info = mock_info(VOTER1, &[]); 1679 | let msg = ExecuteMsg::UpdateOp { 1680 | op: Addr::unchecked("new_op1"), 1681 | remove: false, 1682 | }; 1683 | execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); 1684 | OPS.load(deps.as_ref().storage, &Addr::unchecked("new_op1")) 1685 | .unwrap(); 1686 | assert_eq!(3, get_number_of_ops(deps.as_ref().storage)); 1687 | } 1688 | 1689 | #[test] 1690 | fn test_execute_update_op_remove_works() { 1691 | let mut deps = mock_dependencies(); 1692 | 1693 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1694 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1695 | 1696 | let info = mock_info(VOTER1, &[]); 1697 | let msg = ExecuteMsg::UpdateOp { 1698 | op: Addr::unchecked(VOTER5), 1699 | remove: true, 1700 | }; 1701 | execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); 1702 | OPS.load(deps.as_ref().storage, &Addr::unchecked(VOTER5)) 1703 | .unwrap_err(); 1704 | assert_eq!(1, get_number_of_ops(deps.as_ref().storage)); 1705 | } 1706 | 1707 | #[test] 1708 | fn test_execute_update_op_unauthorized() { 1709 | let mut deps = mock_dependencies(); 1710 | 1711 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1712 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1713 | 1714 | let info = mock_info(VOTER5, &[]); 1715 | let msg = ExecuteMsg::UpdateOp { 1716 | op: Addr::unchecked("new_op1"), 1717 | remove: false, 1718 | }; 1719 | let err = execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap_err(); 1720 | assert_eq!(err, ContractError::Unauthorized {}); 1721 | OPS.load(deps.as_ref().storage, &Addr::unchecked("new_op1")) 1722 | .unwrap_err(); 1723 | assert_eq!(2, get_number_of_ops(deps.as_ref().storage)); 1724 | } 1725 | 1726 | #[test] 1727 | fn test_execute_internal_withdraw_locked_works() { 1728 | let mut deps = mock_dependencies(); 1729 | 1730 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1731 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1732 | 1733 | let info = mock_info(mock_env().contract.address.as_str(), &[]); 1734 | let proposal = ExecuteMsg::InternalWithdrawLocked { 1735 | dst: Addr::unchecked("destination"), 1736 | }; 1737 | let res = execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap(); 1738 | assert_eq!(1, res.messages.len()); 1739 | assert_eq!( 1740 | vec![] as Vec, 1741 | VESTING_AMOUNTS.load(deps.as_ref().storage).unwrap() 1742 | ); 1743 | assert_eq!( 1744 | vec![] as Vec, 1745 | VESTING_TIMESTAMPS.load(deps.as_ref().storage).unwrap() 1746 | ); 1747 | assert_eq!( 1748 | 48000000, 1749 | WITHDRAWN_LOCKED.load(deps.as_ref().storage).unwrap() 1750 | ); 1751 | } 1752 | 1753 | #[test] 1754 | fn test_execute_internal_withdraw_locked_unauthorized() { 1755 | let mut deps = mock_dependencies(); 1756 | 1757 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1758 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1759 | 1760 | let info = mock_info(VOTER1, &[]); 1761 | let proposal = ExecuteMsg::InternalWithdrawLocked { 1762 | dst: Addr::unchecked("destination"), 1763 | }; 1764 | let err = execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap_err(); 1765 | assert_eq!(ContractError::Unauthorized {}, err); 1766 | assert_eq!( 1767 | 37, 1768 | VESTING_AMOUNTS.load(deps.as_ref().storage).unwrap().len() 1769 | ); 1770 | assert_eq!( 1771 | 37, 1772 | VESTING_TIMESTAMPS 1773 | .load(deps.as_ref().storage) 1774 | .unwrap() 1775 | .len() 1776 | ); 1777 | assert_eq!(0, WITHDRAWN_LOCKED.load(deps.as_ref().storage).unwrap()); 1778 | } 1779 | 1780 | #[test] 1781 | fn test_query_proposals() { 1782 | let mut deps = mock_dependencies(); 1783 | PROPOSALS 1784 | .save( 1785 | deps.as_mut().storage, 1786 | 1, 1787 | &Proposal { 1788 | title: "title".to_string(), 1789 | description: "description".to_string(), 1790 | start_height: 1, 1791 | expires: Expiration::Never {}, 1792 | msgs: vec![], 1793 | status: Status::Open, 1794 | votes: Votes::yes(1), 1795 | threshold: Threshold::AbsolutePercentage { 1796 | percentage: Decimal::percent(75), 1797 | }, 1798 | total_weight: 4, 1799 | proposer: Addr::unchecked("proposer"), 1800 | deposit: None, 1801 | }, 1802 | ) 1803 | .unwrap(); 1804 | let msg = QueryMsg::ListProposals {}; 1805 | let bin = query(deps.as_ref(), mock_env(), msg).unwrap(); 1806 | let res: ProposalListResponse = from_binary(&bin).unwrap(); 1807 | assert_eq!( 1808 | res.proposals, 1809 | vec![ProposalResponse { 1810 | id: 1, 1811 | title: "title".to_string(), 1812 | description: "description".to_string(), 1813 | expires: Expiration::Never {}, 1814 | msgs: vec![], 1815 | status: Status::Open, 1816 | threshold: ThresholdResponse::AbsolutePercentage { 1817 | percentage: Decimal::percent(75), 1818 | total_weight: 4, 1819 | }, 1820 | proposer: Addr::unchecked("proposer"), 1821 | deposit: None, 1822 | }] 1823 | ); 1824 | } 1825 | 1826 | #[test] 1827 | fn test_query_votes() { 1828 | let mut deps = mock_dependencies(); 1829 | BALLOTS 1830 | .save( 1831 | deps.as_mut().storage, 1832 | (1, &Addr::unchecked("admin")), 1833 | &Ballot { 1834 | weight: 1, 1835 | vote: Vote::Yes, 1836 | }, 1837 | ) 1838 | .unwrap(); 1839 | BALLOTS 1840 | .save( 1841 | deps.as_mut().storage, 1842 | (2, &Addr::unchecked("admin")), 1843 | &Ballot { 1844 | weight: 1, 1845 | vote: Vote::No, 1846 | }, 1847 | ) 1848 | .unwrap(); 1849 | let msg = QueryMsg::ListVotes { proposal_id: 1 }; 1850 | let bin = query(deps.as_ref(), mock_env(), msg).unwrap(); 1851 | let res: VoteListResponse = from_binary(&bin).unwrap(); 1852 | assert_eq!( 1853 | res.votes, 1854 | vec![VoteInfo { 1855 | proposal_id: 1, 1856 | voter: "admin".to_string(), 1857 | vote: Vote::Yes, 1858 | weight: 1, 1859 | }] 1860 | ); 1861 | } 1862 | 1863 | #[test] 1864 | fn test_query_admins() { 1865 | let mut deps = mock_dependencies(); 1866 | 1867 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1868 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1869 | let msg = QueryMsg::ListAdmins {}; 1870 | let bin = query(deps.as_ref(), mock_env(), msg).unwrap(); 1871 | let res: AdminListResponse = from_binary(&bin).unwrap(); 1872 | assert_eq!( 1873 | res.admins, 1874 | vec![ 1875 | Addr::unchecked(VOTER1), 1876 | Addr::unchecked(VOTER2), 1877 | Addr::unchecked(VOTER3), 1878 | Addr::unchecked(VOTER4), 1879 | ] 1880 | ); 1881 | } 1882 | 1883 | #[test] 1884 | fn test_query_ops() { 1885 | let mut deps = mock_dependencies(); 1886 | 1887 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1888 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1889 | let msg = QueryMsg::ListOps {}; 1890 | let bin = query(deps.as_ref(), mock_env(), msg).unwrap(); 1891 | let res: OpListResponse = from_binary(&bin).unwrap(); 1892 | assert_eq!( 1893 | res.ops, 1894 | vec![Addr::unchecked(VOTER5), Addr::unchecked(VOTER6),] 1895 | ); 1896 | } 1897 | 1898 | #[test] 1899 | fn test_query_info() { 1900 | let mut deps = mock_dependencies(); 1901 | 1902 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1903 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1904 | let msg = QueryMsg::Info {}; 1905 | let bin = query(deps.as_ref(), mock_env(), msg).unwrap(); 1906 | let res: ShowInfoResponse = from_binary(&bin).unwrap(); 1907 | assert_eq!( 1908 | res, 1909 | ShowInfoResponse { 1910 | denom: "usei".to_string(), 1911 | vesting_timestamps: VESTING_TIMESTAMPS.load(deps.as_ref().storage).unwrap(), 1912 | vesting_amounts: VESTING_AMOUNTS.load(deps.as_ref().storage).unwrap(), 1913 | unlock_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 1914 | staking_reward_address: Addr::unchecked(REWARD_ADDR1), 1915 | withdrawn_staking_rewards: 0, 1916 | withdrawn_locked: 0, 1917 | withdrawn_unlocked: 0, 1918 | } 1919 | ); 1920 | } 1921 | 1922 | #[test] 1923 | fn test_query_config() { 1924 | let mut deps = mock_dependencies(); 1925 | 1926 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1927 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1928 | let msg = QueryMsg::Config {}; 1929 | let bin = query(deps.as_ref(), mock_env(), msg).unwrap(); 1930 | let res: ShowConfigResponse = from_binary(&bin).unwrap(); 1931 | assert_eq!( 1932 | res, 1933 | ShowConfigResponse { 1934 | max_voting_period: Duration::Time(3600), 1935 | admin_voting_threshold: Threshold::AbsolutePercentage { 1936 | percentage: Decimal::percent(75) 1937 | }, 1938 | } 1939 | ); 1940 | } 1941 | 1942 | #[test] 1943 | fn test_query_total_vested_amount() { 1944 | let mut deps = mock_dependencies(); 1945 | 1946 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1947 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1948 | let msg = QueryMsg::TotalVested {}; 1949 | let vesting_timestamps = VESTING_TIMESTAMPS.load(deps.as_ref().storage); 1950 | let mut env = mock_env(); 1951 | env.block.time = *(vesting_timestamps.unwrap().first().unwrap()); 1952 | let bin = query(deps.as_ref(), env, msg).unwrap(); 1953 | let res: ShowTotalVestedResponse = from_binary(&bin).unwrap(); 1954 | assert_eq!(res.vested_amount, 12000000); 1955 | } 1956 | 1957 | #[test] 1958 | fn test_migration() { 1959 | let mut deps = mock_dependencies(); 1960 | 1961 | let info = mock_info(OWNER, &[Coin::new(48000000, "usei".to_string())]); 1962 | setup_test_case(deps.as_mut(), info.clone()).unwrap(); 1963 | 1964 | // test incorrect contract name to assert error 1965 | cw2::set_contract_version(&mut deps.storage, "this_is_the_wrong_contract", "0.0.1") 1966 | .unwrap(); 1967 | let res = migrate(deps.as_mut(), mock_env(), MigrateMsg {}); 1968 | match res { 1969 | Err(ContractError::Std(x)) => { 1970 | assert_eq!(x, StdError::generic_err("Can only upgrade from same type")) 1971 | } 1972 | _ => panic!("This should raise error on contract type mismatch"), 1973 | }; 1974 | 1975 | // set contract version to older one so we can test migrations 1976 | cw2::set_contract_version(&mut deps.storage, CONTRACT_NAME, "0.0.1").unwrap(); 1977 | 1978 | let res = migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap(); 1979 | assert_eq!(res, Response::default(),); 1980 | 1981 | // This should raise an error on curr version >= proposed version 1982 | let res = migrate(deps.as_mut(), mock_env(), MigrateMsg {}); 1983 | match res { 1984 | Err(ContractError::Std(x)) => assert_eq!( 1985 | x, 1986 | StdError::generic_err("Cannot upgrade from a newer version") 1987 | ), 1988 | _ => panic!("This should raise error on version validation failure"), 1989 | }; 1990 | } 1991 | } 1992 | -------------------------------------------------------------------------------- /src/data_structure.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{Addr, Coin, Env, Timestamp}; 3 | 4 | use crate::ContractError; 5 | 6 | const HUNDRED_YEARS_IN_SECONDS: u64 = 100 * 365 * 24 * 60 * 60; 7 | 8 | #[cw_serde] 9 | pub struct EmptyStruct {} 10 | 11 | #[cw_serde] 12 | pub struct Tranche { 13 | pub denom: String, 14 | pub vesting_timestamps: Vec, 15 | pub vesting_amounts: Vec, 16 | pub unlocked_token_distribution_address: Addr, 17 | pub staking_reward_distribution_address: Addr, 18 | } 19 | 20 | impl Tranche { 21 | pub fn validate(&self, env: Env, funds: Vec) -> Result<(), ContractError> { 22 | if self.vesting_amounts.len() != self.vesting_timestamps.len() { 23 | return Err(ContractError::InvalidTranche( 24 | "mismatched vesting amounts and schedule".to_string(), 25 | )); 26 | } 27 | if self.vesting_amounts.is_empty() { 28 | return Err(ContractError::InvalidTranche("nothing to vest".to_string())); 29 | } 30 | let mut total_vesting_amount = 0u128; 31 | for amount in self.vesting_amounts.iter() { 32 | if *amount == 0 { 33 | return Err(ContractError::InvalidTranche( 34 | "zero vesting amount is not allowed".to_string(), 35 | )); 36 | } 37 | total_vesting_amount += *amount; 38 | } 39 | let mut deposited_amount = 0u128; 40 | for fund in funds.iter() { 41 | if fund.denom == self.denom { 42 | deposited_amount += fund.amount.u128(); 43 | } 44 | } 45 | if total_vesting_amount > deposited_amount { 46 | return Err(ContractError::InvalidTranche( 47 | "insufficient deposit for the vesting plan".to_string(), 48 | )); 49 | } 50 | self.validate_timestamps(env)?; 51 | 52 | Ok(()) 53 | } 54 | 55 | pub fn validate_timestamps(&self, env: Env) -> Result<(), ContractError> { 56 | let mut last_ts_nanos = Timestamp::from_seconds(0).nanos(); 57 | for ts in &self.vesting_timestamps { 58 | let ts_nanos = ts.nanos(); 59 | if ts_nanos <= last_ts_nanos { 60 | return Err(ContractError::InvalidTranche( 61 | "vesting schedule must be monotonic increasing".to_string(), 62 | )); 63 | } 64 | 65 | // Check if the nanoseconds are at least current 66 | if ts_nanos < env.block.time.nanos() { 67 | return Err(ContractError::InvalidTranche( 68 | "Timestamp nanoseconds are out of range".to_string(), 69 | )); 70 | } 71 | 72 | // ts should not be too far in the future (e.g. example not more than 100 years) 73 | if ts.seconds() > env.block.time.seconds() + HUNDRED_YEARS_IN_SECONDS { 74 | return Err(ContractError::InvalidTranche( 75 | "Timestamp is too far in the future".to_string(), 76 | )); 77 | } 78 | last_ts_nanos = ts_nanos 79 | } 80 | 81 | Ok(()) 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use super::*; 88 | use cosmwasm_std::{testing::mock_env, Uint128}; 89 | 90 | const UNLOCK_ADDR1: &str = "unlock0001"; 91 | 92 | #[test] 93 | fn test_validate_success() { 94 | let env = mock_env(); 95 | let tranche = Tranche { 96 | vesting_amounts: vec![100, 200, 300], 97 | unlocked_token_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 98 | vesting_timestamps: vec![ 99 | Timestamp::from_seconds(1).plus_nanos(env.block.time.nanos()), 100 | Timestamp::from_seconds(2).plus_nanos(env.block.time.nanos()), 101 | Timestamp::from_seconds(3).plus_nanos(env.block.time.nanos()), 102 | ], 103 | denom: "token".to_string(), 104 | staking_reward_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 105 | }; 106 | let funds = vec![Coin { 107 | denom: "token".to_string(), 108 | amount: Uint128::from(600u128), 109 | }]; 110 | assert!(tranche.validate(env, funds).is_ok()); 111 | } 112 | 113 | #[test] 114 | fn test_validate_mismatched_amount_timestamp_lengths() { 115 | let env = mock_env(); 116 | let tranche = Tranche { 117 | vesting_amounts: vec![100, 200], 118 | unlocked_token_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 119 | vesting_timestamps: vec![ 120 | Timestamp::from_seconds(1).plus_nanos(env.block.time.nanos()), 121 | Timestamp::from_seconds(2).plus_nanos(env.block.time.nanos()), 122 | Timestamp::from_seconds(3).plus_nanos(env.block.time.nanos()), 123 | ], 124 | denom: "token".to_string(), 125 | staking_reward_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 126 | }; 127 | let funds = vec![]; 128 | assert!(matches!( 129 | tranche.validate(env, funds), 130 | Err(ContractError::InvalidTranche(msg)) if msg.contains("mismatched vesting amounts and schedule") 131 | )); 132 | } 133 | 134 | #[test] 135 | fn test_validate_empty_amounts_and_timestamps() { 136 | let env = mock_env(); 137 | let tranche = Tranche { 138 | vesting_amounts: vec![], 139 | unlocked_token_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 140 | vesting_timestamps: vec![], 141 | denom: "token".to_string(), 142 | staking_reward_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 143 | }; 144 | let funds = vec![]; 145 | assert!(matches!( 146 | tranche.validate(env, funds), 147 | Err(ContractError::InvalidTranche(msg)) if msg.contains("nothing to vest") 148 | )); 149 | } 150 | 151 | #[test] 152 | fn test_validate_zero_vesting_amount() { 153 | let env = mock_env(); 154 | let tranche = Tranche { 155 | vesting_amounts: vec![0, 100], 156 | unlocked_token_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 157 | vesting_timestamps: vec![ 158 | Timestamp::from_seconds(1).plus_nanos(env.block.time.nanos()), 159 | Timestamp::from_seconds(2).plus_nanos(env.block.time.nanos()), 160 | ], 161 | denom: "token".to_string(), 162 | staking_reward_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 163 | }; 164 | let funds = vec![Coin { 165 | denom: "token".to_string(), 166 | amount: Uint128::new(100), 167 | }]; 168 | let result = tranche.validate(env, funds); 169 | assert!(matches!( 170 | result, 171 | Err(ContractError::InvalidTranche(msg)) if msg.contains("zero vesting amount is not allowed") 172 | )); 173 | } 174 | 175 | #[test] 176 | fn test_validate_insufficient_deposit() { 177 | let env = mock_env(); 178 | let tranche = Tranche { 179 | vesting_amounts: vec![200, 200], 180 | unlocked_token_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 181 | vesting_timestamps: vec![ 182 | Timestamp::from_seconds(1).plus_nanos(env.block.time.nanos()), 183 | Timestamp::from_seconds(2).plus_nanos(env.block.time.nanos()), 184 | ], 185 | denom: "token".to_string(), 186 | staking_reward_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 187 | }; 188 | let funds = vec![Coin { 189 | denom: "token".to_string(), 190 | amount: Uint128::new(300), 191 | }]; 192 | let result = tranche.validate(env, funds); 193 | assert!(matches!( 194 | result, 195 | Err(ContractError::InvalidTranche(msg)) if msg.contains("insufficient deposit for the vesting plan") 196 | )); 197 | } 198 | 199 | #[test] 200 | fn test_validate_non_monotonic_vesting_timestamps() { 201 | let env = mock_env(); 202 | let tranche = Tranche { 203 | vesting_amounts: vec![100, 100], 204 | unlocked_token_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 205 | vesting_timestamps: vec![ 206 | Timestamp::from_seconds(2).plus_nanos(env.block.time.nanos()), 207 | Timestamp::from_seconds(1).plus_nanos(env.block.time.nanos()), 208 | ], 209 | denom: "token".to_string(), 210 | staking_reward_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 211 | }; 212 | let funds = vec![Coin { 213 | denom: "token".to_string(), 214 | amount: Uint128::new(200), 215 | }]; 216 | let result = tranche.validate(env, funds); 217 | assert!(matches!( 218 | result, 219 | Err(ContractError::InvalidTranche(msg)) if msg.contains("vesting schedule must be monotonic increasing") 220 | )); 221 | } 222 | 223 | #[test] 224 | fn test_validate_timestamps_too_early() { 225 | let env = mock_env(); 226 | let tranche = Tranche { 227 | vesting_amounts: vec![100], 228 | unlocked_token_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 229 | vesting_timestamps: vec![Timestamp::from_seconds(2) 230 | .plus_nanos(env.block.time.nanos()) 231 | .minus_seconds(3)], 232 | denom: "token".to_string(), 233 | staking_reward_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 234 | }; 235 | let funds = vec![Coin { 236 | denom: "token".to_string(), 237 | amount: Uint128::new(200), 238 | }]; 239 | let result = tranche.validate(env, funds); 240 | assert!(matches!( 241 | result, 242 | Err(ContractError::InvalidTranche(msg)) if msg.contains("Timestamp nanoseconds are out of range") 243 | )); 244 | } 245 | 246 | #[test] 247 | fn test_validate_timestamps_too_late() { 248 | let env = mock_env(); 249 | let tranche = Tranche { 250 | vesting_amounts: vec![100], 251 | unlocked_token_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 252 | vesting_timestamps: vec![Timestamp::from_seconds(HUNDRED_YEARS_IN_SECONDS + 1) 253 | .plus_nanos(env.block.time.nanos())], 254 | denom: "token".to_string(), 255 | staking_reward_distribution_address: Addr::unchecked(UNLOCK_ADDR1), 256 | }; 257 | let funds = vec![Coin { 258 | denom: "token".to_string(), 259 | amount: Uint128::new(200), 260 | }]; 261 | let result = tranche.validate(env, funds); 262 | assert!(matches!( 263 | result, 264 | Err(ContractError::InvalidTranche(msg)) if msg.contains("Timestamp is too far in the future") 265 | )); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::StdError; 2 | use cw_utils::ThresholdError; 3 | 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug, PartialEq)] 7 | pub enum ContractError { 8 | #[error("{0}")] 9 | Std(#[from] StdError), 10 | 11 | #[error("{0}")] 12 | Threshold(#[from] ThresholdError), 13 | 14 | #[error("Required weight cannot be zero")] 15 | ZeroWeight {}, 16 | 17 | #[error("Not possible to reach required (passing) weight")] 18 | UnreachableWeight {}, 19 | 20 | #[error("No voters")] 21 | NoVoters {}, 22 | 23 | #[error("Unauthorized")] 24 | Unauthorized {}, 25 | 26 | #[error("Proposal is not open")] 27 | NotOpen {}, 28 | 29 | #[error("Proposal voting period has expired")] 30 | Expired {}, 31 | 32 | #[error("Proposal must expire before you can close it")] 33 | NotExpired {}, 34 | 35 | #[error("Wrong expiration option")] 36 | WrongExpiration {}, 37 | 38 | #[error("Already voted on this proposal")] 39 | AlreadyVoted {}, 40 | 41 | #[error("Proposal must have passed and not yet been executed")] 42 | WrongExecuteStatus {}, 43 | 44 | #[error("Cannot close completed or passed proposals")] 45 | WrongCloseStatus {}, 46 | 47 | #[error("No admins")] 48 | NoAdmins {}, 49 | 50 | #[error("No operators")] 51 | NoOps {}, 52 | 53 | #[error("Invalid tranche: {0}")] 54 | InvalidTranche(String), 55 | 56 | #[error("No sufficient delegation rewards")] 57 | NoSufficientDelegationReward {}, 58 | 59 | #[error("Semver parsing error: {0}")] 60 | SemVer(String), 61 | 62 | #[error("No sufficient vested amount")] 63 | NoSufficientUnlockedTokens {}, 64 | } 65 | 66 | impl From for ContractError { 67 | fn from(err: semver::Error) -> Self { 68 | Self::SemVer(err.to_string()) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod contract; 2 | pub mod data_structure; 3 | mod error; 4 | pub mod msg; 5 | pub mod permission; 6 | pub mod staking; 7 | pub mod state; 8 | pub mod vesting; 9 | 10 | pub use crate::error::ContractError; 11 | -------------------------------------------------------------------------------- /src/msg.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::{cw_serde, QueryResponses}; 2 | use cosmwasm_std::{Addr, CustomQuery, Timestamp, Uint128, VoteOption}; 3 | use cw_utils::{Duration, Threshold}; 4 | 5 | use crate::data_structure::Tranche; 6 | 7 | #[cw_serde] 8 | pub struct MigrateMsg {} 9 | 10 | #[cw_serde] 11 | pub struct InstantiateMsg { 12 | pub admins: Vec, 13 | pub ops: Vec, 14 | pub tranche: Tranche, 15 | pub max_voting_period: Duration, 16 | pub admin_voting_threshold_percentage: u8, 17 | } 18 | 19 | #[cw_serde] 20 | pub enum ExecuteMsg { 21 | Delegate { 22 | validator: String, 23 | amount: u128, 24 | }, 25 | Redelegate { 26 | src_validator: String, 27 | dst_validator: String, 28 | amount: u128, 29 | }, 30 | Undelegate { 31 | validator: String, 32 | amount: u128, 33 | }, 34 | InitiateWithdrawUnlocked { 35 | amount: u128, 36 | }, 37 | InitiateWithdrawReward {}, 38 | UpdateOp { 39 | op: Addr, 40 | remove: bool, 41 | }, 42 | ProposeEmergencyWithdraw { 43 | dst: Addr, 44 | }, 45 | ProposeUpdateAdmin { 46 | admin: Addr, 47 | remove: bool, 48 | }, 49 | ProposeUpdateUnlockedDistributionAddress { 50 | unlocked_distribution_address: Addr, 51 | }, 52 | ProposeUpdateStakingRewardDistributionAddress { 53 | staking_reward_distribution_address: Addr, 54 | }, 55 | ProposeGovVote { 56 | gov_proposal_id: u64, 57 | gov_vote: VoteOption, 58 | }, 59 | VoteProposal { 60 | proposal_id: u64, 61 | }, 62 | ProcessProposal { 63 | proposal_id: u64, 64 | }, 65 | InternalUpdateAdmin { 66 | admin: Addr, 67 | remove: bool, 68 | }, 69 | InternalUpdateUnlockedDistributionAddress { 70 | unlocked_distribution_address: Addr, 71 | }, 72 | InternalUpdateStakingRewardDistributionAddress { 73 | staking_reward_distribution_address: Addr, 74 | }, 75 | InternalWithdrawLocked { 76 | dst: Addr, 77 | }, 78 | } 79 | 80 | #[cw_serde] 81 | #[derive(QueryResponses)] 82 | pub enum QueryMsg { 83 | #[returns(cw3::ProposalListResponse)] 84 | ListProposals {}, 85 | #[returns(cw3::VoteListResponse)] 86 | ListVotes { proposal_id: u64 }, 87 | #[returns(AdminListResponse)] 88 | ListAdmins {}, 89 | #[returns(OpListResponse)] 90 | ListOps {}, 91 | #[returns(ShowInfoResponse)] 92 | Info {}, 93 | #[returns(ShowConfigResponse)] 94 | Config {}, 95 | #[returns(ShowTotalVestedResponse)] 96 | TotalVested {}, 97 | } 98 | 99 | #[cw_serde] 100 | pub struct AdminListResponse { 101 | pub admins: Vec, 102 | } 103 | 104 | #[cw_serde] 105 | pub struct OpListResponse { 106 | pub ops: Vec, 107 | } 108 | 109 | #[cw_serde] 110 | pub struct ShowInfoResponse { 111 | pub denom: String, 112 | pub vesting_timestamps: Vec, 113 | pub vesting_amounts: Vec, 114 | pub unlock_distribution_address: Addr, 115 | pub staking_reward_address: Addr, 116 | pub withdrawn_staking_rewards: u128, 117 | pub withdrawn_unlocked: u128, 118 | pub withdrawn_locked: u128, 119 | } 120 | 121 | #[cw_serde] 122 | pub struct ShowConfigResponse { 123 | pub max_voting_period: Duration, 124 | pub admin_voting_threshold: Threshold, 125 | } 126 | 127 | #[cw_serde] 128 | pub struct ShowTotalVestedResponse { 129 | pub vested_amount: u128, 130 | } 131 | 132 | #[cw_serde] 133 | pub struct SeiQueryWrapper { 134 | pub route: SeiRoute, 135 | pub query_data: SeiQuery, 136 | } 137 | 138 | impl CustomQuery for SeiQueryWrapper {} 139 | 140 | #[cw_serde] 141 | pub enum SeiRoute { 142 | Stakingext, 143 | } 144 | 145 | #[cw_serde] 146 | pub enum SeiQuery { 147 | UnbondingDelegations { delegator: String }, 148 | } 149 | 150 | #[cw_serde] 151 | pub struct UnbondingDelegationEntry { 152 | pub creation_height: i64, 153 | pub completion_time: String, 154 | pub initial_balance: Uint128, 155 | pub balance: Uint128, 156 | } 157 | 158 | #[cw_serde] 159 | pub struct UnbondingDelegationsResponse { 160 | pub entries: Vec, 161 | } 162 | -------------------------------------------------------------------------------- /src/permission.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{Addr, Env, MessageInfo, Storage}; 2 | 3 | use crate::{ 4 | state::{ADMINS, OPS}, 5 | ContractError, 6 | }; 7 | 8 | pub fn authorize_op(store: &dyn Storage, caller: Addr) -> Result<(), ContractError> { 9 | match OPS.load(store, &caller) { 10 | Ok(_) => Ok(()), 11 | Err(_) => Err(ContractError::Unauthorized {}), 12 | } 13 | } 14 | 15 | pub fn authorize_admin(store: &dyn Storage, caller: Addr) -> Result<(), ContractError> { 16 | match ADMINS.load(store, &caller) { 17 | Ok(_) => Ok(()), 18 | Err(_) => Err(ContractError::Unauthorized {}), 19 | } 20 | } 21 | 22 | pub fn authorize_self_call(env: Env, info: MessageInfo) -> Result<(), ContractError> { 23 | if env.contract.address != info.sender { 24 | return Err(ContractError::Unauthorized {}); 25 | } 26 | Ok(()) 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use cosmwasm_std::testing::{mock_env, mock_info}; 32 | use cosmwasm_std::{testing::mock_dependencies, Addr}; 33 | 34 | use super::*; 35 | 36 | use crate::data_structure::EmptyStruct; 37 | use crate::state::OPS; 38 | 39 | const GOOD_OP: &str = "good_op"; 40 | const BAD_OP: &str = "bad_op"; 41 | const GOOD_ADMIN: &str = "good_admin"; 42 | const BAD_ADMIN: &str = "bad_admin"; 43 | 44 | #[test] 45 | fn test_authorize_op() { 46 | let mut deps = mock_dependencies(); 47 | let deps_mut = deps.as_mut(); 48 | OPS.save(deps_mut.storage, &Addr::unchecked(GOOD_OP), &EmptyStruct {}) 49 | .unwrap(); 50 | 51 | authorize_op(deps.as_ref().storage, Addr::unchecked(GOOD_OP)).unwrap(); 52 | authorize_op(deps.as_ref().storage, Addr::unchecked(BAD_OP)).unwrap_err(); 53 | } 54 | 55 | #[test] 56 | fn test_authorize_admin() { 57 | let mut deps = mock_dependencies(); 58 | let deps_mut = deps.as_mut(); 59 | ADMINS 60 | .save( 61 | deps_mut.storage, 62 | &Addr::unchecked(GOOD_ADMIN), 63 | &EmptyStruct {}, 64 | ) 65 | .unwrap(); 66 | 67 | authorize_admin(deps.as_ref().storage, Addr::unchecked(GOOD_ADMIN)).unwrap(); 68 | authorize_admin(deps.as_ref().storage, Addr::unchecked(BAD_ADMIN)).unwrap_err(); 69 | } 70 | 71 | #[test] 72 | fn test_authorize_self_call() { 73 | let env = mock_env(); 74 | let info1 = mock_info(&env.contract.address.to_string(), &[]); 75 | let info2 = mock_info("someone", &[]); 76 | 77 | authorize_self_call(env.clone(), info1).unwrap(); 78 | authorize_self_call(env.clone(), info2).unwrap_err(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/staking.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{ 2 | coins, BankMsg, Coin, Deps, DistributionMsg, Env, QueryRequest, Response, StakingMsg, 3 | StdResult, Uint128, 4 | }; 5 | use serde::Deserialize; 6 | 7 | use crate::{ 8 | msg::{SeiQuery, SeiQueryWrapper, SeiRoute, UnbondingDelegationsResponse}, 9 | state::{DENOM, STAKING_REWARD_ADDRESS}, 10 | ContractError, 11 | }; 12 | 13 | pub fn delegate(response: Response, validator: String, amount: u128, denom: String) -> Response { 14 | let msg = StakingMsg::Delegate { 15 | validator, 16 | amount: Coin::new(amount.into(), denom), 17 | }; 18 | response.add_message(msg) 19 | } 20 | 21 | pub fn redelegate( 22 | response: Response, 23 | src_validator: String, 24 | dst_validator: String, 25 | amount: u128, 26 | denom: String, 27 | ) -> Response { 28 | let msg = StakingMsg::Redelegate { 29 | src_validator, 30 | dst_validator, 31 | amount: Coin::new(amount.into(), denom), 32 | }; 33 | response.add_message(msg) 34 | } 35 | 36 | pub fn undelegate(response: Response, validator: String, amount: u128, denom: String) -> Response { 37 | let msg = StakingMsg::Undelegate { 38 | validator, 39 | amount: Coin::new(amount.into(), denom), 40 | }; 41 | response.add_message(msg) 42 | } 43 | 44 | pub fn withdraw_delegation_rewards( 45 | deps: Deps, 46 | response: Response, 47 | validator: String, 48 | amount: u128, 49 | ) -> Result { 50 | let msg = DistributionMsg::WithdrawDelegatorReward { 51 | validator: validator, 52 | }; 53 | let denom = DENOM.load(deps.storage)?; 54 | let to_address = STAKING_REWARD_ADDRESS.load(deps.storage)?; 55 | let send_msg = BankMsg::Send { 56 | to_address: to_address.to_string(), 57 | amount: coins(amount, denom), 58 | }; 59 | let mut new_response = response.add_message(msg); 60 | new_response = new_response.add_message(send_msg); 61 | Ok(new_response) 62 | } 63 | 64 | // the `all_delegations` endpoint do not return full delegation info (i.e. no withdrawable delegation reward) 65 | // so we only return validators here for subsequent logic to query full delegation info one validator at a time 66 | pub fn get_all_delegated_validators( 67 | deps: Deps, 68 | env: Env, 69 | ) -> Result, ContractError> { 70 | Ok(deps 71 | .querier 72 | .query_all_delegations(env.contract.address.to_string()) 73 | .map(|delegations| -> Vec { 74 | delegations 75 | .iter() 76 | .map(|delegation| -> String { delegation.validator.clone() }) 77 | .collect() 78 | })?) 79 | } 80 | 81 | pub fn get_unbonding_balance(deps: Deps, env: Env) -> StdResult { 82 | let request = SeiQueryWrapper { 83 | route: SeiRoute::Stakingext, 84 | query_data: SeiQuery::UnbondingDelegations { 85 | delegator: env.contract.address.to_string(), 86 | }, 87 | }; 88 | let wrapped_request = QueryRequest::Custom(request); 89 | let response: UnbondingDelegationsResponse = deps.querier.query(&wrapped_request)?; 90 | Ok(response 91 | .entries 92 | .iter() 93 | .map(|entry| -> u128 { entry.balance.u128() }) 94 | .sum()) 95 | } 96 | 97 | pub fn get_delegation_rewards( 98 | deps: Deps, 99 | env: Env, 100 | validator: String, 101 | ) -> Result { 102 | let delegation = deps 103 | .querier 104 | .query_delegation(env.contract.address.to_string(), validator)?; 105 | if delegation.is_none() { 106 | return Ok(0); 107 | } 108 | let denom = DENOM.load(deps.storage)?; 109 | let mut reward_amount = 0u128; 110 | for reward in delegation.unwrap().accumulated_rewards.iter() { 111 | if reward.denom == denom { 112 | reward_amount += reward.amount.u128(); 113 | } 114 | } 115 | Ok(reward_amount) 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use core::marker::PhantomData; 121 | use cosmwasm_std::{ 122 | testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}, 123 | Addr, Coin, Decimal, FullDelegation, OwnedDeps, Validator, 124 | }; 125 | 126 | use crate::msg::SeiQueryWrapper; 127 | use crate::state::DENOM; 128 | 129 | use super::get_delegation_rewards; 130 | 131 | const VALIDATOR: &str = "val"; 132 | const DELEGATOR: &str = "del"; 133 | 134 | fn mock_dependencies() -> OwnedDeps { 135 | OwnedDeps { 136 | storage: MockStorage::default(), 137 | api: MockApi::default(), 138 | querier: MockQuerier::default(), 139 | custom_query_type: PhantomData::default(), 140 | } 141 | } 142 | 143 | #[test] 144 | fn test_get_delegation_rewards_empty() { 145 | let mut deps = mock_dependencies(); 146 | let mut env = mock_env(); 147 | env.contract.address = Addr::unchecked(DELEGATOR); 148 | DENOM 149 | .save(deps.as_mut().storage, &"usei".to_string()) 150 | .unwrap(); 151 | 152 | let result = get_delegation_rewards(deps.as_ref(), env, VALIDATOR.to_string()).unwrap(); 153 | assert_eq!(0u128, result); 154 | } 155 | 156 | #[test] 157 | fn test_get_delegation_rewards() { 158 | let mut deps = mock_dependencies(); 159 | deps.querier.update_staking( 160 | "usei", 161 | &[Validator { 162 | address: VALIDATOR.to_string(), 163 | commission: Decimal::zero(), 164 | max_commission: Decimal::zero(), 165 | max_change_rate: Decimal::zero(), 166 | }], 167 | &[FullDelegation { 168 | delegator: Addr::unchecked(DELEGATOR), 169 | validator: VALIDATOR.to_string(), 170 | amount: Coin::new(1000000, "usei"), 171 | can_redelegate: Coin::new(0, "usei"), 172 | accumulated_rewards: vec![Coin::new(10, "usei"), Coin::new(20, "usei")], 173 | }], 174 | ); 175 | let mut env = mock_env(); 176 | env.contract.address = Addr::unchecked(DELEGATOR); 177 | DENOM 178 | .save(deps.as_mut().storage, &"usei".to_string()) 179 | .unwrap(); 180 | 181 | let result = get_delegation_rewards(deps.as_ref(), env, VALIDATOR.to_string()).unwrap(); 182 | assert_eq!(30u128, result); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{Addr, StdResult, Storage, Timestamp}; 2 | use cw3::{Ballot, Proposal}; 3 | use cw_storage_plus::{Item, Map}; 4 | use cw_utils::{Duration, Threshold}; 5 | 6 | use crate::data_structure::EmptyStruct; 7 | 8 | pub const DENOM: Item = Item::new("denom"); 9 | pub const VESTING_TIMESTAMPS: Item> = Item::new("ts"); 10 | pub const VESTING_AMOUNTS: Item> = Item::new("amounts"); 11 | pub const TOTAL_AMOUNT: Item = Item::new("ttam"); 12 | pub const UNLOCK_DISTRIBUTION_ADDRESS: Item = Item::new("uda"); 13 | pub const STAKING_REWARD_ADDRESS: Item = Item::new("sra"); 14 | pub const WITHDRAWN_STAKING_REWARDS: Item = Item::new("wsr"); 15 | pub const WITHDRAWN_UNLOCKED: Item = Item::new("wu"); 16 | pub const WITHDRAWN_LOCKED: Item = Item::new("wl"); 17 | 18 | pub const ADMINS: Map<&Addr, EmptyStruct> = Map::new("admins"); 19 | pub const OPS: Map<&Addr, EmptyStruct> = Map::new("ops"); 20 | 21 | pub fn get_number_of_admins(store: &dyn Storage) -> usize { 22 | ADMINS 23 | .keys( 24 | store, 25 | Option::None, 26 | Option::None, 27 | cosmwasm_std::Order::Ascending, 28 | ) 29 | .count() 30 | } 31 | 32 | pub fn get_number_of_ops(store: &dyn Storage) -> usize { 33 | OPS.keys( 34 | store, 35 | Option::None, 36 | Option::None, 37 | cosmwasm_std::Order::Ascending, 38 | ) 39 | .count() 40 | } 41 | 42 | // ADMIN STATES 43 | pub const MAX_VOTING_PERIOD: Item = Item::new("max_voting_period"); 44 | pub const ADMIN_VOTING_THRESHOLD: Item = Item::new("threshold"); 45 | 46 | pub const PROPOSAL_COUNT: Item = Item::new("proposal_count"); 47 | pub const BALLOTS: Map<(u64, &Addr), Ballot> = Map::new("votes"); 48 | pub const PROPOSALS: Map = Map::new("proposals"); 49 | 50 | pub fn next_proposal_id(store: &mut dyn Storage) -> StdResult { 51 | let id: u64 = PROPOSAL_COUNT.may_load(store)?.unwrap_or_default() + 1; 52 | PROPOSAL_COUNT.save(store, &id)?; 53 | Ok(id) 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use cosmwasm_std::{testing::mock_dependencies, Addr}; 59 | 60 | use crate::{ 61 | data_structure::EmptyStruct, 62 | state::{get_number_of_admins, get_number_of_ops, ADMINS, OPS}, 63 | }; 64 | 65 | #[test] 66 | fn test_get_number_of_admins() { 67 | let mut deps = mock_dependencies(); 68 | assert_eq!(0, get_number_of_admins(deps.as_ref().storage)); 69 | 70 | ADMINS 71 | .save( 72 | deps.as_mut().storage, 73 | &Addr::unchecked("admin"), 74 | &EmptyStruct {}, 75 | ) 76 | .unwrap(); 77 | assert_eq!(1, get_number_of_admins(deps.as_ref().storage)); 78 | ADMINS 79 | .save( 80 | deps.as_mut().storage, 81 | &Addr::unchecked("admin2"), 82 | &EmptyStruct {}, 83 | ) 84 | .unwrap(); 85 | assert_eq!(2, get_number_of_admins(deps.as_ref().storage)); 86 | } 87 | 88 | #[test] 89 | fn test_get_number_of_ops() { 90 | let mut deps = mock_dependencies(); 91 | assert_eq!(0, get_number_of_ops(deps.as_ref().storage)); 92 | 93 | OPS.save( 94 | deps.as_mut().storage, 95 | &Addr::unchecked("op"), 96 | &EmptyStruct {}, 97 | ) 98 | .unwrap(); 99 | assert_eq!(1, get_number_of_ops(deps.as_ref().storage)); 100 | OPS.save( 101 | deps.as_mut().storage, 102 | &Addr::unchecked("op2"), 103 | &EmptyStruct {}, 104 | ) 105 | .unwrap(); 106 | assert_eq!(2, get_number_of_ops(deps.as_ref().storage)); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/vesting.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{coins, BankMsg, Response, StdResult, Storage, Timestamp}; 2 | 3 | use crate::{ 4 | state::{DENOM, UNLOCK_DISTRIBUTION_ADDRESS, VESTING_AMOUNTS, VESTING_TIMESTAMPS}, 5 | ContractError, 6 | }; 7 | 8 | pub fn collect_vested( 9 | storage: &mut dyn Storage, 10 | now: Timestamp, 11 | requested_amount: u128, 12 | ) -> Result { 13 | let vesting_ts = VESTING_TIMESTAMPS.load(storage)?; 14 | let vesting_amounts = VESTING_AMOUNTS.load(storage)?; 15 | let mut vested_amount = 0u128; 16 | let mut amount_to_subtract = 0u128; 17 | let mut remaining_first_idx: usize = 0; 18 | for (i, (ts, amount)) in vesting_ts.iter().zip(vesting_amounts.iter()).enumerate() { 19 | if *ts > now { 20 | break; 21 | } 22 | vested_amount += *amount; 23 | if vested_amount >= requested_amount { 24 | amount_to_subtract = *amount + requested_amount - vested_amount; 25 | if vested_amount == requested_amount { 26 | remaining_first_idx += 1; 27 | amount_to_subtract = 0; 28 | } 29 | vested_amount = requested_amount; 30 | break; 31 | } 32 | remaining_first_idx = i + 1; 33 | } 34 | if vested_amount < requested_amount { 35 | return Err(ContractError::NoSufficientUnlockedTokens {}); 36 | } 37 | if remaining_first_idx >= vesting_amounts.len() { 38 | VESTING_AMOUNTS.save(storage, &vec![])?; 39 | VESTING_TIMESTAMPS.save(storage, &vec![])?; 40 | } else { 41 | let mut remaining_amounts = vesting_amounts[remaining_first_idx..].to_vec(); 42 | if amount_to_subtract > 0 { 43 | remaining_amounts[0] -= amount_to_subtract; 44 | } 45 | VESTING_AMOUNTS.save(storage, &remaining_amounts)?; 46 | let remaining_ts = vesting_ts[remaining_first_idx..].to_vec(); 47 | VESTING_TIMESTAMPS.save(storage, &remaining_ts)?; 48 | } 49 | 50 | Ok(vested_amount) 51 | } 52 | 53 | pub fn total_vested_amount(storage: &dyn Storage, now: Timestamp) -> StdResult { 54 | let vesting_timestamps = VESTING_TIMESTAMPS.load(storage)?; 55 | let vesting_amounts = VESTING_AMOUNTS.load(storage)?; 56 | let mut total_vested_amount = 0u128; 57 | for i in 0..vesting_timestamps.len() { 58 | if vesting_timestamps[i] <= now { 59 | total_vested_amount += vesting_amounts[i]; 60 | } else { 61 | break; 62 | } 63 | } 64 | Ok(total_vested_amount) 65 | } 66 | 67 | pub fn distribute_vested( 68 | storage: &dyn Storage, 69 | amount: u128, 70 | response: Response, 71 | ) -> Result { 72 | if amount == 0 { 73 | return Ok(response); 74 | } 75 | let addr = UNLOCK_DISTRIBUTION_ADDRESS.load(storage)?; 76 | let denom = DENOM.load(storage)?; 77 | let msg = BankMsg::Send { 78 | to_address: addr.to_string(), 79 | amount: coins(amount, denom), 80 | }; 81 | Ok(response.add_message(msg)) 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use cosmwasm_std::testing::{mock_dependencies, mock_env}; 87 | use cosmwasm_std::{Addr, Response}; 88 | 89 | use crate::state::{DENOM, UNLOCK_DISTRIBUTION_ADDRESS, VESTING_AMOUNTS, VESTING_TIMESTAMPS}; 90 | use crate::ContractError; 91 | 92 | use super::{collect_vested, distribute_vested}; 93 | 94 | #[test] 95 | fn test_nothing_to_vest() { 96 | let mut deps = mock_dependencies(); 97 | let deps_mut = deps.as_mut(); 98 | let now = mock_env().block.time; 99 | VESTING_TIMESTAMPS.save(deps_mut.storage, &vec![]).unwrap(); 100 | VESTING_AMOUNTS.save(deps_mut.storage, &vec![]).unwrap(); 101 | 102 | assert_eq!(0, collect_vested(deps_mut.storage, now, 0).unwrap()); 103 | assert_eq!( 104 | ContractError::NoSufficientUnlockedTokens {}, 105 | collect_vested(deps_mut.storage, now, 10).expect_err("should error") 106 | ); 107 | } 108 | 109 | #[test] 110 | fn test_vest_single_full() { 111 | let mut deps = mock_dependencies(); 112 | let deps_mut = deps.as_mut(); 113 | let now = mock_env().block.time; 114 | VESTING_TIMESTAMPS 115 | .save(deps_mut.storage, &vec![now]) 116 | .unwrap(); 117 | VESTING_AMOUNTS.save(deps_mut.storage, &vec![10]).unwrap(); 118 | 119 | assert_eq!(10, collect_vested(deps_mut.storage, now, 10).unwrap()); 120 | assert_eq!(VESTING_TIMESTAMPS.load(deps_mut.storage).unwrap(), vec![]); 121 | assert_eq!(VESTING_AMOUNTS.load(deps_mut.storage).unwrap(), vec![]); 122 | } 123 | 124 | #[test] 125 | fn test_vest_single_more() { 126 | let mut deps = mock_dependencies(); 127 | let deps_mut = deps.as_mut(); 128 | let now = mock_env().block.time; 129 | VESTING_TIMESTAMPS 130 | .save(deps_mut.storage, &vec![now]) 131 | .unwrap(); 132 | VESTING_AMOUNTS.save(deps_mut.storage, &vec![10]).unwrap(); 133 | 134 | assert_eq!( 135 | ContractError::NoSufficientUnlockedTokens {}, 136 | collect_vested(deps_mut.storage, now, 15).expect_err("should error") 137 | ); 138 | assert_eq!( 139 | VESTING_TIMESTAMPS.load(deps_mut.storage).unwrap(), 140 | vec![now] 141 | ); 142 | assert_eq!(VESTING_AMOUNTS.load(deps_mut.storage).unwrap(), vec![10]); 143 | } 144 | 145 | #[test] 146 | fn test_vest_single_partial() { 147 | let mut deps = mock_dependencies(); 148 | let deps_mut = deps.as_mut(); 149 | let now = mock_env().block.time; 150 | VESTING_TIMESTAMPS 151 | .save(deps_mut.storage, &vec![now]) 152 | .unwrap(); 153 | VESTING_AMOUNTS.save(deps_mut.storage, &vec![10]).unwrap(); 154 | 155 | assert_eq!(5, collect_vested(deps_mut.storage, now, 5).unwrap()); 156 | assert_eq!( 157 | VESTING_TIMESTAMPS.load(deps_mut.storage).unwrap(), 158 | vec![now] 159 | ); 160 | assert_eq!(VESTING_AMOUNTS.load(deps_mut.storage).unwrap(), vec![5]); 161 | } 162 | 163 | #[test] 164 | fn test_not_vest_single() { 165 | let mut deps = mock_dependencies(); 166 | let deps_mut = deps.as_mut(); 167 | let now = mock_env().block.time; 168 | VESTING_TIMESTAMPS 169 | .save(deps_mut.storage, &vec![now.plus_seconds(1)]) 170 | .unwrap(); 171 | VESTING_AMOUNTS.save(deps_mut.storage, &vec![10]).unwrap(); 172 | 173 | assert_eq!( 174 | ContractError::NoSufficientUnlockedTokens {}, 175 | collect_vested(deps_mut.storage, now, 10).expect_err("should error") 176 | ); 177 | assert_eq!( 178 | VESTING_TIMESTAMPS.load(deps_mut.storage).unwrap(), 179 | vec![now.plus_seconds(1)] 180 | ); 181 | assert_eq!(VESTING_AMOUNTS.load(deps_mut.storage).unwrap(), vec![10]); 182 | } 183 | 184 | #[test] 185 | fn test_vest_multiple() { 186 | let mut deps = mock_dependencies(); 187 | let deps_mut = deps.as_mut(); 188 | let now = mock_env().block.time; 189 | VESTING_TIMESTAMPS 190 | .save( 191 | deps_mut.storage, 192 | &vec![now.minus_seconds(1), now, now.plus_seconds(1)], 193 | ) 194 | .unwrap(); 195 | VESTING_AMOUNTS 196 | .save(deps_mut.storage, &vec![10, 9, 11]) 197 | .unwrap(); 198 | 199 | assert_eq!(18, collect_vested(deps_mut.storage, now, 18).unwrap()); 200 | assert_eq!( 201 | VESTING_TIMESTAMPS.load(deps_mut.storage).unwrap(), 202 | vec![now, now.plus_seconds(1)] 203 | ); 204 | assert_eq!( 205 | VESTING_AMOUNTS.load(deps_mut.storage).unwrap(), 206 | vec![1u128, 11u128] 207 | ); 208 | 209 | assert_eq!( 210 | 2, 211 | collect_vested(deps_mut.storage, now.plus_seconds(1), 2).unwrap() 212 | ); 213 | assert_eq!( 214 | VESTING_TIMESTAMPS.load(deps_mut.storage).unwrap(), 215 | vec![now.plus_seconds(1)] 216 | ); 217 | assert_eq!( 218 | VESTING_AMOUNTS.load(deps_mut.storage).unwrap(), 219 | vec![10u128] 220 | ); 221 | } 222 | 223 | #[test] 224 | fn test_vest_multiple_all() { 225 | let mut deps = mock_dependencies(); 226 | let deps_mut = deps.as_mut(); 227 | let now = mock_env().block.time; 228 | VESTING_TIMESTAMPS 229 | .save( 230 | deps_mut.storage, 231 | &vec![now.minus_seconds(2), now.minus_seconds(1), now], 232 | ) 233 | .unwrap(); 234 | VESTING_AMOUNTS 235 | .save(deps_mut.storage, &vec![10, 9, 11]) 236 | .unwrap(); 237 | 238 | assert_eq!(30, collect_vested(deps_mut.storage, now, 30).unwrap()); 239 | assert_eq!(VESTING_TIMESTAMPS.load(deps_mut.storage).unwrap(), vec![]); 240 | assert_eq!(VESTING_AMOUNTS.load(deps_mut.storage).unwrap(), vec![]); 241 | } 242 | 243 | #[test] 244 | fn test_vest_multiple_none() { 245 | let mut deps = mock_dependencies(); 246 | let deps_mut = deps.as_mut(); 247 | let now = mock_env().block.time; 248 | VESTING_TIMESTAMPS 249 | .save( 250 | deps_mut.storage, 251 | &vec![ 252 | now.plus_seconds(1), 253 | now.plus_seconds(2), 254 | now.plus_seconds(3), 255 | ], 256 | ) 257 | .unwrap(); 258 | VESTING_AMOUNTS 259 | .save(deps_mut.storage, &vec![10, 9, 11]) 260 | .unwrap(); 261 | 262 | assert_eq!( 263 | ContractError::NoSufficientUnlockedTokens {}, 264 | collect_vested(deps_mut.storage, now, 30).expect_err("should error") 265 | ); 266 | assert_eq!( 267 | VESTING_TIMESTAMPS.load(deps_mut.storage).unwrap(), 268 | vec![ 269 | now.plus_seconds(1), 270 | now.plus_seconds(2), 271 | now.plus_seconds(3) 272 | ] 273 | ); 274 | assert_eq!( 275 | VESTING_AMOUNTS.load(deps_mut.storage).unwrap(), 276 | vec![10, 9, 11] 277 | ); 278 | } 279 | 280 | #[test] 281 | fn test_distribute_vested_zero_amount() { 282 | let deps = mock_dependencies(); 283 | let deps_ref = deps.as_ref(); 284 | let mut response = Response::new(); 285 | response = distribute_vested(deps_ref.storage, 0, response).unwrap(); 286 | assert_eq!(response.messages.len(), 0); 287 | } 288 | 289 | #[test] 290 | fn test_distribute_vested() { 291 | let mut deps = mock_dependencies(); 292 | let deps_mut = deps.as_mut(); 293 | let mut response = Response::new(); 294 | DENOM.save(deps_mut.storage, &"usei".to_string()).unwrap(); 295 | UNLOCK_DISTRIBUTION_ADDRESS 296 | .save(deps_mut.storage, &Addr::unchecked("unlock_address")) 297 | .unwrap(); 298 | response = distribute_vested(deps_mut.storage, 20, response).unwrap(); 299 | assert_eq!(response.messages.len(), 1); 300 | } 301 | } 302 | --------------------------------------------------------------------------------