33992542b1
* updated contracts to cosmwasm2.2 and fixed build issues * removed old coconut contract code + additional dkg fixes * replace deprecated to_binary and from_binary functions * mixnet contract tests compiling some are failing due to incorrect addresses * made other contract tests compile * fixed remaining tests * allow usage of manually dispatching contract replies * nym-api test fixes * removed old toolchain from contracts CI * linter fixes * regenerated contract schema * fixed easy_addr * further license fixes * post rebase fixes + update to 2.2.2 * change ci runner * minor CI adjustments * change wallet CI to use node 20 * more CI changes... * run cosmwasm-check against release contracts * test ci changes * wip...
471 lines
15 KiB
Rust
471 lines
15 KiB
Rust
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
use crate::constants::{BLACKLIST_PROPOSAL_REPLY_ID, REDEMPTION_PROPOSAL_REPLY_ID};
|
|
use crate::contract::helpers::Invariants;
|
|
use crate::deposit::DepositStorage;
|
|
use crate::helpers::{
|
|
BlacklistKey, Config, MultisigReply, BLACKLIST_PAGE_DEFAULT_LIMIT, BLACKLIST_PAGE_MAX_LIMIT,
|
|
CONTRACT_NAME, CONTRACT_VERSION, DEPOSITS_PAGE_DEFAULT_LIMIT, DEPOSITS_PAGE_MAX_LIMIT,
|
|
};
|
|
use cosmwasm_std::{coin, BankMsg, Coin, Event, Order, Reply, Response, StdResult};
|
|
use cw4::Cw4Contract;
|
|
use cw_controllers::Admin;
|
|
use cw_storage_plus::{Bound, Item, Map};
|
|
use nym_contracts_common::set_build_information;
|
|
use nym_ecash_contract_common::blacklist::{
|
|
BlacklistedAccount, BlacklistedAccountResponse, Blacklisting, PagedBlacklistedAccountResponse,
|
|
};
|
|
use nym_ecash_contract_common::counters::PoolCounters;
|
|
use nym_ecash_contract_common::deposit::{
|
|
DepositData, DepositResponse, LatestDepositResponse, PagedDepositsResponse,
|
|
};
|
|
use nym_ecash_contract_common::events::{
|
|
DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ID, PROPOSAL_ID_ATTRIBUTE_NAME,
|
|
};
|
|
use nym_ecash_contract_common::EcashContractError;
|
|
use nym_network_defaults::TICKETBOOK_SIZE;
|
|
use sylvia::ctx::{ExecCtx, InstantiateCtx, MigrateCtx, QueryCtx};
|
|
use sylvia::{contract, entry_points};
|
|
|
|
mod helpers;
|
|
|
|
mod queued_migrations;
|
|
#[cfg(test)]
|
|
mod test;
|
|
|
|
pub struct NymEcashContract {
|
|
pub(crate) contract_admin: Admin,
|
|
pub(crate) multisig: Admin,
|
|
pub(crate) config: Item<Config>,
|
|
pub(crate) pool_counters: Item<PoolCounters>,
|
|
pub(crate) expected_invariants: Item<Invariants>,
|
|
|
|
pub(crate) blacklist: Map<BlacklistKey, Blacklisting>,
|
|
|
|
pub(crate) deposits: DepositStorage,
|
|
}
|
|
|
|
#[entry_points]
|
|
#[contract]
|
|
#[sv::error(EcashContractError)]
|
|
impl NymEcashContract {
|
|
#[allow(clippy::new_without_default)]
|
|
pub const fn new() -> Self {
|
|
Self {
|
|
contract_admin: Admin::new("contract_admin"),
|
|
multisig: Admin::new("multisig"),
|
|
config: Item::new("config"),
|
|
pool_counters: Item::new("pool_counters"),
|
|
expected_invariants: Item::new("expected_invariants"),
|
|
blacklist: Map::new("blacklist"),
|
|
deposits: DepositStorage::new(),
|
|
}
|
|
}
|
|
|
|
#[sv::msg(instantiate)]
|
|
pub fn instantiate(
|
|
&self,
|
|
mut ctx: InstantiateCtx,
|
|
holding_account: String,
|
|
multisig_addr: String,
|
|
group_addr: String,
|
|
deposit_amount: Coin,
|
|
) -> Result<Response, EcashContractError> {
|
|
let multisig_addr = ctx.deps.api.addr_validate(&multisig_addr)?;
|
|
let holding_account = ctx.deps.api.addr_validate(&holding_account)?;
|
|
let group_addr = Cw4Contract(ctx.deps.api.addr_validate(&group_addr).map_err(|_| {
|
|
EcashContractError::InvalidGroup {
|
|
addr: group_addr.clone(),
|
|
}
|
|
})?);
|
|
|
|
// by default the sender becomes the admin
|
|
self.contract_admin
|
|
.set(ctx.deps.branch(), Some(ctx.info.sender))?;
|
|
self.multisig
|
|
.set(ctx.deps.branch(), Some(multisig_addr.clone()))?;
|
|
|
|
self.expected_invariants.save(
|
|
ctx.deps.storage,
|
|
&Invariants {
|
|
ticket_book_size: TICKETBOOK_SIZE,
|
|
},
|
|
)?;
|
|
|
|
self.pool_counters.save(
|
|
ctx.deps.storage,
|
|
&PoolCounters {
|
|
total_deposited: coin(0, &deposit_amount.denom),
|
|
total_redeemed: coin(0, &deposit_amount.denom),
|
|
},
|
|
)?;
|
|
|
|
self.config.save(
|
|
ctx.deps.storage,
|
|
&Config {
|
|
group_addr,
|
|
holding_account,
|
|
deposit_amount,
|
|
},
|
|
)?;
|
|
|
|
cw2::set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
|
set_build_information!(ctx.deps.storage)?;
|
|
|
|
Ok(Response::default())
|
|
}
|
|
|
|
/*==================
|
|
======QUERIES=======
|
|
==================*/
|
|
#[sv::msg(query)]
|
|
pub fn get_blacklist_paged(
|
|
&self,
|
|
ctx: QueryCtx,
|
|
limit: Option<u32>,
|
|
start_after: Option<String>,
|
|
) -> StdResult<PagedBlacklistedAccountResponse> {
|
|
let limit = limit
|
|
.unwrap_or(BLACKLIST_PAGE_DEFAULT_LIMIT)
|
|
.min(BLACKLIST_PAGE_MAX_LIMIT) as usize;
|
|
|
|
let start = start_after.as_deref().map(Bound::exclusive);
|
|
|
|
let nodes = self
|
|
.blacklist
|
|
.range(ctx.deps.storage, start, None, Order::Ascending)
|
|
.take(limit)
|
|
.map(|res| res.map(Into::into))
|
|
.collect::<StdResult<Vec<BlacklistedAccount>>>()?;
|
|
|
|
let start_next_after = nodes
|
|
.last()
|
|
.map(|account: &BlacklistedAccount| account.public_key().to_string());
|
|
|
|
Ok(PagedBlacklistedAccountResponse::new(
|
|
nodes,
|
|
limit,
|
|
start_next_after,
|
|
))
|
|
}
|
|
|
|
#[sv::msg(query)]
|
|
pub fn get_blacklisted_account(
|
|
&self,
|
|
ctx: QueryCtx,
|
|
public_key: String,
|
|
) -> StdResult<BlacklistedAccountResponse> {
|
|
let account = self.blacklist.may_load(ctx.deps.storage, public_key)?;
|
|
Ok(BlacklistedAccountResponse::new(account))
|
|
}
|
|
|
|
#[sv::msg(query)]
|
|
pub fn get_required_deposit_amount(&self, ctx: QueryCtx) -> StdResult<Coin> {
|
|
let deposit_amount = self.config.load(ctx.deps.storage)?.deposit_amount;
|
|
|
|
Ok(deposit_amount)
|
|
}
|
|
|
|
#[sv::msg(query)]
|
|
pub fn get_deposit(
|
|
&self,
|
|
ctx: QueryCtx,
|
|
deposit_id: u32,
|
|
) -> Result<DepositResponse, EcashContractError> {
|
|
Ok(DepositResponse {
|
|
id: deposit_id,
|
|
deposit: self.deposits.try_load_by_id(ctx.deps.storage, deposit_id)?,
|
|
})
|
|
}
|
|
|
|
#[sv::msg(query)]
|
|
pub fn get_latest_deposit(
|
|
&self,
|
|
ctx: QueryCtx,
|
|
) -> Result<LatestDepositResponse, EcashContractError> {
|
|
let Some(latest_id) = self.deposits.latest_deposit(ctx.deps.storage)? else {
|
|
return Ok(LatestDepositResponse::default());
|
|
};
|
|
|
|
let maybe_deposit = self.deposits.try_load_by_id(ctx.deps.storage, latest_id)?;
|
|
|
|
Ok(LatestDepositResponse {
|
|
deposit: maybe_deposit.map(|deposit| DepositData {
|
|
id: latest_id,
|
|
deposit,
|
|
}),
|
|
})
|
|
}
|
|
|
|
#[sv::msg(query)]
|
|
pub fn get_deposits_paged(
|
|
&self,
|
|
ctx: QueryCtx,
|
|
limit: Option<u32>,
|
|
start_after: Option<u32>,
|
|
) -> StdResult<PagedDepositsResponse> {
|
|
let limit = limit
|
|
.unwrap_or(DEPOSITS_PAGE_DEFAULT_LIMIT)
|
|
.min(DEPOSITS_PAGE_MAX_LIMIT) as usize;
|
|
|
|
let start = start_after.map(Bound::exclusive);
|
|
|
|
let deposits = self
|
|
.deposits
|
|
.range(ctx.deps.storage, start, None, Order::Ascending)
|
|
.take(limit)
|
|
.map(|res| res.map(Into::into))
|
|
.collect::<StdResult<Vec<DepositData>>>()?;
|
|
|
|
let start_next_after = deposits.last().map(|deposit| deposit.id);
|
|
|
|
Ok(PagedDepositsResponse {
|
|
deposits,
|
|
start_next_after,
|
|
})
|
|
}
|
|
|
|
/*=====================
|
|
======EXECUTIONS=======
|
|
=====================*/
|
|
|
|
#[sv::msg(exec)]
|
|
pub fn deposit_ticket_book_funds(
|
|
&self,
|
|
ctx: ExecCtx,
|
|
identity_key: String,
|
|
) -> Result<Response, EcashContractError> {
|
|
let required_deposit = self.config.load(ctx.deps.storage)?.deposit_amount;
|
|
|
|
let submitted = cw_utils::must_pay(&ctx.info, &required_deposit.denom)?;
|
|
|
|
if submitted != required_deposit.amount {
|
|
let mut funds = ctx.info.funds;
|
|
return Err(EcashContractError::WrongAmount {
|
|
// SAFETY: the call to `must_pay` ensured a single coin has been sent
|
|
#[allow(clippy::unwrap_used)]
|
|
received: funds.pop().unwrap(),
|
|
amount: required_deposit,
|
|
});
|
|
}
|
|
|
|
self.pool_counters
|
|
.update(ctx.deps.storage, |mut counters| -> StdResult<_> {
|
|
counters.total_deposited.amount += submitted;
|
|
Ok(counters)
|
|
})?;
|
|
|
|
let deposit_id = self.deposits.save_deposit(ctx.deps.storage, identity_key)?;
|
|
|
|
Ok(Response::new()
|
|
.add_event(
|
|
Event::new(DEPOSITED_FUNDS_EVENT_TYPE)
|
|
.add_attribute(DEPOSIT_ID, deposit_id.to_string()),
|
|
)
|
|
.set_data(deposit_id.to_be_bytes()))
|
|
}
|
|
|
|
#[sv::msg(exec)]
|
|
pub fn request_redemption(
|
|
&self,
|
|
ctx: ExecCtx,
|
|
commitment_bs58: String,
|
|
number_of_tickets: u16,
|
|
) -> Result<Response, EcashContractError> {
|
|
// basic validation of commitment to make sure it's a valid sha256 digest
|
|
let Ok(digest) = bs58::decode(&commitment_bs58).into_vec() else {
|
|
return Err(EcashContractError::MalformedRedemptionCommitment);
|
|
};
|
|
if digest.len() != 32 {
|
|
return Err(EcashContractError::MalformedRedemptionCommitment);
|
|
}
|
|
|
|
let msg = self.create_redemption_proposal(ctx, commitment_bs58, number_of_tickets)?;
|
|
Ok(Response::new().add_submessage(msg))
|
|
}
|
|
|
|
#[sv::msg(exec)]
|
|
pub fn redeem_tickets(
|
|
&self,
|
|
ctx: ExecCtx,
|
|
n: u16,
|
|
gw: String,
|
|
) -> Result<Response, EcashContractError> {
|
|
// preserve the gateway argument so that upon scraping the chain and going through transactions,
|
|
// one could see which gateway attempted to redeem it.
|
|
// in the long run it will be needed to determine work factor.
|
|
let _ = gw;
|
|
|
|
// only a mutlisig proposal can do that
|
|
self.multisig
|
|
.assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?;
|
|
|
|
let config = self.config.load(ctx.deps.storage)?;
|
|
let to_return = self.tickets_redemption_amount(ctx.deps.storage, &config, n)?;
|
|
if to_return.amount.is_zero() {
|
|
return Ok(Response::new());
|
|
}
|
|
|
|
self.pool_counters
|
|
.update(ctx.deps.storage, |mut counters| -> StdResult<_> {
|
|
counters.total_redeemed.amount += to_return.amount;
|
|
Ok(counters)
|
|
})?;
|
|
|
|
Ok(Response::new().add_message(BankMsg::Send {
|
|
to_address: config.holding_account.to_string(),
|
|
amount: vec![to_return],
|
|
}))
|
|
}
|
|
|
|
#[sv::msg(exec)]
|
|
pub fn update_admin(
|
|
&self,
|
|
ctx: ExecCtx,
|
|
admin: String,
|
|
) -> Result<Response, EcashContractError> {
|
|
let new_admin = ctx.deps.api.addr_validate(&admin)?;
|
|
|
|
// note: the below performs validation to ensure the sender IS the current admin
|
|
Ok(self
|
|
.contract_admin
|
|
.execute_update_admin(ctx.deps, ctx.info, Some(new_admin))?)
|
|
}
|
|
|
|
#[sv::msg(exec)]
|
|
pub fn update_deposit_value(
|
|
&self,
|
|
ctx: ExecCtx,
|
|
new_deposit: Coin,
|
|
) -> Result<Response, EcashContractError> {
|
|
// only current admin can do that
|
|
self.contract_admin
|
|
.assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?;
|
|
|
|
let deposit_str = new_deposit.to_string();
|
|
self.config
|
|
.update(ctx.deps.storage, |mut cfg| -> StdResult<_> {
|
|
cfg.deposit_amount = new_deposit;
|
|
Ok(cfg)
|
|
})?;
|
|
Ok(Response::new().add_attribute("updated_deposit", deposit_str))
|
|
}
|
|
|
|
#[sv::msg(exec)]
|
|
pub fn propose_to_blacklist(
|
|
&self,
|
|
ctx: ExecCtx,
|
|
public_key: String,
|
|
) -> Result<Response, EcashContractError> {
|
|
let _ = ctx;
|
|
let _ = public_key;
|
|
Err(EcashContractError::UnimplementedBlacklisting)
|
|
// let cfg = self.config.load(ctx.deps.storage)?;
|
|
// cfg.group_addr
|
|
// .is_voting_member(&ctx.deps.querier, &ctx.info.sender, ctx.env.block.height)?
|
|
// .ok_or(EcashContractError::Unauthorized)?;
|
|
//
|
|
// if let Some(blacklisted) = self
|
|
// .blacklist
|
|
// .may_load(ctx.deps.storage, public_key.clone())?
|
|
// {
|
|
// // return existing proposal id
|
|
// Ok(Response::new().add_attribute(
|
|
// PROPOSAL_ID_ATTRIBUTE_NAME,
|
|
// blacklisted.proposal_id.to_string(),
|
|
// ))
|
|
// } else {
|
|
// let msg = self.create_blacklist_proposal(ctx, public_key)?;
|
|
// Ok(Response::new().add_submessage(msg))
|
|
// }
|
|
}
|
|
|
|
#[sv::msg(exec)]
|
|
pub fn add_to_blacklist(
|
|
&self,
|
|
ctx: ExecCtx,
|
|
public_key: String,
|
|
) -> Result<Response, EcashContractError> {
|
|
let _ = ctx;
|
|
let _ = public_key;
|
|
Err(EcashContractError::UnimplementedBlacklisting)
|
|
// //Only by multisig contract, actually add public key to blacklist
|
|
// self.multisig
|
|
// .assert_admin(ctx.deps.as_ref(), &ctx.info.sender)?;
|
|
//
|
|
// let mut blacklisting = self.blacklist.load(ctx.deps.storage, public_key.clone())?;
|
|
// blacklisting.finalized_at_height = Some(ctx.env.block.height);
|
|
// self.blacklist
|
|
// .save(ctx.deps.storage, public_key.clone(), &blacklisting)?;
|
|
//
|
|
// Ok(Response::new())
|
|
}
|
|
|
|
/*=====================
|
|
=========REPLY=========
|
|
=====================*/
|
|
#[sv::msg(reply)]
|
|
#[allow(deprecated)]
|
|
pub fn reply(
|
|
&self,
|
|
ctx: sylvia::types::ReplyCtx,
|
|
msg: Reply,
|
|
) -> Result<Response, EcashContractError> {
|
|
match msg.id {
|
|
n if n == BLACKLIST_PROPOSAL_REPLY_ID => self.handle_blacklist_proposal_reply(ctx, msg),
|
|
n if n == REDEMPTION_PROPOSAL_REPLY_ID => {
|
|
self.handle_redemption_proposal_reply(ctx, msg)
|
|
}
|
|
other => Err(EcashContractError::InvalidReplyId { id: other }),
|
|
}
|
|
}
|
|
|
|
#[allow(deprecated)]
|
|
fn handle_blacklist_proposal_reply(
|
|
&self,
|
|
ctx: sylvia::types::ReplyCtx,
|
|
msg: Reply,
|
|
) -> Result<Response, EcashContractError> {
|
|
let proposal_id = msg.multisig_proposal_id()?;
|
|
|
|
let proposal = self.query_multisig_proposal(ctx.deps.as_ref(), proposal_id)?;
|
|
let public_key = proposal.description;
|
|
self.blacklist.save(
|
|
ctx.deps.storage,
|
|
public_key,
|
|
&Blacklisting::new(proposal_id),
|
|
)?;
|
|
|
|
// TODO: that `BLACKLIST_PROPOSAL_ID` might be redundant since it should be available from cw3 event
|
|
Ok(Response::new().add_attribute(PROPOSAL_ID_ATTRIBUTE_NAME, proposal_id.to_string()))
|
|
}
|
|
|
|
#[allow(deprecated)]
|
|
fn handle_redemption_proposal_reply(
|
|
&self,
|
|
_ctx: sylvia::types::ReplyCtx,
|
|
msg: Reply,
|
|
) -> Result<Response, EcashContractError> {
|
|
let proposal_id = msg.multisig_proposal_id()?;
|
|
|
|
// emit the proposal_id in the response data for easy client access and to make sure it can't be tampered with
|
|
// (since it's included as part of block hash)
|
|
|
|
Ok(Response::new().set_data(proposal_id.to_be_bytes()))
|
|
}
|
|
|
|
/*=====================
|
|
=======MIGRATION=======
|
|
=====================*/
|
|
#[sv::msg(migrate)]
|
|
pub fn migrate(&self, ctx: MigrateCtx) -> Result<Response, EcashContractError> {
|
|
set_build_information!(ctx.deps.storage)?;
|
|
cw2::ensure_from_older_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
|
|
|
queued_migrations::remove_redemption_gateway_share(ctx.deps)?;
|
|
|
|
Ok(Response::new())
|
|
}
|
|
}
|