Differentiate staking and ownership (#961)

* Differentiate staking and ownership

* Ownership transfer, tests

* Consistent random keys

* Improve account tests

* Update Cargo.lock

* Make everybody happy

Co-authored-by: Drazen Urch <durch@users.noreply.guthub.com>
This commit is contained in:
Drazen Urch
2021-12-22 13:22:46 +01:00
committed by GitHub
parent d8cb6199e0
commit 614b99a36e
24 changed files with 488 additions and 198 deletions
Generated
+8 -6
View File
@@ -907,9 +907,9 @@ dependencies = [
[[package]]
name = "cosmwasm-crypto"
version = "1.0.0-beta2"
version = "1.0.0-beta3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c16b255449b3f5cd7fa4b79acd5225b5185655261087a3d8aaac44f88a0e23e9"
checksum = "a380b87642204557629c9b72988c47b55fbfe6d474960adba56b22331504956a"
dependencies = [
"digest 0.9.0",
"ed25519-zebra",
@@ -920,18 +920,18 @@ dependencies = [
[[package]]
name = "cosmwasm-derive"
version = "1.0.0-beta2"
version = "1.0.0-beta3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abad1a6ff427a2f66890a4dce6354b4563cd07cee91a942300e011c921c09ed2"
checksum = "866713b2fe13f23038c7d8824c3059d1f28dd94685fb406d1533c4eeeefeefae"
dependencies = [
"syn",
]
[[package]]
name = "cosmwasm-std"
version = "1.0.0-beta2"
version = "1.0.0-beta3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1660ee3d5734672e1eb4f0ceda403e2d83345e15143a48845f340f3252ce99a6"
checksum = "8dbb9939b31441dfa9af3ec9740c8a24d585688401eff1b6b386abb7ad0d10a8"
dependencies = [
"base64",
"cosmwasm-crypto",
@@ -7275,7 +7275,9 @@ dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"getrandom 0.2.3",
"mixnet-contract",
"rand 0.8.4",
"schemars",
"serde",
"thiserror",
+23 -9
View File
@@ -1,25 +1,36 @@
all: clippy test fmt
clippy: clippy-main clippy-contracts clippy-wallet
all: clippy-all test fmt
happy: clippy-happy test fmt
clippy-all: clippy-all-main clippy-all-contracts clippy-all-wallet
clippy-happy: clippy-happy-main clippy-happy-contracts clippy-happy-wallet
test: test-main test-contracts test-wallet
fmt: fmt-main fmt-contracts fmt-wallet
clippy-main:
clippy-happy-main:
cargo clippy
clippy-contracts:
cargo clippy --manifest-path contracts/Cargo.toml
clippy-happy-contracts:
cargo clippy --manifest-path contracts/Cargo.toml --target wasm32-unknown-unknown
clippy-wallet:
clippy-happy-wallet:
cargo clippy --manifest-path nym-wallet/Cargo.toml
clippy-all-main:
cargo clippy --all-features -- -D warnings
clippy-all-contracts:
cargo clippy --manifest-path contracts/Cargo.toml --all-features --target wasm32-unknown-unknown -- -D warnings
clippy-all-wallet:
cargo clippy --manifest-path nym-wallet/Cargo.toml --all-features -- -D warnings
test-main:
cargo test
cargo test --all-features
test-contracts:
cargo test --manifest-path contracts/Cargo.toml
cargo test --manifest-path contracts/Cargo.toml --all-features
test-wallet:
cargo test --manifest-path nym-wallet/Cargo.toml
cargo test --manifest-path nym-wallet/Cargo.toml --all-features
fmt-main:
cargo fmt --all
@@ -29,3 +40,6 @@ fmt-contracts:
fmt-wallet:
cargo fmt --manifest-path nym-wallet/Cargo.toml --all
wasm:
RUSTFLAGS='-C link-arg=-s' cargo build --manifest-path contracts/Cargo.toml --release --target wasm32-unknown-unknown
@@ -33,7 +33,7 @@ prost = { version = "0.9", default-features = false, optional = true }
flate2 = { version = "1.0.20", optional = true }
sha2 = { version = "0.9.5", optional = true }
itertools = { version = "0.10", optional = true }
cosmwasm-std = { version = "1.0.0-beta2", optional = true }
cosmwasm-std = { version = "1.0.0-beta3", optional = true }
ts-rs = {version = "5.1", optional = true}
[features]
@@ -64,7 +64,8 @@ pub trait VestingSigningClient {
async fn create_periodic_vesting_account(
&self,
address: &str,
owner_address: &str,
staking_address: Option<String>,
start_time: Option<u64>,
amount: Coin,
) -> Result<ExecuteResult, NymdError>;
@@ -271,13 +272,15 @@ impl<C: SigningCosmWasmClient + Sync + Send> VestingSigningClient for NymdClient
}
async fn create_periodic_vesting_account(
&self,
address: &str,
owner_address: &str,
staking_address: Option<String>,
start_time: Option<u64>,
amount: Coin,
) -> Result<ExecuteResult, NymdError> {
let fee = self.operation_fee(Operation::CreatePeriodicVestingAccount);
let req = VestingExecuteMsg::CreateAccount {
address: address.to_string(),
owner_address: owner_address.to_string(),
staking_address,
start_time,
};
self.client
+1 -1
View File
@@ -7,7 +7,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmwasm-std = "1.0.0-beta2"
cosmwasm-std = "1.0.0-beta3"
serde = { version = "1.0", features = ["derive"] }
serde_repr = "0.1"
+53 -18
View File
@@ -238,9 +238,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "cosmwasm-crypto"
version = "1.0.0-beta2"
version = "1.0.0-beta3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c16b255449b3f5cd7fa4b79acd5225b5185655261087a3d8aaac44f88a0e23e9"
checksum = "a380b87642204557629c9b72988c47b55fbfe6d474960adba56b22331504956a"
dependencies = [
"digest 0.9.0",
"ed25519-zebra",
@@ -251,18 +251,18 @@ dependencies = [
[[package]]
name = "cosmwasm-derive"
version = "1.0.0-beta2"
version = "1.0.0-beta3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abad1a6ff427a2f66890a4dce6354b4563cd07cee91a942300e011c921c09ed2"
checksum = "866713b2fe13f23038c7d8824c3059d1f28dd94685fb406d1533c4eeeefeefae"
dependencies = [
"syn",
]
[[package]]
name = "cosmwasm-schema"
version = "1.0.0-beta2"
version = "1.0.0-beta3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe52b19d45fe3f8359db6cc24df44dbe05e5ae32539afc0f5b7f790a21aa6fd0"
checksum = "818b928263c09a3269c2bed22494a62107a43ef87900e273af8ad2cb9f7e4440"
dependencies = [
"schemars",
"serde_json",
@@ -270,9 +270,9 @@ dependencies = [
[[package]]
name = "cosmwasm-std"
version = "1.0.0-beta2"
version = "1.0.0-beta3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1660ee3d5734672e1eb4f0ceda403e2d83345e15143a48845f340f3252ce99a6"
checksum = "8dbb9939b31441dfa9af3ec9740c8a24d585688401eff1b6b386abb7ad0d10a8"
dependencies = [
"base64",
"cosmwasm-crypto",
@@ -286,9 +286,9 @@ dependencies = [
[[package]]
name = "cosmwasm-storage"
version = "1.0.0-beta2"
version = "1.0.0-beta3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf3b4efe3b4f86df668520a02e9a29c23eea99b64dfcacb0e59b98346418af7f"
checksum = "b4a4e55f0d64fed54cd2202301b8d466af8de044589247dabd77a4222f52f749"
dependencies = [
"cosmwasm-std",
"serde",
@@ -326,7 +326,7 @@ dependencies = [
"log",
"nymsphinx-types",
"pemstore",
"rand",
"rand 0.7.3",
"subtle-encoding",
"x25519-dalek",
]
@@ -458,7 +458,7 @@ checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d"
dependencies = [
"curve25519-dalek",
"ed25519",
"rand",
"rand 0.7.3",
"serde",
"sha2",
"zeroize",
@@ -592,8 +592,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.10.2+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -856,8 +858,8 @@ dependencies = [
"cw-storage-plus",
"fixed",
"mixnet-contract",
"rand",
"rand_chacha",
"rand 0.7.3",
"rand_chacha 0.2.2",
"schemars",
"serde",
"thiserror",
@@ -1065,9 +1067,21 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc",
"rand_hc 0.2.0",
]
[[package]]
name = "rand"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.3",
"rand_hc 0.3.1",
]
[[package]]
@@ -1080,6 +1094,16 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.3",
]
[[package]]
name = "rand_core"
version = "0.5.1"
@@ -1105,7 +1129,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9532ada3929fb8b2e9dbe28d1e06c9b2cc65813f074fcb6bd5fbefeff9d56"
dependencies = [
"num-traits",
"rand",
"rand 0.7.3",
]
[[package]]
@@ -1117,6 +1141,15 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rand_hc"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
dependencies = [
"rand_core 0.6.3",
]
[[package]]
name = "regex"
version = "1.5.4"
@@ -1297,7 +1330,7 @@ dependencies = [
"hmac",
"lioness",
"log",
"rand",
"rand 0.7.3",
"rand_distr",
"sha2",
"subtle 2.4.1",
@@ -1525,7 +1558,9 @@ dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"getrandom 0.2.3",
"mixnet-contract",
"rand 0.8.4",
"schemars",
"serde",
"thiserror",
+2 -6
View File
@@ -8,18 +8,14 @@ edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
[dev-dependencies]
config = { path = "../../common/config"}
[dependencies]
bandwidth-claim-contract = { path = "../../common/bandwidth-claim-contract" }
cosmwasm-std = "1.0.0-beta2"
cosmwasm-storage = "1.0.0-beta2"
cosmwasm-std = "1.0.0-beta3"
cosmwasm-storage = "1.0.0-beta3"
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
+3 -7
View File
@@ -15,17 +15,13 @@ exclude = [
[lib]
crate-type = ["cdylib", "rlib"]
[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
[dependencies]
mixnet-contract = { path = "../../common/mixnet-contract" }
config = { path = "../../common/config"}
vesting-contract = { path = "../vesting" }
cosmwasm-std = "1.0.0-beta2"
cosmwasm-storage = "1.0.0-beta2"
cosmwasm-std = "1.0.0-beta3"
cosmwasm-storage = "1.0.0-beta3"
cw-storage-plus = "0.10.3"
bs58 = "0.4.0"
@@ -34,7 +30,7 @@ serde = { version = "1.0.103", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.23" }
[dev-dependencies]
cosmwasm-schema = "1.0.0-beta2"
cosmwasm-schema = "1.0.0-beta3"
fixed = "1.1"
rand_chacha = "0.2"
rand = "0.7"
+3 -6
View File
@@ -13,18 +13,15 @@ exclude = [
[lib]
crate-type = ["cdylib", "rlib"]
[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
[dependencies]
mixnet-contract = { path = "../../common/mixnet-contract" }
config = { path = "../../common/config" }
# this branch is identical to 0.14.1 with addition of updated k256 dependency required to help poor cargo choose correct version
cosmwasm-std = { version = "1.0.0-beta2", features = ["iterator"]}
cosmwasm-std = { version = "1.0.0-beta3"}
cw-storage-plus = { version = "0.10.3", features = ["iterator"] }
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.23" }
rand = {version = "0.8.4", features = ["std_rng"]}
getrandom = { version = "0.2.3", features = ["js"]}
+95 -37
View File
@@ -45,9 +45,17 @@ pub fn execute(
try_undelegate_from_mixnode(mix_identity, info, deps)
}
ExecuteMsg::CreateAccount {
address,
owner_address,
staking_address,
start_time,
} => try_create_periodic_vesting_account(&address, start_time, info, env, deps),
} => try_create_periodic_vesting_account(
&owner_address,
staking_address,
start_time,
info,
env,
deps,
),
ExecuteMsg::WithdrawVestedCoins { amount } => {
try_withdraw_vested_coins(amount, env, info, deps)
}
@@ -72,9 +80,84 @@ pub fn execute(
ExecuteMsg::TrackUnbondGateway { owner, amount } => {
try_track_unbond_gateway(&owner, amount, info, deps)
}
ExecuteMsg::TransferOwnership { to_address } => {
try_transfer_ownership(to_address, info, deps)
}
ExecuteMsg::UpdateStakingAddress { to_address } => {
try_update_staking_address(to_address, info, deps)
}
}
}
// Only owner
pub fn try_withdraw_vested_coins(
amount: Coin,
env: Env,
info: MessageInfo,
deps: DepsMut,
) -> Result<Response, ContractError> {
let address = info.sender.clone();
let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
if address != account.owner_address() {
return Err(ContractError::NotOwner(account.owner_address().to_string()));
}
let spendable_coins = account.spendable_coins(None, &env, deps.storage)?;
if amount.amount <= spendable_coins.amount {
let new_balance = account
.load_balance(deps.storage)?
.u128()
.saturating_sub(amount.amount.u128());
account.save_balance(Uint128::new(new_balance), deps.storage)?;
let send_tokens = BankMsg::Send {
to_address: account.owner_address().as_str().to_string(),
amount: vec![amount],
};
Ok(Response::new()
.add_attribute("action", "whitdraw")
.add_message(send_tokens))
} else {
Err(ContractError::InsufficientSpendable(
account.owner_address().as_str().to_string(),
spendable_coins.amount.u128(),
))
}
}
fn try_transfer_ownership(
to_address: String,
info: MessageInfo,
deps: DepsMut,
) -> Result<Response, ContractError> {
let address = info.sender.clone();
let to_address = deps.api.addr_validate(&to_address)?;
let mut account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
if address == account.owner_address() {
account.transfer_ownership(&to_address, deps.storage)?;
Ok(Response::default())
} else {
Err(ContractError::NotOwner(account.owner_address().to_string()))
}
}
fn try_update_staking_address(
to_address: Option<String>,
info: MessageInfo,
deps: DepsMut,
) -> Result<Response, ContractError> {
let address = info.sender.clone();
let to_address = to_address.and_then(|x| deps.api.addr_validate(&x).ok());
let mut account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
if address == account.owner_address() {
account.update_staking_address(to_address, deps.storage)?;
Ok(Response::default())
} else {
Err(ContractError::NotOwner(account.owner_address().to_string()))
}
}
// Owner or staking
pub fn try_bond_gateway(
gateway: Gateway,
owner_signature: String,
@@ -137,38 +220,6 @@ pub fn try_track_unbond_mixnode(
Ok(Response::default())
}
pub fn try_withdraw_vested_coins(
amount: Coin,
env: Env,
info: MessageInfo,
deps: DepsMut,
) -> Result<Response, ContractError> {
let address = info.sender.clone();
let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?;
let spendable_coins = account.spendable_coins(None, &env, deps.storage)?;
if amount.amount <= spendable_coins.amount {
let new_balance = account
.load_balance(deps.storage)?
.u128()
.saturating_sub(amount.amount.u128());
account.save_balance(Uint128::new(new_balance), deps.storage)?;
let send_tokens = BankMsg::Send {
to_address: address.as_str().to_string(),
amount: vec![amount],
};
Ok(Response::new()
.add_attribute("action", "whitdraw")
.add_message(send_tokens))
} else {
Err(ContractError::InsufficientSpendable(
address.as_str().to_string(),
spendable_coins.amount.u128(),
))
}
}
fn try_track_undelegation(
address: &str,
mix_identity: IdentityKey,
@@ -205,7 +256,8 @@ fn try_undelegate_from_mixnode(
}
fn try_create_periodic_vesting_account(
address: &str,
owner_address: &str,
staking_address: Option<String>,
start_time: Option<u64>,
info: MessageInfo,
env: Env,
@@ -215,11 +267,17 @@ fn try_create_periodic_vesting_account(
return Err(ContractError::NotAdmin(info.sender.as_str().to_string()));
}
let coin = validate_funds(&info.funds)?;
let address = deps.api.addr_validate(address)?;
let owner_address = deps.api.addr_validate(owner_address)?;
let staking_address = if let Some(staking_address) = staking_address {
Some(deps.api.addr_validate(&staking_address)?)
} else {
None
};
let start_time = start_time.unwrap_or_else(|| env.block.time.seconds());
let periods = populate_vesting_periods(start_time, NUM_VESTING_PERIODS);
Account::new(
address,
owner_address,
staking_address,
coin,
Timestamp::from_seconds(start_time),
periods,
+4
View File
@@ -38,4 +38,8 @@ pub enum ContractError {
Underflow,
#[error("No bond found for account {0}")]
NoBondFound(String),
#[error("Action can only be executed by account owner -> {0}")]
NotOwner(String),
#[error("Invalid address: {0}")]
InvalidAddress(String),
}
+8 -1
View File
@@ -18,7 +18,8 @@ pub enum ExecuteMsg {
mix_identity: IdentityKey,
},
CreateAccount {
address: String,
owner_address: String,
staking_address: Option<String>,
start_time: Option<u64>,
},
WithdrawVestedCoins {
@@ -47,6 +48,12 @@ pub enum ExecuteMsg {
owner: String,
amount: Coin,
},
TransferOwnership {
to_address: String,
},
UpdateStakingAddress {
to_address: Option<String>,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+11 -1
View File
@@ -5,8 +5,18 @@ use cw_storage_plus::Map;
const ACCOUNTS: Map<Addr, Account> = Map::new("acc");
pub fn delete_account(address: &Addr, storage: &mut dyn Storage) -> Result<(), ContractError> {
ACCOUNTS.remove(storage, address.to_owned());
Ok(())
}
pub fn save_account(account: &Account, storage: &mut dyn Storage) -> Result<(), ContractError> {
Ok(ACCOUNTS.save(storage, account.address(), account)?)
// This is a bit dirty, but its a simple way to allow for both staking account and owner to load it from storage
if let Some(staking_address) = account.staking_address() {
ACCOUNTS.save(storage, staking_address.to_owned(), account)?;
}
ACCOUNTS.save(storage, account.owner_address(), account)?;
Ok(())
}
pub fn load_account(
+2 -1
View File
@@ -21,7 +21,8 @@ pub mod helpers {
let periods = populate_vesting_periods(start_time.seconds(), NUM_VESTING_PERIODS);
Account::new(
Addr::unchecked("fixture"),
Addr::unchecked("owner"),
Some(Addr::unchecked("staking")),
Coin {
amount: Uint128::new(1_000_000_000_000),
denom: DENOM.to_string(),
@@ -1,5 +1,5 @@
use crate::errors::ContractError;
use cosmwasm_std::{Coin, Env, Storage, Timestamp};
use cosmwasm_std::{Addr, Coin, Env, Storage, Timestamp};
pub trait VestingAccount {
// locked_coins returns the set of coins that are not spendable (can still be delegated tough) (i.e. locked),
@@ -62,4 +62,14 @@ pub trait VestingAccount {
env: &Env,
storage: &dyn Storage,
) -> Result<Coin, ContractError>;
fn transfer_ownership(
&mut self,
to_address: &Addr,
storage: &mut dyn Storage,
) -> Result<(), ContractError>;
fn update_staking_address(
&mut self,
to_address: Option<Addr>,
storage: &mut dyn Storage,
) -> Result<(), ContractError>;
}
@@ -2,6 +2,7 @@ use crate::errors::ContractError;
use crate::traits::DelegatingAccount;
use config::defaults::{DEFAULT_MIXNET_CONTRACT_ADDRESS, DENOM};
use cosmwasm_std::{wasm_execute, Coin, Env, Order, Response, Storage, Timestamp, Uint128};
use cw_storage_plus::Map;
use mixnet_contract::ExecuteMsg as MixnetExecuteMsg;
use mixnet_contract::IdentityKey;
@@ -19,14 +20,14 @@ impl DelegatingAccount for Account {
if current_balance < coin.amount {
return Err(ContractError::InsufficientBalance(
self.address.as_str().to_string(),
self.owner_address().as_str().to_string(),
current_balance.u128(),
));
}
let msg = MixnetExecuteMsg::DelegateToMixnodeOnBehalf {
mix_identity: mix_identity.clone(),
delegate: self.address().into_string(),
delegate: self.owner_address().into_string(),
};
let delegate_to_mixnode =
wasm_execute(DEFAULT_MIXNET_CONTRACT_ADDRESS, &msg, vec![coin.clone()])?;
@@ -44,14 +45,14 @@ impl DelegatingAccount for Account {
) -> Result<Response, ContractError> {
if !self.any_delegation_for_mix(&mix_identity, storage) {
return Err(ContractError::NoSuchDelegation(
self.address(),
self.owner_address(),
mix_identity,
));
}
let msg = MixnetExecuteMsg::UndelegateFromMixnodeOnBehalf {
mix_identity,
delegate: self.address().into_string(),
delegate: self.owner_address().into_string(),
};
let undelegate_from_mixnode = wasm_execute(
DEFAULT_MIXNET_CONTRACT_ADDRESS,
@@ -76,17 +77,17 @@ impl DelegatingAccount for Account {
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
let delegation_key = (mix_identity.as_bytes(), block_time.seconds());
let delegations_storage_key = self.delegations_key();
let delegations: Map<(&[u8], u64), Uint128> = Map::new(&delegations_storage_key);
let new_delegation = if let Some(existing_delegation) =
self.delegations().may_load(storage, delegation_key)?
{
existing_delegation + delegation.amount
} else {
delegation.amount
};
let new_delegation =
if let Some(existing_delegation) = delegations.may_load(storage, delegation_key)? {
existing_delegation + delegation.amount
} else {
delegation.amount
};
self.delegations()
.save(storage, delegation_key, &new_delegation)?;
delegations.save(storage, delegation_key, &new_delegation)?;
let new_balance = Uint128::new(current_balance.u128() - delegation.amount.u128());
@@ -102,10 +103,11 @@ impl DelegatingAccount for Account {
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
let mix_bytes = mix_identity.as_bytes();
let delegations_key = self.delegations_key();
let delegations: Map<(&[u8], u64), Uint128> = Map::new(&delegations_key);
// Iterate over keys matching the prefix and remove them from the map
let block_times = self
.delegations()
let block_times = delegations
.prefix_de(mix_bytes)
.keys_de(storage, None, None, Order::Ascending)
// Scan will blow up on first error
@@ -113,7 +115,7 @@ impl DelegatingAccount for Account {
.collect::<Vec<u64>>();
for t in block_times {
self.delegations().remove(storage, (mix_bytes, t))
delegations.remove(storage, (mix_bytes, t))
}
let new_balance = Uint128::new(self.load_balance(storage)?.u128() + amount.amount.u128());
@@ -20,14 +20,14 @@ impl GatewayBondingAccount for Account {
if current_balance < pledge.amount {
return Err(ContractError::InsufficientBalance(
self.address.as_str().to_string(),
self.owner_address().as_str().to_string(),
current_balance.u128(),
));
}
let pledge_data = if self.load_gateway_pledge(storage)?.is_some() {
return Err(ContractError::AlreadyBonded(
self.address.as_str().to_string(),
self.owner_address().as_str().to_string(),
));
} else {
PledgeData {
@@ -38,7 +38,7 @@ impl GatewayBondingAccount for Account {
let msg = MixnetExecuteMsg::BondGatewayOnBehalf {
gateway,
owner: self.address().into_string(),
owner: self.owner_address().into_string(),
owner_signature,
};
@@ -56,7 +56,7 @@ impl GatewayBondingAccount for Account {
fn try_unbond_gateway(&self, storage: &dyn Storage) -> Result<Response, ContractError> {
let msg = MixnetExecuteMsg::UnbondGatewayOnBehalf {
owner: self.address().into_string(),
owner: self.owner_address().into_string(),
};
if let Some(_bond) = self.load_gateway_pledge(storage)? {
@@ -67,7 +67,7 @@ impl GatewayBondingAccount for Account {
.add_message(unbond_msg))
} else {
Err(ContractError::NoBondFound(
self.address.as_str().to_string(),
self.owner_address().as_str().to_string(),
))
}
}
@@ -80,7 +80,7 @@ impl GatewayBondingAccount for Account {
let new_balance = Uint128::new(self.load_balance(storage)?.u128() + amount.amount.u128());
self.save_balance(new_balance, storage)?;
self.remove_gateway_bond(storage)?;
self.remove_gateway_pledge(storage)?;
Ok(())
}
}
@@ -20,14 +20,14 @@ impl MixnodeBondingAccount for Account {
if current_balance < pledge.amount {
return Err(ContractError::InsufficientBalance(
self.address.as_str().to_string(),
self.owner_address().as_str().to_string(),
current_balance.u128(),
));
}
let pledge_data = if self.load_mixnode_pledge(storage)?.is_some() {
return Err(ContractError::AlreadyBonded(
self.address.as_str().to_string(),
self.owner_address().as_str().to_string(),
));
} else {
PledgeData {
@@ -38,7 +38,7 @@ impl MixnodeBondingAccount for Account {
let msg = MixnetExecuteMsg::BondMixnodeOnBehalf {
mix_node,
owner: self.address().into_string(),
owner: self.owner_address().into_string(),
owner_signature,
};
@@ -56,7 +56,7 @@ impl MixnodeBondingAccount for Account {
fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result<Response, ContractError> {
let msg = MixnetExecuteMsg::UnbondMixnodeOnBehalf {
owner: self.address().into_string(),
owner: self.owner_address().into_string(),
};
if self.load_mixnode_pledge(storage)?.is_some() {
@@ -67,7 +67,7 @@ impl MixnodeBondingAccount for Account {
.add_message(unbond_msg))
} else {
Err(ContractError::NoBondFound(
self.address.as_str().to_string(),
self.owner_address().as_str().to_string(),
))
}
}
@@ -80,7 +80,7 @@ impl MixnodeBondingAccount for Account {
let new_balance = Uint128::new(self.load_balance(storage)?.u128() + amount.amount.u128());
self.save_balance(new_balance, storage)?;
self.remove_mixnode_bond(storage)?;
self.remove_mixnode_pledge(storage)?;
Ok(())
}
}
+81 -46
View File
@@ -5,6 +5,8 @@ use crate::storage::save_account;
use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128};
use cw_storage_plus::{Item, Map};
use mixnet_contract::IdentityKey;
use rand::rngs::StdRng;
use rand::{RngCore, SeedableRng};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -18,44 +20,75 @@ const BALANCE_SUFFIX: &str = "ba";
const PLEDGE_SUFFIX: &str = "bo";
const GATEWAY_SUFFIX: &str = "ga";
fn generate_storage_key(b: &[u8], storage: &dyn Storage) -> Result<String, ContractError> {
let mut rng = StdRng::seed_from_u64(b.iter().fold(0, |acc, x| acc + *x as u64));
// Be paranoid and check for collisions
loop {
let key = rng.next_u64().to_string();
let balance_key = format!("{}{}", key, BALANCE_SUFFIX);
let balance: Item<Uint128> = Item::new(&balance_key);
if balance.may_load(storage)?.is_none() {
return Ok(key);
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Account {
address: Addr,
owner_address: Addr,
staking_address: Option<Addr>,
start_time: Timestamp,
periods: Vec<VestingPeriod>,
coin: Coin,
delegations_key: String,
balance_key: String,
mixnode_pledge_key: String,
gateway_pledge_key: String,
storage_key: String,
}
impl Account {
pub fn new(
address: Addr,
owner_address: Addr,
staking_address: Option<Addr>,
coin: Coin,
start_time: Timestamp,
periods: Vec<VestingPeriod>,
storage: &mut dyn Storage,
) -> Result<Self, ContractError> {
let storage_key = generate_storage_key(owner_address.as_bytes(), storage)?;
let amount = coin.amount;
let account = Account {
address: address.to_owned(),
owner_address,
staking_address,
start_time,
periods,
coin,
delegations_key: format!("{}_{}", address, DELEGATIONS_SUFFIX),
balance_key: format!("{}_{}", address, BALANCE_SUFFIX),
mixnode_pledge_key: format!("{}_{}", address, PLEDGE_SUFFIX),
gateway_pledge_key: format!("{}_{}", address, GATEWAY_SUFFIX),
storage_key,
};
save_account(&account, storage)?;
account.save_balance(amount, storage)?;
Ok(account)
}
pub fn address(&self) -> Addr {
self.address.clone()
pub fn delegations_key(&self) -> String {
format!("{}{}", self.storage_key, DELEGATIONS_SUFFIX)
}
pub fn balance_key(&self) -> String {
format!("{}{}", self.storage_key, BALANCE_SUFFIX)
}
pub fn mixnode_pledge_key(&self) -> String {
format!("{}{}", self.storage_key, PLEDGE_SUFFIX)
}
pub fn gateway_pledge_key(&self) -> String {
format!("{}{}", self.storage_key, GATEWAY_SUFFIX)
}
pub fn owner_address(&self) -> Addr {
self.owner_address.clone()
}
pub fn staking_address(&self) -> Option<&Addr> {
self.staking_address.as_ref()
}
#[allow(dead_code)]
@@ -100,10 +133,9 @@ impl Account {
}
pub fn load_balance(&self, storage: &dyn Storage) -> Result<Uint128, ContractError> {
Ok(self
.balance()
.may_load(storage)?
.unwrap_or_else(Uint128::zero))
let key = self.balance_key();
let balance = Item::new(&key);
Ok(balance.may_load(storage)?.unwrap_or_else(Uint128::zero))
}
pub fn save_balance(
@@ -111,18 +143,18 @@ impl Account {
amount: Uint128,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
Ok(self.balance().save(storage, &amount)?)
}
fn balance(&self) -> Item<Uint128> {
Item::new(self.balance_key.as_ref())
let key = self.balance_key();
let balance = Item::new(&key);
Ok(balance.save(storage, &amount)?)
}
pub fn load_mixnode_pledge(
&self,
storage: &dyn Storage,
) -> Result<Option<PledgeData>, ContractError> {
Ok(self.mixnode_pledge().may_load(storage)?)
let key = self.mixnode_pledge_key();
let mixnode_pledge = Item::new(&key);
Ok(mixnode_pledge.may_load(storage)?)
}
pub fn save_mixnode_pledge(
@@ -130,23 +162,25 @@ impl Account {
pledge: PledgeData,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
Ok(self.mixnode_pledge().save(storage, &pledge)?)
let key = self.mixnode_pledge_key();
let mixnode_pledge = Item::new(&key);
Ok(mixnode_pledge.save(storage, &pledge)?)
}
pub fn remove_mixnode_bond(&self, storage: &mut dyn Storage) -> Result<(), ContractError> {
self.mixnode_pledge().remove(storage);
pub fn remove_mixnode_pledge(&self, storage: &mut dyn Storage) -> Result<(), ContractError> {
let key = self.mixnode_pledge_key();
let mixnode_pledge: Item<PledgeData> = Item::new(&key);
mixnode_pledge.remove(storage);
Ok(())
}
fn mixnode_pledge(&self) -> Item<PledgeData> {
Item::new(self.mixnode_pledge_key.as_ref())
}
pub fn load_gateway_pledge(
&self,
storage: &dyn Storage,
) -> Result<Option<PledgeData>, ContractError> {
Ok(self.gateway_pledge().may_load(storage)?)
let key = self.gateway_pledge_key();
let gateway_pledge = Item::new(&key);
Ok(gateway_pledge.may_load(storage)?)
}
pub fn save_gateway_pledge(
@@ -154,25 +188,23 @@ impl Account {
pledge: PledgeData,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
Ok(self.gateway_pledge().save(storage, &pledge)?)
let key = self.gateway_pledge_key();
let gateway_pledge = Item::new(&key);
Ok(gateway_pledge.save(storage, &pledge)?)
}
pub fn remove_gateway_bond(&self, storage: &mut dyn Storage) -> Result<(), ContractError> {
self.gateway_pledge().remove(storage);
pub fn remove_gateway_pledge(&self, storage: &mut dyn Storage) -> Result<(), ContractError> {
let key = self.gateway_pledge_key();
let gateway_pledge: Item<PledgeData> = Item::new(&key);
gateway_pledge.remove(storage);
Ok(())
}
fn gateway_pledge(&self) -> Item<PledgeData> {
Item::new(self.gateway_pledge_key.as_ref())
}
fn delegations(&self) -> Map<(&[u8], u64), Uint128> {
Map::new(self.delegations_key.as_ref())
}
// Returns block_time part of the delegation key
pub fn delegation_keys_for_mix(&self, mix: &str, storage: &dyn Storage) -> Vec<u64> {
self.delegations()
let key = self.delegations_key();
let delegations: Map<(&[u8], u64), Uint128> = Map::new(&key);
delegations
.prefix_de(mix.as_bytes())
.keys_de(storage, None, None, Order::Ascending)
// Scan will blow up on first error
@@ -191,10 +223,12 @@ impl Account {
) -> Result<Vec<Uint128>, ContractError> {
let mix_bytes = mix.as_bytes();
let keys = self.delegation_keys_for_mix(&mix, storage);
let delegations_key = self.delegations_key();
let delegations: Map<(&[u8], u64), Uint128> = Map::new(&delegations_key);
let mut delegation_amounts = Vec::new();
for key in keys {
delegation_amounts.push(self.delegations().load(storage, (mix_bytes, key))?)
delegation_amounts.push(delegations.load(storage, (mix_bytes, key))?)
}
Ok(delegation_amounts)
@@ -213,8 +247,9 @@ impl Account {
}
pub fn total_delegations(&self, storage: &dyn Storage) -> Result<Uint128, ContractError> {
Ok(self
.delegations()
let delegations_key = self.delegations_key();
let delegations: Map<(&[u8], u64), Uint128> = Map::new(&delegations_key);
Ok(delegations
.range(storage, None, None, Order::Ascending)
.scan((), |_, x| x.ok())
.fold(Uint128::zero(), |acc, (_, x)| acc + x))
@@ -1,8 +1,10 @@
use crate::contract::NUM_VESTING_PERIODS;
use crate::errors::ContractError;
use crate::storage::{delete_account, save_account};
use crate::traits::VestingAccount;
use config::defaults::DENOM;
use cosmwasm_std::{Coin, Env, Order, Storage, Timestamp, Uint128};
use cosmwasm_std::{Addr, Coin, Env, Order, Storage, Timestamp, Uint128};
use cw_storage_plus::Map;
use super::Account;
@@ -113,9 +115,10 @@ impl VestingAccount for Account {
let period = self.get_current_vesting_period(block_time);
let max_vested = self.tokens_per_period()? * period as u128;
let start_time = self.periods[period].start_time;
let delegations_key = self.delegations_key();
let delegations: Map<(&[u8], u64), Uint128> = Map::new(&delegations_key);
let delegations_keys = self
.delegations()
let delegations_keys = delegations
.keys_de(storage, None, None, Order::Ascending)
.scan((), |_, x| x.ok())
.filter(|(_mix, block_time)| *block_time < start_time)
@@ -124,7 +127,7 @@ impl VestingAccount for Account {
let mut amount = Uint128::zero();
for (mix, block_time) in delegations_keys {
amount += self.delegations().load(storage, (&mix, block_time))?
amount += delegations.load(storage, (&mix, block_time))?
}
amount = Uint128::new(amount.u128().min(max_vested));
@@ -209,4 +212,28 @@ impl VestingAccount for Account {
})
}
}
fn transfer_ownership(
&mut self,
to_address: &Addr,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
delete_account(&self.owner_address(), storage)?;
self.owner_address = to_address.to_owned();
save_account(self, storage)?;
Ok(())
}
fn update_staking_address(
&mut self,
to_address: Option<Addr>,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
if let Some(staking_address) = self.staking_address() {
delete_account(staking_address, storage)?;
}
self.staking_address = to_address;
save_account(self, storage)?;
Ok(())
}
}
+110 -13
View File
@@ -36,35 +36,132 @@ pub fn populate_vesting_periods(start_time: u64, n: usize) -> Vec<VestingPeriod>
#[cfg(test)]
mod tests {
use crate::contract::{NUM_VESTING_PERIODS, VESTING_PERIOD};
use crate::contract::{execute, ADMIN_ADDRESS, NUM_VESTING_PERIODS, VESTING_PERIOD};
use crate::messages::ExecuteMsg;
use crate::storage::load_account;
use crate::support::tests::helpers::{init_contract, vesting_account_fixture};
use crate::traits::DelegatingAccount;
use crate::traits::VestingAccount;
use crate::traits::{GatewayBondingAccount, MixnodeBondingAccount};
use config::defaults::DENOM;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::{Addr, Coin, Timestamp, Uint128};
use cosmwasm_std::testing::{mock_env, mock_info};
use cosmwasm_std::{coins, Addr, Coin, Timestamp, Uint128};
use mixnet_contract::{Gateway, MixNode};
#[test]
fn test_account_creation() {
let mut deps = init_contract();
let env = mock_env();
let account = vesting_account_fixture(&mut deps.storage, &env);
let created_account = load_account(&account.address(), &deps.storage).unwrap();
let created_account_test =
load_account(&Addr::unchecked("fixture"), &deps.storage).unwrap();
assert_eq!(Some(&account), created_account.as_ref());
assert_eq!(Some(&account), created_account_test.as_ref());
let info = mock_info("not_admin", &coins(1_000_000_000_000, DENOM));
let msg = ExecuteMsg::CreateAccount {
owner_address: "owner".to_string(),
staking_address: Some("staking".to_string()),
start_time: None,
};
let response = execute(deps.as_mut(), env.clone(), info, msg.clone());
assert!(response.is_err());
let info = mock_info(ADMIN_ADDRESS, &coins(1_000_000_000_000, DENOM));
let _response = execute(deps.as_mut(), env.clone(), info, msg.clone());
let created_account = load_account(&Addr::unchecked("owner"), &deps.storage)
.unwrap()
.unwrap();
let created_account_test_by_staking =
load_account(&Addr::unchecked("staking"), &deps.storage)
.unwrap()
.unwrap();
assert_eq!(created_account_test_by_staking, created_account);
assert_eq!(
account.load_balance(&deps.storage).unwrap(),
created_account.load_balance(&deps.storage).unwrap(),
Uint128::new(1_000_000_000_000)
);
// Test key collision avoidance
let account_again = vesting_account_fixture(&mut deps.storage, &env);
assert_eq!(
account.load_balance(&deps.storage).unwrap(),
Uint128::new(1_000_000_000_000)
)
created_account.balance_key(),
"5032709489228919411ba".to_string()
);
assert_ne!(created_account.balance_key(), account_again.balance_key());
}
#[test]
fn test_ownership_transfer() {
let mut deps = init_contract();
let mut env = mock_env();
let info = mock_info("owner", &[]);
let account = vesting_account_fixture(&mut deps.storage, &env);
let msg = ExecuteMsg::TransferOwnership {
to_address: "new_owner".to_string(),
};
let _response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()).unwrap();
let new_owner_account = load_account(&Addr::unchecked("new_owner"), &deps.storage)
.unwrap()
.unwrap();
assert_eq!(
new_owner_account.load_balance(&deps.storage),
account.load_balance(&deps.storage)
);
// Check old account is gone
let old_owner_account = load_account(&Addr::unchecked("owner"), &deps.storage).unwrap();
assert!(old_owner_account.is_none());
// Not the owner
let response = execute(deps.as_mut(), env.clone(), info.clone(), msg);
assert!(response.is_err());
let info = mock_info("new_owner", &[]);
let msg = ExecuteMsg::UpdateStakingAddress {
to_address: Some("new_staking".to_string()),
};
let _response = execute(deps.as_mut(), env.clone(), info, msg).unwrap();
let new_staking_account = load_account(&Addr::unchecked("new_staking"), &deps.storage)
.unwrap()
.unwrap();
assert_eq!(new_staking_account.owner_address(), "new_owner".to_string());
let old_staking_account = load_account(&Addr::unchecked("staking"), &deps.storage).unwrap();
assert!(old_staking_account.is_none());
let msg = ExecuteMsg::WithdrawVestedCoins {
amount: Coin {
amount: Uint128::new(1),
denom: "nym".to_string(),
},
};
let info = mock_info("new_owner", &[]);
env.block.time = Timestamp::from_nanos(env.block.time.nanos() + 100_000_000_000_000_000);
let response = execute(deps.as_mut(), env.clone(), info, msg.clone());
assert!(response.is_ok());
let info = mock_info("owner", &[]);
let response = execute(deps.as_mut(), env.clone(), info, msg.clone());
assert!(response.is_err());
}
#[test]
fn test_staking_account() {
let mut deps = init_contract();
let mut env = mock_env();
let info = mock_info("staking", &[]);
let msg = ExecuteMsg::TransferOwnership {
to_address: "new_owner".to_string(),
};
let response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone());
// Only owner can transfer
assert!(response.is_err());
let msg = ExecuteMsg::WithdrawVestedCoins {
amount: Coin {
amount: Uint128::new(1),
denom: "nym".to_string(),
},
};
env.block.time = Timestamp::from_nanos(env.block.time.nanos() + 100_000_000_000_000_000);
let response = execute(deps.as_mut(), env.clone(), info, msg.clone());
// Only owner can withdraw
assert!(response.is_err());
}
#[test]
+2 -8
View File
@@ -5,14 +5,9 @@ use crate::commands::*;
use crate::config::{persistence::pathfinder::GatewayPathfinder, Config};
use clap::{App, Arg, ArgMatches};
use colored::Colorize;
#[cfg(feature = "coconut")]
use config::defaults::BECH32_PREFIX;
use config::NymConfig;
use crypto::asymmetric::identity;
use log::error;
use std::process;
#[cfg(feature = "coconut")]
use subtle_encoding::bech32;
#[cfg(not(feature = "coconut"))]
use validator_client::nymd::AccountId;
@@ -111,7 +106,7 @@ fn sign_derived_address(private_key: &identity::PrivateKey, address: &AccountId)
// we do tiny bit of sanity check validation
#[cfg(feature = "coconut")]
fn print_signed_address(private_key: &identity::PrivateKey, raw_address: &str) -> String {
fn sign_provided_address(private_key: &identity::PrivateKey, raw_address: &str) {
let trimmed = raw_address.trim();
validate_bech32_address_or_exit(trimmed);
let signature = private_key.sign_text(trimmed);
@@ -119,8 +114,7 @@ fn print_signed_address(private_key: &identity::PrivateKey, raw_address: &str) -
println!(
"The base58-encoded signature on '{}' is: {}",
trimmed, signature
);
signature
)
}
fn print_signed_text(private_key: &identity::PrivateKey, text: &str) {
+2
View File
@@ -5188,7 +5188,9 @@ dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"getrandom 0.2.3",
"mixnet-contract",
"rand 0.8.4",
"schemars",
"serde",
"thiserror",
+1 -1
View File
@@ -30,7 +30,7 @@ eyre = "0.6.5"
cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev="e5a1872083abb3d88fa62dda966e7f5408deba58", features = ["rpc", "bip32", "cosmwasm"] }
cosmwasm-std = "1.0.0-beta2"
cosmwasm-std = "1.0.0-beta3"
validator-client = { path = "../../common/client-libs/validator-client", features = [
"nymd-client",