Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

SNIP-24 - Query Permits #22

Merged
merged 41 commits into from
Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
cd78f1e
Working prototype :tada:
assafmo Sep 25, 2021
e299b4d
Validate permit message
assafmo Sep 25, 2021
7065602
Rename account to query_balance_of
assafmo Sep 25, 2021
4dd5088
Fix permit's content serialization to bytes
assafmo Sep 26, 2021
468eda9
Update make start-server
assafmo Sep 26, 2021
0582c62
Refactor, make permit API UX better
assafmo Sep 26, 2021
476c043
remain::sorted on SignedPermit
assafmo Sep 26, 2021
2365946
Query with permit for all authenticated queries
assafmo Sep 26, 2021
7d8a365
Refactor some var/func names
assafmo Sep 26, 2021
e3ac874
Implement revoke permit
assafmo Sep 26, 2021
b1f3d46
Clean makefile
assafmo Sep 26, 2021
ca14f16
Refactor RevokedPermits storage
assafmo Sep 27, 2021
7c0d408
:shrug:
assafmo Sep 27, 2021
a8dace9
simplified permit query interface
reuvenpo Sep 27, 2021
c3071e9
Fix permit for query_allowance
assafmo Sep 29, 2021
d986d2e
Rename struct SignedPermit -> PermitParams
assafmo Sep 29, 2021
8a8d419
A small refactor
assafmo Sep 29, 2021
2323bf4
Code comment rephrase
assafmo Sep 29, 2021
e14e857
cargo schema
reuvenpo Sep 29, 2021
3844f6c
Simplify return logic of is_permit_revoked()
assafmo Sep 29, 2021
934452e
Merge branch 'query-balance-prmit' of github.com:enigmampc/snip20-ref…
assafmo Sep 29, 2021
38bc240
Merge pull request #23 from enigmampc/query-balance-prmit-2
assafmo Sep 29, 2021
37b191f
Fix merge conflict
assafmo Sep 29, 2021
1ce2de6
Rename query_with_permit() -> permit_queries()
assafmo Sep 29, 2021
716ee8a
Rename permit.signed -> permit.params
assafmo Sep 29, 2021
7af1d05
cargo schema
reuvenpo Sep 29, 2021
736a0d9
Query permit permissions
assafmo Sep 29, 2021
128e3f7
Add prefix for RevokedPemits storage keys
assafmo Oct 4, 2021
c9b53b5
Test permits for wrong token
assafmo Oct 8, 2021
59d61c7
Permit: make errors more readable for humans
assafmo Oct 13, 2021
7a27a75
Permits: test every failure scenario
assafmo Oct 13, 2021
0384401
Permit: test query balance
assafmo Oct 13, 2021
60b4c53
Permit: test query history & allowance
assafmo Oct 13, 2021
8e16bb4
Restore all the other tests
assafmo Oct 13, 2021
0400dcb
Move permit operations to secret-toolkit
assafmo Oct 14, 2021
942debd
Fix secret-toolkit dependency
assafmo Oct 17, 2021
ea27639
Pass storage key prefix to RevokedPemits
assafmo Oct 18, 2021
caa8ab7
RevokedPemits -> RevokedPermits
assafmo Oct 18, 2021
b2f764e
Use cosmwasm-std v1.0.0
assafmo Oct 18, 2021
bae405b
Update secret-toolkit
assafmo Oct 19, 2021
2d1d606
cargo schema
assafmo Oct 19, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ name = "snip20-reference-impl"
version = "0.1.0"
authors = ["Itzik <[email protected]>"]
edition = "2018"

exclude = [
# Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
"contract.wasm",
"hash.txt",
]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib", "rlib"]

Expand Down Expand Up @@ -44,10 +42,11 @@ bincode2 = "2.0.1"
subtle = { version = "2.2.3", default-features = false }
base64 = "0.12.3"
hex = "0.4.2"

rand_chacha = { version = "0.2.2", default-features = false }
rand_core = { version = "0.5.1", default-features = false }
rand_core = { version = "0.5.1", default-features = false }
sha2 = { version = "0.9.1", default-features = false }
ripemd160 = "0.9.1"
secp256k1 = "0.19.0"

[dev-dependencies]
cosmwasm-schema = { version = "0.9.2" }
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ contract.wasm.gz: contract.wasm
.PHONY: start-server
start-server: # CTRL+C to stop
docker run -it --rm \
-p 26657:26657 -p 26656:26656 -p 1317:1317 \
-p 26657:26657 -p 26656:26656 -p 1337:1337 \
-v $$(pwd):/root/code \
--name secretdev enigmampc/secret-network-sw-dev:v1.0.4-3
--name secretdev enigmampc/secret-network-sw-dev:latest
assafmo marked this conversation as resolved.
Show resolved Hide resolved

