From 325a34e94a5d0cad921b2e295f66bb8e2ec384cc Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 17 Dec 2024 13:22:41 +1300 Subject: [PATCH 01/18] dev: datetime util for parsing ISO 8601 date-time UTC+0 string to Timestamp --- packages/utils/src/datetime.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 packages/utils/src/datetime.rs diff --git a/packages/utils/src/datetime.rs b/packages/utils/src/datetime.rs new file mode 100644 index 0000000..0379408 --- /dev/null +++ b/packages/utils/src/datetime.rs @@ -0,0 +1,23 @@ +use chrono::{DateTime, Utc}; +use cosmwasm_std::{StdError, StdResult, Timestamp}; + + +/// Converts an ISO 8601 date-time string in Zulu (UTC+0) format to a Timestamp. +/// +/// String format: {YYYY}-{MM}-{DD}T{hh}:{mm}:{ss}.{uuu}Z +/// +/// * `iso8601_str` - The ISO 8601 date-time string to convert. +/// +/// Returns StdResult +pub fn iso8601_utc0_to_timestamp(iso8601_str: &str) -> StdResult { + let Ok(datetime) = iso8601_str.parse::>() else { + return Err(StdError::generic_err("ISO 8601 string could not be parsed")); + }; + + // Verify the timezone is UTC (Zulu time) + if iso8601_str.ends_with("Z") { + Ok(Timestamp::from_seconds(datetime.timestamp().try_into().unwrap_or_default())) + } else { + Err(StdError::generic_err("ISO 8601 string not in Zulu (UTC+0)")) + } +} \ No newline at end of file From 0e1643a2c8cee87ead38da0d263aab8c43b48197 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 17 Dec 2024 13:23:20 +1300 Subject: [PATCH 02/18] dev: blanket permits --- packages/permit/Cargo.toml | 7 +- packages/permit/src/funcs.rs | 105 ++++++++++++++++++++++--- packages/permit/src/state.rs | 137 ++++++++++++++++++++++++++++++--- packages/permit/src/structs.rs | 10 ++- 4 files changed, 236 insertions(+), 23 deletions(-) diff --git a/packages/permit/Cargo.toml b/packages/permit/Cargo.toml index 34ca782..eefe594 100644 --- a/packages/permit/Cargo.toml +++ b/packages/permit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-permit" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -20,6 +20,9 @@ ripemd = { version = "0.1.3", default-features = false } schemars = { workspace = true } bech32 = "0.9.1" remain = "0.2.8" -secret-toolkit-crypto = { version = "0.10.2", path = "../crypto", features = [ +secret-toolkit-crypto = { version = "0.10.3", path = "../crypto", features = [ "hash", ] } +secret-toolkit-utils = { version = "0.10.3", path = "../utils" } +secret-toolkit-storage = { version = "0.10.3", path = "../storage" } + diff --git a/packages/permit/src/funcs.rs b/packages/permit/src/funcs.rs index 2386d00..efff6b6 100644 --- a/packages/permit/src/funcs.rs +++ b/packages/permit/src/funcs.rs @@ -1,20 +1,36 @@ -use cosmwasm_std::{to_binary, Binary, CanonicalAddr, Deps, StdError, StdResult}; +use std::u64; + +use cosmwasm_std::{to_binary, Binary, CanonicalAddr, Deps, Env, StdError, StdResult, Timestamp}; use ripemd::{Digest, Ripemd160}; +use secret_toolkit_utils::iso8601_utc0_to_timestamp; -use crate::{Permissions, Permit, RevokedPermits, SignedPermit}; +use crate::{Permissions, Permit, RevokedPermits, RevokedPermitsStore, SignedPermit, BLANKET_PERMIT_TOKEN}; use bech32::{ToBase32, Variant}; use secret_toolkit_crypto::sha_256; pub fn validate( deps: Deps, - storage_prefix: &str, + env: Env, permit: &Permit, current_token_address: String, hrp: Option<&str>, ) -> StdResult { let account_hrp = hrp.unwrap_or("secret"); - if !permit.check_token(¤t_token_address) { + if permit.params.allowed_tokens.contains(&BLANKET_PERMIT_TOKEN.to_string()) { + // using blanket permit + + // assert allowed_tokens list has an exact length of 1 + if permit.params.allowed_tokens.len() != 1 { + return Err(StdError::generic_err("Blanket permits cannot contain other allowed tokens")); + } + + // assert created field is specified + if permit.params.created.is_none() { + return Err(StdError::generic_err("Blanket permits must have a `created` time")); + } + } else if !permit.check_token(¤t_token_address) { + // check that current token address is in allowed tokens return Err(StdError::generic_err(format!( "Permit doesn't apply to token {:?}, allowed tokens: {:?}", current_token_address.as_str(), @@ -27,16 +43,75 @@ pub fn validate( ))); } + // Convert the permit created field to a Timestamp + let created_timestamp = permit.params.created.clone() + .map(|created| + iso8601_utc0_to_timestamp(&created) + ) + .transpose()?; + + if let Some(created) = created_timestamp { + // Verify that the permit was not created after the current block time + if created > env.block.time { + return Err(StdError::generic_err("Permit `created` after current block time")); + } + } + + // Convert the permit expires field to a Timestamp + let expires_timestamp = permit.params.expires.clone() + .map(|created| + iso8601_utc0_to_timestamp(&created) + ) + .transpose()?; + + if let Some(expires) = expires_timestamp { + // Verify that the permit did not expire before the current block time + if expires <= env.block.time { + return Err(StdError::generic_err("Permit has expired")) + } + } + // Derive account from pubkey let pubkey = &permit.signature.pub_key.value; let base32_addr = pubkey_to_account(pubkey).0.as_slice().to_base32(); let account: String = bech32::encode(account_hrp, base32_addr, Variant::Bech32).unwrap(); + // Get the list of all revocations for this address + let revocations = RevokedPermits::list_revocations(deps.storage, &account)?; + + // Check if there are any revocation intervals blocking all permits + // TODO: An interval or segment tree might be preferable to make this more efficient for cases + // when the number of revocations is allowed to grow to a large amount. + for revocation in revocations { + // If this revocation has no `created_before` or `created_after`, then reject all permit queries + if revocation.interval.created_before.is_none() && revocation.interval.created_after.is_none() { + return Err(StdError::generic_err( + format!("Permits revoked by {:?}", account.as_str()) + )); + } + + // If the permit has a `created` field + if let Some(created) = created_timestamp { + // Revocation created before field, default 0 + let created_before = revocation.interval.created_before.unwrap_or(Timestamp::from_nanos(0)); + + // Revocation created after field, default max u64 + let created_after = revocation.interval.created_after.unwrap_or(Timestamp::from_nanos(u64::MAX)); + + // If the permit's `created` field falls in between created after and created before, then reject it + if created > created_after || created < created_before { + return Err(StdError::generic_err( + format!("Permits created at {:?} revoked by account {:?}", created, account.as_str()) + )); + } + } + } + // Validate permit_name let permit_name = &permit.params.permit_name; let is_permit_revoked = - RevokedPermits::is_permit_revoked(deps.storage, storage_prefix, &account, permit_name); + RevokedPermits::is_permit_revoked(deps.storage, &account, permit_name); if is_permit_revoked { return Err(StdError::generic_err(format!( "Permit {:?} was revoked by account {:?}", @@ -73,7 +148,7 @@ pub fn pubkey_to_account(pubkey: &Binary) -> CanonicalAddr { mod tests { use super::*; use crate::{PermitParams, PermitSignature, PubKey, TokenPermissions}; - use cosmwasm_std::testing::mock_dependencies; + use cosmwasm_std::testing::{mock_dependencies, mock_env}; #[test] fn test_verify_permit() { @@ -88,7 +163,9 @@ mod tests { allowed_tokens: vec![token.clone()], permit_name: "memo_secret1rf03820fp8gngzg2w02vd30ns78qkc8rg8dxaq".to_string(), chain_id: "pulsar-2".to_string(), - permissions: vec![TokenPermissions::History] + permissions: vec![TokenPermissions::History], + created: None, + expires: None, }, signature: PermitSignature { pub_key: PubKey { @@ -99,9 +176,11 @@ mod tests { } }; + let env = mock_env(); + let address = validate::<_>( deps.as_ref(), - "test", + env, &permit, token.clone(), Some("secret"), @@ -113,7 +192,15 @@ mod tests { "secret1399pyvvk3hvwgxwt3udkslsc5jl3rqv4yshfrl".to_string() ); - let address = validate::<_>(deps.as_ref(), "test", &permit, token, Some("cosmos")).unwrap(); + let env = mock_env(); + + let address = validate::<_>( + deps.as_ref(), + env, + &permit, + token, + Some("cosmos") + ).unwrap(); assert_eq!( address, diff --git a/packages/permit/src/state.rs b/packages/permit/src/state.rs index 9a47e93..140ad46 100644 --- a/packages/permit/src/state.rs +++ b/packages/permit/src/state.rs @@ -1,32 +1,147 @@ -use cosmwasm_std::Storage; +use cosmwasm_std::{StdError, StdResult, Storage, Timestamp, Uint64}; +use schemars::JsonSchema; +use secret_toolkit_storage::{Item, Keymap}; +use serde::{Deserialize, Serialize}; +/// This is the default implementation of the revoked permits store, using the "revoked_permits" +/// storage prefix for named permits and "all_revoked_permits" for revoked blanket permits. +/// It also sets the maximum number of all permit revocations to 10 by default. +/// +/// You can use another storage location by implementing `RevokedPermitsStore` for your own type. pub struct RevokedPermits; -impl RevokedPermits { - pub fn is_permit_revoked( - storgae: &dyn Storage, - storage_prefix: &str, +impl<'a> RevokedPermitsStore<'a> for RevokedPermits { + const NAMED_REVOKED_PERMITS_PREFIX: &'static [u8] = b"revoked_permits"; + const ALL_REVOKED_PERMITS: Keymap<'a, u64, AllRevokedInterval> = Keymap::new(b"all_revoked_permits"); + const ALL_REVOKED_NEXT_ID: Item<'a, u64> = Item::new(b"all_revoked_permits_serial_id"); + const MAX_ALL_REVOKED_INTERVALS: u8 = 10; +} + +/// A trait describing the interface of a RevokedPermits store/vault. +/// +/// It includes a default implementation that only requires specifying where in the storage +/// the keys should be held. +pub trait RevokedPermitsStore<'a> { + const NAMED_REVOKED_PERMITS_PREFIX: &'static [u8]; + const ALL_REVOKED_PERMITS: Keymap<'a, u64, AllRevokedInterval>; + const ALL_REVOKED_NEXT_ID: Item<'a, u64>; + const MAX_ALL_REVOKED_INTERVALS: u8; + + /// returns a bool indicating if a named permit is revoked + fn is_permit_revoked( + storage: &dyn Storage, account: &str, permit_name: &str, ) -> bool { - let storage_key = storage_prefix.to_string() + account + permit_name; + let mut storage_key = Vec::new(); + storage_key.extend_from_slice(Self::NAMED_REVOKED_PERMITS_PREFIX); + storage_key.extend_from_slice(account.as_bytes()); + storage_key.extend_from_slice(permit_name.as_bytes()); - storgae.get(storage_key.as_bytes()).is_some() + storage.get(&storage_key).is_some() } - pub fn revoke_permit( + /// revokes a named permit permanently + fn revoke_permit( storage: &mut dyn Storage, - storage_prefix: &str, account: &str, permit_name: &str, ) { - let storage_key = storage_prefix.to_string() + account + permit_name; + let mut storage_key = Vec::new(); + storage_key.extend_from_slice(Self::NAMED_REVOKED_PERMITS_PREFIX); + storage_key.extend_from_slice(account.as_bytes()); + storage_key.extend_from_slice(permit_name.as_bytes()); // Since cosmwasm V1.0 it's not possible to set an empty value, hence set some unimportant // character '_' // // Here is the line of the new panic that was added when trying to insert an empty value: // https://github.com/scrtlabs/cosmwasm/blob/f7e2b1dbf11e113e258d796288752503a5012367/packages/std/src/storage.rs#L30 - storage.set(storage_key.as_bytes(), "_".as_bytes()) + storage.set(&storage_key, "_".as_bytes()) + } + + /// revokes all permits created after and before + fn revoke_all_permits( + storage: &mut dyn Storage, + account: &str, + interval: &AllRevokedInterval, + ) -> StdResult { + // get the revocations store for this account + let all_revocations_store = Self::ALL_REVOKED_PERMITS.add_suffix(account.as_bytes()); + + // check that maximum number of revocations has not been met + if all_revocations_store.get_len(storage)? >= Self::MAX_ALL_REVOKED_INTERVALS.into() { + return Err(StdError::generic_err( + format!("Maximum number of permit revocations ({}) has been met", Self::MAX_ALL_REVOKED_INTERVALS) + )); + } + + // get the next id store for this account + let next_id_store = Self::ALL_REVOKED_NEXT_ID.add_suffix(account.as_bytes()); + + // get the next id + let next_id = next_id_store.may_load(storage)?.unwrap_or_default(); + + // store the revocation + all_revocations_store.insert(storage, &next_id, interval)?; + + // increment next id + next_id_store.save(storage, &(next_id.wrapping_add(1)))?; + + Ok(Uint64::from(next_id)) } + + /// deletes the permit revocation with the given id for this account + fn delete_revocation( + storage: &mut dyn Storage, + account: &str, + id: Uint64, + ) -> StdResult<()> { + // get the revocations store for this account + let all_revocations_store = Self::ALL_REVOKED_PERMITS.add_suffix(account.as_bytes()); + + // remove the permit revocation with the given id + all_revocations_store.remove(storage, &id.u64()) + } + + /// lists all the revocations for the account + /// returns a vec of revocations + fn list_revocations( + storage: &dyn Storage, + account: &str, + ) -> StdResult> { + // get the revocations store for this account + let all_revocations_store = Self::ALL_REVOKED_PERMITS.add_suffix(account.as_bytes()); + + // select elements and convert to AllRevocation structs + let result = all_revocations_store + .iter(storage)? + .filter_map(|r| { + match r { + Ok(r) => Some(AllRevocation { + id: Uint64::from(r.0), + interval: r.1.clone() + }), + Err(_) => None + } + }) + .collect(); + + Ok(result) + } + } + +/// An interval over which all permits will be rejected +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct AllRevokedInterval { + pub created_before: Option, + pub created_after: Option, +} + +/// Revocation id and interval data struct +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct AllRevocation { + pub id: Uint64, + pub interval: AllRevokedInterval, +} \ No newline at end of file diff --git a/packages/permit/src/structs.rs b/packages/permit/src/structs.rs index 9c03c41..50e71eb 100644 --- a/packages/permit/src/structs.rs +++ b/packages/permit/src/structs.rs @@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize}; use crate::pubkey_to_account; use cosmwasm_std::{Binary, CanonicalAddr, Uint128}; +pub const BLANKET_PERMIT_TOKEN: &str = "ANY_TOKEN"; + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct Permit { @@ -16,7 +18,7 @@ pub struct Permit { impl Permit { pub fn check_token(&self, token: &str) -> bool { - self.params.allowed_tokens.contains(&token.to_string()) + self.params.allowed_tokens.contains(&token.to_string()) } pub fn check_permission(&self, permission: &Permission) -> bool { @@ -32,6 +34,8 @@ pub struct PermitParams { pub chain_id: String, #[serde(bound = "")] pub permissions: Vec, + pub created: Option, + pub expires: Option, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] @@ -162,6 +166,8 @@ impl PermitMsg { #[serde(rename_all = "snake_case")] pub struct PermitContent { pub allowed_tokens: Vec, + pub created: Option, + pub expires: Option, #[serde(bound = "")] pub permissions: Vec, pub permit_name: String, @@ -171,6 +177,8 @@ impl PermitContent { pub fn from_params(params: &PermitParams) -> Self { Self { allowed_tokens: params.allowed_tokens.clone(), + created: params.created.clone(), + expires: params.expires.clone(), permit_name: params.permit_name.clone(), permissions: params.permissions.clone(), } From 0cb991a7fef1d14cd26e6454927a959b65063a79 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 17 Dec 2024 13:23:48 +1300 Subject: [PATCH 03/18] bump to version 0.10.3 --- Cargo.toml | 22 +++++++++++----------- packages/crypto/Cargo.toml | 2 +- packages/incubator/Cargo.toml | 4 ++-- packages/notification/Cargo.toml | 4 ++-- packages/serialization/Cargo.toml | 2 +- packages/snip20/Cargo.toml | 4 ++-- packages/snip721/Cargo.toml | 4 ++-- packages/storage/Cargo.toml | 4 ++-- packages/utils/Cargo.toml | 4 +++- packages/utils/src/lib.rs | 2 ++ packages/viewing_key/Cargo.toml | 6 +++--- 11 files changed, 31 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 548c708..d900d80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "LICENSE" @@ -37,16 +37,16 @@ viewing-key = ["secret-toolkit-viewing-key"] notification = ["secret-toolkit-notification"] [dependencies] -secret-toolkit-crypto = { version = "0.10.2", path = "packages/crypto", optional = true } -secret-toolkit-incubator = { version = "0.10.2", path = "packages/incubator", optional = true } -secret-toolkit-permit = { version = "0.10.2", path = "packages/permit", optional = true } -secret-toolkit-serialization = { version = "0.10.2", path = "packages/serialization", optional = true } -secret-toolkit-snip20 = { version = "0.10.2", path = "packages/snip20", optional = true } -secret-toolkit-snip721 = { version = "0.10.2", path = "packages/snip721", optional = true } -secret-toolkit-storage = { version = "0.10.2", path = "packages/storage", optional = true } -secret-toolkit-utils = { version = "0.10.2", path = "packages/utils", optional = true } -secret-toolkit-viewing-key = { version = "0.10.2", path = "packages/viewing_key", optional = true } -secret-toolkit-notification = { version = "0.10.2", path = "packages/notification", optional = true } +secret-toolkit-crypto = { version = "0.10.3", path = "packages/crypto", optional = true } +secret-toolkit-incubator = { version = "0.10.3", path = "packages/incubator", optional = true } +secret-toolkit-permit = { version = "0.10.3", path = "packages/permit", optional = true } +secret-toolkit-serialization = { version = "0.10.3", path = "packages/serialization", optional = true } +secret-toolkit-snip20 = { version = "0.10.3", path = "packages/snip20", optional = true } +secret-toolkit-snip721 = { version = "0.10.3", path = "packages/snip721", optional = true } +secret-toolkit-storage = { version = "0.10.3", path = "packages/storage", optional = true } +secret-toolkit-utils = { version = "0.10.3", path = "packages/utils", optional = true } +secret-toolkit-viewing-key = { version = "0.10.3", path = "packages/viewing_key", optional = true } +secret-toolkit-notification = { version = "0.10.3", path = "packages/notification", optional = true } [workspace] members = ["packages/*"] diff --git a/packages/crypto/Cargo.toml b/packages/crypto/Cargo.toml index 6dfef7c..7c1b0ae 100644 --- a/packages/crypto/Cargo.toml +++ b/packages/crypto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-crypto" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" diff --git a/packages/incubator/Cargo.toml b/packages/incubator/Cargo.toml index 53f0ac8..f0dbb0d 100644 --- a/packages/incubator/Cargo.toml +++ b/packages/incubator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-incubator" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -16,7 +16,7 @@ all-features = true [dependencies] serde = { workspace = true, optional = true } cosmwasm-std = { workspace = true, optional = true } -secret-toolkit-serialization = { version = "0.10.2", path = "../serialization", optional = true } +secret-toolkit-serialization = { version = "0.10.3", path = "../serialization", optional = true } [features] generational-store = ["secret-toolkit-serialization", "serde", "cosmwasm-std"] diff --git a/packages/notification/Cargo.toml b/packages/notification/Cargo.toml index 861ec81..0098cd9 100644 --- a/packages/notification/Cargo.toml +++ b/packages/notification/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-notification" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["darwinzer0","blake-regalia"] license-file = "../../LICENSE" @@ -30,6 +30,6 @@ primitive-types = { version = "0.12.2", default-features = false } hex = "0.4.3" minicbor = "0.25.1" -secret-toolkit-crypto = { version = "0.10.2", path = "../crypto", features = [ +secret-toolkit-crypto = { version = "0.10.3", path = "../crypto", features = [ "hash", "hkdf" ] } diff --git a/packages/serialization/Cargo.toml b/packages/serialization/Cargo.toml index 2174c7f..5c6fea6 100644 --- a/packages/serialization/Cargo.toml +++ b/packages/serialization/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-serialization" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" diff --git a/packages/snip20/Cargo.toml b/packages/snip20/Cargo.toml index 3b3da51..8c164ea 100644 --- a/packages/snip20/Cargo.toml +++ b/packages/snip20/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-snip20" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -17,4 +17,4 @@ all-features = true serde = { workspace = true } schemars = { workspace = true } cosmwasm-std = { workspace = true } -secret-toolkit-utils = { version = "0.10.2", path = "../utils" } +secret-toolkit-utils = { version = "0.10.3", path = "../utils" } diff --git a/packages/snip721/Cargo.toml b/packages/snip721/Cargo.toml index 7a4f043..80157ec 100644 --- a/packages/snip721/Cargo.toml +++ b/packages/snip721/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-snip721" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -17,4 +17,4 @@ all-features = true serde = { workspace = true } schemars = { workspace = true } cosmwasm-std = { workspace = true } -secret-toolkit-utils = { version = "0.10.2", path = "../utils" } +secret-toolkit-utils = { version = "0.10.3", path = "../utils" } diff --git a/packages/storage/Cargo.toml b/packages/storage/Cargo.toml index 6924d50..3a5843d 100644 --- a/packages/storage/Cargo.toml +++ b/packages/storage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-storage" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -17,4 +17,4 @@ all-features = true serde = { workspace = true } cosmwasm-std = { workspace = true } cosmwasm-storage = { workspace = true } -secret-toolkit-serialization = { version = "0.10.2", path = "../serialization" } +secret-toolkit-serialization = { version = "0.10.3", path = "../serialization" } diff --git a/packages/utils/Cargo.toml b/packages/utils/Cargo.toml index 80783ee..52c73ed 100644 --- a/packages/utils/Cargo.toml +++ b/packages/utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-utils" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -18,3 +18,5 @@ serde = { workspace = true } schemars = { workspace = true } cosmwasm-std = { workspace = true } cosmwasm-storage = { workspace = true } + +chrono = "0.4" \ No newline at end of file diff --git a/packages/utils/src/lib.rs b/packages/utils/src/lib.rs index ec584aa..791e44c 100644 --- a/packages/utils/src/lib.rs +++ b/packages/utils/src/lib.rs @@ -4,6 +4,8 @@ pub mod calls; pub mod feature_toggle; pub mod padding; pub mod types; +pub mod datetime; pub use calls::*; pub use padding::*; +pub use datetime::*; diff --git a/packages/viewing_key/Cargo.toml b/packages/viewing_key/Cargo.toml index 7dd968f..a1f76fa 100644 --- a/packages/viewing_key/Cargo.toml +++ b/packages/viewing_key/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secret-toolkit-viewing-key" -version = "0.10.2" +version = "0.10.3" edition = "2021" authors = ["SCRT Labs "] license-file = "../../LICENSE" @@ -20,8 +20,8 @@ base64 = "0.21.0" subtle = { version = "2.2.3", default-features = false } cosmwasm-std = { workspace = true } cosmwasm-storage = { workspace = true } -secret-toolkit-crypto = { version = "0.10.2", path = "../crypto", features = [ +secret-toolkit-crypto = { version = "0.10.3", path = "../crypto", features = [ "hash", "rand", ] } -secret-toolkit-utils = { version = "0.10.2", path = "../utils" } +secret-toolkit-utils = { version = "0.10.3", path = "../utils" } From 73569ebf5f6d163178239ed498655013f2020a74 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 17 Dec 2024 13:33:37 +1300 Subject: [PATCH 04/18] dev: make max revocations option type --- packages/permit/src/state.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/permit/src/state.rs b/packages/permit/src/state.rs index 140ad46..c349466 100644 --- a/packages/permit/src/state.rs +++ b/packages/permit/src/state.rs @@ -7,14 +7,15 @@ use serde::{Deserialize, Serialize}; /// storage prefix for named permits and "all_revoked_permits" for revoked blanket permits. /// It also sets the maximum number of all permit revocations to 10 by default. /// -/// You can use another storage location by implementing `RevokedPermitsStore` for your own type. +/// You can use different storage locations and parameters by implementing `RevokedPermitsStore` +/// for your own type. pub struct RevokedPermits; impl<'a> RevokedPermitsStore<'a> for RevokedPermits { const NAMED_REVOKED_PERMITS_PREFIX: &'static [u8] = b"revoked_permits"; const ALL_REVOKED_PERMITS: Keymap<'a, u64, AllRevokedInterval> = Keymap::new(b"all_revoked_permits"); const ALL_REVOKED_NEXT_ID: Item<'a, u64> = Item::new(b"all_revoked_permits_serial_id"); - const MAX_ALL_REVOKED_INTERVALS: u8 = 10; + const MAX_ALL_REVOKED_INTERVALS: Option = Some(10); } /// A trait describing the interface of a RevokedPermits store/vault. @@ -25,7 +26,7 @@ pub trait RevokedPermitsStore<'a> { const NAMED_REVOKED_PERMITS_PREFIX: &'static [u8]; const ALL_REVOKED_PERMITS: Keymap<'a, u64, AllRevokedInterval>; const ALL_REVOKED_NEXT_ID: Item<'a, u64>; - const MAX_ALL_REVOKED_INTERVALS: u8; + const MAX_ALL_REVOKED_INTERVALS: Option; /// returns a bool indicating if a named permit is revoked fn is_permit_revoked( @@ -70,10 +71,12 @@ pub trait RevokedPermitsStore<'a> { let all_revocations_store = Self::ALL_REVOKED_PERMITS.add_suffix(account.as_bytes()); // check that maximum number of revocations has not been met - if all_revocations_store.get_len(storage)? >= Self::MAX_ALL_REVOKED_INTERVALS.into() { - return Err(StdError::generic_err( - format!("Maximum number of permit revocations ({}) has been met", Self::MAX_ALL_REVOKED_INTERVALS) - )); + if let Some(max_revocations) = Self::MAX_ALL_REVOKED_INTERVALS { + if all_revocations_store.get_len(storage)? >= max_revocations.into() { + return Err(StdError::generic_err( + format!("Maximum number of permit revocations ({}) has been met", max_revocations) + )); + } } // get the next id store for this account From ef3d6411e87aca10e368028671941cf7d9075655 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 17 Dec 2024 13:59:04 +1300 Subject: [PATCH 05/18] fix: do not serialize created, expires if None --- packages/permit/src/structs.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/permit/src/structs.rs b/packages/permit/src/structs.rs index 50e71eb..df0d57e 100644 --- a/packages/permit/src/structs.rs +++ b/packages/permit/src/structs.rs @@ -166,7 +166,9 @@ impl PermitMsg { #[serde(rename_all = "snake_case")] pub struct PermitContent { pub allowed_tokens: Vec, + #[serde(skip_serializing_if = "Option::is_none")] pub created: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub expires: Option, #[serde(bound = "")] pub permissions: Vec, From 7b9c68814cc8efaafef7079194b28a65f0c8d13e Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 17 Dec 2024 14:30:28 +1300 Subject: [PATCH 06/18] change id -> revocation_id --- packages/permit/src/state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/permit/src/state.rs b/packages/permit/src/state.rs index c349466..b385ff4 100644 --- a/packages/permit/src/state.rs +++ b/packages/permit/src/state.rs @@ -122,7 +122,7 @@ pub trait RevokedPermitsStore<'a> { .filter_map(|r| { match r { Ok(r) => Some(AllRevocation { - id: Uint64::from(r.0), + revocation_id: Uint64::from(r.0), interval: r.1.clone() }), Err(_) => None @@ -145,6 +145,6 @@ pub struct AllRevokedInterval { /// Revocation id and interval data struct #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct AllRevocation { - pub id: Uint64, + pub revocation_id: Uint64, pub interval: AllRevokedInterval, } \ No newline at end of file From 97e069dc06080b8988a0dcb8c09db39eeb16fa04 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 17 Dec 2024 17:15:33 +1300 Subject: [PATCH 07/18] unit test --- packages/utils/src/datetime.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/utils/src/datetime.rs b/packages/utils/src/datetime.rs index 0379408..f6ddc23 100644 --- a/packages/utils/src/datetime.rs +++ b/packages/utils/src/datetime.rs @@ -20,4 +20,24 @@ pub fn iso8601_utc0_to_timestamp(iso8601_str: &str) -> StdResult { } else { Err(StdError::generic_err("ISO 8601 string not in Zulu (UTC+0)")) } +} + +#[cfg(test)] +mod tests { + use super::iso8601_utc0_to_timestamp; + + #[test] + fn test_iso8601_utc0_to_timestamp() { + let dt_string = "2024-12-17T16:59:00.000Z"; + let timestamp = iso8601_utc0_to_timestamp(dt_string).unwrap(); + assert_eq!(timestamp.nanos(), 1734454740000000000); + + let dt_string = "2024-12-17T16:59:00.000"; + let timestamp = iso8601_utc0_to_timestamp(dt_string); + assert!(timestamp.is_err(), "datetime string without Z Ok: {:?}", timestamp); + + let dt_string = "not a datetime"; + let timestamp = iso8601_utc0_to_timestamp(dt_string); + assert!(timestamp.is_err(), "invalid datetime string Ok: {:?}", timestamp); + } } \ No newline at end of file From 41e74ffb967d97cf355d221dcc949c1573adc76c Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 18 Dec 2024 15:11:16 +1300 Subject: [PATCH 08/18] dev: store intervals with u64 --- packages/permit/src/state.rs | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/permit/src/state.rs b/packages/permit/src/state.rs index b385ff4..821dc27 100644 --- a/packages/permit/src/state.rs +++ b/packages/permit/src/state.rs @@ -13,7 +13,7 @@ pub struct RevokedPermits; impl<'a> RevokedPermitsStore<'a> for RevokedPermits { const NAMED_REVOKED_PERMITS_PREFIX: &'static [u8] = b"revoked_permits"; - const ALL_REVOKED_PERMITS: Keymap<'a, u64, AllRevokedInterval> = Keymap::new(b"all_revoked_permits"); + const ALL_REVOKED_PERMITS: Keymap<'a, u64, StoredAllRevokedInterval> = Keymap::new(b"all_revoked_permits"); const ALL_REVOKED_NEXT_ID: Item<'a, u64> = Item::new(b"all_revoked_permits_serial_id"); const MAX_ALL_REVOKED_INTERVALS: Option = Some(10); } @@ -24,7 +24,7 @@ impl<'a> RevokedPermitsStore<'a> for RevokedPermits { /// the keys should be held. pub trait RevokedPermitsStore<'a> { const NAMED_REVOKED_PERMITS_PREFIX: &'static [u8]; - const ALL_REVOKED_PERMITS: Keymap<'a, u64, AllRevokedInterval>; + const ALL_REVOKED_PERMITS: Keymap<'a, u64, StoredAllRevokedInterval>; const ALL_REVOKED_NEXT_ID: Item<'a, u64>; const MAX_ALL_REVOKED_INTERVALS: Option; @@ -86,7 +86,7 @@ pub trait RevokedPermitsStore<'a> { let next_id = next_id_store.may_load(storage)?.unwrap_or_default(); // store the revocation - all_revocations_store.insert(storage, &next_id, interval)?; + all_revocations_store.insert(storage, &next_id, &interval.into_stored())?; // increment next id next_id_store.save(storage, &(next_id.wrapping_add(1)))?; @@ -123,7 +123,7 @@ pub trait RevokedPermitsStore<'a> { match r { Ok(r) => Some(AllRevocation { revocation_id: Uint64::from(r.0), - interval: r.1.clone() + interval: r.1.to_humanized() }), Err(_) => None } @@ -142,6 +142,31 @@ pub struct AllRevokedInterval { pub created_after: Option, } +impl AllRevokedInterval { + fn into_stored(&self) -> StoredAllRevokedInterval { + StoredAllRevokedInterval { + created_before: self.created_before.and_then(|cb| Some(cb.seconds())), + created_after: self.created_after.and_then(|ca| Some(ca.seconds())), + } + } +} + +/// An interval over which all permits will be rejected +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct StoredAllRevokedInterval { + pub created_before: Option, + pub created_after: Option, +} + +impl StoredAllRevokedInterval { + fn to_humanized(&self) -> AllRevokedInterval { + AllRevokedInterval { + created_before: self.created_before.and_then(|cb| Some(Timestamp::from_seconds(cb))), + created_after: self.created_after.and_then(|ca| Some(Timestamp::from_seconds(ca))), + } + } +} + /// Revocation id and interval data struct #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct AllRevocation { From 9886123125f838facd8d8498fc61290f78f9d620 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 18 Dec 2024 15:11:32 +1300 Subject: [PATCH 09/18] unit tests added --- packages/permit/src/funcs.rs | 247 ++++++++++++++++++++++++++++++++++- 1 file changed, 246 insertions(+), 1 deletion(-) diff --git a/packages/permit/src/funcs.rs b/packages/permit/src/funcs.rs index efff6b6..0ad7e94 100644 --- a/packages/permit/src/funcs.rs +++ b/packages/permit/src/funcs.rs @@ -171,8 +171,9 @@ mod tests { pub_key: PubKey { r#type: "tendermint/PubKeySecp256k1".to_string(), value: Binary::from_base64("A5M49l32ZrV+SDsPnoRv8fH7ivNC4gEX9prvd4RwvRaL").unwrap(), + }, - signature: Binary::from_base64("hw/Mo3ZZYu1pEiDdymElFkuCuJzg9soDHw+4DxK7cL9rafiyykh7VynS+guotRAKXhfYMwCiyWmiznc6R+UlsQ==").unwrap() + signature: Binary::from_base64("hw/Mo3ZZYu1pEiDdymElFkuCuJzg9soDHw+4DxK7cL9rafiyykh7VynS+guotRAKXhfYMwCiyWmiznc6R+UlsQ==").unwrap() } }; @@ -207,4 +208,248 @@ mod tests { "cosmos1399pyvvk3hvwgxwt3udkslsc5jl3rqv4x4rq7r".to_string() ); } + + #[test] + fn test_verify_permit_created_expires() { + let deps = mock_dependencies(); + + // test both created and expired set + + //{"permit": {"params":{"chain_id":"secret-4","permit_name":"test","allowed_tokens":["secret18vd8fpwxzck93qlwghaj6arh4p7c5n8978vsyg"],"permissions":["balance"],"created":"2024-12-17T16:59:00.000Z","expires":"2024-12-20T06:59:30.333Z"},"signature":{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr"},"signature":"mFDn5w59gaDTHZ5UzEA6l+sUOtlWDx/HcSi1NpZM13YuamMehIi3mseqXcQy4loE63N0hYhyXiVZdzrPM28A+g=="}}} + + let token = "secret18vd8fpwxzck93qlwghaj6arh4p7c5n8978vsyg".to_string(); + + let permit: Permit = Permit{ + params: PermitParams { + allowed_tokens: vec![token.clone()], + permit_name: "test".to_string(), + chain_id: "secret-4".to_string(), + permissions: vec![TokenPermissions::Balance], + created: Some("2024-12-17T16:59:00.000Z".to_string()), + expires: Some("2024-12-20T06:59:30.333Z".to_string()), + }, + // {"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr"},"signature":"mFDn5w59gaDTHZ5UzEA6l+sUOtlWDx/HcSi1NpZM13YuamMehIi3mseqXcQy4loE63N0hYhyXiVZdzrPM28A+g=="} + signature: PermitSignature { + pub_key: PubKey { + r#type: "tendermint/PubKeySecp256k1".to_string(), + value: Binary::from_base64("AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr").unwrap(), + }, + signature: Binary::from_base64("mFDn5w59gaDTHZ5UzEA6l+sUOtlWDx/HcSi1NpZM13YuamMehIi3mseqXcQy4loE63N0hYhyXiVZdzrPM28A+g==").unwrap() + } + }; + + let created_seconds: u64 = 1734454740; + let expires_seconds: u64 = 1734677970; + + // validate after created, before expires + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(created_seconds + 100); + + // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 + let address = validate::<_>( + deps.as_ref(), + env, + &permit, + token.clone(), + Some("secret"), + ).unwrap(); + + assert_eq!( + address, + "secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9".to_string() + ); + + // validate before created + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(created_seconds - 100); + + let address = validate::<_>( + deps.as_ref(), + env, + &permit, + token.clone(), + Some("secret"), + ); + + assert!(address.is_err(), "validated before created"); + + // validate after expires + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(expires_seconds + 100); + + let address = validate::<_>( + deps.as_ref(), + env, + &permit, + token.clone(), + Some("secret"), + ); + + assert!(address.is_err(), "validated after expires"); + + } + + #[test] + fn test_verify_blanket_permit() { + let deps = mock_dependencies(); + + // blanket permit + + //{"permit": {"params":{"chain_id":"secret-4","permit_name":"test","allowed_tokens":["ANY_TOKEN"],"permissions":["balance"],"created":"2024-12-17T16:59:00.000Z","expires":"2024-12-20T06:59:30.333Z"},"signature":{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr"},"signature":"Qte1iS54RsyRCN3rmOjA96yXQTn+eg4YaEUAU/Q5mLVGU9mOCEw6LMZjU2owLB4ogcziWrMkLOL3dtOrj3dL4Q=="}}} + + let token = BLANKET_PERMIT_TOKEN.to_string(); + + let permit: Permit = Permit{ + params: PermitParams { + allowed_tokens: vec![token.clone()], + permit_name: "test".to_string(), + chain_id: "secret-4".to_string(), + permissions: vec![TokenPermissions::Balance], + created: Some("2024-12-17T16:59:00.000Z".to_string()), + expires: Some("2024-12-20T06:59:30.333Z".to_string()), + }, + signature: PermitSignature { + pub_key: PubKey { + r#type: "tendermint/PubKeySecp256k1".to_string(), + value: Binary::from_base64("AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr").unwrap(), + }, + signature: Binary::from_base64("Qte1iS54RsyRCN3rmOjA96yXQTn+eg4YaEUAU/Q5mLVGU9mOCEw6LMZjU2owLB4ogcziWrMkLOL3dtOrj3dL4Q==").unwrap() + } + }; + + let created_seconds: u64 = 1734454740; + let expires_seconds: u64 = 1734677970; + + // validate after created, before expires + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(created_seconds + 100); + + // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 + let address = validate::<_>( + deps.as_ref(), + env, + &permit, + token.clone(), + Some("secret"), + ).unwrap(); + + assert_eq!( + address, + "secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9".to_string() + ); + + // validate before created + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(created_seconds - 100); + + let address = validate::<_>( + deps.as_ref(), + env, + &permit, + token.clone(), + Some("secret"), + ); + + assert!(address.is_err(), "validated before created"); + + // validate after expires + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(expires_seconds + 100); + + let address = validate::<_>( + deps.as_ref(), + env, + &permit, + token.clone(), + Some("secret"), + ); + + assert!(address.is_err(), "validated after expires"); + + // blanket permit invalid with another token in addition to ANY_TOKEN + + //{"permit": {"params":{"chain_id":"secret-4","permit_name":"test","allowed_tokens":["secret18vd8fpwxzck93qlwghaj6arh4p7c5n8978vsyg","ANY_TOKEN"],"permissions":["balance"],"created":"2024-12-17T16:59:00.000Z","expires":"2024-12-20T06:59:30.333Z"},"signature":{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr"},"signature":"vc36PM85beBIOmimreAD428O3ldyyUqNHxmzUYlsHaJ+560Ce8G5ibJR7KCvHJitRuds/3TvGX4dPp6l6xfrUg=="}}} + + let token = BLANKET_PERMIT_TOKEN.to_string(); + + let permit: Permit = Permit{ + params: PermitParams { + allowed_tokens: vec!["secret18vd8fpwxzck93qlwghaj6arh4p7c5n8978vsyg".to_string(), token.clone()], + permit_name: "test".to_string(), + chain_id: "secret-4".to_string(), + permissions: vec![TokenPermissions::Balance], + created: Some("2024-12-17T16:59:00.000Z".to_string()), + expires: Some("2024-12-20T06:59:30.333Z".to_string()), + }, + signature: PermitSignature { + pub_key: PubKey { + r#type: "tendermint/PubKeySecp256k1".to_string(), + value: Binary::from_base64("AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr").unwrap(), + }, + signature: Binary::from_base64("vc36PM85beBIOmimreAD428O3ldyyUqNHxmzUYlsHaJ+560Ce8G5ibJR7KCvHJitRuds/3TvGX4dPp6l6xfrUg==").unwrap() + } + }; + + let created_seconds: u64 = 1734454740; + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(created_seconds + 100); + + // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 + let address = validate::<_>( + deps.as_ref(), + env, + &permit, + token.clone(), + Some("secret"), + ); + + assert!(address.is_err(), "passed with second token in addition to ANY_TOKEN"); + + // blanket permit invalid with no created + + //{"permit": {"params":{"chain_id":"secret-4","permit_name":"test","allowed_tokens":["ANY_TOKEN"],"permissions":["balance"],"expires":"2024-12-20T06:59:30.333Z"},"signature":{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr"},"signature":"k2tdjChWUeIfs63qcHwzUdt1C92gQ5lwvEPS4fv7GpM2geaWGpUsy6Ne+m0pda0AJEpdbiZ38KjiKNlU3CmkOw=="}}} + + let token = BLANKET_PERMIT_TOKEN.to_string(); + + let permit: Permit = Permit{ + params: PermitParams { + allowed_tokens: vec![token.clone()], + permit_name: "test".to_string(), + chain_id: "secret-4".to_string(), + permissions: vec![TokenPermissions::Balance], + created: None, + expires: Some("2024-12-20T06:59:30.333Z".to_string()), + }, + signature: PermitSignature { + pub_key: PubKey { + r#type: "tendermint/PubKeySecp256k1".to_string(), + value: Binary::from_base64("AwSFyMndr25JX03rGSlXQ5oSO6F+9GoqQILZu/DytRrr").unwrap(), + }, + signature: Binary::from_base64("k2tdjChWUeIfs63qcHwzUdt1C92gQ5lwvEPS4fv7GpM2geaWGpUsy6Ne+m0pda0AJEpdbiZ38KjiKNlU3CmkOw==").unwrap() + } + }; + + let created_seconds: u64 = 1734454740; + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(created_seconds + 100); + + // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 + let address = validate::<_>( + deps.as_ref(), + env, + &permit, + token.clone(), + Some("secret"), + ); + + assert!(address.is_err(), "blanket permit passed with no created field"); + } } From 5526042c0b5af981a54c989d8b1b09c681c6b24a Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 18 Dec 2024 16:58:33 +1300 Subject: [PATCH 10/18] pass env by ref --- packages/permit/src/funcs.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/permit/src/funcs.rs b/packages/permit/src/funcs.rs index 0ad7e94..b2f943e 100644 --- a/packages/permit/src/funcs.rs +++ b/packages/permit/src/funcs.rs @@ -10,7 +10,7 @@ use secret_toolkit_crypto::sha_256; pub fn validate( deps: Deps, - env: Env, + env: &Env, permit: &Permit, current_token_address: String, hrp: Option<&str>, @@ -181,7 +181,7 @@ mod tests { let address = validate::<_>( deps.as_ref(), - env, + &env, &permit, token.clone(), Some("secret"), @@ -197,7 +197,7 @@ mod tests { let address = validate::<_>( deps.as_ref(), - env, + &env, &permit, token, Some("cosmos") @@ -249,7 +249,7 @@ mod tests { // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 let address = validate::<_>( deps.as_ref(), - env, + &env, &permit, token.clone(), Some("secret"), @@ -267,7 +267,7 @@ mod tests { let address = validate::<_>( deps.as_ref(), - env, + &env, &permit, token.clone(), Some("secret"), @@ -282,7 +282,7 @@ mod tests { let address = validate::<_>( deps.as_ref(), - env, + &env, &permit, token.clone(), Some("secret"), @@ -331,7 +331,7 @@ mod tests { // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 let address = validate::<_>( deps.as_ref(), - env, + &env, &permit, token.clone(), Some("secret"), @@ -349,7 +349,7 @@ mod tests { let address = validate::<_>( deps.as_ref(), - env, + &env, &permit, token.clone(), Some("secret"), @@ -364,7 +364,7 @@ mod tests { let address = validate::<_>( deps.as_ref(), - env, + &env, &permit, token.clone(), Some("secret"), @@ -404,7 +404,7 @@ mod tests { // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 let address = validate::<_>( deps.as_ref(), - env, + &env, &permit, token.clone(), Some("secret"), @@ -444,7 +444,7 @@ mod tests { // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 let address = validate::<_>( deps.as_ref(), - env, + &env, &permit, token.clone(), Some("secret"), From be621610d342416b9e3591cc4a61a05853673c13 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 18 Dec 2024 18:07:22 +1300 Subject: [PATCH 11/18] dev: make times u64 --- packages/permit/src/funcs.rs | 12 ++++++------ packages/permit/src/state.rs | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/permit/src/funcs.rs b/packages/permit/src/funcs.rs index b2f943e..0168841 100644 --- a/packages/permit/src/funcs.rs +++ b/packages/permit/src/funcs.rs @@ -1,6 +1,6 @@ use std::u64; -use cosmwasm_std::{to_binary, Binary, CanonicalAddr, Deps, Env, StdError, StdResult, Timestamp}; +use cosmwasm_std::{to_binary, Binary, CanonicalAddr, Deps, Env, StdError, StdResult, Timestamp, Uint64}; use ripemd::{Digest, Ripemd160}; use secret_toolkit_utils::iso8601_utc0_to_timestamp; @@ -52,7 +52,7 @@ pub fn validate( if let Some(created) = created_timestamp { // Verify that the permit was not created after the current block time - if created > env.block.time { + if created.seconds() > env.block.time.seconds() { return Err(StdError::generic_err("Permit `created` after current block time")); } } @@ -66,7 +66,7 @@ pub fn validate( if let Some(expires) = expires_timestamp { // Verify that the permit did not expire before the current block time - if expires <= env.block.time { + if expires.seconds() <= env.block.time.seconds() { return Err(StdError::generic_err("Permit has expired")) } } @@ -94,13 +94,13 @@ pub fn validate( // If the permit has a `created` field if let Some(created) = created_timestamp { // Revocation created before field, default 0 - let created_before = revocation.interval.created_before.unwrap_or(Timestamp::from_nanos(0)); + let created_before = revocation.interval.created_before.unwrap_or(Uint64::from(0u64)); // Revocation created after field, default max u64 - let created_after = revocation.interval.created_after.unwrap_or(Timestamp::from_nanos(u64::MAX)); + let created_after = revocation.interval.created_after.unwrap_or(Uint64::from(u64::MAX)); // If the permit's `created` field falls in between created after and created before, then reject it - if created > created_after || created < created_before { + if created.seconds() > created_after.u64() || created.seconds() < created_before.u64() { return Err(StdError::generic_err( format!("Permits created at {:?} revoked by account {:?}", created, account.as_str()) )); diff --git a/packages/permit/src/state.rs b/packages/permit/src/state.rs index 821dc27..e38be2a 100644 --- a/packages/permit/src/state.rs +++ b/packages/permit/src/state.rs @@ -138,15 +138,15 @@ pub trait RevokedPermitsStore<'a> { /// An interval over which all permits will be rejected #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct AllRevokedInterval { - pub created_before: Option, - pub created_after: Option, + pub created_before: Option, + pub created_after: Option, } impl AllRevokedInterval { fn into_stored(&self) -> StoredAllRevokedInterval { StoredAllRevokedInterval { - created_before: self.created_before.and_then(|cb| Some(cb.seconds())), - created_after: self.created_after.and_then(|ca| Some(ca.seconds())), + created_before: self.created_before.and_then(|cb| Some(cb.u64())), + created_after: self.created_after.and_then(|ca| Some(ca.u64())), } } } @@ -161,8 +161,8 @@ pub struct StoredAllRevokedInterval { impl StoredAllRevokedInterval { fn to_humanized(&self) -> AllRevokedInterval { AllRevokedInterval { - created_before: self.created_before.and_then(|cb| Some(Timestamp::from_seconds(cb))), - created_after: self.created_after.and_then(|ca| Some(Timestamp::from_seconds(ca))), + created_before: self.created_before.and_then(|cb| Some(Uint64::from(cb))), + created_after: self.created_after.and_then(|ca| Some(Uint64::from(ca))), } } } From af9e5581aabfafd489a351cd53adc3e0e45112bb Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 18 Dec 2024 21:20:28 +1300 Subject: [PATCH 12/18] remove unused imports --- packages/permit/src/funcs.rs | 4 ++-- packages/permit/src/state.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/permit/src/funcs.rs b/packages/permit/src/funcs.rs index 0168841..6c5c4a6 100644 --- a/packages/permit/src/funcs.rs +++ b/packages/permit/src/funcs.rs @@ -1,6 +1,6 @@ use std::u64; -use cosmwasm_std::{to_binary, Binary, CanonicalAddr, Deps, Env, StdError, StdResult, Timestamp, Uint64}; +use cosmwasm_std::{to_binary, Binary, CanonicalAddr, Deps, Env, StdError, StdResult, Uint64}; use ripemd::{Digest, Ripemd160}; use secret_toolkit_utils::iso8601_utc0_to_timestamp; @@ -148,7 +148,7 @@ pub fn pubkey_to_account(pubkey: &Binary) -> CanonicalAddr { mod tests { use super::*; use crate::{PermitParams, PermitSignature, PubKey, TokenPermissions}; - use cosmwasm_std::testing::{mock_dependencies, mock_env}; + use cosmwasm_std::{testing::{mock_dependencies, mock_env}, Timestamp}; #[test] fn test_verify_permit() { diff --git a/packages/permit/src/state.rs b/packages/permit/src/state.rs index e38be2a..8fe5729 100644 --- a/packages/permit/src/state.rs +++ b/packages/permit/src/state.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{StdError, StdResult, Storage, Timestamp, Uint64}; +use cosmwasm_std::{StdError, StdResult, Storage, Uint64}; use schemars::JsonSchema; use secret_toolkit_storage::{Item, Keymap}; use serde::{Deserialize, Serialize}; From 5f37828ba78278e16d4e86c5515a3eae7deeb805 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 19 Dec 2024 07:49:33 +1300 Subject: [PATCH 13/18] dev: add all time idempotent revocation --- packages/permit/src/state.rs | 67 +++++++++++++++++++++++++++++----- packages/permit/src/structs.rs | 1 + 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/packages/permit/src/state.rs b/packages/permit/src/state.rs index 8fe5729..30677c4 100644 --- a/packages/permit/src/state.rs +++ b/packages/permit/src/state.rs @@ -3,6 +3,8 @@ use schemars::JsonSchema; use secret_toolkit_storage::{Item, Keymap}; use serde::{Deserialize, Serialize}; +use crate::REVOKED_ALL; + /// This is the default implementation of the revoked permits store, using the "revoked_permits" /// storage prefix for named permits and "all_revoked_permits" for revoked blanket permits. /// It also sets the maximum number of all permit revocations to 10 by default. @@ -13,9 +15,10 @@ pub struct RevokedPermits; impl<'a> RevokedPermitsStore<'a> for RevokedPermits { const NAMED_REVOKED_PERMITS_PREFIX: &'static [u8] = b"revoked_permits"; - const ALL_REVOKED_PERMITS: Keymap<'a, u64, StoredAllRevokedInterval> = Keymap::new(b"all_revoked_permits"); - const ALL_REVOKED_NEXT_ID: Item<'a, u64> = Item::new(b"all_revoked_permits_serial_id"); + const ALL_REVOKED_PERMITS: Keymap<'a, u64, StoredAllRevokedInterval> = Keymap::new(b"__all_revoked_permits__1"); + const ALL_REVOKED_NEXT_ID: Item<'a, u64> = Item::new(b"__all_revoked_permits_serial_id__1"); const MAX_ALL_REVOKED_INTERVALS: Option = Some(10); + const ALL_TIME_REVOKED_ALL:Item<'a, bool> = Item::new(b"__all_time_revoked_all__1"); } /// A trait describing the interface of a RevokedPermits store/vault. @@ -27,6 +30,7 @@ pub trait RevokedPermitsStore<'a> { const ALL_REVOKED_PERMITS: Keymap<'a, u64, StoredAllRevokedInterval>; const ALL_REVOKED_NEXT_ID: Item<'a, u64>; const MAX_ALL_REVOKED_INTERVALS: Option; + const ALL_TIME_REVOKED_ALL:Item<'a, bool>; /// returns a bool indicating if a named permit is revoked fn is_permit_revoked( @@ -66,7 +70,23 @@ pub trait RevokedPermitsStore<'a> { storage: &mut dyn Storage, account: &str, interval: &AllRevokedInterval, - ) -> StdResult { + ) -> StdResult { + // Check if *neither* `created_before` or `created_after` is supplied + // In this case, we are globally turning off all permits for this address, which + // makes it so ANY permit will be rejected. This special case does not count + // toward the maximum number of revoked intervals. + if interval.created_before.is_none() && interval.created_after.is_none() { + // get all time revocations store for this account + let all_time_revoked_store = Self::ALL_TIME_REVOKED_ALL.add_suffix(account.as_bytes()); + + // set all time revocations to true, this is idempotent + all_time_revoked_store.save(storage, &true)?; + + // return a revocation ID of "REVOKED_ALL" + return Ok(REVOKED_ALL.to_string()); + } + + // get the revocations store for this account let all_revocations_store = Self::ALL_REVOKED_PERMITS.add_suffix(account.as_bytes()); @@ -91,20 +111,37 @@ pub trait RevokedPermitsStore<'a> { // increment next id next_id_store.save(storage, &(next_id.wrapping_add(1)))?; - Ok(Uint64::from(next_id)) + Ok(format!("{}", next_id)) } /// deletes the permit revocation with the given id for this account fn delete_revocation( storage: &mut dyn Storage, account: &str, - id: Uint64, + id: &str, ) -> StdResult<()> { + // check if this is the all time special case + if id == REVOKED_ALL { + // get all time revocations store for this account + let all_time_revoked_store = Self::ALL_TIME_REVOKED_ALL.add_suffix(account.as_bytes()); + + // set all time revocations to false + all_time_revoked_store.save(storage, &false)?; + + // return + return Ok(()) + } + // get the revocations store for this account let all_revocations_store = Self::ALL_REVOKED_PERMITS.add_suffix(account.as_bytes()); + // try to convert id to a u64 + let Ok(id_str) = u64::from_str_radix(id, 10) else { + return Err(StdError::generic_err("Deleted revocation id not Uint64")); + }; + // remove the permit revocation with the given id - all_revocations_store.remove(storage, &id.u64()) + all_revocations_store.remove(storage, &id_str) } /// lists all the revocations for the account @@ -117,12 +154,12 @@ pub trait RevokedPermitsStore<'a> { let all_revocations_store = Self::ALL_REVOKED_PERMITS.add_suffix(account.as_bytes()); // select elements and convert to AllRevocation structs - let result = all_revocations_store + let mut result: Vec = all_revocations_store .iter(storage)? .filter_map(|r| { match r { Ok(r) => Some(AllRevocation { - revocation_id: Uint64::from(r.0), + revocation_id: format!("{}", r.0), interval: r.1.to_humanized() }), Err(_) => None @@ -130,6 +167,18 @@ pub trait RevokedPermitsStore<'a> { }) .collect(); + // check if there is an all time revocation and add that as well + let all_time_revoked_store = Self::ALL_TIME_REVOKED_ALL.add_suffix(account.as_bytes()); + if all_time_revoked_store.may_load(storage)?.unwrap_or_default() { + result.push(AllRevocation { + revocation_id: REVOKED_ALL.to_string(), + interval: AllRevokedInterval { + created_before: None, + created_after: None + } + }); + } + Ok(result) } @@ -170,6 +219,6 @@ impl StoredAllRevokedInterval { /// Revocation id and interval data struct #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct AllRevocation { - pub revocation_id: Uint64, + pub revocation_id: String, pub interval: AllRevokedInterval, } \ No newline at end of file diff --git a/packages/permit/src/structs.rs b/packages/permit/src/structs.rs index df0d57e..20daf04 100644 --- a/packages/permit/src/structs.rs +++ b/packages/permit/src/structs.rs @@ -7,6 +7,7 @@ use crate::pubkey_to_account; use cosmwasm_std::{Binary, CanonicalAddr, Uint128}; pub const BLANKET_PERMIT_TOKEN: &str = "ANY_TOKEN"; +pub const REVOKED_ALL: &str = "REVOKED_ALL"; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] From 4a3d119d24bcf00cc30baf3f20a5b8f6bed65ca4 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 19 Dec 2024 08:02:21 +1300 Subject: [PATCH 14/18] dev: check all time revocation in verify --- packages/permit/src/funcs.rs | 14 +++++++------- packages/permit/src/state.rs | 16 +++++++++++++--- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/permit/src/funcs.rs b/packages/permit/src/funcs.rs index 6c5c4a6..c78e023 100644 --- a/packages/permit/src/funcs.rs +++ b/packages/permit/src/funcs.rs @@ -80,17 +80,17 @@ pub fn validate( // Get the list of all revocations for this address let revocations = RevokedPermits::list_revocations(deps.storage, &account)?; + // Check if account has an all time permit revocation + if RevokedPermits::is_all_time_revoked(deps.storage, account.as_str())? { + return Err(StdError::generic_err( + format!("Permits revoked by {:?}", account.as_str()) + )); + } + // Check if there are any revocation intervals blocking all permits // TODO: An interval or segment tree might be preferable to make this more efficient for cases // when the number of revocations is allowed to grow to a large amount. for revocation in revocations { - // If this revocation has no `created_before` or `created_after`, then reject all permit queries - if revocation.interval.created_before.is_none() && revocation.interval.created_after.is_none() { - return Err(StdError::generic_err( - format!("Permits revoked by {:?}", account.as_str()) - )); - } - // If the permit has a `created` field if let Some(created) = created_timestamp { // Revocation created before field, default 0 diff --git a/packages/permit/src/state.rs b/packages/permit/src/state.rs index 30677c4..99fa9f3 100644 --- a/packages/permit/src/state.rs +++ b/packages/permit/src/state.rs @@ -167,9 +167,9 @@ pub trait RevokedPermitsStore<'a> { }) .collect(); - // check if there is an all time revocation and add that as well - let all_time_revoked_store = Self::ALL_TIME_REVOKED_ALL.add_suffix(account.as_bytes()); - if all_time_revoked_store.may_load(storage)?.unwrap_or_default() { + // check if there is an all time revocation + if Self::is_all_time_revoked(storage, account)? { + // add that to the result result.push(AllRevocation { revocation_id: REVOKED_ALL.to_string(), interval: AllRevokedInterval { @@ -182,6 +182,16 @@ pub trait RevokedPermitsStore<'a> { Ok(result) } + /// returns bool if queries are all time revoked for this account + fn is_all_time_revoked(storage: &dyn Storage, account: &str) -> StdResult { + // get the all time revoked store for this account + let all_time_revoked_store = Self::ALL_TIME_REVOKED_ALL.add_suffix(account.as_bytes()); + + let result = all_time_revoked_store.may_load(storage)?.unwrap_or_default(); + + Ok(result) + } + } /// An interval over which all permits will be rejected From 76bf3a254d52fc8fa318cff3e1f0d2846eecb93d Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Thu, 19 Dec 2024 19:41:10 +1300 Subject: [PATCH 15/18] add data time string to seconds --- packages/utils/src/datetime.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/utils/src/datetime.rs b/packages/utils/src/datetime.rs index f6ddc23..6ec4760 100644 --- a/packages/utils/src/datetime.rs +++ b/packages/utils/src/datetime.rs @@ -22,6 +22,30 @@ pub fn iso8601_utc0_to_timestamp(iso8601_str: &str) -> StdResult { } } +/// Converts an ISO 8601 date-time string in Zulu (UTC+0) format to seconds. +/// +/// String format: {YYYY}-{MM}-{DD}T{hh}:{mm}:{ss}.{uuu}Z +/// +/// * `iso8601_str` - The ISO 8601 date-time string to convert. +/// +/// Returns StdResult +pub fn iso8601_utc0_to_seconds(iso8601_str: &str) -> StdResult { + let Ok(datetime) = iso8601_str.parse::>() else { + return Err(StdError::generic_err("ISO 8601 string could not be parsed")); + }; + + // Verify the timezone is UTC (Zulu time) + if iso8601_str.ends_with("Z") { + let seconds = datetime.timestamp(); + if seconds < 0 { + return Err(StdError::generic_err("Date time before January 1, 1970 0:00:00 UTC not supported")) + } + Ok(seconds as u64) + } else { + Err(StdError::generic_err("ISO 8601 string not in Zulu (UTC+0)")) + } +} + #[cfg(test)] mod tests { use super::iso8601_utc0_to_timestamp; @@ -30,6 +54,7 @@ mod tests { fn test_iso8601_utc0_to_timestamp() { let dt_string = "2024-12-17T16:59:00.000Z"; let timestamp = iso8601_utc0_to_timestamp(dt_string).unwrap(); + println!("{:?}", timestamp); assert_eq!(timestamp.nanos(), 1734454740000000000); let dt_string = "2024-12-17T16:59:00.000"; From df89b582bc207f4a2f697c31b9a7c64faac10195 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Sat, 4 Jan 2025 17:25:14 +1300 Subject: [PATCH 16/18] remove default features on chrono --- packages/utils/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/Cargo.toml b/packages/utils/Cargo.toml index 52c73ed..9b30edb 100644 --- a/packages/utils/Cargo.toml +++ b/packages/utils/Cargo.toml @@ -19,4 +19,4 @@ schemars = { workspace = true } cosmwasm-std = { workspace = true } cosmwasm-storage = { workspace = true } -chrono = "0.4" \ No newline at end of file +chrono = { version = "0.4", default-features = false } \ No newline at end of file From c61b213ed9654e05c888fed33390517d91659f82 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 7 Jan 2025 10:17:41 +1300 Subject: [PATCH 17/18] cargo fmt --- packages/permit/src/funcs.rs | 176 ++++++++++++++------------------- packages/permit/src/state.rs | 85 +++++++--------- packages/permit/src/structs.rs | 2 +- packages/utils/src/datetime.rs | 35 ++++--- packages/utils/src/lib.rs | 4 +- 5 files changed, 135 insertions(+), 167 deletions(-) diff --git a/packages/permit/src/funcs.rs b/packages/permit/src/funcs.rs index c78e023..10e10dc 100644 --- a/packages/permit/src/funcs.rs +++ b/packages/permit/src/funcs.rs @@ -4,7 +4,9 @@ use cosmwasm_std::{to_binary, Binary, CanonicalAddr, Deps, Env, StdError, StdRes use ripemd::{Digest, Ripemd160}; use secret_toolkit_utils::iso8601_utc0_to_timestamp; -use crate::{Permissions, Permit, RevokedPermits, RevokedPermitsStore, SignedPermit, BLANKET_PERMIT_TOKEN}; +use crate::{ + Permissions, Permit, RevokedPermits, RevokedPermitsStore, SignedPermit, BLANKET_PERMIT_TOKEN, +}; use bech32::{ToBase32, Variant}; use secret_toolkit_crypto::sha_256; @@ -17,17 +19,25 @@ pub fn validate( ) -> StdResult { let account_hrp = hrp.unwrap_or("secret"); - if permit.params.allowed_tokens.contains(&BLANKET_PERMIT_TOKEN.to_string()) { + if permit + .params + .allowed_tokens + .contains(&BLANKET_PERMIT_TOKEN.to_string()) + { // using blanket permit - + // assert allowed_tokens list has an exact length of 1 if permit.params.allowed_tokens.len() != 1 { - return Err(StdError::generic_err("Blanket permits cannot contain other allowed tokens")); + return Err(StdError::generic_err( + "Blanket permits cannot contain other allowed tokens", + )); } // assert created field is specified if permit.params.created.is_none() { - return Err(StdError::generic_err("Blanket permits must have a `created` time")); + return Err(StdError::generic_err( + "Blanket permits must have a `created` time", + )); } } else if !permit.check_token(¤t_token_address) { // check that current token address is in allowed tokens @@ -44,30 +54,34 @@ pub fn validate( } // Convert the permit created field to a Timestamp - let created_timestamp = permit.params.created.clone() - .map(|created| - iso8601_utc0_to_timestamp(&created) - ) + let created_timestamp = permit + .params + .created + .clone() + .map(|created| iso8601_utc0_to_timestamp(&created)) .transpose()?; if let Some(created) = created_timestamp { // Verify that the permit was not created after the current block time if created.seconds() > env.block.time.seconds() { - return Err(StdError::generic_err("Permit `created` after current block time")); + return Err(StdError::generic_err( + "Permit `created` after current block time", + )); } } // Convert the permit expires field to a Timestamp - let expires_timestamp = permit.params.expires.clone() - .map(|created| - iso8601_utc0_to_timestamp(&created) - ) + let expires_timestamp = permit + .params + .expires + .clone() + .map(|created| iso8601_utc0_to_timestamp(&created)) .transpose()?; if let Some(expires) = expires_timestamp { // Verify that the permit did not expire before the current block time if expires.seconds() <= env.block.time.seconds() { - return Err(StdError::generic_err("Permit has expired")) + return Err(StdError::generic_err("Permit has expired")); } } @@ -82,36 +96,44 @@ pub fn validate( // Check if account has an all time permit revocation if RevokedPermits::is_all_time_revoked(deps.storage, account.as_str())? { - return Err(StdError::generic_err( - format!("Permits revoked by {:?}", account.as_str()) - )); + return Err(StdError::generic_err(format!( + "Permits revoked by {:?}", + account.as_str() + ))); } // Check if there are any revocation intervals blocking all permits - // TODO: An interval or segment tree might be preferable to make this more efficient for cases + // TODO: An interval or segment tree might be preferable to make this more efficient for cases // when the number of revocations is allowed to grow to a large amount. for revocation in revocations { // If the permit has a `created` field if let Some(created) = created_timestamp { // Revocation created before field, default 0 - let created_before = revocation.interval.created_before.unwrap_or(Uint64::from(0u64)); + let created_before = revocation + .interval + .created_before + .unwrap_or(Uint64::from(0u64)); // Revocation created after field, default max u64 - let created_after = revocation.interval.created_after.unwrap_or(Uint64::from(u64::MAX)); + let created_after = revocation + .interval + .created_after + .unwrap_or(Uint64::from(u64::MAX)); // If the permit's `created` field falls in between created after and created before, then reject it if created.seconds() > created_after.u64() || created.seconds() < created_before.u64() { - return Err(StdError::generic_err( - format!("Permits created at {:?} revoked by account {:?}", created, account.as_str()) - )); - } + return Err(StdError::generic_err(format!( + "Permits created at {:?} revoked by account {:?}", + created, + account.as_str() + ))); + } } } // Validate permit_name let permit_name = &permit.params.permit_name; - let is_permit_revoked = - RevokedPermits::is_permit_revoked(deps.storage, &account, permit_name); + let is_permit_revoked = RevokedPermits::is_permit_revoked(deps.storage, &account, permit_name); if is_permit_revoked { return Err(StdError::generic_err(format!( "Permit {:?} was revoked by account {:?}", @@ -148,7 +170,10 @@ pub fn pubkey_to_account(pubkey: &Binary) -> CanonicalAddr { mod tests { use super::*; use crate::{PermitParams, PermitSignature, PubKey, TokenPermissions}; - use cosmwasm_std::{testing::{mock_dependencies, mock_env}, Timestamp}; + use cosmwasm_std::{ + testing::{mock_dependencies, mock_env}, + Timestamp, + }; #[test] fn test_verify_permit() { @@ -179,14 +204,8 @@ mod tests { let env = mock_env(); - let address = validate::<_>( - deps.as_ref(), - &env, - &permit, - token.clone(), - Some("secret"), - ) - .unwrap(); + let address = + validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")).unwrap(); assert_eq!( address, @@ -195,13 +214,7 @@ mod tests { let env = mock_env(); - let address = validate::<_>( - deps.as_ref(), - &env, - &permit, - token, - Some("cosmos") - ).unwrap(); + let address = validate::<_>(deps.as_ref(), &env, &permit, token, Some("cosmos")).unwrap(); assert_eq!( address, @@ -247,13 +260,8 @@ mod tests { env.block.time = Timestamp::from_seconds(created_seconds + 100); // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 - let address = validate::<_>( - deps.as_ref(), - &env, - &permit, - token.clone(), - Some("secret"), - ).unwrap(); + let address = + validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")).unwrap(); assert_eq!( address, @@ -265,13 +273,7 @@ mod tests { let mut env = mock_env(); env.block.time = Timestamp::from_seconds(created_seconds - 100); - let address = validate::<_>( - deps.as_ref(), - &env, - &permit, - token.clone(), - Some("secret"), - ); + let address = validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")); assert!(address.is_err(), "validated before created"); @@ -280,16 +282,9 @@ mod tests { let mut env = mock_env(); env.block.time = Timestamp::from_seconds(expires_seconds + 100); - let address = validate::<_>( - deps.as_ref(), - &env, - &permit, - token.clone(), - Some("secret"), - ); + let address = validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")); assert!(address.is_err(), "validated after expires"); - } #[test] @@ -329,13 +324,8 @@ mod tests { env.block.time = Timestamp::from_seconds(created_seconds + 100); // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 - let address = validate::<_>( - deps.as_ref(), - &env, - &permit, - token.clone(), - Some("secret"), - ).unwrap(); + let address = + validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")).unwrap(); assert_eq!( address, @@ -347,13 +337,7 @@ mod tests { let mut env = mock_env(); env.block.time = Timestamp::from_seconds(created_seconds - 100); - let address = validate::<_>( - deps.as_ref(), - &env, - &permit, - token.clone(), - Some("secret"), - ); + let address = validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")); assert!(address.is_err(), "validated before created"); @@ -362,13 +346,7 @@ mod tests { let mut env = mock_env(); env.block.time = Timestamp::from_seconds(expires_seconds + 100); - let address = validate::<_>( - deps.as_ref(), - &env, - &permit, - token.clone(), - Some("secret"), - ); + let address = validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")); assert!(address.is_err(), "validated after expires"); @@ -402,15 +380,12 @@ mod tests { env.block.time = Timestamp::from_seconds(created_seconds + 100); // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 - let address = validate::<_>( - deps.as_ref(), - &env, - &permit, - token.clone(), - Some("secret"), - ); + let address = validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")); - assert!(address.is_err(), "passed with second token in addition to ANY_TOKEN"); + assert!( + address.is_err(), + "passed with second token in addition to ANY_TOKEN" + ); // blanket permit invalid with no created @@ -442,14 +417,11 @@ mod tests { env.block.time = Timestamp::from_seconds(created_seconds + 100); // secret16v498l7d335wlzxpzg0mwkucrszdlza008dhc9 - let address = validate::<_>( - deps.as_ref(), - &env, - &permit, - token.clone(), - Some("secret"), - ); + let address = validate::<_>(deps.as_ref(), &env, &permit, token.clone(), Some("secret")); - assert!(address.is_err(), "blanket permit passed with no created field"); + assert!( + address.is_err(), + "blanket permit passed with no created field" + ); } } diff --git a/packages/permit/src/state.rs b/packages/permit/src/state.rs index 99fa9f3..4db98bf 100644 --- a/packages/permit/src/state.rs +++ b/packages/permit/src/state.rs @@ -9,16 +9,17 @@ use crate::REVOKED_ALL; /// storage prefix for named permits and "all_revoked_permits" for revoked blanket permits. /// It also sets the maximum number of all permit revocations to 10 by default. /// -/// You can use different storage locations and parameters by implementing `RevokedPermitsStore` +/// You can use different storage locations and parameters by implementing `RevokedPermitsStore` /// for your own type. pub struct RevokedPermits; impl<'a> RevokedPermitsStore<'a> for RevokedPermits { const NAMED_REVOKED_PERMITS_PREFIX: &'static [u8] = b"revoked_permits"; - const ALL_REVOKED_PERMITS: Keymap<'a, u64, StoredAllRevokedInterval> = Keymap::new(b"__all_revoked_permits__1"); + const ALL_REVOKED_PERMITS: Keymap<'a, u64, StoredAllRevokedInterval> = + Keymap::new(b"__all_revoked_permits__1"); const ALL_REVOKED_NEXT_ID: Item<'a, u64> = Item::new(b"__all_revoked_permits_serial_id__1"); const MAX_ALL_REVOKED_INTERVALS: Option = Some(10); - const ALL_TIME_REVOKED_ALL:Item<'a, bool> = Item::new(b"__all_time_revoked_all__1"); + const ALL_TIME_REVOKED_ALL: Item<'a, bool> = Item::new(b"__all_time_revoked_all__1"); } /// A trait describing the interface of a RevokedPermits store/vault. @@ -30,14 +31,10 @@ pub trait RevokedPermitsStore<'a> { const ALL_REVOKED_PERMITS: Keymap<'a, u64, StoredAllRevokedInterval>; const ALL_REVOKED_NEXT_ID: Item<'a, u64>; const MAX_ALL_REVOKED_INTERVALS: Option; - const ALL_TIME_REVOKED_ALL:Item<'a, bool>; + const ALL_TIME_REVOKED_ALL: Item<'a, bool>; /// returns a bool indicating if a named permit is revoked - fn is_permit_revoked( - storage: &dyn Storage, - account: &str, - permit_name: &str, - ) -> bool { + fn is_permit_revoked(storage: &dyn Storage, account: &str, permit_name: &str) -> bool { let mut storage_key = Vec::new(); storage_key.extend_from_slice(Self::NAMED_REVOKED_PERMITS_PREFIX); storage_key.extend_from_slice(account.as_bytes()); @@ -47,11 +44,7 @@ pub trait RevokedPermitsStore<'a> { } /// revokes a named permit permanently - fn revoke_permit( - storage: &mut dyn Storage, - account: &str, - permit_name: &str, - ) { + fn revoke_permit(storage: &mut dyn Storage, account: &str, permit_name: &str) { let mut storage_key = Vec::new(); storage_key.extend_from_slice(Self::NAMED_REVOKED_PERMITS_PREFIX); storage_key.extend_from_slice(account.as_bytes()); @@ -78,7 +71,7 @@ pub trait RevokedPermitsStore<'a> { if interval.created_before.is_none() && interval.created_after.is_none() { // get all time revocations store for this account let all_time_revoked_store = Self::ALL_TIME_REVOKED_ALL.add_suffix(account.as_bytes()); - + // set all time revocations to true, this is idempotent all_time_revoked_store.save(storage, &true)?; @@ -86,16 +79,16 @@ pub trait RevokedPermitsStore<'a> { return Ok(REVOKED_ALL.to_string()); } - // get the revocations store for this account let all_revocations_store = Self::ALL_REVOKED_PERMITS.add_suffix(account.as_bytes()); // check that maximum number of revocations has not been met if let Some(max_revocations) = Self::MAX_ALL_REVOKED_INTERVALS { if all_revocations_store.get_len(storage)? >= max_revocations.into() { - return Err(StdError::generic_err( - format!("Maximum number of permit revocations ({}) has been met", max_revocations) - )); + return Err(StdError::generic_err(format!( + "Maximum number of permit revocations ({}) has been met", + max_revocations + ))); } } @@ -115,21 +108,17 @@ pub trait RevokedPermitsStore<'a> { } /// deletes the permit revocation with the given id for this account - fn delete_revocation( - storage: &mut dyn Storage, - account: &str, - id: &str, - ) -> StdResult<()> { + fn delete_revocation(storage: &mut dyn Storage, account: &str, id: &str) -> StdResult<()> { // check if this is the all time special case if id == REVOKED_ALL { // get all time revocations store for this account let all_time_revoked_store = Self::ALL_TIME_REVOKED_ALL.add_suffix(account.as_bytes()); - + // set all time revocations to false all_time_revoked_store.save(storage, &false)?; // return - return Ok(()) + return Ok(()); } // get the revocations store for this account @@ -146,36 +135,31 @@ pub trait RevokedPermitsStore<'a> { /// lists all the revocations for the account /// returns a vec of revocations - fn list_revocations( - storage: &dyn Storage, - account: &str, - ) -> StdResult> { + fn list_revocations(storage: &dyn Storage, account: &str) -> StdResult> { // get the revocations store for this account let all_revocations_store = Self::ALL_REVOKED_PERMITS.add_suffix(account.as_bytes()); // select elements and convert to AllRevocation structs let mut result: Vec = all_revocations_store .iter(storage)? - .filter_map(|r| { - match r { - Ok(r) => Some(AllRevocation { - revocation_id: format!("{}", r.0), - interval: r.1.to_humanized() - }), - Err(_) => None - } + .filter_map(|r| match r { + Ok(r) => Some(AllRevocation { + revocation_id: format!("{}", r.0), + interval: r.1.to_humanized(), + }), + Err(_) => None, }) .collect(); - // check if there is an all time revocation + // check if there is an all time revocation if Self::is_all_time_revoked(storage, account)? { // add that to the result result.push(AllRevocation { revocation_id: REVOKED_ALL.to_string(), - interval: AllRevokedInterval { - created_before: None, - created_after: None - } + interval: AllRevokedInterval { + created_before: None, + created_after: None, + }, }); } @@ -187,11 +171,12 @@ pub trait RevokedPermitsStore<'a> { // get the all time revoked store for this account let all_time_revoked_store = Self::ALL_TIME_REVOKED_ALL.add_suffix(account.as_bytes()); - let result = all_time_revoked_store.may_load(storage)?.unwrap_or_default(); + let result = all_time_revoked_store + .may_load(storage)? + .unwrap_or_default(); Ok(result) } - } /// An interval over which all permits will be rejected @@ -203,9 +188,9 @@ pub struct AllRevokedInterval { impl AllRevokedInterval { fn into_stored(&self) -> StoredAllRevokedInterval { - StoredAllRevokedInterval { - created_before: self.created_before.and_then(|cb| Some(cb.u64())), - created_after: self.created_after.and_then(|ca| Some(ca.u64())), + StoredAllRevokedInterval { + created_before: self.created_before.and_then(|cb| Some(cb.u64())), + created_after: self.created_after.and_then(|ca| Some(ca.u64())), } } } @@ -220,7 +205,7 @@ pub struct StoredAllRevokedInterval { impl StoredAllRevokedInterval { fn to_humanized(&self) -> AllRevokedInterval { AllRevokedInterval { - created_before: self.created_before.and_then(|cb| Some(Uint64::from(cb))), + created_before: self.created_before.and_then(|cb| Some(Uint64::from(cb))), created_after: self.created_after.and_then(|ca| Some(Uint64::from(ca))), } } @@ -231,4 +216,4 @@ impl StoredAllRevokedInterval { pub struct AllRevocation { pub revocation_id: String, pub interval: AllRevokedInterval, -} \ No newline at end of file +} diff --git a/packages/permit/src/structs.rs b/packages/permit/src/structs.rs index 20daf04..6b779b9 100644 --- a/packages/permit/src/structs.rs +++ b/packages/permit/src/structs.rs @@ -19,7 +19,7 @@ pub struct Permit { impl Permit { pub fn check_token(&self, token: &str) -> bool { - self.params.allowed_tokens.contains(&token.to_string()) + self.params.allowed_tokens.contains(&token.to_string()) } pub fn check_permission(&self, permission: &Permission) -> bool { diff --git a/packages/utils/src/datetime.rs b/packages/utils/src/datetime.rs index 6ec4760..85b9185 100644 --- a/packages/utils/src/datetime.rs +++ b/packages/utils/src/datetime.rs @@ -1,13 +1,12 @@ use chrono::{DateTime, Utc}; use cosmwasm_std::{StdError, StdResult, Timestamp}; - /// Converts an ISO 8601 date-time string in Zulu (UTC+0) format to a Timestamp. -/// +/// /// String format: {YYYY}-{MM}-{DD}T{hh}:{mm}:{ss}.{uuu}Z -/// +/// /// * `iso8601_str` - The ISO 8601 date-time string to convert. -/// +/// /// Returns StdResult pub fn iso8601_utc0_to_timestamp(iso8601_str: &str) -> StdResult { let Ok(datetime) = iso8601_str.parse::>() else { @@ -16,18 +15,20 @@ pub fn iso8601_utc0_to_timestamp(iso8601_str: &str) -> StdResult { // Verify the timezone is UTC (Zulu time) if iso8601_str.ends_with("Z") { - Ok(Timestamp::from_seconds(datetime.timestamp().try_into().unwrap_or_default())) + Ok(Timestamp::from_seconds( + datetime.timestamp().try_into().unwrap_or_default(), + )) } else { Err(StdError::generic_err("ISO 8601 string not in Zulu (UTC+0)")) } } /// Converts an ISO 8601 date-time string in Zulu (UTC+0) format to seconds. -/// +/// /// String format: {YYYY}-{MM}-{DD}T{hh}:{mm}:{ss}.{uuu}Z -/// +/// /// * `iso8601_str` - The ISO 8601 date-time string to convert. -/// +/// /// Returns StdResult pub fn iso8601_utc0_to_seconds(iso8601_str: &str) -> StdResult { let Ok(datetime) = iso8601_str.parse::>() else { @@ -38,7 +39,9 @@ pub fn iso8601_utc0_to_seconds(iso8601_str: &str) -> StdResult { if iso8601_str.ends_with("Z") { let seconds = datetime.timestamp(); if seconds < 0 { - return Err(StdError::generic_err("Date time before January 1, 1970 0:00:00 UTC not supported")) + return Err(StdError::generic_err( + "Date time before January 1, 1970 0:00:00 UTC not supported", + )); } Ok(seconds as u64) } else { @@ -59,10 +62,18 @@ mod tests { let dt_string = "2024-12-17T16:59:00.000"; let timestamp = iso8601_utc0_to_timestamp(dt_string); - assert!(timestamp.is_err(), "datetime string without Z Ok: {:?}", timestamp); + assert!( + timestamp.is_err(), + "datetime string without Z Ok: {:?}", + timestamp + ); let dt_string = "not a datetime"; let timestamp = iso8601_utc0_to_timestamp(dt_string); - assert!(timestamp.is_err(), "invalid datetime string Ok: {:?}", timestamp); + assert!( + timestamp.is_err(), + "invalid datetime string Ok: {:?}", + timestamp + ); } -} \ No newline at end of file +} diff --git a/packages/utils/src/lib.rs b/packages/utils/src/lib.rs index 791e44c..2610a79 100644 --- a/packages/utils/src/lib.rs +++ b/packages/utils/src/lib.rs @@ -1,11 +1,11 @@ #![doc = include_str!("../Readme.md")] pub mod calls; +pub mod datetime; pub mod feature_toggle; pub mod padding; pub mod types; -pub mod datetime; pub use calls::*; -pub use padding::*; pub use datetime::*; +pub use padding::*; From 93114ffe7e9b5cd21a856b406d47e502997447a2 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 7 Jan 2025 16:30:06 +1300 Subject: [PATCH 18/18] fix some clippy errors --- packages/permit/src/funcs.rs | 2 -- packages/permit/src/state.rs | 14 +++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/permit/src/funcs.rs b/packages/permit/src/funcs.rs index 10e10dc..edcd434 100644 --- a/packages/permit/src/funcs.rs +++ b/packages/permit/src/funcs.rs @@ -1,5 +1,3 @@ -use std::u64; - use cosmwasm_std::{to_binary, Binary, CanonicalAddr, Deps, Env, StdError, StdResult, Uint64}; use ripemd::{Digest, Ripemd160}; use secret_toolkit_utils::iso8601_utc0_to_timestamp; diff --git a/packages/permit/src/state.rs b/packages/permit/src/state.rs index 4db98bf..7447e53 100644 --- a/packages/permit/src/state.rs +++ b/packages/permit/src/state.rs @@ -99,7 +99,7 @@ pub trait RevokedPermitsStore<'a> { let next_id = next_id_store.may_load(storage)?.unwrap_or_default(); // store the revocation - all_revocations_store.insert(storage, &next_id, &interval.into_stored())?; + all_revocations_store.insert(storage, &next_id, &interval.as_stored())?; // increment next id next_id_store.save(storage, &(next_id.wrapping_add(1)))?; @@ -125,7 +125,7 @@ pub trait RevokedPermitsStore<'a> { let all_revocations_store = Self::ALL_REVOKED_PERMITS.add_suffix(account.as_bytes()); // try to convert id to a u64 - let Ok(id_str) = u64::from_str_radix(id, 10) else { + let Ok(id_str) = id.parse::() else { return Err(StdError::generic_err("Deleted revocation id not Uint64")); }; @@ -187,10 +187,10 @@ pub struct AllRevokedInterval { } impl AllRevokedInterval { - fn into_stored(&self) -> StoredAllRevokedInterval { + fn as_stored(&self) -> StoredAllRevokedInterval { StoredAllRevokedInterval { - created_before: self.created_before.and_then(|cb| Some(cb.u64())), - created_after: self.created_after.and_then(|ca| Some(ca.u64())), + created_before: self.created_before.map(|cb| cb.u64()), + created_after: self.created_after.map(|ca| ca.u64()), } } } @@ -205,8 +205,8 @@ pub struct StoredAllRevokedInterval { impl StoredAllRevokedInterval { fn to_humanized(&self) -> AllRevokedInterval { AllRevokedInterval { - created_before: self.created_before.and_then(|cb| Some(Uint64::from(cb))), - created_after: self.created_after.and_then(|ca| Some(Uint64::from(ca))), + created_before: self.created_before.map(|cb| Uint64::from(cb)), + created_after: self.created_after.map(|ca| Uint64::from(ca)), } } }