Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Contract pausing implementation #51

Merged
merged 3 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,7 @@ Once the round is over, proposals are deployed using Timewave, a product which a
The Hydro forum post mentions “tribute”- funds that proposal creators can attach to proposals which is paid out to the winning proposal. This is not implemented within the main Hydro contract, but it is possible for tribute to be awarded with pluggable tribute contracts that read from the Hydro contract. These can be switched out permissionlessly and even customized or reinvented by proposal authors.

We will deploy an example default tribute contract which pays out tribute to anyone who voted for a proposal- but only if that proposal wins. This can be used as is by proposal authors, or used as a starting point for custom tribute contracts.

# Pausing/Un-pausing of the contract
In case of emergency, the Oversight committee will be able to suspend any actions on the smart contract by submitting the Pause transaction to the smart contract. Once the contract is paused, it can be un-paused through the Cosmos Hub governance by submitting the proposal to migrate the contract state (potentially to the same Code ID, in case the contract is safe to unpause without any changes to the code). Such proposal execution will trigger the `migrate()` function on the new contract code. This function will un-pause the contract and allow normal functioning of the contract to continue.
For more information on contract upgrade through the Cosmos Hub governance see [here](./docs/contract_upgrade.md).
87 changes: 74 additions & 13 deletions contracts/hydro/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use cosmwasm_std::{
use cw2::set_contract_version;

use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg};
use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg};
use crate::query::{QueryMsg, RoundProposalsResponse, UserLockupsResponse};
use crate::state::{
Constants, CovenantParams, LockEntry, Proposal, Tranche, Vote, CONSTANTS, LOCKED_TOKENS,
Expand Down Expand Up @@ -56,6 +56,7 @@ pub fn instantiate(
lock_epoch_length: msg.lock_epoch_length,
first_round_start: msg.first_round_start,
max_locked_tokens: msg.max_locked_tokens,
paused: false,
};

CONSTANTS.save(deps.storage, &state)?;
Expand Down Expand Up @@ -130,6 +131,7 @@ pub fn execute(
ExecuteMsg::UpdateMaxLockedTokens { max_locked_tokens } => {
update_max_locked_tokens(deps, info, max_locked_tokens)
}
ExecuteMsg::Pause {} => pause_contract(deps, info),
}
}

