feat: DKG contract method for updating announce address (#6050)
* added new dkg execute methods for ownership transfer and announce address update * cherry-pick TestableNymContract for the dkg contract from #5091 * tests * schema fixes * removed old queued migrations
This commit is contained in:
committed by
GitHub
parent
92a88cdf9a
commit
fc98c497b4
Generated
+5
@@ -633,6 +633,7 @@ dependencies = [
|
||||
"cw4-group",
|
||||
"easy-addr",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-group-contract-common",
|
||||
"nym-multisig-contract-common",
|
||||
]
|
||||
@@ -663,6 +664,7 @@ dependencies = [
|
||||
"cw4",
|
||||
"easy-addr",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-group-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -1098,11 +1100,13 @@ dependencies = [
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"cw3-flex-multisig",
|
||||
"cw4",
|
||||
"cw4-group",
|
||||
"easy-addr",
|
||||
"nym-coconut-dkg-common",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-group-contract-common",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
@@ -1142,6 +1146,7 @@ dependencies = [
|
||||
"cosmwasm-std",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"nym-contracts-common",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"serde",
|
||||
|
||||
@@ -18,6 +18,8 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
nym-coconut-dkg-common = { path = "../../common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true }
|
||||
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract", optional = true }
|
||||
|
||||
cosmwasm-schema = { workspace = true, optional = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
@@ -28,13 +30,19 @@ cw2 = { workspace = true }
|
||||
cw4 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
cw3-flex-multisig = { path = "../multisig/cw3-flex-multisig", features = ["testable-cw3-contract"], optional = true }
|
||||
cw4-group = { path = "../multisig/cw4-group", features = ["testable-cw4-contract"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract" }
|
||||
cw-multi-test = { workspace = true }
|
||||
cw4-group = { path = "../multisig/cw4-group" }
|
||||
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract" }
|
||||
|
||||
[features]
|
||||
schema-gen = ["nym-coconut-dkg-common/schema", "cosmwasm-schema"]
|
||||
testable-dkg-contract = ["nym-contracts-common-testing", "cw3-flex-multisig/testable-cw3-contract", "nym-group-contract-common", "cw4-group/testable-cw4-contract"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -280,6 +280,50 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Transfers ownership of the epoch dealer to another address. This assumes off-chain hand-over of keys",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"transfer_ownership"
|
||||
],
|
||||
"properties": {
|
||||
"transfer_ownership": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"transfer_to"
|
||||
],
|
||||
"properties": {
|
||||
"transfer_to": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Update announce address of this signer",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"update_announce_address"
|
||||
],
|
||||
"properties": {
|
||||
"update_announce_address": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"new_address"
|
||||
],
|
||||
"properties": {
|
||||
"new_address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
|
||||
@@ -191,6 +191,50 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Transfers ownership of the epoch dealer to another address. This assumes off-chain hand-over of keys",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"transfer_ownership"
|
||||
],
|
||||
"properties": {
|
||||
"transfer_ownership": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"transfer_to"
|
||||
],
|
||||
"properties": {
|
||||
"transfer_to": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Update announce address of this signer",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"update_announce_address"
|
||||
],
|
||||
"properties": {
|
||||
"update_announce_address": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"new_address"
|
||||
],
|
||||
"properties": {
|
||||
"new_address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
|
||||
@@ -6,7 +6,9 @@ use crate::dealers::queries::{
|
||||
query_epoch_dealers_addresses_paged, query_epoch_dealers_paged,
|
||||
query_registered_dealer_details,
|
||||
};
|
||||
use crate::dealers::transactions::try_add_dealer;
|
||||
use crate::dealers::transactions::{
|
||||
try_add_dealer, try_transfer_ownership, try_update_announce_address,
|
||||
};
|
||||
use crate::dealings::queries::{
|
||||
query_dealer_dealings_status, query_dealing_chunk, query_dealing_chunk_status,
|
||||
query_dealing_metadata, query_dealing_status,
|
||||
@@ -21,7 +23,6 @@ use crate::epoch_state::transactions::{
|
||||
try_advance_epoch_state, try_initiate_dkg, try_trigger_reset, try_trigger_resharing,
|
||||
};
|
||||
use crate::error::ContractError;
|
||||
use crate::queued_migrations::introduce_historical_epochs;
|
||||
use crate::state::queries::query_state;
|
||||
use crate::state::storage::{DKG_ADMIN, MULTISIG, STATE};
|
||||
use crate::verification_key_shares::queries::{query_vk_share, query_vk_shares_paged};
|
||||
@@ -127,6 +128,12 @@ pub fn execute(
|
||||
ExecuteMsg::AdvanceEpochState {} => try_advance_epoch_state(deps, env),
|
||||
ExecuteMsg::TriggerReset {} => try_trigger_reset(deps, env, info),
|
||||
ExecuteMsg::TriggerResharing {} => try_trigger_resharing(deps, env, info),
|
||||
ExecuteMsg::TransferOwnership { transfer_to } => {
|
||||
try_transfer_ownership(deps, env, info, transfer_to)
|
||||
}
|
||||
ExecuteMsg::UpdateAnnounceAddress { new_address } => {
|
||||
try_update_announce_address(deps, info, new_address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,12 +255,10 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
|
||||
}
|
||||
|
||||
#[entry_point]
|
||||
pub fn migrate(deps: DepsMut<'_>, env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
|
||||
pub fn migrate(deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
|
||||
set_build_information!(deps.storage)?;
|
||||
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
introduce_historical_epochs(deps, env)?;
|
||||
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::error::ContractError;
|
||||
use crate::Dealer;
|
||||
use cosmwasm_std::{StdResult, Storage};
|
||||
use cw_storage_plus::{Item, Map};
|
||||
use nym_coconut_dkg_common::dealer::{BlockHeight, OwnershipTransfer, TransactionIndex};
|
||||
use nym_coconut_dkg_common::types::{DealerDetails, DealerRegistrationDetails, EpochId, NodeIndex};
|
||||
|
||||
pub(crate) const DEALER_INDICES_PAGE_MAX_LIMIT: u32 = 80;
|
||||
@@ -23,6 +24,9 @@ pub(crate) const DEALERS_INDICES: Map<Dealer, NodeIndex> = Map::new("dealer_inde
|
||||
pub(crate) const EPOCH_DEALERS_MAP: Map<(EpochId, Dealer), DealerRegistrationDetails> =
|
||||
Map::new("epoch_dealers");
|
||||
|
||||
pub const OWNERSHIP_TRANSFER_LOG: Map<(Dealer, BlockHeight, TransactionIndex), OwnershipTransfer> =
|
||||
Map::new("transfer_log");
|
||||
|
||||
/// Attempts to retrieve a pre-assign node index associated with given dealer.
|
||||
/// If one doesn't exist, a new one is assigned.
|
||||
pub(crate) fn get_or_assign_index(
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::dealers::storage::{
|
||||
get_or_assign_index, is_dealer, save_dealer_details_if_not_a_dealer,
|
||||
ensure_dealer, get_or_assign_index, is_dealer, save_dealer_details_if_not_a_dealer,
|
||||
DEALERS_INDICES, EPOCH_DEALERS_MAP, OWNERSHIP_TRANSFER_LOG,
|
||||
};
|
||||
use crate::epoch_state::storage::{load_current_epoch, save_epoch};
|
||||
use crate::epoch_state::utils::check_epoch_state;
|
||||
use crate::error::ContractError;
|
||||
use crate::state::storage::STATE;
|
||||
use crate::Dealer;
|
||||
use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, Response};
|
||||
use nym_coconut_dkg_common::dealer::DealerRegistrationDetails;
|
||||
use cosmwasm_std::{Deps, DepsMut, Env, Event, MessageInfo, Response};
|
||||
use nym_coconut_dkg_common::dealer::{DealerRegistrationDetails, OwnershipTransfer};
|
||||
use nym_coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState};
|
||||
|
||||
fn ensure_group_member(deps: Deps, dealer: Dealer) -> Result<(), ContractError> {
|
||||
@@ -57,7 +58,8 @@ pub fn try_add_dealer(
|
||||
)?;
|
||||
|
||||
// check if it's a resharing dealer
|
||||
|
||||
// SAFETY: resharing isn't allowed on 0th epoch
|
||||
#[allow(clippy::expect_used)]
|
||||
let is_resharing_dealer = resharing
|
||||
&& is_dealer(
|
||||
deps.storage,
|
||||
@@ -83,6 +85,90 @@ pub fn try_add_dealer(
|
||||
Ok(Response::new().add_attribute("node_index", node_index.to_string()))
|
||||
}
|
||||
|
||||
pub fn try_transfer_ownership(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
transfer_to: String,
|
||||
) -> Result<Response, ContractError> {
|
||||
let transfer_to = deps.api.addr_validate(&transfer_to)?;
|
||||
|
||||
let epoch = load_current_epoch(deps.storage)?;
|
||||
|
||||
// make sure we're not mid-exchange
|
||||
check_epoch_state(deps.storage, EpochState::InProgress)?;
|
||||
|
||||
// make sure the requester is actually a dealer for this epoch
|
||||
ensure_dealer(deps.storage, &info.sender, epoch.epoch_id)?;
|
||||
|
||||
// make sure the new target dealer actually belong to the group
|
||||
ensure_group_member(deps.as_ref(), &transfer_to)?;
|
||||
|
||||
// update the index information
|
||||
let current_index = DEALERS_INDICES.load(deps.storage, &info.sender)?;
|
||||
DEALERS_INDICES.save(deps.storage, &transfer_to, ¤t_index)?;
|
||||
DEALERS_INDICES.remove(deps.storage, &info.sender);
|
||||
|
||||
// update registration detail for every epoch the current dealer has participated in the protocol
|
||||
// ideally, we'd have only updated the current epoch, but the way the contract is constructed
|
||||
// forbids that otherwise we'd have introduced inconsistency
|
||||
for epoch_id in 0..=epoch.epoch_id {
|
||||
if let Some(details) = EPOCH_DEALERS_MAP.may_load(deps.storage, (epoch_id, &info.sender))? {
|
||||
EPOCH_DEALERS_MAP.remove(deps.storage, (epoch_id, &info.sender));
|
||||
EPOCH_DEALERS_MAP.save(deps.storage, (epoch_id, &transfer_to), &details)?;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(transaction_info) = env.transaction else {
|
||||
return Err(ContractError::ExecutedOutsideTransaction);
|
||||
};
|
||||
|
||||
// save information about the transfer for more convenient history rebuilding
|
||||
OWNERSHIP_TRANSFER_LOG.save(
|
||||
deps.storage,
|
||||
(&info.sender, env.block.height, transaction_info.index),
|
||||
&OwnershipTransfer {
|
||||
node_index: current_index,
|
||||
from: info.sender.clone(),
|
||||
to: transfer_to.clone(),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(Response::new().add_event(
|
||||
Event::new("dkg-ownership-transfer")
|
||||
.add_attribute("from", info.sender)
|
||||
.add_attribute("to", transfer_to)
|
||||
.add_attribute("node_index", current_index.to_string()),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn try_update_announce_address(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
new_address: String,
|
||||
) -> Result<Response, ContractError> {
|
||||
let epoch = load_current_epoch(deps.storage)?;
|
||||
|
||||
// make sure we're not mid-exchange
|
||||
check_epoch_state(deps.storage, EpochState::InProgress)?;
|
||||
|
||||
// make sure the requester is actually a dealer for this epoch
|
||||
ensure_dealer(deps.storage, &info.sender, epoch.epoch_id)?;
|
||||
|
||||
let mut details = EPOCH_DEALERS_MAP.load(deps.storage, (epoch.epoch_id, &info.sender))?;
|
||||
let old_address = details.announce_address;
|
||||
|
||||
details.announce_address = new_address.clone();
|
||||
EPOCH_DEALERS_MAP.save(deps.storage, (epoch.epoch_id, &info.sender), &details)?;
|
||||
|
||||
Ok(Response::new().add_event(
|
||||
Event::new("dkg-announce-address-update")
|
||||
.add_attribute("dealer", info.sender)
|
||||
.add_attribute("old_address", old_address)
|
||||
.add_attribute("new_address", new_address),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
@@ -137,3 +223,222 @@ pub(crate) mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "testable-dkg-contract")]
|
||||
mod tests_with_mock {
|
||||
use super::*;
|
||||
use crate::testable_dkg_contract::{init_contract_tester, DkgContractTesterExt};
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use nym_contracts_common_testing::ContractOpts;
|
||||
|
||||
#[test]
|
||||
fn transferring_ownership() -> anyhow::Result<()> {
|
||||
let mut contract = init_contract_tester();
|
||||
let group_member = contract.random_group_member();
|
||||
|
||||
// sanity check, pre-dkg
|
||||
assert!(DEALERS_INDICES
|
||||
.may_load(&contract, &group_member)?
|
||||
.is_none());
|
||||
assert!(EPOCH_DEALERS_MAP
|
||||
.may_load(&contract, (0, &group_member))?
|
||||
.is_none());
|
||||
|
||||
contract.run_initial_dummy_dkg();
|
||||
let old_index = DEALERS_INDICES.load(&contract, &group_member)?;
|
||||
let old_details = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
|
||||
|
||||
let not_group_member = contract.addr_make("not_group_member");
|
||||
let (deps, env) = contract.deps_mut_env();
|
||||
assert!(try_transfer_ownership(
|
||||
deps,
|
||||
env,
|
||||
message_info(&group_member, &[]),
|
||||
not_group_member.to_string()
|
||||
)
|
||||
.is_err());
|
||||
|
||||
let new_group_member = contract.addr_make("new_group_member");
|
||||
contract.add_group_member(new_group_member.clone());
|
||||
let (deps, env) = contract.deps_mut_env();
|
||||
assert!(try_transfer_ownership(
|
||||
deps,
|
||||
env.clone(),
|
||||
message_info(&group_member, &[]),
|
||||
new_group_member.to_string()
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
// data under old key doesn't exist anymore
|
||||
assert!(DEALERS_INDICES
|
||||
.may_load(&contract, &group_member)?
|
||||
.is_none());
|
||||
assert!(EPOCH_DEALERS_MAP
|
||||
.may_load(&contract, (0, &group_member))?
|
||||
.is_none());
|
||||
|
||||
let new_index = DEALERS_INDICES.load(&contract, &new_group_member)?;
|
||||
let new_details = EPOCH_DEALERS_MAP.load(&contract, (0, &new_group_member))?;
|
||||
|
||||
// the underlying info hasn't changed
|
||||
assert_eq!(old_index, new_index);
|
||||
assert_eq!(old_details, new_details);
|
||||
|
||||
assert_eq!(
|
||||
OWNERSHIP_TRANSFER_LOG.load(
|
||||
&contract,
|
||||
(
|
||||
&group_member,
|
||||
env.block.height,
|
||||
env.transaction.unwrap().index
|
||||
)
|
||||
)?,
|
||||
OwnershipTransfer {
|
||||
node_index: new_index,
|
||||
from: group_member,
|
||||
to: new_group_member,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transferring_ownership_in_next_epochs() -> anyhow::Result<()> {
|
||||
let mut contract = init_contract_tester();
|
||||
let group_member = contract.random_group_member();
|
||||
|
||||
contract.run_initial_dummy_dkg(); // => epoch 0
|
||||
contract.run_reset_dkg(); // => epoch 1
|
||||
|
||||
// LEAVE DKG MEMBERSHIP
|
||||
contract.remove_group_member(group_member.clone());
|
||||
contract.run_reset_dkg(); // => epoch 2
|
||||
|
||||
// COME BACK
|
||||
contract.add_group_member(group_member.clone());
|
||||
contract.run_reset_dkg(); // => epoch 3
|
||||
|
||||
let old_index = DEALERS_INDICES.load(&contract, &group_member)?;
|
||||
let old_details0 = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
|
||||
let old_details1 = EPOCH_DEALERS_MAP.load(&contract, (1, &group_member))?;
|
||||
let old_details2 = EPOCH_DEALERS_MAP.may_load(&contract, (2, &group_member))?;
|
||||
assert!(old_details2.is_none());
|
||||
|
||||
let old_details3 = EPOCH_DEALERS_MAP.load(&contract, (3, &group_member))?;
|
||||
|
||||
// sanity check because we haven't changed our registration details:
|
||||
assert_eq!(old_details0, old_details1);
|
||||
assert_eq!(old_details1, old_details3);
|
||||
|
||||
let new_group_member = contract.addr_make("new_group_member");
|
||||
contract.add_group_member(new_group_member.clone());
|
||||
let (deps, env) = contract.deps_mut_env();
|
||||
assert!(try_transfer_ownership(
|
||||
deps,
|
||||
env.clone(),
|
||||
message_info(&group_member, &[]),
|
||||
new_group_member.to_string()
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
// data under old key doesn't exist anymore
|
||||
assert!(DEALERS_INDICES
|
||||
.may_load(&contract, &group_member)?
|
||||
.is_none());
|
||||
assert!(EPOCH_DEALERS_MAP
|
||||
.may_load(&contract, (0, &group_member))?
|
||||
.is_none());
|
||||
assert!(EPOCH_DEALERS_MAP
|
||||
.may_load(&contract, (1, &group_member))?
|
||||
.is_none());
|
||||
assert!(EPOCH_DEALERS_MAP
|
||||
.may_load(&contract, (2, &group_member))?
|
||||
.is_none());
|
||||
assert!(EPOCH_DEALERS_MAP
|
||||
.may_load(&contract, (3, &group_member))?
|
||||
.is_none());
|
||||
|
||||
let new_index = DEALERS_INDICES.load(&contract, &new_group_member)?;
|
||||
let new_details0 = EPOCH_DEALERS_MAP.load(&contract, (0, &new_group_member))?;
|
||||
let new_details1 = EPOCH_DEALERS_MAP.load(&contract, (1, &new_group_member))?;
|
||||
let new_details2 = EPOCH_DEALERS_MAP.may_load(&contract, (2, &new_group_member))?;
|
||||
let new_details3 = EPOCH_DEALERS_MAP.load(&contract, (3, &new_group_member))?;
|
||||
|
||||
// the underlying info hasn't changed
|
||||
assert_eq!(old_index, new_index);
|
||||
assert_eq!(old_details0, new_details0);
|
||||
assert_eq!(old_details1, new_details1);
|
||||
assert_eq!(old_details2, new_details2);
|
||||
assert_eq!(old_details3, new_details3);
|
||||
|
||||
assert_eq!(
|
||||
OWNERSHIP_TRANSFER_LOG.load(
|
||||
&contract,
|
||||
(
|
||||
&group_member,
|
||||
env.block.height,
|
||||
env.transaction.unwrap().index
|
||||
)
|
||||
)?,
|
||||
OwnershipTransfer {
|
||||
node_index: new_index,
|
||||
from: group_member,
|
||||
to: new_group_member,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updating_announce_address() -> anyhow::Result<()> {
|
||||
let mut contract = init_contract_tester();
|
||||
let group_member = contract.random_group_member();
|
||||
|
||||
contract.run_initial_dummy_dkg(); // => epoch 0
|
||||
contract.run_reset_dkg(); // => epoch 1
|
||||
|
||||
// LEAVE DKG MEMBERSHIP
|
||||
contract.remove_group_member(group_member.clone());
|
||||
contract.run_reset_dkg(); // => epoch 2
|
||||
|
||||
// COME BACK
|
||||
contract.add_group_member(group_member.clone());
|
||||
contract.run_reset_dkg(); // => epoch 3
|
||||
|
||||
let old_details0 = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
|
||||
let old_details1 = EPOCH_DEALERS_MAP.load(&contract, (1, &group_member))?;
|
||||
let old_details2 = EPOCH_DEALERS_MAP.may_load(&contract, (2, &group_member))?;
|
||||
assert!(old_details2.is_none());
|
||||
let old_details3 = EPOCH_DEALERS_MAP.load(&contract, (3, &group_member))?;
|
||||
|
||||
// sanity check because we haven't changed our registration details:
|
||||
assert_eq!(old_details0, old_details1);
|
||||
assert_eq!(old_details1, old_details3);
|
||||
|
||||
let new_address = "https://new-address.com".to_string();
|
||||
try_update_announce_address(
|
||||
contract.deps_mut(),
|
||||
message_info(&group_member, &[]),
|
||||
new_address.clone(),
|
||||
)?;
|
||||
|
||||
let new_details0 = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
|
||||
let new_details1 = EPOCH_DEALERS_MAP.load(&contract, (1, &group_member))?;
|
||||
let new_details2 = EPOCH_DEALERS_MAP.may_load(&contract, (2, &group_member))?;
|
||||
assert!(new_details2.is_none());
|
||||
let new_details3 = EPOCH_DEALERS_MAP.load(&contract, (3, &group_member))?;
|
||||
|
||||
// old epoch data is unchanged
|
||||
assert_eq!(old_details0, new_details0);
|
||||
assert_eq!(old_details1, new_details1);
|
||||
assert_eq!(old_details2, new_details2);
|
||||
|
||||
// most recent entry is updated
|
||||
assert_eq!(new_details3.announce_address, new_address);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,4 +139,7 @@ pub enum ContractError {
|
||||
|
||||
#[error("retrieved the maximum allowed number of cw4 members. for more the contracts have to be refactored")]
|
||||
PossiblyIncompleteGroupMembersQuery,
|
||||
|
||||
#[error("this method has been called outside transaction context")]
|
||||
ExecutedOutsideTransaction,
|
||||
}
|
||||
|
||||
@@ -15,3 +15,6 @@ mod queued_migrations;
|
||||
mod state;
|
||||
mod support;
|
||||
mod verification_key_shares;
|
||||
|
||||
#[cfg(feature = "testable-dkg-contract")]
|
||||
pub mod testable_dkg_contract;
|
||||
|
||||
@@ -1,21 +1,2 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::epoch_state::storage::HISTORICAL_EPOCH;
|
||||
use crate::error::ContractError;
|
||||
use cosmwasm_std::{DepsMut, Env};
|
||||
|
||||
pub fn introduce_historical_epochs(deps: DepsMut, env: Env) -> Result<(), ContractError> {
|
||||
if HISTORICAL_EPOCH.may_load(deps.storage)?.is_some() {
|
||||
return Err(ContractError::FailedMigration {
|
||||
comment: "this migration has already been run before".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
let current = crate::epoch_state::storage::CURRENT_EPOCH.load(deps.storage)?;
|
||||
// we won't have information on intermediate states prior to now, but that's not the end of the world
|
||||
HISTORICAL_EPOCH.save(deps.storage, ¤t, env.block.height)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::fixtures::TEST_MIX_DENOM;
|
||||
use crate::contract::instantiate;
|
||||
use crate::dealers::storage::{DEALERS_INDICES, EPOCH_DEALERS_MAP};
|
||||
use crate::epoch_state::storage::load_current_epoch;
|
||||
@@ -17,8 +18,6 @@ use nym_coconut_dkg_common::msg::InstantiateMsg;
|
||||
use nym_coconut_dkg_common::types::{DealerDetails, EpochId};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use super::fixtures::TEST_MIX_DENOM;
|
||||
|
||||
pub const ADMIN_ADDRESS: &str = addr!("admin address");
|
||||
pub const GROUP_CONTRACT: &str = addr!("group contract address");
|
||||
pub const MULTISIG_CONTRACT: &str = addr!("multisig contract address");
|
||||
@@ -74,6 +73,7 @@ pub fn add_fixture_dealer(deps: DepsMut<'_>) {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
fn querier_handler(query: &WasmQuery) -> QuerierResult {
|
||||
let bin = match query {
|
||||
WasmQuery::Smart { contract_addr, msg } => {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::ContractError;
|
||||
use cosmwasm_std::{Addr, QuerierWrapper};
|
||||
use cw4::Cw4Contract;
|
||||
|
||||
pub(crate) fn group_members(
|
||||
querier_wrapper: &QuerierWrapper,
|
||||
contract: &Cw4Contract,
|
||||
) -> Result<Vec<Addr>, ContractError> {
|
||||
// we shouldn't ever have more group members than the default limit but IN CASE
|
||||
// something changes down the line, do go through the pagination flow
|
||||
let mut group_members = Vec::new();
|
||||
|
||||
// current max limit
|
||||
let limit = 30;
|
||||
let mut start_after = None;
|
||||
loop {
|
||||
let members = contract.list_members(querier_wrapper, start_after, Some(limit))?;
|
||||
start_after = members.last().as_ref().map(|d| d.addr.clone());
|
||||
for member in &members {
|
||||
group_members.push(Addr::unchecked(&member.addr));
|
||||
}
|
||||
|
||||
if members.len() < limit as usize {
|
||||
// we have already exhausted the data
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(group_members)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::testable_dkg_contract::helpers::group_members;
|
||||
use crate::testable_dkg_contract::init_contract_tester_with_group_members;
|
||||
use cw4::Cw4Contract;
|
||||
use cw4_group::testable_cw4_contract::GroupContract;
|
||||
use nym_contracts_common_testing::ContractOpts;
|
||||
|
||||
#[test]
|
||||
fn getting_group_members() -> anyhow::Result<()> {
|
||||
for members in [0, 10, 100, 1000] {
|
||||
let tester = init_contract_tester_with_group_members(members);
|
||||
let group_contract =
|
||||
Cw4Contract::new(tester.unchecked_contract_address::<GroupContract>());
|
||||
let querier = tester.deps().querier;
|
||||
|
||||
let addresses = group_members(&querier, &group_contract)?;
|
||||
assert_eq!(addresses.len(), members);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// fine in test code
|
||||
#![allow(clippy::unwrap_used)]
|
||||
#![allow(clippy::expect_used)]
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use crate::error::ContractError;
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use cosmwasm_std::Addr;
|
||||
use cw4::{Cw4Contract, Member};
|
||||
use nym_contracts_common_testing::{
|
||||
AdminExt, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt, ChainOpts,
|
||||
CommonStorageKeys, ContractFn, ContractOpts, ContractTester, ContractTesterBuilder, DenomExt,
|
||||
PermissionedFn, QueryFn, RandExt, SliceRandom, TEST_DENOM,
|
||||
};
|
||||
|
||||
use crate::epoch_state::storage::load_current_epoch;
|
||||
use crate::state::storage::{MULTISIG, STATE};
|
||||
use crate::testable_dkg_contract::helpers::group_members;
|
||||
use nym_coconut_dkg_common::dealing::{DealingChunkInfo, PartialContractDealing};
|
||||
use nym_coconut_dkg_common::types::{Epoch, EpochState};
|
||||
use nym_contracts_common::dealings::ContractSafeBytes;
|
||||
|
||||
pub use cw3_flex_multisig::testable_cw3_contract::{Duration, MultisigContract, Threshold};
|
||||
pub use cw4_group::testable_cw4_contract::GroupContract;
|
||||
pub use nym_coconut_dkg_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use nym_contracts_common_testing::TestableNymContract;
|
||||
|
||||
pub(crate) mod helpers;
|
||||
|
||||
pub struct DkgContract;
|
||||
|
||||
const DEFAULT_GROUP_MEMBERS: usize = 15;
|
||||
|
||||
impl TestableNymContract for DkgContract {
|
||||
const NAME: &'static str = "dkg-contract";
|
||||
type InitMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
type MigrateMsg = MigrateMsg;
|
||||
type ContractError = ContractError;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
|
||||
instantiate
|
||||
}
|
||||
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
|
||||
execute
|
||||
}
|
||||
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|
||||
query
|
||||
}
|
||||
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
|
||||
migrate
|
||||
}
|
||||
|
||||
fn init() -> ContractTester<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
init_contract_tester_with_group_members(DEFAULT_GROUP_MEMBERS)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_contract_tester() -> ContractTester<DkgContract> {
|
||||
DkgContract::init().with_common_storage_key(CommonStorageKeys::Admin, "dkg-admin")
|
||||
}
|
||||
|
||||
pub fn prepare_contract_tester_builder_with_group_members<C>(
|
||||
members: usize,
|
||||
) -> ContractTesterBuilder<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
let mut builder = ContractTesterBuilder::<C>::new();
|
||||
let api = builder.api();
|
||||
|
||||
// 1. init the CW4 group contract
|
||||
let group_init_msg = cw4_group::testable_cw4_contract::InstantiateMsg {
|
||||
admin: Some(builder.master_address().to_string()),
|
||||
members: (0..members)
|
||||
.map(|i| Member {
|
||||
addr: api.addr_make(&format!("group-member-{i}")).to_string(),
|
||||
weight: 1,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
builder.instantiate_contract::<GroupContract>(Some(group_init_msg));
|
||||
|
||||
// we just instantiated it
|
||||
let group_contract_address = builder.unchecked_contract_address::<GroupContract>();
|
||||
|
||||
// 2. init the CW3 multisig contract WITH DUMMY VALUES
|
||||
let multisig_init_msg = cw3_flex_multisig::testable_cw3_contract::InstantiateMsg {
|
||||
group_addr: group_contract_address.to_string(),
|
||||
// \/ PLACEHOLDERS
|
||||
coconut_bandwidth_contract_address: group_contract_address.to_string(),
|
||||
coconut_dkg_contract_address: group_contract_address.to_string(),
|
||||
// /\ PLACEHOLDERS
|
||||
threshold: Threshold::AbsolutePercentage {
|
||||
percentage: "0.67".parse().unwrap(),
|
||||
},
|
||||
max_voting_period: Duration::Time(3600),
|
||||
executor: None,
|
||||
proposal_deposit: None,
|
||||
};
|
||||
builder.instantiate_contract::<MultisigContract>(Some(multisig_init_msg));
|
||||
|
||||
// we just instantiated it
|
||||
let multisig_contract_address = builder.unchecked_contract_address::<MultisigContract>();
|
||||
|
||||
// 3. init the DKG contract
|
||||
let dkg_init_msg = InstantiateMsg {
|
||||
group_addr: group_contract_address.to_string(),
|
||||
multisig_addr: multisig_contract_address.to_string(),
|
||||
time_configuration: None,
|
||||
mix_denom: TEST_DENOM.to_string(),
|
||||
key_size: 5,
|
||||
};
|
||||
builder.instantiate_contract::<DkgContract>(Some(dkg_init_msg));
|
||||
|
||||
// we just instantiated it
|
||||
let dkg_contract_address = builder.unchecked_contract_address::<DkgContract>();
|
||||
|
||||
// 4. migrate the multisig contract to hold correct addresses
|
||||
let multisig_migrate_msg = cw3_flex_multisig::testable_cw3_contract::MigrateMsg {
|
||||
// \/ STILL A PLACEHOLDER (this contract does not care about interactions with the ecash contract)
|
||||
coconut_bandwidth_address: dkg_contract_address.to_string(),
|
||||
// /\ STILL A PLACEHOLDER
|
||||
coconut_dkg_address: dkg_contract_address.to_string(),
|
||||
};
|
||||
builder.migrate_contract::<MultisigContract>(&multisig_migrate_msg);
|
||||
builder
|
||||
}
|
||||
|
||||
pub fn init_contract_tester_with_group_members(members: usize) -> ContractTester<DkgContract> {
|
||||
prepare_contract_tester_builder_with_group_members(members)
|
||||
.build()
|
||||
.with_common_storage_key(CommonStorageKeys::Admin, "dkg-admin")
|
||||
}
|
||||
|
||||
pub trait DkgContractTesterExt:
|
||||
ContractOpts<ExecuteMsg = ExecuteMsg, QueryMsg = QueryMsg, ContractError = ContractError>
|
||||
+ ChainOpts
|
||||
+ AdminExt
|
||||
+ DenomExt
|
||||
+ RandExt
|
||||
+ BankExt
|
||||
+ ArbitraryContractStorageReader
|
||||
+ ArbitraryContractStorageWriter
|
||||
{
|
||||
fn epoch(&self) -> Epoch {
|
||||
load_current_epoch(self.storage()).unwrap()
|
||||
}
|
||||
|
||||
fn multisig_contract(&self) -> Addr {
|
||||
MULTISIG.get(self.deps()).unwrap().unwrap()
|
||||
}
|
||||
|
||||
fn group_contract_wrapper(&self) -> Cw4Contract {
|
||||
STATE.load(self.storage()).unwrap().group_addr
|
||||
}
|
||||
|
||||
fn remove_group_member(&mut self, addr: Addr) {
|
||||
// we have the same admin for all contracts
|
||||
let admin = self.admin().unwrap();
|
||||
|
||||
self.execute_arbitrary_contract(
|
||||
self.unchecked_contract_address::<GroupContract>(),
|
||||
message_info(&admin, &[]),
|
||||
&nym_group_contract_common::msg::ExecuteMsg::UpdateMembers {
|
||||
remove: vec![addr.to_string()],
|
||||
add: vec![],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn add_group_member(&mut self, addr: Addr) {
|
||||
let querier = self.deps().querier;
|
||||
|
||||
let members = self
|
||||
.group_contract_wrapper()
|
||||
.list_members(&querier, None, None)
|
||||
.unwrap();
|
||||
let weight = members.first().map(|m| m.weight).unwrap_or(1);
|
||||
|
||||
// we have the same admin for all contracts
|
||||
let admin = self.admin().unwrap();
|
||||
|
||||
self.execute_arbitrary_contract(
|
||||
self.unchecked_contract_address::<GroupContract>(),
|
||||
message_info(&admin, &[]),
|
||||
&nym_group_contract_common::msg::ExecuteMsg::UpdateMembers {
|
||||
remove: vec![],
|
||||
add: vec![Member {
|
||||
addr: addr.to_string(),
|
||||
weight,
|
||||
}],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn group_members(&self) -> Vec<Addr> {
|
||||
let querier = self.deps().querier;
|
||||
let group_contract = self.group_contract_wrapper();
|
||||
group_members(&querier, &group_contract).unwrap()
|
||||
}
|
||||
|
||||
fn random_group_member(&mut self) -> Addr {
|
||||
let members = self.group_members();
|
||||
members
|
||||
.choose(&mut self.raw_rng())
|
||||
.expect("no group members available")
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn dummy_dkg_steps(&mut self, resharing: bool) {
|
||||
let admin = self.admin().unwrap();
|
||||
let group_members = self.group_members();
|
||||
|
||||
// 2. register dealers
|
||||
for group_member in &group_members {
|
||||
self.execute_msg(
|
||||
group_member.clone(),
|
||||
&ExecuteMsg::RegisterDealer {
|
||||
bte_key_with_proof: format!("btekey-{group_member}"),
|
||||
identity_key: format!("identity-{group_member}"),
|
||||
announce_address: format!("announce-address-{group_member}"),
|
||||
resharing,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// PublicKeySubmission => DealingExchange
|
||||
self.advance_time_by(600);
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::DealingExchange { resharing }
|
||||
);
|
||||
|
||||
// 3. exchange dealings
|
||||
for group_member in &group_members {
|
||||
self.execute_msg(
|
||||
group_member.clone(),
|
||||
&ExecuteMsg::CommitDealingsMetadata {
|
||||
dealing_index: 1,
|
||||
chunks: vec![DealingChunkInfo { size: 1 }],
|
||||
resharing,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
self.execute_msg(
|
||||
group_member.clone(),
|
||||
&ExecuteMsg::CommitDealingsChunk {
|
||||
chunk: PartialContractDealing {
|
||||
dealing_index: 1,
|
||||
chunk_index: 0,
|
||||
data: ContractSafeBytes(vec![0]),
|
||||
},
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// DealingExchange => VerificationKeySubmission
|
||||
self.advance_time_by(300);
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::VerificationKeySubmission { resharing }
|
||||
);
|
||||
|
||||
// 4. derive keypairs
|
||||
for group_member in &group_members {
|
||||
self.execute_msg(
|
||||
group_member.clone(),
|
||||
&ExecuteMsg::CommitVerificationKeyShare {
|
||||
share: format!("partial-vk-{group_member}"),
|
||||
resharing,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// VerificationKeySubmission => VerificationKeyValidation
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
|
||||
.unwrap();
|
||||
self.advance_time_by(60);
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::VerificationKeyValidation { resharing }
|
||||
);
|
||||
|
||||
// VerificationKeyValidation => VerificationKeyFinalization
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::VerificationKeyFinalization { resharing }
|
||||
);
|
||||
|
||||
// 5. validate keys
|
||||
for group_member in &group_members {
|
||||
self.execute_msg(
|
||||
self.multisig_contract(),
|
||||
&ExecuteMsg::VerifyVerificationKeyShare {
|
||||
owner: group_member.to_string(),
|
||||
resharing,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// VerificationKeyFinalization => InProgress
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
|
||||
.unwrap();
|
||||
assert_eq!(self.epoch().state, EpochState::InProgress)
|
||||
}
|
||||
|
||||
fn run_initial_dummy_dkg(&mut self) {
|
||||
assert_eq!(self.epoch().state, EpochState::WaitingInitialisation);
|
||||
// 1. initiate DKG
|
||||
// WaitingInitialisation => PublicKeySubmission
|
||||
let admin = self.admin().unwrap();
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::InitiateDkg {})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::PublicKeySubmission { resharing: false }
|
||||
);
|
||||
|
||||
self.dummy_dkg_steps(false)
|
||||
}
|
||||
|
||||
fn run_reset_dkg(&mut self) {
|
||||
// 1. reset DKG
|
||||
// InProgress => PublicKeySubmission
|
||||
let admin = self.admin().unwrap();
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::TriggerReset {})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::PublicKeySubmission { resharing: false }
|
||||
);
|
||||
self.dummy_dkg_steps(false)
|
||||
}
|
||||
|
||||
fn run_resharing_dkg(&mut self) {
|
||||
assert_eq!(self.epoch().state, EpochState::InProgress);
|
||||
|
||||
let group_members = self.group_members();
|
||||
println!(
|
||||
"epoch: {} members: {}",
|
||||
self.epoch().epoch_id,
|
||||
group_members.len()
|
||||
);
|
||||
|
||||
// 1. initiate DKG
|
||||
// InProgress => PublicKeySubmission
|
||||
let admin = self.admin().unwrap();
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::TriggerResharing {})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::PublicKeySubmission { resharing: true }
|
||||
);
|
||||
self.dummy_dkg_steps(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl DkgContractTesterExt for ContractTester<DkgContract> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dealers::storage::EPOCH_DEALERS_MAP;
|
||||
|
||||
#[test]
|
||||
fn dummy_resharing() {
|
||||
let mut contract = init_contract_tester_with_group_members(10);
|
||||
contract.run_initial_dummy_dkg();
|
||||
|
||||
let dealer = contract.random_group_member();
|
||||
let details = EPOCH_DEALERS_MAP
|
||||
.may_load(contract.storage(), (0, &dealer))
|
||||
.unwrap();
|
||||
assert!(details.is_some());
|
||||
|
||||
assert_eq!(contract.epoch().epoch_id, 0);
|
||||
|
||||
contract.run_resharing_dkg();
|
||||
assert_eq!(contract.epoch().epoch_id, 1);
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ pub mod test_helpers {
|
||||
use cosmwasm_std::{Env, Response, Timestamp, Uint128};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::events::{
|
||||
may_find_attribute, MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY,
|
||||
MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY,
|
||||
};
|
||||
use mixnet_contract_common::helpers::compare_decimals;
|
||||
use mixnet_contract_common::mixnode::{NodeRewarding, UnbondedMixnode};
|
||||
@@ -100,8 +100,8 @@ pub mod test_helpers {
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub(crate) use nym_contracts_common_testing::helpers::{find_attribute, FindAttribute};
|
||||
|
||||
pub(crate) fn sorted_addresses(n: usize) -> Vec<Addr> {
|
||||
let mut rng = test_rng();
|
||||
@@ -1592,83 +1592,6 @@ pub mod test_helpers {
|
||||
None
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn find_attribute<S: Into<String>>(
|
||||
event_type: Option<S>,
|
||||
attribute: &str,
|
||||
response: &Response,
|
||||
) -> String {
|
||||
let event_type = event_type.map(Into::into);
|
||||
for event in &response.events {
|
||||
if let Some(typ) = &event_type {
|
||||
if &event.ty != typ {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(attr) = may_find_attribute(event, attribute) {
|
||||
return attr;
|
||||
}
|
||||
}
|
||||
// this is only used in tests so panic here is fine
|
||||
panic!("did not find the attribute")
|
||||
}
|
||||
|
||||
pub(crate) trait FindAttribute {
|
||||
fn attribute<E, S>(&self, event_type: E, attribute: &str) -> String
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>;
|
||||
|
||||
fn any_attribute(&self, attribute: &str) -> String {
|
||||
self.attribute::<_, String>(None, attribute)
|
||||
}
|
||||
|
||||
fn any_parsed_attribute<T>(&self, attribute: &str) -> T
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: Debug,
|
||||
{
|
||||
self.parsed_attribute::<_, String, T>(None, attribute)
|
||||
}
|
||||
|
||||
fn parsed_attribute<E, S, T>(&self, event_type: E, attribute: &str) -> T
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>,
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: Debug;
|
||||
|
||||
fn decimal<E, S>(&self, event_type: E, attribute: &str) -> Decimal
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>,
|
||||
{
|
||||
self.parsed_attribute(event_type, attribute)
|
||||
}
|
||||
}
|
||||
|
||||
impl FindAttribute for Response {
|
||||
fn attribute<E, S>(&self, event_type: E, attribute: &str) -> String
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>,
|
||||
{
|
||||
find_attribute(event_type.into(), attribute, self)
|
||||
}
|
||||
|
||||
fn parsed_attribute<E, S, T>(&self, event_type: E, attribute: &str) -> T
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>,
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: Debug,
|
||||
{
|
||||
find_attribute(event_type.into(), attribute, self)
|
||||
.parse()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
// using floats in tests is fine
|
||||
// (what it does is converting % value, like 12.34 into `Performance` (`Percent`)
|
||||
// which internally is represented by decimal `0.1234`
|
||||
|
||||
@@ -16,10 +16,6 @@ required-features = ["cosmwasm-schema"]
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
# use library feature to disable all instantiate/execute/query exports
|
||||
library = []
|
||||
|
||||
[dependencies]
|
||||
cw-utils = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
@@ -34,9 +30,15 @@ cosmwasm-std = { workspace = true }
|
||||
nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-multisig-contract-common = { path = "../../../common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-contracts-common = { path = "../../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-contracts-common-testing = { path = "../../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
easy-addr = { path = "../../../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
cw4-group = { path = "../cw4-group" }
|
||||
cw-multi-test = { workspace = true }
|
||||
cw20-base = { workspace = true }
|
||||
|
||||
[features]
|
||||
# use library feature to disable all instantiate/execute/query exports
|
||||
library = []
|
||||
testable-cw3-contract = ["nym-contracts-common-testing"]
|
||||
@@ -23,3 +23,6 @@ For more information on this contract, please check out the
|
||||
*/
|
||||
|
||||
pub mod contract;
|
||||
|
||||
#[cfg(feature = "testable-cw3-contract")]
|
||||
pub mod testable_cw3_contract;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use nym_contracts_common_testing::{ContractFn, PermissionedFn, QueryFn};
|
||||
use nym_multisig_contract_common::error::ContractError;
|
||||
|
||||
pub use cw_utils::{Duration, Threshold};
|
||||
pub use nym_contracts_common_testing::TestableNymContract;
|
||||
pub use nym_multisig_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
|
||||
pub struct MultisigContract;
|
||||
|
||||
impl TestableNymContract for MultisigContract {
|
||||
const NAME: &'static str = "cw3-flex-multisig-contract";
|
||||
type InitMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
type MigrateMsg = MigrateMsg;
|
||||
type ContractError = ContractError;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
|
||||
instantiate
|
||||
}
|
||||
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
|
||||
execute
|
||||
}
|
||||
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|
||||
|deps, env, msg| query(deps, env, msg).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
|
||||
migrate
|
||||
}
|
||||
|
||||
fn base_init_msg() -> Self::InitMsg {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
@@ -22,14 +22,10 @@ name = "schema"
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
# use library feature to disable all instantiate/execute/query exports
|
||||
library = []
|
||||
|
||||
|
||||
[dependencies]
|
||||
nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-contracts-common = { path = "../../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-contracts-common-testing = { path = "../../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true }
|
||||
|
||||
cw-utils = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
@@ -38,9 +34,14 @@ cw-controllers = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
schemars = "0.8.1"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, default-features = false, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
easy-addr = { path = "../../../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
easy-addr = { path = "../../../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
|
||||
[features]
|
||||
# use library feature to disable all instantiate/execute/query exports
|
||||
library = []
|
||||
testable-cw4-contract = ["nym-contracts-common-testing"]
|
||||
@@ -23,3 +23,6 @@ pub use crate::error::ContractError;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "testable-cw4-contract")]
|
||||
pub mod testable_cw4_contract;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use crate::error::ContractError;
|
||||
use nym_contracts_common_testing::{ContractFn, PermissionedFn, QueryFn};
|
||||
|
||||
pub use nym_contracts_common_testing::TestableNymContract;
|
||||
pub use nym_group_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
|
||||
pub struct GroupContract;
|
||||
|
||||
impl TestableNymContract for GroupContract {
|
||||
const NAME: &'static str = "cw4-group-contract";
|
||||
type InitMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
type MigrateMsg = MigrateMsg;
|
||||
type ContractError = ContractError;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
|
||||
instantiate
|
||||
}
|
||||
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
|
||||
execute
|
||||
}
|
||||
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|
||||
|deps, env, msg| query(deps, env, msg).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
|
||||
migrate
|
||||
}
|
||||
|
||||
fn base_init_msg() -> Self::InitMsg {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user