.PHONY: schema
schema:
Expand Down
105 changes: 105 additions & 0 deletions src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@ use cosmwasm_std::{
HandleResponse, HumanAddr, InitResponse, Querier, QueryResult, ReadonlyStorage, StdError,
StdResult, Storage, Uint128,
};
use ripemd160::{Digest, Ripemd160};
use secp256k1::Secp256k1;
use sha2::Sha256;

use crate::batch;
use crate::msg::{
space_pad, ContractStatusLevel, HandleAnswer, HandleMsg, InitMsg, QueryAnswer, QueryMsg,
ResponseStatus::Success,
};
use crate::msg::{Signature, SignedPermit};
use crate::rand::sha_256;
use crate::receiver::Snip20ReceiveMsg;
use crate::state::{
get_receiver_hash, read_allowance, read_viewing_key, set_receiver_hash, write_allowance,
write_viewing_key, Balances, Config, Constants, ReadonlyBalances, ReadonlyConfig,
QUERY_BALANCE_PERMIT_MSG,
};
use crate::transaction_history::{
get_transfers, get_txs, store_burn, store_deposit, store_mint, store_redeem, store_transfer,
Expand Down Expand Up @@ -90,6 +95,7 @@ pub fn init<S: Storage, A: Api, Q: Querier>(
redeem_is_enabled: init_config.redeem_enabled(),
mint_is_enabled: init_config.mint_enabled(),
burn_is_enabled: init_config.burn_enabled(),
query_balance_permit_msg: QUERY_BALANCE_PERMIT_MSG.to_string(),
assafmo marked this conversation as resolved.
Show resolved Hide resolved
})?;
config.set_total_supply(total_supply);
config.set_contract_status(ContractStatusLevel::NormalRun);
Expand Down Expand Up @@ -242,10 +248,103 @@ pub fn query<S: Storage, A: Api, Q: Querier>(deps: &Extern<S, A, Q>, msg: QueryM
QueryMsg::ContractStatus {} => query_contract_status(&deps.storage),
QueryMsg::ExchangeRate {} => query_exchange_rate(&deps.storage),
QueryMsg::Minters { .. } => query_minters(deps),
QueryMsg::BalanceWithPermit { signed, signature } => {
query_balance_with_permit(deps, signed, signature)
}
_ => authenticated_queries(deps, msg),
}
}

fn query_balance_with_permit<S: Storage, A: Api, Q: Querier>(
deps: &Extern<S, A, Q>,
signed: SignedPermit,
signature: Signature,
) -> Result<Binary, StdError> {
if signed.msgs.len() != 1 {
return Err(StdError::generic_err(format!(
"Must sign exactly 1 permit message, got: {:?}.",
signed.msgs.len()
)));
}

if signed.msgs[0].r#type != "query_balance_permit" {
return Err(StdError::generic_err(format!(
"Type must be 'query_balance_permit', got: {:?}.",
signed.msgs[0].r#type
)));
}

let permit = &signed.msgs[0].value;

// Validate permit_user_id
let _permit_user_id = &permit.permit_user_id;
// TODO fail if permit_user_id is revoked

// Validate permit message
let query_balance_permit_msg = ReadonlyConfig::from_storage(&deps.storage)
.constants()?
.query_balance_permit_msg;
if permit.message != query_balance_permit_msg {
return Err(StdError::generic_err(format!(
"Message must be '{:?}', got: '{:?}'.",
query_balance_permit_msg, permit.message
)));
}

// Validate that account is derived from pubkey
let pubkey = signature.pub_key.value;
let account_from_pubkey = deps.api.human_address(&pubkey_to_account(&pubkey))?;

if account_from_pubkey != permit.query_balance_of {
return Err(StdError::generic_err(format!(
"Input account {:?} not the same as account {:?} that was derived from pubkey {:?}.",
permit.query_balance_of,
account_from_pubkey,
pubkey.clone()
)));
}
assafmo marked this conversation as resolved.
Show resolved Hide resolved

// Validate signature, source: https://github.com/enigmampc/SecretNetwork/blob/master/cosmwasm/packages/wasmi-runtime/src/crypto/secp256k1.rs#L49-L82
let signed_bytes = &to_binary(&signed)?.0;
assafmo marked this conversation as resolved.
Show resolved Hide resolved

let signed_bytes_hash = Sha256::digest(signed_bytes);
let msg = secp256k1::Message::from_slice(signed_bytes_hash.as_slice()).map_err(|err| {
StdError::generic_err(format!(
"Failed to create a secp256k1 message from signed_bytes: {:?}",
err
))
})?;

let verifier = Secp256k1::verification_only();

// Create `secp256k1`'s types
let secp256k1_signature = secp256k1::Signature::from_compact(&signature.signature.0)
.map_err(|err| StdError::generic_err(format!("Malformed signature: {:?}", err)))?;
let secp256k1_pubkey = secp256k1::PublicKey::from_slice(pubkey.0.as_slice())
.map_err(|err| StdError::generic_err(format!("Malformed pubkey: {:?}", err)))?;