Expand All @@ -145,6 +147,7 @@ fn lock_tokens(
) -> Result<Response, ContractError> {
let constants = CONSTANTS.load(deps.storage)?;

validate_contract_is_not_paused(&constants)?;
validate_lock_duration(constants.lock_epoch_length, lock_duration)?;
must_pay(&info, &constants.denom)?;

Expand Down Expand Up @@ -209,6 +212,8 @@ fn refresh_lock_duration(
lock_duration: u64,
) -> Result<Response, ContractError> {
let constants = CONSTANTS.load(deps.storage)?;

validate_contract_is_not_paused(&constants)?;
validate_lock_duration(constants.lock_epoch_length, lock_duration)?;

// try to get the lock with the given id
Expand Down Expand Up @@ -291,6 +296,9 @@ fn validate_lock_duration(lock_epoch_length: u64, lock_duration: u64) -> Result<
// Send `amount` tokens back to caller
// Delete entry from LocksMap
fn unlock_tokens(deps: DepsMut, env: Env, info: MessageInfo) -> Result<Response, ContractError> {
let constants = CONSTANTS.load(deps.storage)?;

validate_contract_is_not_paused(&constants)?;
validate_previous_round_vote(&deps, &env, info.sender.clone())?;

// Iterate all locks for the caller and unlock them if lock_end < now
Expand Down Expand Up @@ -376,9 +384,12 @@ fn create_proposal(
description: String,
covenant_params: CovenantParams,
) -> Result<Response, ContractError> {
let constants = CONSTANTS.load(deps.storage)?;
validate_contract_is_not_paused(&constants)?;

// check that the tranche with the given id exists
TRANCHE_MAP.load(deps.storage, tranche_id)?;

let constants = CONSTANTS.load(deps.storage)?;
let round_id = compute_current_round_id(&env, &constants)?;
let proposal_id = PROP_ID.load(deps.storage)?;

Expand Down Expand Up @@ -443,9 +454,11 @@ fn vote(
// - To enable switching votes (and for other stuff too), we store the vote in VOTE_MAP.
// - When a user votes the second time in a round, the information about their previous vote from VOTE_MAP is used to reverse the effect of their previous vote.
// - This leads to slightly higher gas costs for each vote, in exchange for a much lower gas cost at the end of the round.
TRANCHE_MAP.load(deps.storage, tranche_id)?;

let constants = CONSTANTS.load(deps.storage)?;
validate_contract_is_not_paused(&constants)?;

// check that the tranche with the given id exists
TRANCHE_MAP.load(deps.storage, tranche_id)?;

// compute the round_id
let round_id = compute_current_round_id(&env, &constants)?;
Expand Down Expand Up @@ -597,6 +610,9 @@ fn add_to_whitelist(
info: MessageInfo,
covenant_params: CovenantParams,
) -> Result<Response, ContractError> {
let constants = CONSTANTS.load(deps.storage)?;

validate_contract_is_not_paused(&constants)?;
validate_sender_is_whitelist_admin(&deps, &info)?;

// Add covenant_params to whitelist
Expand All @@ -621,6 +637,9 @@ fn remove_from_whitelist(
info: MessageInfo,
covenant_params: CovenantParams,
) -> Result<Response, ContractError> {
let constants = CONSTANTS.load(deps.storage)?;

validate_contract_is_not_paused(&constants)?;
validate_sender_is_whitelist_admin(&deps, &info)?;

// Remove covenant_params from whitelist
Expand All @@ -636,23 +655,39 @@ fn update_max_locked_tokens(
info: MessageInfo,
max_locked_tokens: u128,
) -> Result<Response, ContractError> {
validate_sender_is_whitelist_admin(&deps, &info)?;
let mut constants = CONSTANTS.load(deps.storage)?;

CONSTANTS.update(
deps.storage,
|mut constants| -> Result<Constants, ContractError> {
constants.max_locked_tokens = max_locked_tokens;
validate_contract_is_not_paused(&constants)?;
validate_sender_is_whitelist_admin(&deps, &info)?;

Ok(constants)
},
)?;
constants.max_locked_tokens = max_locked_tokens;
CONSTANTS.save(deps.storage, &constants)?;

Ok(Response::new()
.add_attribute("action", "update_max_locked_tokens")
.add_attribute("sender", info.sender.clone())
.add_attribute("max_locked_tokens", max_locked_tokens.to_string()))
}

// Pause:
// Validate that the contract isn't already paused
// Validate sender is whitelist admin
// Set paused to true and save the changes
fn pause_contract(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
let mut constants = CONSTANTS.load(deps.storage)?;

validate_contract_is_not_paused(&constants)?;
validate_sender_is_whitelist_admin(&deps, &info)?;

constants.paused = true;
CONSTANTS.save(deps.storage, &constants)?;

Ok(Response::new()
.add_attribute("action", "pause_contract")
.add_attribute("sender", info.sender.clone())
.add_attribute("paused", "true"))
}

fn validate_sender_is_whitelist_admin(
deps: &DepsMut,
info: &MessageInfo,
Expand All @@ -665,6 +700,13 @@ fn validate_sender_is_whitelist_admin(
Ok(())
}

fn validate_contract_is_not_paused(constants: &Constants) -> Result<(), ContractError> {
match constants.paused {
true => Err(ContractError::Paused),
false => Ok(()),
}
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
Expand Down Expand Up @@ -726,7 +768,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
)?),
QueryMsg::Whitelist {} => to_json_binary(&query_whitelist(deps)?),
QueryMsg::WhitelistAdmins {} => to_json_binary(&query_whitelist_admins(deps)?),
QueryMsg::TotalLockedTokens => to_json_binary(&LOCKED_TOKENS.load(deps.storage)?),
QueryMsg::TotalLockedTokens {} => to_json_binary(&LOCKED_TOKENS.load(deps.storage)?),
}
}

Expand Down Expand Up @@ -998,3 +1040,22 @@ fn get_lock_count(deps: Deps, user_address: Addr) -> usize {
.range(deps.storage, None, None, Order::Ascending)
.count()
}

/// In the first version of Hydro, we allow contract to be un-paused through the Cosmos Hub governance
/// by migrating contract to the same code ID. This will trigger the migrate() function where we set
/// the paused flag to false.
/// Keep in mind that, for the future versions, this function should check the `CONTRACT_VERSION` and
/// perform any state changes needed. It should also handle the un-pausing of the contract, depending if
/// it was previously paused or not.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
CONSTANTS.update(
deps.storage,
|mut constants| -> Result<Constants, ContractError> {
constants.paused = false;
Ok(constants)
},
)?;

Ok(Response::default())
}
3 changes: 3 additions & 0 deletions contracts/hydro/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ pub enum ContractError {

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

#[error("Paused")]
Paused,
}
4 changes: 4 additions & 0 deletions contracts/hydro/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ pub enum ExecuteMsg {
UpdateMaxLockedTokens {
max_locked_tokens: u128,
},
Pause {},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MigrateMsg {}
2 changes: 1 addition & 1 deletion contracts/hydro/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub enum QueryMsg {
},
Whitelist {},
WhitelistAdmins {},
TotalLockedTokens,
TotalLockedTokens {},
}

#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)]
Expand Down
1 change: 1 addition & 0 deletions contracts/hydro/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct Constants {
pub lock_epoch_length: u64,
pub first_round_start: Timestamp,
pub max_locked_tokens: u128,
pub paused: bool,
}

// the total number of tokens locked in the contract
Expand Down
63 changes: 63 additions & 0 deletions contracts/hydro/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -991,3 +991,66 @@ fn max_locked_tokens_test() {
.to_string()
.contains("The limit for locking tokens has been reached. No more tokens can be locked."));
}

#[test]
fn contract_pausing_test() {
let (mut deps, env, mut info) = (mock_dependencies(), mock_env(), mock_info("addr0000", &[]));

let whitelist_admin = "addr0001";
let mut msg = get_default_instantiate_msg();
msg.whitelist_admins = vec![whitelist_admin.to_string()];

let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg.clone());
assert!(res.is_ok());

// verify that non-privileged user can not pause the contract
let msg = ExecuteMsg::Pause {};
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone());
assert!(res.is_err());
assert!(res.unwrap_err().to_string().contains("Unauthorized"));

// verify that privileged user can pause the contract
info = mock_info(whitelist_admin, &[]);
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone());
assert!(res.is_ok());

let constants = query_constants(deps.as_ref());
assert!(constants.is_ok());
assert!(constants.unwrap().paused);

// verify that no action can be executed while the contract is paused
let msgs = vec![
dusan-maksimovic marked this conversation as resolved.
Show resolved Hide resolved
ExecuteMsg::LockTokens { lock_duration: 0 },
ExecuteMsg::RefreshLockDuration {
lock_id: 0,
lock_duration: 0,
},
ExecuteMsg::UnlockTokens {},
ExecuteMsg::CreateProposal {
tranche_id: 0,
title: "".to_string(),
description: "".to_string(),
covenant_params: get_default_covenant_params(),
},
ExecuteMsg::Vote {
tranche_id: 0,
proposal_id: 0,
},
ExecuteMsg::AddToWhitelist {
covenant_params: get_default_covenant_params(),
},
ExecuteMsg::RemoveFromWhitelist {
covenant_params: get_default_covenant_params(),
},
ExecuteMsg::UpdateMaxLockedTokens {
max_locked_tokens: 0,
},
ExecuteMsg::Pause {},
];

for msg in msgs {
let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone());
assert!(res.is_err());
assert!(res.unwrap_err().to_string().contains("Paused"));
}
}
7 changes: 6 additions & 1 deletion contracts/tribute/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use cosmwasm_std::{
use cw2::set_contract_version;

use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg};
use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg};
use crate::query::QueryMsg;
use crate::state::{Config, Tribute, CONFIG, TRIBUTE_CLAIMS, TRIBUTE_ID, TRIBUTE_MAP};
use hydro::query::QueryMsg as HydroQueryMsg;
Expand Down Expand Up @@ -388,3 +388,8 @@ fn get_top_n_proposal(

Ok(None)
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
Ok(Response::default())
}
3 changes: 3 additions & 0 deletions contracts/tribute/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ pub enum ExecuteMsg {
tribute_id: u64,
},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MigrateMsg {}
Loading