verifier
.verify(&msg, &secp256k1_signature, &secp256k1_pubkey)
.map_err(|err| {
StdError::generic_err(format!(
"Failed to verify signatures for the given permit: {:?}",
err
))
})?;
reuvenpo marked this conversation as resolved.
Show resolved Hide resolved

let amount = Uint128(
ReadonlyBalances::from_storage(&deps.storage)
.account_amount(&deps.api.canonical_address(&permit.query_balance_of)?),
);
to_binary(&QueryAnswer::Balance { amount })
}

fn pubkey_to_account(pubkey: &Binary) -> CanonicalAddr {
let mut hasher = Ripemd160::new();
hasher.update(Sha256::digest(&pubkey.0));
CanonicalAddr(Binary(hasher.finalize().to_vec()))
}
reuvenpo marked this conversation as resolved.
Show resolved Hide resolved

pub fn authenticated_queries<S: Storage, A: Api, Q: Querier>(
deps: &Extern<S, A, Q>,
msg: QueryMsg,
Expand Down Expand Up @@ -327,6 +426,7 @@ fn query_token_info<S: ReadonlyStorage>(storage: &S) -> QueryResult {
symbol: constants.symbol,
decimals: constants.decimals,
total_supply,
query_balance_permit_msg: constants.query_balance_permit_msg,
})
}

Expand Down Expand Up @@ -3609,11 +3709,16 @@ mod tests {
symbol,
decimals,
total_supply,
query_balance_permit_msg,
} => {
assert_eq!(name, init_name);
assert_eq!(symbol, init_symbol);
assert_eq!(decimals, init_decimals);
assert_eq!(total_supply, Some(Uint128(5000)));
assert_eq!(
query_balance_permit_msg,
QUERY_BALANCE_PERMIT_MSG.to_string()
);
}
_ => panic!("unexpected"),
}
Expand Down
71 changes: 71 additions & 0 deletions src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,10 @@ pub enum QueryMsg {
address: HumanAddr,
key: String,
},
BalanceWithPermit {
signed: SignedPermit,
signature: Signature,
},
TransferHistory {
address: HumanAddr,
key: String,
Expand Down Expand Up @@ -365,6 +369,7 @@ pub enum QueryAnswer {
symbol: String,
decimals: u8,
total_supply: Option<Uint128>,
query_balance_permit_msg: String,
},
TokenConfig {
public_total_supply: bool,
Expand Down Expand Up @@ -456,6 +461,72 @@ pub fn space_pad(block_size: usize, message: &mut Vec<u8>) -> &mut Vec<u8> {
message
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct Signature {
pub pub_key: PubKey,
pub signature: Binary,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct PubKey {
assafmo marked this conversation as resolved.
Show resolved Hide resolved
/// ignored, but must be "tendermint/PubKeySecp256k1"
pub r#type: String,
assafmo marked this conversation as resolved.
Show resolved Hide resolved
/// Secp256k1 PubKey
pub value: Binary,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
// Note: The order of fields in this struct is important for the permit signature verification!
pub struct SignedPermit {
/// ignored
pub account_number: Uint128,
/// ignored, no Env in query
pub chain_id: String,
/// ignored
pub fee: Fee,
/// ignored
pub memo: String,
/// the signed message
pub msgs: Vec<PermitMsg>,
/// ignored
pub sequence: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
// Note: The order of fields in this struct is important for the permit signature verification!
pub struct Fee {
pub amount: Vec<Coin>,
pub gas: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
// Note: The order of fields in this struct is important for the permit signature verification!
pub struct Coin {
pub amount: Uint128,
pub denom: String,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
// Note: The order of fields in this struct is important for the permit signature verification!
pub struct PermitMsg {
pub r#type: String,
pub value: PermitContent,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
// Note: The order of fields in this struct is important for the permit signature verification!
pub struct PermitContent {
pub message: String,
pub permit_user_id: String,
pub query_balance_of: HumanAddr,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
3 changes: 3 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ pub const PREFIX_ALLOWANCES: &[u8] = b"allowances";
pub const PREFIX_VIEW_KEY: &[u8] = b"viewingkey";
pub const PREFIX_RECEIVERS: &[u8] = b"receivers";

pub const QUERY_BALANCE_PERMIT_MSG: &str = "This signature is a permit to query my balance.";

// Config

#[derive(Serialize, Debug, Deserialize, Clone, PartialEq, JsonSchema)]
Expand All @@ -47,6 +49,7 @@ pub struct Constants {
pub mint_is_enabled: bool,
// is burn enabled
pub burn_is_enabled: bool,
pub query_balance_permit_msg: String,
}

pub struct ReadonlyConfig<'a, S: ReadonlyStorage> {
Expand Down