From 4b643995f415436e4cb07d09aeb3fd16a2465964 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sat, 25 Apr 2026 17:10:05 +0200 Subject: [PATCH 01/35] standards: add reusable Dusk contract primitives --- standards/dusk-contract-standards/Cargo.toml | 23 + standards/dusk-contract-standards/Makefile | 17 + .../src/access/access_control.rs | 214 +++++++ .../dusk-contract-standards/src/access/mod.rs | 11 + .../src/access/ownable.rs | 246 ++++++++ .../src/access/owner_set.rs | 190 ++++++ .../src/access/pausable.rs | 45 ++ .../dusk-contract-standards/src/auth/mod.rs | 560 ++++++++++++++++++ .../src/core/context.rs | 63 ++ .../dusk-contract-standards/src/core/error.rs | 16 + .../dusk-contract-standards/src/core/mod.rs | 12 + .../dusk-contract-standards/src/core/nonce.rs | 130 ++++ .../src/core/principal.rs | 216 +++++++ .../src/core/replay.rs | 74 +++ .../src/governance/controller.rs | 183 ++++++ .../src/governance/mod.rs | 10 + .../src/governance/timelock.rs | 121 ++++ standards/dusk-contract-standards/src/lib.rs | 30 + .../dusk-contract-standards/src/proxy/mod.rs | 12 + .../src/proxy/state_store.rs | 101 ++++ .../src/proxy/upgrade.rs | 341 +++++++++++ .../src/security/mod.rs | 5 + .../src/security/reentrancy_guard.rs | 62 ++ .../src/token/drc20/events.rs | 41 ++ .../src/token/drc20/extensions.rs | 230 +++++++ .../src/token/drc20/mod.rs | 314 ++++++++++ .../src/token/drc20/types.rs | 144 +++++ .../src/token/drc721/events.rs | 58 ++ .../src/token/drc721/mod.rs | 365 ++++++++++++ .../src/token/drc721/royalty.rs | 130 ++++ .../src/token/drc721/types.rs | 170 ++++++ .../dusk-contract-standards/src/token/mod.rs | 4 + 32 files changed, 4138 insertions(+) create mode 100644 standards/dusk-contract-standards/Cargo.toml create mode 100644 standards/dusk-contract-standards/Makefile create mode 100644 standards/dusk-contract-standards/src/access/access_control.rs create mode 100644 standards/dusk-contract-standards/src/access/mod.rs create mode 100644 standards/dusk-contract-standards/src/access/ownable.rs create mode 100644 standards/dusk-contract-standards/src/access/owner_set.rs create mode 100644 standards/dusk-contract-standards/src/access/pausable.rs create mode 100644 standards/dusk-contract-standards/src/auth/mod.rs create mode 100644 standards/dusk-contract-standards/src/core/context.rs create mode 100644 standards/dusk-contract-standards/src/core/error.rs create mode 100644 standards/dusk-contract-standards/src/core/mod.rs create mode 100644 standards/dusk-contract-standards/src/core/nonce.rs create mode 100644 standards/dusk-contract-standards/src/core/principal.rs create mode 100644 standards/dusk-contract-standards/src/core/replay.rs create mode 100644 standards/dusk-contract-standards/src/governance/controller.rs create mode 100644 standards/dusk-contract-standards/src/governance/mod.rs create mode 100644 standards/dusk-contract-standards/src/governance/timelock.rs create mode 100644 standards/dusk-contract-standards/src/lib.rs create mode 100644 standards/dusk-contract-standards/src/proxy/mod.rs create mode 100644 standards/dusk-contract-standards/src/proxy/state_store.rs create mode 100644 standards/dusk-contract-standards/src/proxy/upgrade.rs create mode 100644 standards/dusk-contract-standards/src/security/mod.rs create mode 100644 standards/dusk-contract-standards/src/security/reentrancy_guard.rs create mode 100644 standards/dusk-contract-standards/src/token/drc20/events.rs create mode 100644 standards/dusk-contract-standards/src/token/drc20/extensions.rs create mode 100644 standards/dusk-contract-standards/src/token/drc20/mod.rs create mode 100644 standards/dusk-contract-standards/src/token/drc20/types.rs create mode 100644 standards/dusk-contract-standards/src/token/drc721/events.rs create mode 100644 standards/dusk-contract-standards/src/token/drc721/mod.rs create mode 100644 standards/dusk-contract-standards/src/token/drc721/royalty.rs create mode 100644 standards/dusk-contract-standards/src/token/drc721/types.rs create mode 100644 standards/dusk-contract-standards/src/token/mod.rs diff --git a/standards/dusk-contract-standards/Cargo.toml b/standards/dusk-contract-standards/Cargo.toml new file mode 100644 index 00000000..39b35e8f --- /dev/null +++ b/standards/dusk-contract-standards/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "dusk-contract-standards" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["rlib"] + +[dependencies] +dusk-core = { workspace = true } +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } +serde = { workspace = true, optional = true } +serde_with = { workspace = true, optional = true } +time = { workspace = true, optional = true } + +[features] +contract = [] +serde = ["dep:serde", "dep:serde_with", "dep:time", "dusk-core/serde"] + +[dev-dependencies] +dusk-vm = { workspace = true } +rand = { workspace = true, features = ["std_rng"] } diff --git a/standards/dusk-contract-standards/Makefile b/standards/dusk-contract-standards/Makefile new file mode 100644 index 00000000..bd829562 --- /dev/null +++ b/standards/dusk-contract-standards/Makefile @@ -0,0 +1,17 @@ +all: check test clippy + +check: + @cargo check -p dusk-contract-standards + +test: + @cargo test -p dusk-contract-standards + +wasm: + +clippy: + @cargo clippy -p dusk-contract-standards -- -D warnings + +doc: + @cargo doc -p dusk-contract-standards --no-deps + +.PHONY: all check test wasm clippy doc diff --git a/standards/dusk-contract-standards/src/access/access_control.rs b/standards/dusk-contract-standards/src/access/access_control.rs new file mode 100644 index 00000000..84358491 --- /dev/null +++ b/standards/dusk-contract-standards/src/access/access_control.rs @@ -0,0 +1,214 @@ +//! Role-based access control. + +use alloc::collections::{BTreeMap, BTreeSet}; + +use crate::auth::{ + ActionEnvelope, AuthorizationManager, Authorizer, SignedAuthorization, +}; +use crate::core::{error, CallContext, Principal}; + +/// 32-byte role id. +pub type Role = [u8; 32]; + +/// Default admin role. +pub const DEFAULT_ADMIN_ROLE: Role = [0u8; 32]; + +#[derive(Clone, Debug)] +struct RoleData { + members: BTreeSet, + admin_role: Role, +} + +impl RoleData { + fn new(admin_role: Role) -> Self { + Self { + members: BTreeSet::new(), + admin_role, + } + } +} + +/// Role-based access-control module. +#[derive(Clone, Debug, Default)] +pub struct AccessControl { + roles: BTreeMap, +} + +impl AccessControl { + /// Creates an empty access-control module. + pub const fn new() -> Self { + Self { + roles: BTreeMap::new(), + } + } + + /// Bootstraps the default admin role. + pub fn init_admin(&mut self, admin: Principal) { + if admin.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if self.roles.contains_key(&DEFAULT_ADMIN_ROLE) { + panic!("{}", error::ALREADY_INITIALIZED); + } + self.roles + .entry(DEFAULT_ADMIN_ROLE) + .or_insert_with(|| RoleData::new(DEFAULT_ADMIN_ROLE)) + .members + .insert(admin); + } + + /// Returns true when `account` has `role`. + pub fn has_role(&self, role: Role, account: Principal) -> bool { + self.roles + .get(&role) + .map(|data| data.members.contains(&account)) + .unwrap_or(false) + } + + /// Panics unless `account` has `role`. + pub fn assert_role(&self, role: Role, account: Principal) { + if !self.has_role(role, account) { + panic!("{}", error::UNAUTHORIZED); + } + } + + /// Authorizes any principal with `role` through runtime context or signed + /// authorization. + pub fn authorize_role( + &self, + role: Role, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_role_with(role, &mut authorizer, authorization) + } + + /// Authorizes any principal with `role` using a reusable call authorizer. + pub fn authorize_role_with( + &self, + role: Role, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + ) -> Principal { + if let Some(principal) = authorizer.observed_principal() { + if self.has_role(role, principal) { + return principal; + } + } + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + let principal = authorizer.require_signed(authorization); + self.assert_role(role, principal); + principal + } + + /// Authorizes any principal with `role` through runtime context or an + /// action-bound signed authorization. + pub fn authorize_role_action( + &self, + role: Role, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_role_action_with( + role, + &mut authorizer, + authorization, + envelope, + ) + } + + /// Authorizes any principal with `role` using a reusable call authorizer + /// and exact call envelope for signed fallbacks. + pub fn authorize_role_action_with( + &self, + role: Role, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + if let Some(principal) = authorizer.observed_principal() { + if self.has_role(role, principal) { + return principal; + } + } + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + let principal = + authorizer.require_signed_action(authorization, envelope); + self.assert_role(role, principal); + principal + } + + /// Returns a role's admin role. + pub fn get_role_admin(&self, role: Role) -> Role { + self.roles + .get(&role) + .map(|data| data.admin_role) + .unwrap_or(DEFAULT_ADMIN_ROLE) + } + + /// Sets a role's admin role. Caller must have the current admin role. + pub fn set_role_admin( + &mut self, + caller: Principal, + role: Role, + admin_role: Role, + ) { + let current_admin = self.get_role_admin(role); + self.assert_role(current_admin, caller); + self.roles + .entry(role) + .or_insert_with(|| RoleData::new(DEFAULT_ADMIN_ROLE)) + .admin_role = admin_role; + } + + /// Grants `role` to `account`. Caller must have the role admin. + pub fn grant_role( + &mut self, + caller: Principal, + role: Role, + account: Principal, + ) { + if account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let admin = self.get_role_admin(role); + self.assert_role(admin, caller); + self.roles + .entry(role) + .or_insert_with(|| RoleData::new(DEFAULT_ADMIN_ROLE)) + .members + .insert(account); + } + + /// Revokes `role` from `account`. Caller must have the role admin. + pub fn revoke_role( + &mut self, + caller: Principal, + role: Role, + account: Principal, + ) { + let admin = self.get_role_admin(role); + self.assert_role(admin, caller); + if let Some(data) = self.roles.get_mut(&role) { + data.members.remove(&account); + } + } + + /// Renounces `role` from `caller`. + pub fn renounce_role(&mut self, role: Role, caller: Principal) { + if let Some(data) = self.roles.get_mut(&role) { + data.members.remove(&caller); + } + } +} diff --git a/standards/dusk-contract-standards/src/access/mod.rs b/standards/dusk-contract-standards/src/access/mod.rs new file mode 100644 index 00000000..93d4e224 --- /dev/null +++ b/standards/dusk-contract-standards/src/access/mod.rs @@ -0,0 +1,11 @@ +//! Access-control modules. + +pub mod access_control; +pub mod ownable; +pub mod owner_set; +pub mod pausable; + +pub use access_control::{AccessControl, Role, DEFAULT_ADMIN_ROLE}; +pub use ownable::{Ownable, Ownable2Step}; +pub use owner_set::OwnerSet; +pub use pausable::Pausable; diff --git a/standards/dusk-contract-standards/src/access/ownable.rs b/standards/dusk-contract-standards/src/access/ownable.rs new file mode 100644 index 00000000..57e0d855 --- /dev/null +++ b/standards/dusk-contract-standards/src/access/ownable.rs @@ -0,0 +1,246 @@ +//! Ownership primitives. + +use crate::auth::{ + ActionEnvelope, AuthorizationManager, Authorizer, SignedAuthorization, +}; +use crate::core::{error, CallContext, Principal}; + +/// Single-owner access control. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Ownable { + owner: Option, +} + +impl Ownable { + /// Creates an uninitialized owner module. + pub const fn new() -> Self { + Self { owner: None } + } + + /// Initializes ownership. + pub fn init(&mut self, owner: Principal) { + if self.owner.is_some() { + panic!("{}", error::ALREADY_INITIALIZED); + } + if owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.owner = Some(owner); + } + + /// Returns the current owner. + pub const fn owner(&self) -> Option { + self.owner + } + + /// Returns true when `caller` is the owner. + pub fn is_owner(&self, caller: Principal) -> bool { + self.owner == Some(caller) + } + + /// Panics unless `caller` is the owner. + pub fn assert_owner(&self, caller: Principal) { + if !self.is_owner(caller) { + panic!("{}", error::UNAUTHORIZED); + } + } + + /// Authorizes the owner through runtime context or signed authorization. + pub fn authorize_owner( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_owner_with(&mut authorizer, authorization) + } + + /// Authorizes the owner with a reusable call authorizer. + pub fn authorize_owner_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + ) -> Principal { + let owner = self + .owner + .unwrap_or_else(|| panic!("{}", error::NOT_INITIALIZED)); + authorizer.require_principal(owner, authorization) + } + + /// Authorizes the owner through runtime context or an action-bound signed + /// authorization. + pub fn authorize_owner_action( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_owner_action_with( + &mut authorizer, + authorization, + envelope, + ) + } + + /// Authorizes the owner with a reusable call authorizer and exact call + /// envelope for signed fallbacks. + pub fn authorize_owner_action_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + let owner = self + .owner + .unwrap_or_else(|| panic!("{}", error::NOT_INITIALIZED)); + authorizer.require_principal_action(owner, authorization, envelope) + } + + /// Transfers ownership. + pub fn transfer_ownership( + &mut self, + caller: Principal, + new_owner: Principal, + ) { + self.assert_owner(caller); + if new_owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.owner = Some(new_owner); + } + + /// Renounces ownership. + pub fn renounce_ownership(&mut self, caller: Principal) { + self.assert_owner(caller); + self.owner = None; + } +} + +/// Two-step ownership transfer. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Ownable2Step { + ownable: Ownable, + pending_owner: Option, +} + +impl Ownable2Step { + /// Creates an uninitialized module. + pub const fn new() -> Self { + Self { + ownable: Ownable::new(), + pending_owner: None, + } + } + + /// Initializes ownership. + pub fn init(&mut self, owner: Principal) { + self.ownable.init(owner); + } + + /// Returns the current owner. + pub const fn owner(&self) -> Option { + self.ownable.owner() + } + + /// Returns the pending owner. + pub const fn pending_owner(&self) -> Option { + self.pending_owner + } + + /// Panics unless `caller` is the owner. + pub fn assert_owner(&self, caller: Principal) { + self.ownable.assert_owner(caller); + } + + /// Authorizes the owner through runtime context or signed authorization. + pub fn authorize_owner( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + now: u64, + ) -> Principal { + self.ownable.authorize_owner( + authorizations, + context, + authorization, + now, + ) + } + + /// Authorizes the owner with a reusable call authorizer. + pub fn authorize_owner_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + ) -> Principal { + self.ownable.authorize_owner_with(authorizer, authorization) + } + + /// Authorizes the owner through runtime context or an action-bound signed + /// authorization. + pub fn authorize_owner_action( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + self.ownable.authorize_owner_action( + authorizations, + context, + authorization, + envelope, + now, + ) + } + + /// Authorizes the owner with a reusable call authorizer and exact call + /// envelope for signed fallbacks. + pub fn authorize_owner_action_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + self.ownable.authorize_owner_action_with( + authorizer, + authorization, + envelope, + ) + } + + /// Starts ownership transfer. + pub fn transfer_ownership( + &mut self, + caller: Principal, + new_owner: Principal, + ) { + self.ownable.assert_owner(caller); + if new_owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.pending_owner = Some(new_owner); + } + + /// Accepts ownership transfer. + pub fn accept_ownership(&mut self, caller: Principal) { + if self.pending_owner != Some(caller) { + panic!("{}", error::UNAUTHORIZED); + } + self.ownable.owner = Some(caller); + self.pending_owner = None; + } + + /// Renounces ownership. + pub fn renounce_ownership(&mut self, caller: Principal) { + self.ownable.renounce_ownership(caller); + self.pending_owner = None; + } +} diff --git a/standards/dusk-contract-standards/src/access/owner_set.rs b/standards/dusk-contract-standards/src/access/owner_set.rs new file mode 100644 index 00000000..63ec4d2d --- /dev/null +++ b/standards/dusk-contract-standards/src/access/owner_set.rs @@ -0,0 +1,190 @@ +//! Multi-principal ownership. + +use alloc::collections::BTreeSet; +use alloc::vec::Vec; + +use crate::auth::{ + ActionEnvelope, AuthorizationManager, Authorizer, SignedAuthorization, +}; +use crate::core::{error, CallContext, Principal, PrincipalKind}; + +/// Owner registry for contracts that need Moonlight, Phoenix, and contract +/// identities to coexist in one authorization policy. +#[derive(Clone, Debug, Default)] +pub struct OwnerSet { + owners: BTreeSet, +} + +impl OwnerSet { + /// Creates an empty owner set. + pub const fn new() -> Self { + Self { + owners: BTreeSet::new(), + } + } + + /// Initializes with at least one owner. + pub fn init(&mut self, owners: impl IntoIterator) { + if !self.owners.is_empty() { + panic!("{}", error::ALREADY_INITIALIZED); + } + for owner in owners { + self.add_initial_owner(owner); + } + if self.owners.is_empty() { + panic!("{}", error::INVALID_OWNER); + } + } + + /// Returns true when `principal` is an owner. + pub fn is_owner(&self, principal: Principal) -> bool { + self.owners.contains(&principal) + } + + /// Panics unless `principal` is an owner. + pub fn assert_owner(&self, principal: Principal) { + if !self.is_owner(principal) { + panic!("{}", error::UNAUTHORIZED); + } + } + + /// Authorizes any owner through runtime context or signed authorization. + pub fn authorize_owner( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_owner_with(&mut authorizer, authorization) + } + + /// Authorizes any owner with a reusable call authorizer. + pub fn authorize_owner_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + ) -> Principal { + if let Some(principal) = authorizer.observed_principal() { + if self.is_owner(principal) { + return principal; + } + } + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + let principal = authorizer.require_signed(authorization); + self.assert_owner(principal); + principal + } + + /// Authorizes any owner through runtime context or an action-bound signed + /// authorization. + pub fn authorize_owner_action( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_owner_action_with( + &mut authorizer, + authorization, + envelope, + ) + } + + /// Authorizes any owner with a reusable call authorizer and exact call + /// envelope for signed fallbacks. + pub fn authorize_owner_action_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + if let Some(principal) = authorizer.observed_principal() { + if self.is_owner(principal) { + return principal; + } + } + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + let principal = + authorizer.require_signed_action(authorization, envelope); + self.assert_owner(principal); + principal + } + + /// Returns all owners in stable order. + pub fn owners(&self) -> Vec { + self.owners.iter().copied().collect() + } + + /// Returns the number of owners. + pub fn len(&self) -> usize { + self.owners.len() + } + + /// Returns true when no owners are configured. + pub fn is_empty(&self) -> bool { + self.owners.is_empty() + } + + /// Counts owners of a given principal kind. + pub fn count_kind(&self, kind: PrincipalKind) -> usize { + self.owners + .iter() + .filter(|principal| principal.kind() == kind) + .count() + } + + /// Adds an owner. Caller must already be an owner. + pub fn add_owner(&mut self, caller: Principal, new_owner: Principal) { + self.assert_owner(caller); + self.add_initial_owner(new_owner); + } + + /// Removes an owner. The last owner cannot be removed. + pub fn remove_owner(&mut self, caller: Principal, owner: Principal) { + self.assert_owner(caller); + if !self.owners.contains(&owner) { + panic!("{}", error::INVALID_OWNER); + } + if self.owners.len() == 1 { + panic!("{}", error::INVALID_OWNER); + } + self.owners.remove(&owner); + } + + /// Replaces one owner with another. + pub fn replace_owner( + &mut self, + caller: Principal, + old_owner: Principal, + new_owner: Principal, + ) { + self.assert_owner(caller); + if new_owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if !self.owners.contains(&old_owner) { + panic!("{}", error::INVALID_OWNER); + } + if old_owner != new_owner && self.owners.contains(&new_owner) { + panic!("{}", error::INVALID_OWNER); + } + self.owners.remove(&old_owner); + self.owners.insert(new_owner); + } + + fn add_initial_owner(&mut self, owner: Principal) { + if owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.owners.insert(owner); + } +} diff --git a/standards/dusk-contract-standards/src/access/pausable.rs b/standards/dusk-contract-standards/src/access/pausable.rs new file mode 100644 index 00000000..f1a5805c --- /dev/null +++ b/standards/dusk-contract-standards/src/access/pausable.rs @@ -0,0 +1,45 @@ +//! Emergency stop primitive. + +use crate::core::error; + +/// Pausable module. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Pausable { + paused: bool, +} + +impl Pausable { + /// Creates an unpaused module. + pub const fn new() -> Self { + Self { paused: false } + } + + /// Returns whether the module is paused. + pub const fn paused(&self) -> bool { + self.paused + } + + /// Panics when paused. + pub fn assert_not_paused(&self) { + if self.paused { + panic!("{}", error::UNAUTHORIZED); + } + } + + /// Panics when not paused. + pub fn assert_paused(&self) { + if !self.paused { + panic!("{}", error::UNAUTHORIZED); + } + } + + /// Pauses. + pub fn pause(&mut self) { + self.paused = true; + } + + /// Unpauses. + pub fn unpause(&mut self) { + self.paused = false; + } +} diff --git a/standards/dusk-contract-standards/src/auth/mod.rs b/standards/dusk-contract-standards/src/auth/mod.rs new file mode 100644 index 00000000..72268949 --- /dev/null +++ b/standards/dusk-contract-standards/src/auth/mod.rs @@ -0,0 +1,560 @@ +//! Replay-protected signed authorization helpers. + +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use dusk_core::abi::ContractId; +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, Signature as BlsSignature, +}; +use dusk_core::signatures::schnorr::{ + PublicKey as SchnorrPublicKey, Signature as SchnorrSignature, +}; +use dusk_core::BlsScalar; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::{ + error, CallContext, NonceDomain, NonceEntry, NonceManager, Principal, + PrincipalKind, ReplayEntry, ReplayGuard, ReplayKey, +}; + +/// Stable prefix for authorization messages. +pub const AUTH_MESSAGE_PREFIX: &[u8] = b"dusk-contract-standards/auth/v1"; + +/// Expected call envelope for an action-bound authorization. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ActionEnvelope { + /// Contract this authorization is intended for. + pub contract: ContractId, + /// Domain-separated nonce stream. + pub domain: NonceDomain, + /// Application-level action id. + pub action_id: [u8; 32], + /// Hash or commitment of the call payload. + pub payload_hash: [u8; 32], +} + +impl ActionEnvelope { + /// Creates a new expected action envelope. + pub const fn new( + contract: ContractId, + domain: NonceDomain, + action_id: [u8; 32], + payload_hash: [u8; 32], + ) -> Self { + Self { + contract, + domain, + action_id, + payload_hash, + } + } +} + +/// Contract action authorized by a signature. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct AuthorizedAction { + /// Contract this authorization is intended for. + pub contract: ContractId, + /// Domain-separated nonce stream. + pub domain: NonceDomain, + /// Application-level action id. + pub action_id: [u8; 32], + /// Expected nonce. + pub nonce: u64, + /// Expiration timestamp/height unit selected by the contract. + /// + /// A value of zero means no expiry. + pub expires_at: u64, + /// Principal being authorized. + pub principal: Principal, + /// Hash or commitment of the call payload. + pub payload_hash: [u8; 32], +} + +impl AuthorizedAction { + /// Returns true when this action has expired at `now`. + pub const fn is_expired(&self, now: u64) -> bool { + self.expires_at != 0 && now > self.expires_at + } + + /// Builds the stable message bytes signed for this action. + pub fn message_bytes(&self) -> Vec { + let principal = self.principal.to_bytes(); + let mut out = Vec::with_capacity( + AUTH_MESSAGE_PREFIX.len() + + 32 + + 32 + + 32 + + 8 + + 8 + + 2 + + principal.len() + + 32, + ); + out.extend_from_slice(AUTH_MESSAGE_PREFIX); + out.extend_from_slice(&self.contract.to_bytes()); + out.extend_from_slice(&self.domain); + out.extend_from_slice(&self.action_id); + out.extend_from_slice(&self.nonce.to_be_bytes()); + out.extend_from_slice(&self.expires_at.to_be_bytes()); + out.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + out.extend_from_slice(&principal); + out.extend_from_slice(&self.payload_hash); + out + } + + /// Hashes the stable message bytes into the scalar signed by Phoenix keys. + pub fn message_hash(&self) -> BlsScalar { + hash_message(self.message_bytes()) + } + + /// Panics unless this action matches the expected contract, nonce domain, + /// action id, and payload hash. + pub fn assert_matches( + &self, + contract: ContractId, + domain: NonceDomain, + action_id: [u8; 32], + payload_hash: [u8; 32], + ) { + self.assert_envelope(ActionEnvelope::new( + contract, + domain, + action_id, + payload_hash, + )); + } + + /// Panics unless this action matches the expected call envelope. + pub fn assert_envelope(&self, envelope: ActionEnvelope) { + if self.contract != envelope.contract + || self.domain != envelope.domain + || self.action_id != envelope.action_id + || self.payload_hash != envelope.payload_hash + { + panic!("{}", error::UNAUTHORIZED); + } + } +} + +/// Moonlight BLS authorization. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MoonlightAuthorization { + /// Authorized action. + pub action: AuthorizedAction, + /// Moonlight public key. + pub public_key: BlsPublicKey, + /// Signature over `action.message_bytes()`. + pub signature: BlsSignature, +} + +/// Phoenix Schnorr authorization. +/// +/// This proves control of the Phoenix Schnorr public key represented by +/// `action.principal`. It does not prove ownership or spending of a specific +/// Phoenix note. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct PhoenixSignatureAuthorization { + /// Authorized action. + pub action: AuthorizedAction, + /// Phoenix Schnorr public key. + pub public_key: SchnorrPublicKey, + /// Signature over `action.message_hash()`. + pub signature: SchnorrSignature, + /// Optional extra replay key consumed with the signature. + pub replay_key: Option, +} + +/// Explicit signed authorization supplied with a call. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub enum SignedAuthorization { + /// Moonlight BLS signed action. + Moonlight(MoonlightAuthorization), + /// Phoenix Schnorr signed action. + Phoenix(PhoenixSignatureAuthorization), +} + +impl SignedAuthorization { + /// Returns the action covered by the signature. + pub const fn action(&self) -> &AuthorizedAction { + match self { + Self::Moonlight(auth) => &auth.action, + Self::Phoenix(auth) => &auth.action, + } + } + + /// Panics unless the signed action matches the exact call envelope. + pub fn assert_action( + &self, + contract: ContractId, + domain: NonceDomain, + action_id: [u8; 32], + payload_hash: [u8; 32], + ) { + self.assert_envelope(ActionEnvelope::new( + contract, + domain, + action_id, + payload_hash, + )); + } + + /// Panics unless the signed action matches the exact call envelope. + pub fn assert_envelope(&self, envelope: ActionEnvelope) { + self.action().assert_envelope(envelope); + } +} + +/// Ergonomic runtime-or-signature authorizer for composing contracts. +/// +/// Moonlight and contract principals can pass through the observed call +/// context. Phoenix principals must pass through explicit signed +/// authorization because Phoenix calls do not expose a stable caller identity. +pub struct Authorizer<'a> { + authorizations: &'a mut AuthorizationManager, + context: CallContext, + now: u64, +} + +impl<'a> Authorizer<'a> { + /// Creates a reusable authorizer for one contract call. + pub fn new( + authorizations: &'a mut AuthorizationManager, + context: CallContext, + now: u64, + ) -> Self { + Self { + authorizations, + context, + now, + } + } + + /// Returns the observed runtime principal, if any. + pub const fn observed_principal(&self) -> Option { + self.context.principal + } + + /// Verifies an exact expected principal. + pub fn require_principal( + &mut self, + expected: Principal, + authorization: Option<&SignedAuthorization>, + ) -> Principal { + self.authorizations.authorize_principal( + expected, + self.context, + authorization, + self.now, + ) + } + + /// Verifies an exact expected principal and binds signed fallbacks to a + /// call envelope before consuming nonce/replay state. + pub fn require_principal_action( + &mut self, + expected: Principal, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + self.authorizations.authorize_principal_action( + expected, + self.context, + authorization, + envelope, + self.now, + ) + } + + /// Verifies a signed authorization and consumes its nonce/replay state. + /// + /// This is a low-level primitive. Public contract methods should usually + /// prefer `require_signed_action` so the intended call envelope is checked + /// before nonce state is consumed. + pub fn require_signed( + &mut self, + authorization: &SignedAuthorization, + ) -> Principal { + self.authorizations + .authorize_signed(authorization, self.now) + } + + /// Verifies a signed authorization for an exact call envelope and consumes + /// its nonce/replay state. + pub fn require_signed_action( + &mut self, + authorization: &SignedAuthorization, + envelope: ActionEnvelope, + ) -> Principal { + self.authorizations.authorize_signed_action( + authorization, + envelope, + self.now, + ) + } +} + +/// Authorization manager with nonce and replay-key state. +#[derive(Clone, Debug, Default)] +pub struct AuthorizationManager { + nonces: NonceManager, + replays: ReplayGuard, +} + +impl AuthorizationManager { + /// Creates an empty manager. + pub const fn new() -> Self { + Self { + nonces: NonceManager::new(), + replays: ReplayGuard::new(), + } + } + + /// Returns current nonce for a principal/domain stream. + pub fn nonce(&self, principal: Principal, domain: NonceDomain) -> u64 { + self.nonces.current(principal, domain) + } + + /// Returns whether a replay key has been consumed. + pub fn replay_used(&self, principal: Principal, key: ReplayKey) -> bool { + self.replays.is_used(principal, key) + } + + /// Authorizes an exact expected principal. + /// + /// Moonlight and contract principals can be authorized by the runtime call + /// context. Moonlight can also be authorized by a BLS signed action. + /// Phoenix requires a Schnorr signed action because Phoenix does not expose + /// a stable runtime caller identity to contracts. + pub fn authorize_principal( + &mut self, + expected: Principal, + context: CallContext, + authorization: Option<&SignedAuthorization>, + now: u64, + ) -> Principal { + match expected.kind() { + PrincipalKind::Moonlight | PrincipalKind::Contract => { + if context.principal == Some(expected) { + return expected; + } + } + PrincipalKind::Phoenix => {} + } + + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + let principal = self.authorize_signed(authorization, now); + if principal != expected { + panic!("{}", error::UNAUTHORIZED); + } + principal + } + + /// Authorizes an exact expected principal and binds signed fallbacks to a + /// call envelope before nonce/replay state is consumed. + pub fn authorize_principal_action( + &mut self, + expected: Principal, + context: CallContext, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + match expected.kind() { + PrincipalKind::Moonlight | PrincipalKind::Contract => { + if context.principal == Some(expected) { + return expected; + } + } + PrincipalKind::Phoenix => {} + } + + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + let principal = + self.authorize_signed_action(authorization, envelope, now); + if principal != expected { + panic!("{}", error::UNAUTHORIZED); + } + principal + } + + /// Verifies and consumes a signed authorization, returning its principal. + /// + /// This is a low-level primitive. Public contract methods should usually + /// prefer `authorize_signed_action` so the intended call envelope is + /// checked before nonce state is consumed. + pub fn authorize_signed( + &mut self, + authorization: &SignedAuthorization, + now: u64, + ) -> Principal { + match authorization { + SignedAuthorization::Moonlight(auth) => { + self.authorize_moonlight(auth, now) + } + SignedAuthorization::Phoenix(auth) => { + self.authorize_phoenix(auth, now) + } + } + } + + /// Verifies a signed authorization for an exact call envelope and consumes + /// its nonce/replay state. + pub fn authorize_signed_action( + &mut self, + authorization: &SignedAuthorization, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + authorization.assert_envelope(envelope); + self.authorize_signed(authorization, now) + } + + /// Verifies and consumes a Moonlight BLS authorization. + pub fn authorize_moonlight( + &mut self, + auth: &MoonlightAuthorization, + now: u64, + ) -> Principal { + assert_live(&auth.action, now); + let principal = Principal::moonlight(&auth.public_key); + if auth.action.principal != principal { + panic!("{}", error::UNAUTHORIZED); + } + let message = auth.action.message_bytes(); + if !verify_bls(&message, &auth.public_key, &auth.signature) { + panic!("{}", error::UNAUTHORIZED); + } + self.nonces + .consume(principal, auth.action.domain, auth.action.nonce); + principal + } + + /// Verifies and consumes a Phoenix authorization. + pub fn authorize_phoenix( + &mut self, + auth: &PhoenixSignatureAuthorization, + now: u64, + ) -> Principal { + assert_live(&auth.action, now); + let principal = Principal::phoenix_public_key(&auth.public_key); + if auth.action.principal != principal { + panic!("{}", error::UNAUTHORIZED); + } + if !verify_schnorr( + auth.action.message_hash(), + &auth.public_key, + &auth.signature, + ) { + panic!("{}", error::UNAUTHORIZED); + } + if let Some(key) = auth.replay_key { + if self.replays.is_used(principal, key) { + panic!("{}", error::REPLAY); + } + } + self.nonces + .consume(principal, auth.action.domain, auth.action.nonce); + if let Some(key) = auth.replay_key { + self.replays.consume(principal, key); + } + principal + } + + /// Exports nonce streams. + pub fn nonce_entries(&self) -> Vec { + self.nonces.entries() + } + + /// Imports nonce streams, keeping the greatest value for duplicates. + pub fn import_nonce_entries( + &mut self, + entries: impl IntoIterator, + ) { + self.nonces.import_entries(entries); + } + + /// Exports replay keys. + pub fn replay_entries(&self) -> Vec { + self.replays.entries() + } + + /// Imports replay keys. + pub fn import_replay_entries( + &mut self, + entries: impl IntoIterator, + ) { + self.replays.import_entries(entries); + } +} + +fn assert_live(action: &AuthorizedAction, now: u64) { + if action.is_expired(now) { + panic!("Authorization: expired"); + } +} + +#[cfg(all(target_family = "wasm", feature = "contract"))] +fn hash_message(message: Vec) -> BlsScalar { + dusk_core::abi::hash(message) +} + +#[cfg(not(all(target_family = "wasm", feature = "contract")))] +fn hash_message(message: Vec) -> BlsScalar { + BlsScalar::hash_to_scalar(&message) +} + +#[cfg(all(target_family = "wasm", feature = "contract"))] +fn verify_bls( + message: &[u8], + public_key: &BlsPublicKey, + signature: &BlsSignature, +) -> bool { + dusk_core::abi::verify_bls(message.to_vec(), *public_key, *signature) +} + +#[cfg(not(all(target_family = "wasm", feature = "contract")))] +fn verify_bls( + message: &[u8], + public_key: &BlsPublicKey, + signature: &BlsSignature, +) -> bool { + public_key.verify(signature, message).is_ok() +} + +#[cfg(all(target_family = "wasm", feature = "contract"))] +fn verify_schnorr( + message: BlsScalar, + public_key: &SchnorrPublicKey, + signature: &SchnorrSignature, +) -> bool { + dusk_core::abi::verify_schnorr(message, *public_key, *signature) +} + +#[cfg(not(all(target_family = "wasm", feature = "contract")))] +fn verify_schnorr( + message: BlsScalar, + public_key: &SchnorrPublicKey, + signature: &SchnorrSignature, +) -> bool { + public_key.verify(signature, message).is_ok() +} diff --git a/standards/dusk-contract-standards/src/core/context.rs b/standards/dusk-contract-standards/src/core/context.rs new file mode 100644 index 00000000..7007d861 --- /dev/null +++ b/standards/dusk-contract-standards/src/core/context.rs @@ -0,0 +1,63 @@ +//! Runtime call context helpers. + +use crate::core::principal::Principal; + +/// Current call context. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct CallContext { + /// Current actor, if the runtime exposes one. + pub principal: Option, +} + +impl CallContext { + /// Creates a context from an explicit principal. + pub const fn from_principal(principal: Principal) -> Self { + Self { + principal: Some(principal), + } + } + + /// Creates an empty context. + pub const fn none() -> Self { + Self { principal: None } + } + + /// Returns the principal or panics with `msg`. + pub fn require_principal(&self, msg: &str) -> Principal { + self.principal.unwrap_or_else(|| panic!("{}", msg)) + } + + /// Reads the current Dusk runtime context. + /// + /// A root Moonlight call is represented by `public_sender`. An + /// inter-contract call is represented by the immediate caller contract id. + /// Phoenix transactions do not expose a stable owner identity here; Phoenix + /// authorization should be modeled with an explicit Schnorr signature plus + /// nonce/replay protection. + #[cfg(all(target_family = "wasm", feature = "contract"))] + pub fn current() -> Self { + use dusk_core::abi; + + match abi::callstack().len() { + 0 => Self::none(), + 1 => { + let Some(pk) = abi::public_sender() else { + return Self::none(); + }; + Self::from_principal(Principal::moonlight(&pk)) + } + _ => { + let Some(caller) = abi::caller() else { + return Self::none(); + }; + Self::from_principal(Principal::Contract(caller)) + } + } + } + + /// Native tests must inject context explicitly. + #[cfg(not(all(target_family = "wasm", feature = "contract")))] + pub const fn current() -> Self { + Self::none() + } +} diff --git a/standards/dusk-contract-standards/src/core/error.rs b/standards/dusk-contract-standards/src/core/error.rs new file mode 100644 index 00000000..a508866f --- /dev/null +++ b/standards/dusk-contract-standards/src/core/error.rs @@ -0,0 +1,16 @@ +//! Stable error strings used by the reusable primitives. + +pub const ALREADY_INITIALIZED: &str = "DuskStandards: already initialized"; +pub const NOT_INITIALIZED: &str = "DuskStandards: not initialized"; +pub const UNAUTHORIZED: &str = "DuskStandards: unauthorized"; +pub const INVALID_OWNER: &str = "DuskStandards: invalid owner"; +pub const INVALID_PRINCIPAL: &str = "DuskStandards: invalid principal"; +pub const ZERO_PRINCIPAL: &str = "DuskStandards: zero principal"; +pub const REPLAY: &str = "DuskStandards: replay"; +pub const INVALID_NONCE: &str = "DuskStandards: invalid nonce"; +pub const DELAY_NOT_ELAPSED: &str = "DuskStandards: delay not elapsed"; +pub const OPERATION_DONE: &str = "DuskStandards: operation already done"; +pub const OPERATION_UNKNOWN: &str = "DuskStandards: operation unknown"; +pub const INVALID_OPERATION: &str = "DuskStandards: invalid operation"; +pub const OVERFLOW: &str = "DuskStandards: arithmetic overflow"; +pub const UNDERFLOW: &str = "DuskStandards: arithmetic underflow"; diff --git a/standards/dusk-contract-standards/src/core/mod.rs b/standards/dusk-contract-standards/src/core/mod.rs new file mode 100644 index 00000000..a7d8d7b0 --- /dev/null +++ b/standards/dusk-contract-standards/src/core/mod.rs @@ -0,0 +1,12 @@ +//! Shared identity, context, replay, and error primitives. + +pub mod context; +pub mod error; +pub mod nonce; +pub mod principal; +pub mod replay; + +pub use context::CallContext; +pub use nonce::{NonceDomain, NonceEntry, NonceManager, NonceQuery}; +pub use principal::{Principal, PrincipalKind, BLS_PUBLIC_KEY_BYTES}; +pub use replay::{ReplayEntry, ReplayGuard, ReplayKey}; diff --git a/standards/dusk-contract-standards/src/core/nonce.rs b/standards/dusk-contract-standards/src/core/nonce.rs new file mode 100644 index 00000000..ee095f3a --- /dev/null +++ b/standards/dusk-contract-standards/src/core/nonce.rs @@ -0,0 +1,130 @@ +//! Monotonic nonce management for replay-protected authorizations. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::{error, Principal}; + +/// Domain separator for a nonce stream. +/// +/// A contract can keep independent streams for permits, role delegations, +/// Phoenix signature authorizations, upgrade approvals, or any other signed +/// flow. +pub type NonceDomain = [u8; 32]; + +/// Monotonic nonces keyed by `(principal, domain)`. +#[derive(Clone, Debug, Default)] +pub struct NonceManager { + nonces: BTreeMap<(Principal, NonceDomain), u64>, +} + +/// Portable nonce snapshot entry for migration or tests. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct NonceEntry { + /// Principal owning this nonce stream. + pub principal: Principal, + /// Stream domain. + pub domain: NonceDomain, + /// Current nonce. + pub nonce: u64, +} + +/// Nonce query shared by reference contracts and clients. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct NonceQuery { + /// Principal owning this nonce stream. + pub principal: Principal, + /// Stream domain. + pub domain: NonceDomain, +} + +impl NonceManager { + /// Creates an empty nonce manager. + pub const fn new() -> Self { + Self { + nonces: BTreeMap::new(), + } + } + + /// Returns the current nonce. + pub fn current(&self, principal: Principal, domain: NonceDomain) -> u64 { + self.nonces.get(&(principal, domain)).copied().unwrap_or(0) + } + + /// Consumes exactly `expected` and increments the stream. + pub fn consume( + &mut self, + principal: Principal, + domain: NonceDomain, + expected: u64, + ) -> u64 { + let current = self.current(principal, domain); + if current != expected { + panic!("{}", error::INVALID_NONCE); + } + let next = current.checked_add(1).expect(error::OVERFLOW); + self.nonces.insert((principal, domain), next); + next + } + + /// Consumes the current nonce and returns the consumed value. + pub fn use_next( + &mut self, + principal: Principal, + domain: NonceDomain, + ) -> u64 { + let current = self.current(principal, domain); + self.consume(principal, domain, current); + current + } + + /// Invalidates all nonces below `new_nonce`. + pub fn invalidate_until( + &mut self, + principal: Principal, + domain: NonceDomain, + new_nonce: u64, + ) { + if new_nonce < self.current(principal, domain) { + panic!("{}", error::INVALID_NONCE); + } + self.nonces.insert((principal, domain), new_nonce); + } + + /// Exports all nonce streams. + pub fn entries(&self) -> Vec { + self.nonces + .iter() + .map(|((principal, domain), nonce)| NonceEntry { + principal: *principal, + domain: *domain, + nonce: *nonce, + }) + .collect() + } + + /// Imports nonce streams, keeping the greatest value for duplicates. + pub fn import_entries( + &mut self, + entries: impl IntoIterator, + ) { + for entry in entries { + let current = self.current(entry.principal, entry.domain); + if entry.nonce > current { + self.nonces + .insert((entry.principal, entry.domain), entry.nonce); + } + } + } +} diff --git a/standards/dusk-contract-standards/src/core/principal.rs b/standards/dusk-contract-standards/src/core/principal.rs new file mode 100644 index 00000000..ad8df8d9 --- /dev/null +++ b/standards/dusk-contract-standards/src/core/principal.rs @@ -0,0 +1,216 @@ +//! Dusk principal identity. + +use alloc::vec::Vec; +use core::cmp::Ordering; + +use bytecheck::CheckBytes; +use dusk_core::abi::ContractId; +use dusk_core::signatures::bls::PublicKey as BlsPublicKey; +use dusk_core::signatures::schnorr::PublicKey as SchnorrPublicKey; +use dusk_core::JubJubAffine; +use rkyv::{Archive, Deserialize, Serialize}; + +/// Raw byte length of a Dusk Moonlight BLS public key. +pub const BLS_PUBLIC_KEY_BYTES: usize = 193; + +/// Coarse principal kind for policy decisions and event indexing. +#[derive( + Archive, + Serialize, + Deserialize, + Clone, + Copy, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub enum PrincipalKind { + /// Transparent Moonlight public account. + Moonlight, + /// Privacy-preserving Phoenix authorization identity. + Phoenix, + /// Dusk contract id. + Contract, +} + +/// An actor that can own assets, hold roles, or authorize contract actions. +/// +/// Phoenix identity is represented as compressed Schnorr public-key bytes. The +/// primitive deliberately does not pretend that a Phoenix transaction exposes +/// an Ethereum-like caller address. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[archive_attr(derive(CheckBytes))] +pub enum Principal { + /// Transparent Moonlight public account, stored as raw BLS public-key + /// bytes. + Moonlight([u8; BLS_PUBLIC_KEY_BYTES]), + /// Phoenix Schnorr authorization identity. + Phoenix([u8; 32]), + /// Contract account. + Contract(ContractId), +} + +impl Principal { + /// Returns the principal kind. + pub const fn kind(&self) -> PrincipalKind { + match self { + Self::Moonlight(_) => PrincipalKind::Moonlight, + Self::Phoenix(_) => PrincipalKind::Phoenix, + Self::Contract(_) => PrincipalKind::Contract, + } + } + + /// Constructs a Moonlight principal from a public key. + pub fn moonlight(pk: &BlsPublicKey) -> Self { + Self::Moonlight(pk.to_raw_bytes()) + } + + /// Constructs a contract principal from a contract id. + pub const fn contract(id: ContractId) -> Self { + Self::Contract(id) + } + + /// Constructs a Phoenix principal from raw compressed public-key bytes. + pub const fn phoenix(public_key_bytes: [u8; 32]) -> Self { + Self::Phoenix(public_key_bytes) + } + + /// Constructs a Phoenix principal from a Schnorr public key. + pub fn phoenix_public_key(pk: &SchnorrPublicKey) -> Self { + Self::Phoenix(JubJubAffine::from(pk.as_ref()).to_bytes()) + } + + /// Returns true when this is the reserved all-zero principal value. + pub fn is_zero(&self) -> bool { + match self { + Self::Moonlight(bytes) => bytes.iter().all(|byte| *byte == 0), + Self::Phoenix(bytes) => bytes.iter().all(|byte| *byte == 0), + Self::Contract(id) => id.to_bytes().iter().all(|byte| *byte == 0), + } + } + + /// Stable byte representation used for hashing and replay keys. + pub fn to_bytes(&self) -> Vec { + let mut out = Vec::new(); + out.push(match self { + Self::Moonlight(_) => 0, + Self::Phoenix(_) => 1, + Self::Contract(_) => 2, + }); + match self { + Self::Moonlight(bytes) => out.extend_from_slice(bytes), + Self::Phoenix(bytes) => out.extend_from_slice(bytes), + Self::Contract(id) => out.extend_from_slice(&id.to_bytes()), + } + out + } +} + +impl From for Principal { + fn from(value: BlsPublicKey) -> Self { + Self::moonlight(&value) + } +} + +impl From for Principal { + fn from(value: ContractId) -> Self { + Self::Contract(value) + } +} + +impl PartialOrd for Principal { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Principal { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (Self::Moonlight(lhs), Self::Moonlight(rhs)) => lhs.cmp(rhs), + (Self::Phoenix(lhs), Self::Phoenix(rhs)) => lhs.cmp(rhs), + (Self::Contract(lhs), Self::Contract(rhs)) => lhs.cmp(rhs), + _ => self.kind().cmp(&other.kind()), + } + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for Principal { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + + let mut state = serializer.serialize_struct("Principal", 2)?; + state.serialize_field("kind", &self.kind())?; + match self { + Self::Moonlight(bytes) => { + state.serialize_field("bytes", &bytes.as_slice())? + } + Self::Phoenix(bytes) => { + state.serialize_field("bytes", &bytes.as_slice())? + } + Self::Contract(id) => { + state.serialize_field("bytes", &id.to_bytes().as_slice())? + } + } + state.end() + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Principal { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(serde::Deserialize)] + struct PrincipalJson { + kind: PrincipalKind, + bytes: Vec, + } + + let principal = + ::deserialize(deserializer)?; + match principal.kind { + PrincipalKind::Moonlight => { + let bytes: [u8; BLS_PUBLIC_KEY_BYTES] = + principal.bytes.try_into().map_err(|bytes: Vec| { + serde::de::Error::invalid_length( + bytes.len(), + &"193 Moonlight public-key bytes", + ) + })?; + Ok(Self::Moonlight(bytes)) + } + PrincipalKind::Phoenix => { + let bytes: [u8; 32] = + principal.bytes.try_into().map_err(|bytes: Vec| { + serde::de::Error::invalid_length( + bytes.len(), + &"32 Phoenix public-key bytes", + ) + })?; + Ok(Self::Phoenix(bytes)) + } + PrincipalKind::Contract => { + let bytes: [u8; 32] = + principal.bytes.try_into().map_err(|bytes: Vec| { + serde::de::Error::invalid_length( + bytes.len(), + &"32 contract-id bytes", + ) + })?; + Ok(Self::Contract(ContractId::from_bytes(bytes))) + } + } + } +} diff --git a/standards/dusk-contract-standards/src/core/replay.rs b/standards/dusk-contract-standards/src/core/replay.rs new file mode 100644 index 00000000..99371d9d --- /dev/null +++ b/standards/dusk-contract-standards/src/core/replay.rs @@ -0,0 +1,74 @@ +//! Replay protection for signed authorizations. + +use alloc::collections::BTreeSet; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::error; +use crate::core::principal::Principal; + +/// Replay key supplied by an authorization flow. +pub type ReplayKey = [u8; 32]; + +/// Tracks consumed replay keys per principal. +#[derive(Clone, Debug, Default)] +pub struct ReplayGuard { + used: BTreeSet<(Principal, ReplayKey)>, +} + +/// Portable snapshot entry for migration or tests. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ReplayEntry { + /// Principal that consumed the key. + pub principal: Principal, + /// Consumed key. + pub key: ReplayKey, +} + +impl ReplayGuard { + /// Creates an empty replay guard. + pub const fn new() -> Self { + Self { + used: BTreeSet::new(), + } + } + + /// Returns whether a key has been consumed. + pub fn is_used(&self, principal: Principal, key: ReplayKey) -> bool { + self.used.contains(&(principal, key)) + } + + /// Consumes a replay key or panics when it was already used. + pub fn consume(&mut self, principal: Principal, key: ReplayKey) { + if !self.used.insert((principal, key)) { + panic!("{}", error::REPLAY); + } + } + + /// Exports all entries for migration or inspection. + pub fn entries(&self) -> Vec { + self.used + .iter() + .map(|(principal, key)| ReplayEntry { + principal: *principal, + key: *key, + }) + .collect() + } + + /// Imports entries, rejecting conflicting duplicates through the set. + pub fn import_entries( + &mut self, + entries: impl IntoIterator, + ) { + for entry in entries { + self.used.insert((entry.principal, entry.key)); + } + } +} diff --git a/standards/dusk-contract-standards/src/governance/controller.rs b/standards/dusk-contract-standards/src/governance/controller.rs new file mode 100644 index 00000000..1f100036 --- /dev/null +++ b/standards/dusk-contract-standards/src/governance/controller.rs @@ -0,0 +1,183 @@ +//! Role-gated timelock controller. + +use alloc::vec::Vec; + +use crate::access::{AccessControl, Role}; +use crate::core::{error, Principal}; +use crate::governance::{OperationId, ScheduledOperation, Timelock}; + +/// Role allowed to update timelock policy. +pub const TIMELOCK_ADMIN_ROLE: Role = { + let mut role = [0u8; 32]; + role[0] = 1; + role +}; + +/// Role allowed to schedule operations. +pub const PROPOSER_ROLE: Role = { + let mut role = [0u8; 32]; + role[0] = 2; + role +}; + +/// Role allowed to execute ready operations. +pub const EXECUTOR_ROLE: Role = { + let mut role = [0u8; 32]; + role[0] = 3; + role +}; + +/// Role allowed to cancel pending operations. +pub const CANCELLER_ROLE: Role = { + let mut role = [0u8; 32]; + role[0] = 4; + role +}; + +/// Timelock plus role-based caller policy. +#[derive(Clone, Debug)] +pub struct TimelockController { + access: AccessControl, + timelock: Timelock, + self_principal: Principal, +} + +impl TimelockController { + /// Creates a controller and grants all controller roles to `admin`. + pub fn new( + self_principal: Principal, + admin: Principal, + min_delay: u64, + ) -> Self { + if self_principal.is_zero() || admin.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let mut access = AccessControl::new(); + access.init_admin(admin); + access.grant_role(admin, TIMELOCK_ADMIN_ROLE, admin); + access.grant_role(admin, PROPOSER_ROLE, admin); + access.grant_role(admin, EXECUTOR_ROLE, admin); + access.grant_role(admin, CANCELLER_ROLE, admin); + Self { + access, + timelock: Timelock::new(min_delay), + self_principal, + } + } + + /// Returns the controller principal used for self-governed operations. + pub const fn self_principal(&self) -> Principal { + self.self_principal + } + + /// Returns access-control state. + pub const fn access(&self) -> &AccessControl { + &self.access + } + + /// Returns timelock state. + pub const fn timelock(&self) -> &Timelock { + &self.timelock + } + + /// Returns a scheduled operation. + pub fn get(&self, id: OperationId) -> Option<&ScheduledOperation> { + self.timelock.get(id) + } + + /// Returns whether an account has a role. + pub fn has_role(&self, role: Role, account: Principal) -> bool { + self.access.has_role(role, account) + } + + /// Grants a role through the underlying access-control policy. + pub fn grant_role( + &mut self, + caller: Principal, + role: Role, + account: Principal, + ) { + self.access.grant_role(caller, role, account); + } + + /// Revokes a role through the underlying access-control policy. + pub fn revoke_role( + &mut self, + caller: Principal, + role: Role, + account: Principal, + ) { + self.access.revoke_role(caller, role, account); + } + + /// Updates the minimum delay. Caller must be the controller itself. + /// + /// Composing contracts should reach this path through a scheduled + /// operation, not through an arbitrary administrator call. + pub fn set_min_delay(&mut self, caller: Principal, min_delay: u64) { + if caller != self.self_principal { + panic!("{}", error::UNAUTHORIZED); + } + self.timelock.set_min_delay(min_delay); + } + + /// Schedules a self-governed minimum-delay update. + pub fn schedule_min_delay_change( + &mut self, + caller: Principal, + id: OperationId, + now: u64, + min_delay: u64, + ) -> u64 { + self.access.assert_role(PROPOSER_ROLE, caller); + self.timelock + .schedule(id, now, min_delay.to_be_bytes().to_vec()) + } + + /// Executes a ready minimum-delay update as the controller itself. + pub fn execute_min_delay_change( + &mut self, + caller: Principal, + id: OperationId, + now: u64, + ) -> u64 { + self.access.assert_role(EXECUTOR_ROLE, caller); + let payload = self.timelock.execute(id, now); + let bytes: [u8; 8] = payload + .as_slice() + .try_into() + .unwrap_or_else(|_| panic!("{}", error::INVALID_OPERATION)); + let min_delay = u64::from_be_bytes(bytes); + self.set_min_delay(self.self_principal, min_delay); + min_delay + } + + /// Schedules an operation. Caller must have proposer role. + pub fn schedule( + &mut self, + caller: Principal, + id: OperationId, + now: u64, + payload: Vec, + ) -> u64 { + self.access.assert_role(PROPOSER_ROLE, caller); + self.timelock.schedule(id, now, payload) + } + + /// Cancels a pending operation. Caller must have canceller role. + pub fn cancel(&mut self, caller: Principal, id: OperationId) { + self.access.assert_role(CANCELLER_ROLE, caller); + self.timelock.cancel(id); + } + + /// Executes a ready operation. Caller must have executor role. + pub fn execute( + &mut self, + caller: Principal, + id: OperationId, + now: u64, + ) -> Vec { + self.access.assert_role(EXECUTOR_ROLE, caller); + self.timelock.execute(id, now) + } +} diff --git a/standards/dusk-contract-standards/src/governance/mod.rs b/standards/dusk-contract-standards/src/governance/mod.rs new file mode 100644 index 00000000..2438b650 --- /dev/null +++ b/standards/dusk-contract-standards/src/governance/mod.rs @@ -0,0 +1,10 @@ +//! Governance and scheduling primitives. + +pub mod controller; +pub mod timelock; + +pub use controller::{ + TimelockController, CANCELLER_ROLE, EXECUTOR_ROLE, PROPOSER_ROLE, + TIMELOCK_ADMIN_ROLE, +}; +pub use timelock::{OperationId, ScheduledOperation, Timelock}; diff --git a/standards/dusk-contract-standards/src/governance/timelock.rs b/standards/dusk-contract-standards/src/governance/timelock.rs new file mode 100644 index 00000000..a66d204a --- /dev/null +++ b/standards/dusk-contract-standards/src/governance/timelock.rs @@ -0,0 +1,121 @@ +//! Timelock operation scheduler. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::error; + +/// Operation id. +pub type OperationId = [u8; 32]; + +/// Scheduled operation metadata. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ScheduledOperation { + /// Earliest executable height or timestamp unit selected by the contract. + pub ready_at: u64, + /// Whether the operation has already been executed. + pub done: bool, + /// Optional operation payload for migration or client inspection. + pub payload: Vec, +} + +/// Minimal timelock state machine. +#[derive(Clone, Debug)] +pub struct Timelock { + min_delay: u64, + operations: BTreeMap, +} + +impl Timelock { + /// Creates a timelock with `min_delay`. + pub const fn new(min_delay: u64) -> Self { + Self { + min_delay, + operations: BTreeMap::new(), + } + } + + /// Returns the configured minimum delay. + pub const fn min_delay(&self) -> u64 { + self.min_delay + } + + /// Updates the delay. Caller authorization belongs in the composing + /// contract. + pub fn set_min_delay(&mut self, min_delay: u64) { + self.min_delay = min_delay; + } + + /// Schedules an operation. + pub fn schedule( + &mut self, + id: OperationId, + now: u64, + payload: Vec, + ) -> u64 { + if self.operations.contains_key(&id) { + panic!("{}", error::ALREADY_INITIALIZED); + } + let ready_at = now.checked_add(self.min_delay).expect(error::OVERFLOW); + self.operations.insert( + id, + ScheduledOperation { + ready_at, + done: false, + payload, + }, + ); + ready_at + } + + /// Cancels a pending operation. + pub fn cancel(&mut self, id: OperationId) { + let Some(op) = self.operations.get(&id) else { + panic!("{}", error::OPERATION_UNKNOWN); + }; + if op.done { + panic!("{}", error::OPERATION_DONE); + } + self.operations.remove(&id); + } + + /// Marks an operation as executed and returns its payload. + pub fn execute(&mut self, id: OperationId, now: u64) -> Vec { + let op = self + .operations + .get_mut(&id) + .unwrap_or_else(|| panic!("{}", error::OPERATION_UNKNOWN)); + if op.done { + panic!("{}", error::OPERATION_DONE); + } + if now < op.ready_at { + panic!("{}", error::DELAY_NOT_ELAPSED); + } + op.done = true; + op.payload.clone() + } + + /// Returns an operation. + pub fn get(&self, id: OperationId) -> Option<&ScheduledOperation> { + self.operations.get(&id) + } + + /// Returns true when operation is ready and not done. + pub fn is_ready(&self, id: OperationId, now: u64) -> bool { + self.operations + .get(&id) + .map(|op| !op.done && now >= op.ready_at) + .unwrap_or(false) + } +} + +impl Default for Timelock { + fn default() -> Self { + Self::new(0) + } +} diff --git a/standards/dusk-contract-standards/src/lib.rs b/standards/dusk-contract-standards/src/lib.rs new file mode 100644 index 00000000..5017a4ad --- /dev/null +++ b/standards/dusk-contract-standards/src/lib.rs @@ -0,0 +1,30 @@ +//! Dusk-native reusable contract standards and examples. +//! +//! This crate intentionally does not provide EVM API compatibility. It ports +//! the security concepts into Dusk's account, contract, and call model. + +#![no_std] +#![cfg_attr( + not(all(target_family = "wasm", feature = "contract")), + deny(unused_crate_dependencies) +)] +#![deny(unused_extern_crates)] + +extern crate alloc; + +#[cfg(test)] +use dusk_vm as _; +#[cfg(test)] +use rand as _; +#[cfg(feature = "serde")] +use serde_with as _; +#[cfg(feature = "serde")] +use time as _; + +pub mod access; +pub mod auth; +pub mod core; +pub mod governance; +pub mod proxy; +pub mod security; +pub mod token; diff --git a/standards/dusk-contract-standards/src/proxy/mod.rs b/standards/dusk-contract-standards/src/proxy/mod.rs new file mode 100644 index 00000000..eea204be --- /dev/null +++ b/standards/dusk-contract-standards/src/proxy/mod.rs @@ -0,0 +1,12 @@ +//! Dusk-native proxy and upgrade primitives. + +pub mod state_store; +pub mod upgrade; + +pub use state_store::StateStore; +pub use upgrade::{ + PendingUpgrade, RollbackFinalized, UpgradeActivated, UpgradeAdmin, + UpgradeCancelled, UpgradePrepared, UpgradeRolledBack, + ROLLBACK_FINALIZED_TOPIC, UPGRADE_ACTIVATED_TOPIC, UPGRADE_CANCELLED_TOPIC, + UPGRADE_PREPARED_TOPIC, UPGRADE_ROLLED_BACK_TOPIC, +}; diff --git a/standards/dusk-contract-standards/src/proxy/state_store.rs b/standards/dusk-contract-standards/src/proxy/state_store.rs new file mode 100644 index 00000000..28d1b472 --- /dev/null +++ b/standards/dusk-contract-standards/src/proxy/state_store.rs @@ -0,0 +1,101 @@ +//! Proxy-owned state store. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +/// Key-value state store for proxy-owned implementation state. +#[derive(Clone, Debug, Default)] +pub struct StateStore { + words: BTreeMap<[u8; 32], [u8; 32]>, + bytes: BTreeMap<[u8; 32], Vec>, + namespaced_words: BTreeMap<([u8; 32], [u8; 32]), [u8; 32]>, + namespaced_bytes: BTreeMap<([u8; 32], [u8; 32]), Vec>, +} + +impl StateStore { + /// Creates empty state. + pub const fn new() -> Self { + Self { + words: BTreeMap::new(), + bytes: BTreeMap::new(), + namespaced_words: BTreeMap::new(), + namespaced_bytes: BTreeMap::new(), + } + } + + /// Reads a word. + pub fn get_word(&self, key: [u8; 32]) -> [u8; 32] { + self.words.get(&key).copied().unwrap_or([0u8; 32]) + } + + /// Writes a word. + pub fn set_word(&mut self, key: [u8; 32], value: [u8; 32]) { + self.words.insert(key, value); + } + + /// Reads bytes. + pub fn get_bytes(&self, key: [u8; 32]) -> Vec { + self.bytes.get(&key).cloned().unwrap_or_default() + } + + /// Writes bytes. + pub fn set_bytes(&mut self, key: [u8; 32], value: Vec) { + self.bytes.insert(key, value); + } + + /// Reads a namespaced word. + pub fn get_namespaced_word( + &self, + namespace: [u8; 32], + key: [u8; 32], + ) -> [u8; 32] { + self.namespaced_words + .get(&(namespace, key)) + .copied() + .unwrap_or([0u8; 32]) + } + + /// Writes a namespaced word. + pub fn set_namespaced_word( + &mut self, + namespace: [u8; 32], + key: [u8; 32], + value: [u8; 32], + ) { + self.namespaced_words.insert((namespace, key), value); + } + + /// Reads namespaced bytes. + pub fn get_namespaced_bytes( + &self, + namespace: [u8; 32], + key: [u8; 32], + ) -> Vec { + self.namespaced_bytes + .get(&(namespace, key)) + .cloned() + .unwrap_or_default() + } + + /// Writes namespaced bytes. + pub fn set_namespaced_bytes( + &mut self, + namespace: [u8; 32], + key: [u8; 32], + value: Vec, + ) { + self.namespaced_bytes.insert((namespace, key), value); + } + + /// Deletes a key from both stores. + pub fn delete(&mut self, key: [u8; 32]) { + self.words.remove(&key); + self.bytes.remove(&key); + } + + /// Deletes a key from both namespaced stores. + pub fn delete_namespaced(&mut self, namespace: [u8; 32], key: [u8; 32]) { + self.namespaced_words.remove(&(namespace, key)); + self.namespaced_bytes.remove(&(namespace, key)); + } +} diff --git a/standards/dusk-contract-standards/src/proxy/upgrade.rs b/standards/dusk-contract-standards/src/proxy/upgrade.rs new file mode 100644 index 00000000..bc255967 --- /dev/null +++ b/standards/dusk-contract-standards/src/proxy/upgrade.rs @@ -0,0 +1,341 @@ +//! Upgrade admin state machine. + +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use dusk_core::abi::ContractId; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::auth::{ + ActionEnvelope, AuthorizationManager, Authorizer, SignedAuthorization, +}; +use crate::core::{error, CallContext, Principal}; + +/// Upgrade prepared event topic. +pub const UPGRADE_PREPARED_TOPIC: &str = "proxy/upgrade_prepared"; +/// Upgrade activated event topic. +pub const UPGRADE_ACTIVATED_TOPIC: &str = "proxy/upgrade_activated"; +/// Pending upgrade cancelled event topic. +pub const UPGRADE_CANCELLED_TOPIC: &str = "proxy/upgrade_cancelled"; +/// Upgrade rolled back event topic. +pub const UPGRADE_ROLLED_BACK_TOPIC: &str = "proxy/upgrade_rolled_back"; +/// Rollback window finalized event topic. +pub const ROLLBACK_FINALIZED_TOPIC: &str = "proxy/rollback_finalized"; + +/// Pending upgrade data. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct PendingUpgrade { + /// New implementation id. + pub implementation: ContractId, + /// Earliest activation timestamp/height unit selected by the contract. + pub eta: u64, + /// Migration payload. + pub migrate_data: Vec, +} + +/// Upgrade prepared event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct UpgradePrepared { + /// New implementation id. + pub implementation: ContractId, + /// Earliest activation timestamp/height unit selected by the contract. + pub eta: u64, +} + +/// Upgrade activated event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct UpgradeActivated { + /// Previous implementation id. + pub previous_implementation: ContractId, + /// Active implementation id. + pub implementation: ContractId, + /// Rollback deadline. + pub rollback_deadline: u64, +} + +/// Pending upgrade cancelled event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct UpgradeCancelled { + /// Cancelled implementation id. + pub implementation: ContractId, +} + +/// Upgrade rolled back event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct UpgradeRolledBack { + /// Implementation that was rolled back from. + pub from_implementation: ContractId, + /// Restored implementation id. + pub restored_implementation: ContractId, +} + +/// Rollback window finalized event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RollbackFinalized { + /// Active implementation id. + pub implementation: ContractId, +} + +/// Dusk-native upgrade controller. +#[derive(Clone, Debug)] +pub struct UpgradeAdmin { + admin: Principal, + implementation: ContractId, + previous_implementation: Option, + pending: Option, + delay: u64, + rollback_window: u64, + rollback_deadline: u64, +} + +impl UpgradeAdmin { + /// Creates an upgrade admin. + pub fn new( + admin: Principal, + implementation: ContractId, + delay: u64, + rollback_window: u64, + ) -> Self { + if admin.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if is_zero_contract_id(implementation) { + panic!("Proxy: invalid implementation"); + } + Self { + admin, + implementation, + previous_implementation: None, + pending: None, + delay, + rollback_window, + rollback_deadline: 0, + } + } + + /// Returns the admin. + pub const fn admin(&self) -> Principal { + self.admin + } + + /// Authorizes the upgrade admin through runtime context or signed + /// authorization. + pub fn authorize_admin( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_admin_with(&mut authorizer, authorization) + } + + /// Authorizes the upgrade admin with a reusable call authorizer. + pub fn authorize_admin_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + ) -> Principal { + authorizer.require_principal(self.admin, authorization) + } + + /// Authorizes the upgrade admin through runtime context or an action-bound + /// signed authorization. + pub fn authorize_admin_action( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + let mut authorizer = Authorizer::new(authorizations, context, now); + self.authorize_admin_action_with( + &mut authorizer, + authorization, + envelope, + ) + } + + /// Authorizes the upgrade admin with a reusable call authorizer and exact + /// call envelope for signed fallbacks. + pub fn authorize_admin_action_with( + &self, + authorizer: &mut Authorizer<'_>, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + authorizer.require_principal_action(self.admin, authorization, envelope) + } + + /// Returns the active implementation. + pub const fn implementation(&self) -> ContractId { + self.implementation + } + + /// Returns pending upgrade. + pub const fn pending(&self) -> Option<&PendingUpgrade> { + self.pending.as_ref() + } + + /// Returns rollback deadline. + pub const fn rollback_deadline(&self) -> u64 { + self.rollback_deadline + } + + /// Prepares an upgrade. + pub fn prepare( + &mut self, + caller: Principal, + implementation: ContractId, + now: u64, + migrate_data: Vec, + ) -> UpgradePrepared { + self.assert_admin(caller); + if is_zero_contract_id(implementation) { + panic!("Proxy: invalid implementation"); + } + let eta = now.checked_add(self.delay).expect(error::OVERFLOW); + self.pending = Some(PendingUpgrade { + implementation, + eta, + migrate_data, + }); + UpgradePrepared { + implementation, + eta, + } + } + + /// Activates a prepared upgrade and returns migration data. + pub fn activate(&mut self, caller: Principal, now: u64) -> Vec { + self.activate_with_event(caller, now).0 + } + + /// Activates a prepared upgrade and returns migration data plus event data. + pub fn activate_with_event( + &mut self, + caller: Principal, + now: u64, + ) -> (Vec, UpgradeActivated) { + self.assert_admin(caller); + let eta = self + .pending + .as_ref() + .unwrap_or_else(|| panic!("Proxy: no pending upgrade")) + .eta; + if now < eta { + panic!("Proxy: upgrade delay not elapsed"); + } + let rollback_deadline = now + .checked_add(self.rollback_window) + .expect(error::OVERFLOW); + let pending = self.pending.take().expect("pending was checked above"); + let previous = self.implementation; + self.previous_implementation = Some(previous); + self.implementation = pending.implementation; + self.rollback_deadline = rollback_deadline; + let event = UpgradeActivated { + previous_implementation: previous, + implementation: pending.implementation, + rollback_deadline: self.rollback_deadline, + }; + (pending.migrate_data, event) + } + + /// Cancels a pending upgrade. + pub fn cancel_pending(&mut self, caller: Principal) -> UpgradeCancelled { + self.assert_admin(caller); + let pending = self + .pending + .take() + .unwrap_or_else(|| panic!("Proxy: no pending upgrade")); + UpgradeCancelled { + implementation: pending.implementation, + } + } + + /// Rolls back to the previous implementation. + pub fn rollback(&mut self, caller: Principal, now: u64) { + let _ = self.rollback_with_event(caller, now); + } + + /// Rolls back to the previous implementation and returns event data. + pub fn rollback_with_event( + &mut self, + caller: Principal, + now: u64, + ) -> UpgradeRolledBack { + self.assert_admin(caller); + if self.rollback_deadline == 0 || now > self.rollback_deadline { + panic!("Proxy: rollback window closed"); + } + let from_implementation = self.implementation; + let previous = self + .previous_implementation + .take() + .unwrap_or_else(|| panic!("Proxy: no previous implementation")); + self.implementation = previous; + self.pending = None; + self.rollback_deadline = 0; + UpgradeRolledBack { + from_implementation, + restored_implementation: previous, + } + } + + /// Finalizes rollback window after it has elapsed. + pub fn finalize_rollback_window(&mut self, caller: Principal, now: u64) { + let _ = self.finalize_rollback_window_with_event(caller, now); + } + + /// Finalizes rollback window after it has elapsed and returns event data. + pub fn finalize_rollback_window_with_event( + &mut self, + caller: Principal, + now: u64, + ) -> RollbackFinalized { + self.assert_admin(caller); + if self.rollback_deadline != 0 && now < self.rollback_deadline { + panic!("Proxy: rollback window open"); + } + self.previous_implementation = None; + self.rollback_deadline = 0; + RollbackFinalized { + implementation: self.implementation, + } + } + + fn assert_admin(&self, caller: Principal) { + if caller != self.admin { + panic!("{}", error::UNAUTHORIZED); + } + } +} + +fn is_zero_contract_id(id: ContractId) -> bool { + id.to_bytes().iter().all(|byte| *byte == 0) +} diff --git a/standards/dusk-contract-standards/src/security/mod.rs b/standards/dusk-contract-standards/src/security/mod.rs new file mode 100644 index 00000000..32d51ef2 --- /dev/null +++ b/standards/dusk-contract-standards/src/security/mod.rs @@ -0,0 +1,5 @@ +//! Security primitives. + +pub mod reentrancy_guard; + +pub use reentrancy_guard::{ReentrancyGuard, ReentrancyLock}; diff --git a/standards/dusk-contract-standards/src/security/reentrancy_guard.rs b/standards/dusk-contract-standards/src/security/reentrancy_guard.rs new file mode 100644 index 00000000..c570a409 --- /dev/null +++ b/standards/dusk-contract-standards/src/security/reentrancy_guard.rs @@ -0,0 +1,62 @@ +//! Reentrancy guard. + +/// Reentrancy guard module. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ReentrancyGuard { + entered: bool, +} + +/// Active reentrancy lock. +/// +/// Dropping the lock exits the guarded section. This keeps native tests and +/// non-aborting hosts from leaving the guard stuck when a guarded closure +/// panics. +#[derive(Debug)] +pub struct ReentrancyLock<'a> { + entered: &'a mut bool, +} + +impl ReentrancyGuard { + /// Creates a new guard. + pub const fn new() -> Self { + Self { entered: false } + } + + /// Returns whether the guard is currently entered. + pub const fn entered(&self) -> bool { + self.entered + } + + /// Enters the guarded section. + pub fn enter(&mut self) { + if self.entered { + panic!("ReentrancyGuard: reentrant call"); + } + self.entered = true; + } + + /// Exits the guarded section. + pub fn exit(&mut self) { + self.entered = false; + } + + /// Enters the guarded section and returns an RAII lock. + pub fn lock(&mut self) -> ReentrancyLock<'_> { + self.enter(); + ReentrancyLock { + entered: &mut self.entered, + } + } + + /// Runs a closure in a guarded section. + pub fn run(&mut self, f: impl FnOnce() -> R) -> R { + let _lock = self.lock(); + f() + } +} + +impl Drop for ReentrancyLock<'_> { + fn drop(&mut self) { + *self.entered = false; + } +} diff --git a/standards/dusk-contract-standards/src/token/drc20/events.rs b/standards/dusk-contract-standards/src/token/drc20/events.rs new file mode 100644 index 00000000..61390fcd --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc20/events.rs @@ -0,0 +1,41 @@ +//! DRC20 event payloads. + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::Principal; + +/// Transfer event topic. +pub const TRANSFER_TOPIC: &str = "drc20/transfer"; +/// Approval event topic. +pub const APPROVAL_TOPIC: &str = "drc20/approval"; + +/// Transfer event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Transfer { + /// Sender. + pub from: Principal, + /// Recipient. + pub to: Principal, + /// Amount. + pub amount: u64, +} + +/// Approval event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Approval { + /// Owner. + pub owner: Principal, + /// Spender. + pub spender: Principal, + /// Amount. + pub amount: u64, +} diff --git a/standards/dusk-contract-standards/src/token/drc20/extensions.rs b/standards/dusk-contract-standards/src/token/drc20/extensions.rs new file mode 100644 index 00000000..4b2022ee --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc20/extensions.rs @@ -0,0 +1,230 @@ +//! Optional DRC20 policy helpers. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::{error, Principal}; + +/// Supply cap policy. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SupplyCap { + cap: u64, +} + +impl SupplyCap { + /// Creates a supply cap. + pub const fn new(cap: u64) -> Self { + Self { cap } + } + + /// Returns the cap. + pub const fn cap(&self) -> u64 { + self.cap + } + + /// Sets a new cap. The new cap must not be below current supply. + pub fn set_cap(&mut self, current_supply: u64, cap: u64) { + if cap < current_supply { + panic!("DRC20: cap below current supply"); + } + self.cap = cap; + } + + /// Returns remaining mintable supply. + pub const fn remaining(&self, current_supply: u64) -> u64 { + self.cap.saturating_sub(current_supply) + } + + /// Panics unless minting `amount` keeps supply within cap. + pub fn assert_mint(&self, current_supply: u64, amount: u64) { + let next = current_supply.checked_add(amount).expect(error::OVERFLOW); + if next > self.cap { + panic!("DRC20: cap exceeded"); + } + } +} + +/// Vote or accounting checkpoint. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Checkpoint { + /// Block height, timestamp, epoch, or app-selected timepoint. + pub key: u64, + /// Value at that timepoint. + pub value: u64, +} + +/// Ordered checkpoint trace. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Checkpoints { + checkpoints: Vec, +} + +impl Checkpoints { + /// Creates an empty trace. + pub const fn new() -> Self { + Self { + checkpoints: Vec::new(), + } + } + + /// Returns all checkpoints. + pub fn entries(&self) -> &[Checkpoint] { + &self.checkpoints + } + + /// Returns latest value or zero. + pub fn latest(&self) -> u64 { + self.checkpoints + .last() + .map(|checkpoint| checkpoint.value) + .unwrap_or(0) + } + + /// Writes a checkpoint. + pub fn push(&mut self, key: u64, value: u64) { + if let Some(last) = self.checkpoints.last_mut() { + if key < last.key { + panic!("Checkpoints: non-monotonic key"); + } + if key == last.key { + last.value = value; + return; + } + } + self.checkpoints.push(Checkpoint { key, value }); + } + + /// Returns value at or before `key`. + pub fn get_at(&self, key: u64) -> u64 { + let mut low = 0usize; + let mut high = self.checkpoints.len(); + while low < high { + let mid = (low + high) / 2; + if self.checkpoints[mid].key <= key { + low = mid + 1; + } else { + high = mid; + } + } + if low == 0 { + 0 + } else { + self.checkpoints[low - 1].value + } + } +} + +/// DRC20 voting-unit checkpoint store. +/// +/// Composing contracts call `move_units` after mint, burn, and transfer hooks +/// when they want historical vote/accounting queries. +#[derive(Clone, Debug, Default)] +pub struct VotingUnits { + accounts: BTreeMap, + total_supply: Checkpoints, +} + +impl VotingUnits { + /// Creates empty voting-unit state. + pub const fn new() -> Self { + Self { + accounts: BTreeMap::new(), + total_supply: Checkpoints::new(), + } + } + + /// Returns latest account units. + pub fn latest_votes(&self, account: Principal) -> u64 { + self.accounts + .get(&account) + .map(Checkpoints::latest) + .unwrap_or(0) + } + + /// Returns historical account units. + pub fn past_votes(&self, account: Principal, timepoint: u64) -> u64 { + self.accounts + .get(&account) + .map(|trace| trace.get_at(timepoint)) + .unwrap_or(0) + } + + /// Returns latest total units. + pub fn latest_total_supply(&self) -> u64 { + self.total_supply.latest() + } + + /// Returns historical total units. + pub fn past_total_supply(&self, timepoint: u64) -> u64 { + self.total_supply.get_at(timepoint) + } + + /// Writes an account checkpoint directly. + pub fn write_votes( + &mut self, + account: Principal, + timepoint: u64, + value: u64, + ) { + self.accounts + .entry(account) + .or_default() + .push(timepoint, value); + } + + /// Moves units between accounts, minting from `None` or burning to `None`. + pub fn move_units( + &mut self, + from: Option, + to: Option, + amount: u64, + timepoint: u64, + ) { + if amount == 0 || from == to { + return; + } + + if let Some(from) = from { + let current = self.latest_votes(from); + if current < amount { + panic!("VotingUnits: insufficient units"); + } + self.write_votes(from, timepoint, current - amount); + } + + if let Some(to) = to { + let current = self.latest_votes(to); + let next = current.checked_add(amount).expect(error::OVERFLOW); + self.write_votes(to, timepoint, next); + } + + match (from, to) { + (None, Some(_)) => { + let next = self + .latest_total_supply() + .checked_add(amount) + .expect(error::OVERFLOW); + self.total_supply.push(timepoint, next); + } + (Some(_), None) => { + let current = self.latest_total_supply(); + if current < amount { + panic!("VotingUnits: total supply underflow"); + } + self.total_supply.push(timepoint, current - amount); + } + _ => {} + } + } +} diff --git a/standards/dusk-contract-standards/src/token/drc20/mod.rs b/standards/dusk-contract-standards/src/token/drc20/mod.rs new file mode 100644 index 00000000..b72be207 --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc20/mod.rs @@ -0,0 +1,314 @@ +//! DRC20 fungible token primitive. + +pub mod events; +pub mod extensions; +pub mod types; + +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; + +use crate::core::{error, Principal}; +use events::{Approval, Transfer}; +pub use extensions::{Checkpoint, Checkpoints, SupplyCap, VotingUnits}; +pub use types::{ + Allowance, ApproveCall, BalanceOf, DecreaseAllowanceCall, + IncreaseAllowanceCall, Init, InitBalance, SignedApproveCall, TransferCall, + TransferFromCall, +}; + +/// Reserved zero principal. +pub const ZERO_PRINCIPAL: Principal = + Principal::Contract(dusk_core::abi::ContractId::from_bytes([0u8; 32])); + +/// DRC20 token state and logic. +#[derive(Clone, Debug)] +pub struct Drc20 { + initialized: bool, + name: String, + symbol: String, + decimals: u8, + balances: BTreeMap, + allowances: BTreeMap<(Principal, Principal), u64>, + supply: u64, +} + +impl Drc20 { + /// Creates empty token state. + pub const fn new() -> Self { + Self { + initialized: false, + name: String::new(), + symbol: String::new(), + decimals: 0, + balances: BTreeMap::new(), + allowances: BTreeMap::new(), + supply: 0, + } + } + + /// Initializes token metadata and initial supply. + pub fn init(&mut self, args: Init) -> Vec { + if self.initialized { + panic!("{}", error::ALREADY_INITIALIZED); + } + let mut initial_supply = 0u64; + for entry in &args.initial_balances { + if entry.account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + initial_supply = initial_supply + .checked_add(entry.amount) + .expect(error::OVERFLOW); + } + + self.initialized = true; + self.name = args.name; + self.symbol = args.symbol; + self.decimals = args.decimals; + + let mut events = Vec::new(); + for entry in args.initial_balances { + if entry.amount == 0 { + continue; + } + self.mint_internal(entry.account, entry.amount); + events.push(Transfer { + from: ZERO_PRINCIPAL, + to: entry.account, + amount: entry.amount, + }); + } + events + } + + /// Token name. + pub fn name(&self) -> String { + self.assert_initialized(); + self.name.clone() + } + + /// Token symbol. + pub fn symbol(&self) -> String { + self.assert_initialized(); + self.symbol.clone() + } + + /// Token decimals. + pub fn decimals(&self) -> u8 { + self.assert_initialized(); + self.decimals + } + + /// Total supply. + pub fn total_supply(&self) -> u64 { + self.assert_initialized(); + self.supply + } + + /// Balance of `account`. + pub fn balance_of(&self, args: BalanceOf) -> u64 { + self.assert_initialized(); + self.balances.get(&args.account).copied().unwrap_or(0) + } + + /// Allowance from owner to spender. + pub fn allowance(&self, args: Allowance) -> u64 { + self.assert_initialized(); + self.allowances + .get(&(args.owner, args.spender)) + .copied() + .unwrap_or(0) + } + + /// Transfers from caller to recipient. + pub fn transfer( + &mut self, + caller: Principal, + args: TransferCall, + ) -> Transfer { + self.assert_initialized(); + self.transfer_internal(caller, args.to, args.amount) + } + + /// Approves spender. + pub fn approve( + &mut self, + caller: Principal, + args: ApproveCall, + ) -> Approval { + self.approve_for(caller, args) + } + + /// Approves spender for an explicitly authorized owner. + pub fn approve_for( + &mut self, + owner: Principal, + args: ApproveCall, + ) -> Approval { + self.assert_initialized(); + if args.spender.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.allowances.insert((owner, args.spender), args.amount); + Approval { + owner, + spender: args.spender, + amount: args.amount, + } + } + + /// Increases spender allowance. + pub fn increase_allowance( + &mut self, + caller: Principal, + args: IncreaseAllowanceCall, + ) -> Approval { + self.assert_initialized(); + if args.spender.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let current = self.allowance(Allowance { + owner: caller, + spender: args.spender, + }); + let amount = current + .checked_add(args.added_amount) + .expect(error::OVERFLOW); + self.allowances.insert((caller, args.spender), amount); + Approval { + owner: caller, + spender: args.spender, + amount, + } + } + + /// Decreases spender allowance. + pub fn decrease_allowance( + &mut self, + caller: Principal, + args: DecreaseAllowanceCall, + ) -> Approval { + self.assert_initialized(); + let current = self.allowance(Allowance { + owner: caller, + spender: args.spender, + }); + if current < args.subtracted_amount { + panic!("DRC20: allowance below zero"); + } + let amount = current - args.subtracted_amount; + self.allowances.insert((caller, args.spender), amount); + Approval { + owner: caller, + spender: args.spender, + amount, + } + } + + /// Transfers using allowance. + pub fn transfer_from( + &mut self, + caller: Principal, + args: TransferFromCall, + ) -> Transfer { + self.assert_initialized(); + let current = self.allowance(Allowance { + owner: args.owner, + spender: caller, + }); + if current < args.amount { + panic!("DRC20: allowance too low"); + } + self.allowances + .insert((args.owner, caller), current - args.amount); + self.transfer_internal(args.owner, args.to, args.amount) + } + + /// Mints tokens. Access control belongs in the composing contract. + pub fn mint(&mut self, to: Principal, amount: u64) -> Transfer { + self.assert_initialized(); + self.mint_internal(to, amount); + Transfer { + from: ZERO_PRINCIPAL, + to, + amount, + } + } + + /// Burns caller tokens. + pub fn burn(&mut self, from: Principal, amount: u64) -> Transfer { + self.assert_initialized(); + self.burn_internal(from, amount); + Transfer { + from, + to: ZERO_PRINCIPAL, + amount, + } + } + + fn assert_initialized(&self) { + if !self.initialized { + panic!("{}", error::NOT_INITIALIZED); + } + } + + fn mint_internal(&mut self, to: Principal, amount: u64) { + if to.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let bal = self.balances.entry(to).or_insert(0); + *bal = bal.checked_add(amount).expect(error::OVERFLOW); + self.supply = self.supply.checked_add(amount).expect(error::OVERFLOW); + } + + fn burn_internal(&mut self, from: Principal, amount: u64) { + let current = self.balances.get(&from).copied().unwrap_or(0); + if current < amount { + panic!("DRC20: balance too low"); + } + if amount != 0 { + let next = current - amount; + if next == 0 { + self.balances.remove(&from); + } else { + self.balances.insert(from, next); + } + self.supply = + self.supply.checked_sub(amount).expect(error::UNDERFLOW); + } + } + + fn transfer_internal( + &mut self, + from: Principal, + to: Principal, + amount: u64, + ) -> Transfer { + if to.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let current = self.balances.get(&from).copied().unwrap_or(0); + if current < amount { + panic!("DRC20: balance too low"); + } + if amount != 0 { + let next = current - amount; + if next == 0 { + self.balances.remove(&from); + } else { + self.balances.insert(from, next); + } + let to_balance = self.balances.entry(to).or_insert(0); + *to_balance = + to_balance.checked_add(amount).expect(error::OVERFLOW); + } + Transfer { from, to, amount } + } +} + +impl Default for Drc20 { + fn default() -> Self { + Self::new() + } +} diff --git a/standards/dusk-contract-standards/src/token/drc20/types.rs b/standards/dusk-contract-standards/src/token/drc20/types.rs new file mode 100644 index 00000000..82e1e103 --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc20/types.rs @@ -0,0 +1,144 @@ +//! DRC20 rkyv-compatible call types. + +use alloc::string::String; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::auth::SignedAuthorization; +use crate::core::Principal; + +/// One initial balance entry. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct InitBalance { + /// Recipient. + pub account: Principal, + /// Amount. + pub amount: u64, +} + +/// Initialization input. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Init { + /// Name. + pub name: String, + /// Symbol. + pub symbol: String, + /// Decimals. + pub decimals: u8, + /// Initial balances. + pub initial_balances: Vec, +} + +/// Balance query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct BalanceOf { + /// Account. + pub account: Principal, +} + +/// Allowance query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Allowance { + /// Owner. + pub owner: Principal, + /// Spender. + pub spender: Principal, +} + +/// Transfer call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TransferCall { + /// Recipient. + pub to: Principal, + /// Amount. + pub amount: u64, +} + +/// Approve call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ApproveCall { + /// Spender. + pub spender: Principal, + /// Amount. + pub amount: u64, +} + +/// Increase allowance call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct IncreaseAllowanceCall { + /// Spender. + pub spender: Principal, + /// Added amount. + pub added_amount: u64, +} + +/// Decrease allowance call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct DecreaseAllowanceCall { + /// Spender. + pub spender: Principal, + /// Subtracted amount. + pub subtracted_amount: u64, +} + +/// Transfer from call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TransferFromCall { + /// Owner. + pub owner: Principal, + /// Recipient. + pub to: Principal, + /// Amount. + pub amount: u64, +} + +/// Signed approval call for Moonlight/Phoenix owner authorization. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SignedApproveCall { + /// Allowance owner. Must match the signed action principal. + pub owner: Principal, + /// Spender. + pub spender: Principal, + /// Allowance amount. + pub amount: u64, + /// Replay-protected authorization for this approval payload. + pub authorization: SignedAuthorization, +} diff --git a/standards/dusk-contract-standards/src/token/drc721/events.rs b/standards/dusk-contract-standards/src/token/drc721/events.rs new file mode 100644 index 00000000..3e848ed0 --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc721/events.rs @@ -0,0 +1,58 @@ +//! DRC721 event payloads. + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::Principal; + +/// Transfer event topic. +pub const TRANSFER_TOPIC: &str = "drc721/transfer"; +/// Approval event topic. +pub const APPROVAL_TOPIC: &str = "drc721/approval"; +/// Approval-for-all event topic. +pub const APPROVAL_FOR_ALL_TOPIC: &str = "drc721/approval_for_all"; + +/// Transfer event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Transfer { + /// Sender. + pub from: Principal, + /// Recipient. + pub to: Principal, + /// Token id. + pub token_id: u64, +} + +/// Approval event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Approval { + /// Owner. + pub owner: Principal, + /// Approved account. + pub approved: Principal, + /// Token id. + pub token_id: u64, +} + +/// Approval-for-all event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ApprovalForAll { + /// Owner. + pub owner: Principal, + /// Operator. + pub operator: Principal, + /// Approval. + pub approved: bool, +} diff --git a/standards/dusk-contract-standards/src/token/drc721/mod.rs b/standards/dusk-contract-standards/src/token/drc721/mod.rs new file mode 100644 index 00000000..3db04986 --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc721/mod.rs @@ -0,0 +1,365 @@ +//! DRC721 non-fungible token primitive. + +pub mod events; +pub mod royalty; +pub mod types; + +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::string::{String, ToString}; + +use crate::core::{error, Principal}; +use events::{Approval, ApprovalForAll, Transfer}; +pub use royalty::{ + RoyaltyInfo, RoyaltyQuery, RoyaltyQuote, RoyaltyRegistry, MAX_BASIS_POINTS, +}; +pub use types::{ + ApproveCall, BalanceOf, GetApproved, Init, InitToken, IsApprovedForAll, + OwnerOf, SetApprovalForAllCall, TokenByIndex, TokenOfOwnerByIndex, + TokenUri, TokensOf, TransferFromCall, +}; + +/// Reserved zero principal. +pub const ZERO_PRINCIPAL: Principal = + Principal::Contract(dusk_core::abi::ContractId::from_bytes([0u8; 32])); + +/// DRC721 token state and logic. +#[derive(Clone, Debug)] +pub struct Drc721 { + initialized: bool, + name: String, + symbol: String, + owners: BTreeMap, + balances: BTreeMap, + token_approvals: BTreeMap, + operator_approvals: BTreeMap<(Principal, Principal), bool>, + supply: u64, + base_uri: String, +} + +impl Drc721 { + /// Creates empty state. + pub const fn new() -> Self { + Self { + initialized: false, + name: String::new(), + symbol: String::new(), + owners: BTreeMap::new(), + balances: BTreeMap::new(), + token_approvals: BTreeMap::new(), + operator_approvals: BTreeMap::new(), + supply: 0, + base_uri: String::new(), + } + } + + /// Initializes token metadata and initial distribution. + pub fn init(&mut self, args: Init) -> alloc::vec::Vec { + if self.initialized { + panic!("{}", error::ALREADY_INITIALIZED); + } + let mut token_ids = BTreeSet::new(); + for token in &args.initial_tokens { + if token.account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if !token_ids.insert(token.token_id) { + panic!("DRC721: token already minted"); + } + } + + self.initialized = true; + self.name = args.name; + self.symbol = args.symbol; + self.base_uri = args.base_uri; + let mut out = alloc::vec::Vec::new(); + for token in args.initial_tokens { + self.mint_internal(token.account, token.token_id); + out.push(Transfer { + from: ZERO_PRINCIPAL, + to: token.account, + token_id: token.token_id, + }); + } + out + } + + /// Name. + pub fn name(&self) -> String { + self.assert_initialized(); + self.name.clone() + } + + /// Symbol. + pub fn symbol(&self) -> String { + self.assert_initialized(); + self.symbol.clone() + } + + /// Base URI. + pub fn base_uri(&self) -> String { + self.assert_initialized(); + self.base_uri.clone() + } + + /// Token URI. + pub fn token_uri(&self, args: TokenUri) -> String { + self.assert_initialized(); + self.require_exists(args.token_id); + if self.base_uri.is_empty() { + return String::new(); + } + let mut uri = self.base_uri.clone(); + uri.push_str(&args.token_id.to_string()); + uri + } + + /// Token id by global index. + pub fn token_by_index(&self, args: TokenByIndex) -> u64 { + self.assert_initialized(); + self.owners + .keys() + .nth(args.index as usize) + .copied() + .unwrap_or_else(|| panic!("DRC721: global index out of bounds")) + } + + /// Token id by owner-local index. + pub fn token_of_owner_by_index(&self, args: TokenOfOwnerByIndex) -> u64 { + self.assert_initialized(); + self.owners + .iter() + .filter_map(|(token_id, owner)| { + if *owner == args.owner { + Some(*token_id) + } else { + None + } + }) + .nth(args.index as usize) + .unwrap_or_else(|| panic!("DRC721: owner index out of bounds")) + } + + /// All token ids owned by an account. + pub fn tokens_of(&self, args: TokensOf) -> alloc::vec::Vec { + self.assert_initialized(); + self.owners + .iter() + .filter_map(|(token_id, owner)| { + if *owner == args.owner { + Some(*token_id) + } else { + None + } + }) + .collect() + } + + /// Total supply. + pub fn total_supply(&self) -> u64 { + self.assert_initialized(); + self.supply + } + + /// Balance of an account. + pub fn balance_of(&self, args: BalanceOf) -> u64 { + self.assert_initialized(); + if args.account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.balances.get(&args.account).copied().unwrap_or(0) + } + + /// Owner of a token. + pub fn owner_of(&self, args: OwnerOf) -> Principal { + self.assert_initialized(); + self.owners + .get(&args.token_id) + .copied() + .unwrap_or_else(|| panic!("DRC721: token does not exist")) + } + + /// Approved principal for a token. + pub fn get_approved(&self, args: GetApproved) -> Principal { + self.assert_initialized(); + self.require_exists(args.token_id); + self.token_approvals + .get(&args.token_id) + .copied() + .unwrap_or(ZERO_PRINCIPAL) + } + + /// Operator approval. + pub fn is_approved_for_all(&self, args: IsApprovedForAll) -> bool { + self.assert_initialized(); + self.operator_approvals + .get(&(args.owner, args.operator)) + .copied() + .unwrap_or(false) + } + + /// Approves an account for a token. + pub fn approve( + &mut self, + caller: Principal, + args: ApproveCall, + ) -> Approval { + self.assert_initialized(); + let owner = self.owner_of(OwnerOf { + token_id: args.token_id, + }); + if args.approved == owner { + panic!("DRC721: approval to current owner"); + } + if caller != owner + && !self.is_approved_for_all(IsApprovedForAll { + owner, + operator: caller, + }) + { + panic!("{}", error::UNAUTHORIZED); + } + if args.approved.is_zero() { + self.token_approvals.remove(&args.token_id); + } else { + self.token_approvals.insert(args.token_id, args.approved); + } + Approval { + owner, + approved: args.approved, + token_id: args.token_id, + } + } + + /// Sets operator approval. + pub fn set_approval_for_all( + &mut self, + caller: Principal, + args: SetApprovalForAllCall, + ) -> ApprovalForAll { + self.assert_initialized(); + if args.operator.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if args.operator == caller { + panic!("DRC721: approval to current owner"); + } + self.operator_approvals + .insert((caller, args.operator), args.approved); + ApprovalForAll { + owner: caller, + operator: args.operator, + approved: args.approved, + } + } + + /// Transfers a token. + pub fn transfer_from( + &mut self, + caller: Principal, + args: TransferFromCall, + ) -> Transfer { + self.assert_initialized(); + if args.to.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let owner = self.owner_of(OwnerOf { + token_id: args.token_id, + }); + if owner != args.from { + panic!("DRC721: incorrect owner"); + } + let approved = self.token_approvals.get(&args.token_id).copied(); + let operator = self.is_approved_for_all(IsApprovedForAll { + owner, + operator: caller, + }); + if caller != owner && approved != Some(caller) && !operator { + panic!("{}", error::UNAUTHORIZED); + } + self.token_approvals.remove(&args.token_id); + self.owners.insert(args.token_id, args.to); + self.decrement_balance(owner); + self.increment_balance(args.to); + Transfer { + from: owner, + to: args.to, + token_id: args.token_id, + } + } + + /// Mints a token. Access control belongs in the composing contract. + pub fn mint(&mut self, to: Principal, token_id: u64) -> Transfer { + self.assert_initialized(); + self.mint_internal(to, token_id); + Transfer { + from: ZERO_PRINCIPAL, + to, + token_id, + } + } + + /// Burns a token. + pub fn burn(&mut self, caller: Principal, token_id: u64) -> Transfer { + self.assert_initialized(); + let owner = self.owner_of(OwnerOf { token_id }); + let approved = self.token_approvals.get(&token_id).copied(); + if caller != owner && approved != Some(caller) { + panic!("{}", error::UNAUTHORIZED); + } + self.owners.remove(&token_id); + self.token_approvals.remove(&token_id); + self.decrement_balance(owner); + self.supply = self.supply.checked_sub(1).expect(error::UNDERFLOW); + Transfer { + from: owner, + to: ZERO_PRINCIPAL, + token_id, + } + } + + fn assert_initialized(&self) { + if !self.initialized { + panic!("{}", error::NOT_INITIALIZED); + } + } + + fn mint_internal(&mut self, to: Principal, token_id: u64) { + if to.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if self.owners.contains_key(&token_id) { + panic!("DRC721: token already minted"); + } + self.owners.insert(token_id, to); + self.increment_balance(to); + self.supply = self.supply.checked_add(1).expect(error::OVERFLOW); + } + + fn require_exists(&self, token_id: u64) { + if !self.owners.contains_key(&token_id) { + panic!("DRC721: token does not exist"); + } + } + + fn increment_balance(&mut self, owner: Principal) { + let bal = self.balances.entry(owner).or_insert(0); + *bal = bal.checked_add(1).expect(error::OVERFLOW); + } + + fn decrement_balance(&mut self, owner: Principal) { + let bal = self + .balances + .get_mut(&owner) + .unwrap_or_else(|| panic!("DRC721: incorrect owner")); + *bal = bal.checked_sub(1).expect(error::UNDERFLOW); + if *bal == 0 { + self.balances.remove(&owner); + } + } +} + +impl Default for Drc721 { + fn default() -> Self { + Self::new() + } +} diff --git a/standards/dusk-contract-standards/src/token/drc721/royalty.rs b/standards/dusk-contract-standards/src/token/drc721/royalty.rs new file mode 100644 index 00000000..e6bb41fb --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc721/royalty.rs @@ -0,0 +1,130 @@ +//! Optional DRC721 royalty policy helper. + +use alloc::collections::BTreeMap; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::{error, Principal}; +use crate::token::drc721::ZERO_PRINCIPAL; + +/// Maximum royalty basis points. +pub const MAX_BASIS_POINTS: u16 = 10_000; + +/// Royalty policy for a token or collection. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoyaltyInfo { + /// Receiver principal. + pub receiver: Principal, + /// Royalty fraction in basis points. + pub basis_points: u16, +} + +/// Royalty quote for a sale price. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoyaltyQuote { + /// Receiver principal, or zero principal when no royalty exists. + pub receiver: Principal, + /// Royalty amount. + pub amount: u64, +} + +/// Royalty quote query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoyaltyQuery { + /// Token id. + pub token_id: u64, + /// Sale price used for royalty calculation. + pub sale_price: u64, +} + +/// Default plus token-specific royalty registry. +#[derive(Clone, Debug, Default)] +pub struct RoyaltyRegistry { + default: Option, + token_overrides: BTreeMap, +} + +impl RoyaltyRegistry { + /// Creates an empty registry. + pub const fn new() -> Self { + Self { + default: None, + token_overrides: BTreeMap::new(), + } + } + + /// Returns default royalty policy. + pub const fn default_royalty(&self) -> Option { + self.default + } + + /// Returns token royalty policy, falling back to default. + pub fn royalty_for(&self, token_id: u64) -> Option { + self.token_overrides + .get(&token_id) + .copied() + .or(self.default) + } + + /// Sets default royalty. + pub fn set_default_royalty(&mut self, info: RoyaltyInfo) { + validate(info); + self.default = Some(info); + } + + /// Clears default royalty. + pub fn clear_default_royalty(&mut self) { + self.default = None; + } + + /// Sets token-specific royalty. + pub fn set_token_royalty(&mut self, token_id: u64, info: RoyaltyInfo) { + validate(info); + self.token_overrides.insert(token_id, info); + } + + /// Clears token-specific royalty. + pub fn clear_token_royalty(&mut self, token_id: u64) { + self.token_overrides.remove(&token_id); + } + + /// Returns a royalty quote for `sale_price`. + pub fn royalty_info(&self, token_id: u64, sale_price: u64) -> RoyaltyQuote { + let Some(info) = self.royalty_for(token_id) else { + return RoyaltyQuote { + receiver: ZERO_PRINCIPAL, + amount: 0, + }; + }; + let amount = sale_price + .checked_mul(u64::from(info.basis_points)) + .expect(error::OVERFLOW) + / u64::from(MAX_BASIS_POINTS); + RoyaltyQuote { + receiver: info.receiver, + amount, + } + } +} + +fn validate(info: RoyaltyInfo) { + if info.receiver.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if info.basis_points > MAX_BASIS_POINTS { + panic!("DRC721: royalty exceeds sale price"); + } +} diff --git a/standards/dusk-contract-standards/src/token/drc721/types.rs b/standards/dusk-contract-standards/src/token/drc721/types.rs new file mode 100644 index 00000000..04ba2903 --- /dev/null +++ b/standards/dusk-contract-standards/src/token/drc721/types.rs @@ -0,0 +1,170 @@ +//! DRC721 rkyv-compatible call types. + +use alloc::string::String; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::Principal; + +/// Initial token. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct InitToken { + /// Owner. + pub account: Principal, + /// Token id. + pub token_id: u64, +} + +/// Initialization input. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Init { + /// Name. + pub name: String, + /// Symbol. + pub symbol: String, + /// Base URI. + pub base_uri: String, + /// Initial tokens. + pub initial_tokens: Vec, +} + +/// Balance query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct BalanceOf { + /// Account. + pub account: Principal, +} + +/// Owner query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct OwnerOf { + /// Token id. + pub token_id: u64, +} + +/// URI query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TokenUri { + /// Token id. + pub token_id: u64, +} + +/// Token by global index query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TokenByIndex { + /// Zero-based index. + pub index: u64, +} + +/// Token by owner index query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TokenOfOwnerByIndex { + /// Owner. + pub owner: Principal, + /// Zero-based owner-local index. + pub index: u64, +} + +/// Tokens owned by an account query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TokensOf { + /// Owner. + pub owner: Principal, +} + +/// Approved query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct GetApproved { + /// Token id. + pub token_id: u64, +} + +/// Operator approval query. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct IsApprovedForAll { + /// Owner. + pub owner: Principal, + /// Operator. + pub operator: Principal, +} + +/// Approve call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ApproveCall { + /// Approved account. + pub approved: Principal, + /// Token id. + pub token_id: u64, +} + +/// Operator approval call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetApprovalForAllCall { + /// Operator. + pub operator: Principal, + /// Approval. + pub approved: bool, +} + +/// Transfer call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TransferFromCall { + /// Current owner. + pub from: Principal, + /// Recipient. + pub to: Principal, + /// Token id. + pub token_id: u64, +} diff --git a/standards/dusk-contract-standards/src/token/mod.rs b/standards/dusk-contract-standards/src/token/mod.rs new file mode 100644 index 00000000..cefc0666 --- /dev/null +++ b/standards/dusk-contract-standards/src/token/mod.rs @@ -0,0 +1,4 @@ +//! Dusk token primitives. + +pub mod drc20; +pub mod drc721; From ff55add3af4a87bab2339b949da28bc4f984d1ce Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sat, 25 Apr 2026 17:10:13 +0200 Subject: [PATCH 02/35] standards: cover primitives with native tests --- .../tests/primitives.rs | 1202 +++++++++++++++++ 1 file changed, 1202 insertions(+) create mode 100644 standards/dusk-contract-standards/tests/primitives.rs diff --git a/standards/dusk-contract-standards/tests/primitives.rs b/standards/dusk-contract-standards/tests/primitives.rs new file mode 100644 index 00000000..6f0e2ce6 --- /dev/null +++ b/standards/dusk-contract-standards/tests/primitives.rs @@ -0,0 +1,1202 @@ +use std::panic::{catch_unwind, AssertUnwindSafe}; + +use dusk_contract_standards::access::{ + AccessControl, Ownable, Ownable2Step, OwnerSet, Pausable, + DEFAULT_ADMIN_ROLE, +}; +use dusk_contract_standards::auth::{ + ActionEnvelope, AuthorizationManager, AuthorizedAction, + MoonlightAuthorization, PhoenixSignatureAuthorization, SignedAuthorization, +}; +use dusk_contract_standards::core::{ + CallContext, NonceEntry, NonceManager, Principal, PrincipalKind, + ReplayGuard, +}; +use dusk_contract_standards::governance::{ + Timelock, TimelockController, CANCELLER_ROLE, EXECUTOR_ROLE, PROPOSER_ROLE, + TIMELOCK_ADMIN_ROLE, +}; +use dusk_contract_standards::proxy::{ + StateStore, UpgradeActivated, UpgradeAdmin, UpgradeCancelled, + UpgradePrepared, UpgradeRolledBack, +}; +use dusk_contract_standards::security::ReentrancyGuard; +use dusk_contract_standards::token::drc20::{ + Allowance, ApproveCall, BalanceOf as BalanceOf20, DecreaseAllowanceCall, + Drc20, IncreaseAllowanceCall, Init as Init20, InitBalance, SupplyCap, + TransferCall as Transfer20, TransferFromCall, VotingUnits, +}; +use dusk_contract_standards::token::drc721::{ + ApproveCall as Approve721, BalanceOf as BalanceOf721, Drc721, + Init as Init721, InitToken, IsApprovedForAll, OwnerOf, RoyaltyInfo, + RoyaltyRegistry, SetApprovalForAllCall, TokenByIndex, TokenOfOwnerByIndex, + TokensOf, TransferFromCall as Transfer721, +}; +use dusk_core::abi::ContractId; +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; +use dusk_core::signatures::schnorr::{ + PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, +}; +use dusk_core::JubJubScalar; +use rand::rngs::StdRng; +use rand::SeedableRng; + +fn p(byte: u8) -> Principal { + Principal::phoenix([byte; 32]) +} + +fn c(byte: u8) -> ContractId { + ContractId::from_bytes([byte; 32]) +} + +fn assert_panics(f: impl FnOnce()) { + assert!(catch_unwind(AssertUnwindSafe(f)).is_err()); +} + +fn moonlight_secret(seed: u64) -> BlsSecretKey { + let mut rng = StdRng::seed_from_u64(seed); + BlsSecretKey::random(&mut rng) +} + +#[test] +fn ownable_positive_and_negative_paths() { + let owner = p(1); + let next = p(2); + let stranger = p(3); + + let mut ownable = Ownable::new(); + ownable.init(owner); + assert_eq!(ownable.owner(), Some(owner)); + ownable.assert_owner(owner); + assert_panics(|| ownable.assert_owner(stranger)); + + ownable.transfer_ownership(owner, next); + assert_eq!(ownable.owner(), Some(next)); + assert_panics(|| ownable.transfer_ownership(owner, stranger)); + ownable.renounce_ownership(next); + assert_eq!(ownable.owner(), None); +} + +#[test] +fn ownable_two_step_requires_pending_owner_acceptance() { + let owner = p(1); + let next = p(2); + let stranger = p(3); + + let mut ownable = Ownable2Step::new(); + ownable.init(owner); + ownable.transfer_ownership(owner, next); + assert_eq!(ownable.owner(), Some(owner)); + assert_eq!(ownable.pending_owner(), Some(next)); + + assert_panics(|| ownable.accept_ownership(stranger)); + ownable.accept_ownership(next); + assert_eq!(ownable.owner(), Some(next)); + assert_eq!(ownable.pending_owner(), None); +} + +#[test] +fn access_control_admin_role_gates_grants_and_revokes() { + let admin = p(1); + let minter = p(2); + let stranger = p(3); + let minter_role = [7u8; 32]; + + let mut access = AccessControl::new(); + access.init_admin(admin); + assert!(access.has_role(DEFAULT_ADMIN_ROLE, admin)); + assert_panics(|| access.grant_role(stranger, minter_role, minter)); + + access.grant_role(admin, minter_role, minter); + assert!(access.has_role(minter_role, minter)); + access.assert_role(minter_role, minter); + + assert_panics(|| access.revoke_role(stranger, minter_role, minter)); + access.revoke_role(admin, minter_role, minter); + assert!(!access.has_role(minter_role, minter)); + + access.renounce_role(DEFAULT_ADMIN_ROLE, admin); + assert!(!access.has_role(DEFAULT_ADMIN_ROLE, admin)); + assert_panics(|| access.init_admin(stranger)); +} + +#[test] +fn owner_set_supports_mixed_dusk_principals() { + let moonlight = Principal::Moonlight([1u8; 193]); + let phoenix = p(2); + let contract = Principal::contract(c(3)); + let stranger = p(4); + + let mut owners = OwnerSet::new(); + owners.init([moonlight, phoenix]); + assert!(owners.is_owner(moonlight)); + assert!(owners.is_owner(phoenix)); + assert_eq!(owners.count_kind(PrincipalKind::Moonlight), 1); + assert_eq!(owners.count_kind(PrincipalKind::Phoenix), 1); + assert_panics(|| owners.assert_owner(stranger)); + + owners.add_owner(moonlight, contract); + assert_eq!(owners.count_kind(PrincipalKind::Contract), 1); + assert_panics(|| owners.add_owner(stranger, p(5))); + + owners.remove_owner(phoenix, moonlight); + assert!(!owners.is_owner(moonlight)); + owners.replace_owner(contract, phoenix, moonlight); + assert!(owners.is_owner(moonlight)); + assert!(!owners.is_owner(phoenix)); + + let before = owners.owners(); + assert_panics(|| { + owners.replace_owner(contract, moonlight, Principal::contract(c(0))); + }); + assert_eq!(owners.owners(), before); + assert_panics(|| { + owners.replace_owner(contract, moonlight, contract); + }); + assert_eq!(owners.owners(), before); + assert_panics(|| { + owners.replace_owner(contract, p(99), p(10)); + }); + assert_eq!(owners.owners(), before); +} + +#[test] +fn replay_guard_rejects_reuse_across_same_principal() { + let owner = p(1); + let other = p(2); + let key = [9u8; 32]; + + let mut replay = ReplayGuard::new(); + assert!(!replay.is_used(owner, key)); + replay.consume(owner, key); + assert!(replay.is_used(owner, key)); + replay.consume(other, key); + assert_panics(|| replay.consume(owner, key)); +} + +#[test] +fn nonce_manager_keeps_independent_principal_and_domain_streams() { + let moonlight = Principal::Moonlight([1u8; 193]); + let phoenix = p(2); + let permit_domain = [10u8; 32]; + let vote_domain = [11u8; 32]; + + let mut nonces = NonceManager::new(); + assert_eq!(nonces.current(moonlight, permit_domain), 0); + assert_eq!(nonces.use_next(moonlight, permit_domain), 0); + assert_eq!(nonces.current(moonlight, permit_domain), 1); + assert_eq!(nonces.current(phoenix, permit_domain), 0); + assert_eq!(nonces.current(moonlight, vote_domain), 0); + + assert_panics(|| { + nonces.consume(moonlight, permit_domain, 0); + }); + nonces.invalidate_until(moonlight, permit_domain, 9); + assert_eq!(nonces.current(moonlight, permit_domain), 9); + assert_panics(|| { + nonces.invalidate_until(moonlight, permit_domain, 8); + }); + + nonces.import_entries([NonceEntry { + principal: moonlight, + domain: permit_domain, + nonce: 12, + }]); + assert_eq!(nonces.current(moonlight, permit_domain), 12); +} + +#[test] +fn authorization_manager_verifies_moonlight_and_phoenix_paths() { + let sk = moonlight_secret(7); + let pk = BlsPublicKey::from(&sk); + let moonlight = Principal::moonlight(&pk); + let contract = c(33); + let domain = [44u8; 32]; + let action_id = [55u8; 32]; + + let action = AuthorizedAction { + contract, + domain, + action_id, + nonce: 0, + expires_at: 100, + principal: moonlight, + payload_hash: [66u8; 32], + }; + let signature = sk.sign(&action.message_bytes()); + let auth = MoonlightAuthorization { + action, + public_key: pk, + signature, + }; + + let mut authorizations = AuthorizationManager::new(); + assert_eq!(authorizations.authorize_moonlight(&auth, 99), moonlight); + assert_eq!(authorizations.nonce(moonlight, domain), 1); + assert_panics(|| { + authorizations.authorize_moonlight(&auth, 99); + }); + + let bad_action = AuthorizedAction { + nonce: 1, + principal: p(99), + ..action + }; + let bad_signature = sk.sign(&bad_action.message_bytes()); + let bad_auth = MoonlightAuthorization { + action: bad_action, + public_key: pk, + signature: bad_signature, + }; + assert_panics(|| { + authorizations.authorize_moonlight(&bad_auth, 99); + }); + + let mut rng = StdRng::seed_from_u64(1234); + let phoenix_sk = SchnorrSecretKey::from(JubJubScalar::from(88u64)); + let phoenix_pk = SchnorrPublicKey::from(&phoenix_sk); + let phoenix = Principal::phoenix_public_key(&phoenix_pk); + let phoenix_action = AuthorizedAction { + contract, + domain: [77u8; 32], + action_id, + nonce: 0, + expires_at: 0, + principal: phoenix, + payload_hash: [22u8; 32], + }; + let phoenix_auth = PhoenixSignatureAuthorization { + action: phoenix_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: Some([99u8; 32]), + }; + assert_eq!(authorizations.authorize_phoenix(&phoenix_auth, 1), phoenix); + assert_eq!(authorizations.nonce(phoenix, [77u8; 32]), 1); + assert!(authorizations.replay_used(phoenix, [99u8; 32])); + + let expired_action = AuthorizedAction { + expires_at: 9, + nonce: 1, + ..phoenix_action + }; + let expired = PhoenixSignatureAuthorization { + action: expired_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, expired_action.message_hash()), + replay_key: None, + }; + assert_panics(|| { + authorizations.authorize_phoenix(&expired, 10); + }); + + let bad_signature_action = AuthorizedAction { + nonce: 1, + payload_hash: [23u8; 32], + ..phoenix_action + }; + let bad_signature = PhoenixSignatureAuthorization { + action: bad_signature_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: None, + }; + assert_panics(|| { + authorizations.authorize_phoenix(&bad_signature, 1); + }); + assert_eq!(authorizations.nonce(phoenix, [77u8; 32]), 1); + + let replay_action = AuthorizedAction { + nonce: 1, + payload_hash: [24u8; 32], + ..phoenix_action + }; + let replay = PhoenixSignatureAuthorization { + action: replay_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, replay_action.message_hash()), + replay_key: Some([99u8; 32]), + }; + assert_panics(|| { + authorizations.authorize_phoenix(&replay, 1); + }); + assert_eq!(authorizations.nonce(phoenix, [77u8; 32]), 1); + + let wrong_sk = SchnorrSecretKey::from(JubJubScalar::from(89u64)); + let wrong_pk = SchnorrPublicKey::from(&wrong_sk); + let wrong_key = PhoenixSignatureAuthorization { + action: AuthorizedAction { + nonce: 1, + ..phoenix_action + }, + public_key: wrong_pk, + signature: wrong_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: None, + }; + assert_panics(|| { + authorizations.authorize_phoenix(&wrong_key, 1); + }); + + let bounded_domain = [88u8; 32]; + let bounded_action = AuthorizedAction { + contract, + domain: bounded_domain, + action_id, + nonce: 0, + expires_at: 10, + principal: moonlight, + payload_hash: [1u8; 32], + }; + let bounded_signed = + SignedAuthorization::Moonlight(MoonlightAuthorization { + action: bounded_action, + public_key: pk, + signature: sk.sign(&bounded_action.message_bytes()), + }); + let mut bounded = AuthorizationManager::new(); + assert_panics(|| { + bounded.authorize_signed_action( + &bounded_signed, + ActionEnvelope::new(contract, bounded_domain, action_id, [2u8; 32]), + 9, + ); + }); + assert_eq!(bounded.nonce(moonlight, bounded_domain), 0); + assert_eq!( + bounded.authorize_signed_action( + &bounded_signed, + ActionEnvelope::new(contract, bounded_domain, action_id, [1u8; 32]), + 10, + ), + moonlight + ); + assert_eq!(bounded.nonce(moonlight, bounded_domain), 1); + + let expired_bounded_action = AuthorizedAction { + nonce: 1, + expires_at: 10, + payload_hash: [3u8; 32], + ..bounded_action + }; + let expired_bounded = + SignedAuthorization::Moonlight(MoonlightAuthorization { + action: expired_bounded_action, + public_key: pk, + signature: sk.sign(&expired_bounded_action.message_bytes()), + }); + assert_panics(|| { + bounded.authorize_signed_action( + &expired_bounded, + ActionEnvelope::new(contract, bounded_domain, action_id, [3u8; 32]), + 11, + ); + }); + assert_eq!(bounded.nonce(moonlight, bounded_domain), 1); +} + +#[test] +fn observed_or_signed_authorization_wraps_owner_role_and_admin_checks() { + let moonlight_sk = moonlight_secret(17); + let moonlight_pk = BlsPublicKey::from(&moonlight_sk); + let moonlight = Principal::moonlight(&moonlight_pk); + let contract_principal = Principal::contract(c(44)); + let contract = c(45); + let domain = [46u8; 32]; + let action_id = [47u8; 32]; + + let mut manager = AuthorizationManager::new(); + assert_eq!( + manager.authorize_principal( + moonlight, + CallContext::from_principal(moonlight), + None, + 0, + ), + moonlight + ); + assert_eq!( + manager.authorize_principal( + contract_principal, + CallContext::from_principal(contract_principal), + None, + 0, + ), + contract_principal + ); + + let moonlight_action = AuthorizedAction { + contract, + domain, + action_id, + nonce: 0, + expires_at: 0, + principal: moonlight, + payload_hash: [48u8; 32], + }; + let moonlight_signed = + SignedAuthorization::Moonlight(MoonlightAuthorization { + action: moonlight_action, + public_key: moonlight_pk, + signature: moonlight_sk.sign(&moonlight_action.message_bytes()), + }); + assert_eq!( + manager.authorize_principal( + moonlight, + CallContext::none(), + Some(&moonlight_signed), + 0, + ), + moonlight + ); + + let mut rng = StdRng::seed_from_u64(9876); + let phoenix_sk = SchnorrSecretKey::from(JubJubScalar::from(18u64)); + let phoenix_pk = SchnorrPublicKey::from(&phoenix_sk); + let phoenix = Principal::phoenix_public_key(&phoenix_pk); + let phoenix_action = AuthorizedAction { + contract, + domain, + action_id, + nonce: 0, + expires_at: 0, + principal: phoenix, + payload_hash: [49u8; 32], + }; + let phoenix_signed = + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action: phoenix_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: None, + }); + + let mut ownable = Ownable::new(); + ownable.init(phoenix); + assert_eq!( + ownable.authorize_owner_action( + &mut manager, + CallContext::none(), + Some(&phoenix_signed), + ActionEnvelope::new(contract, domain, action_id, [49u8; 32]), + 0, + ), + phoenix + ); + + let mut access = AccessControl::new(); + let role = [50u8; 32]; + access.init_admin(moonlight); + access.grant_role(moonlight, role, phoenix); + let phoenix_action = AuthorizedAction { + nonce: 1, + payload_hash: [51u8; 32], + ..phoenix_action + }; + let phoenix_signed = + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action: phoenix_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: None, + }); + assert_eq!( + access.authorize_role_action( + role, + &mut manager, + CallContext::none(), + Some(&phoenix_signed), + ActionEnvelope::new(contract, domain, action_id, [51u8; 32]), + 0, + ), + phoenix + ); + + let upgrade = UpgradeAdmin::new(phoenix, c(52), 0, 0); + let phoenix_action = AuthorizedAction { + nonce: 2, + payload_hash: [53u8; 32], + ..phoenix_action + }; + let phoenix_signed = + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action: phoenix_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: None, + }); + assert_eq!( + upgrade.authorize_admin_action( + &mut manager, + CallContext::none(), + Some(&phoenix_signed), + ActionEnvelope::new(contract, domain, action_id, [53u8; 32]), + 0, + ), + phoenix + ); + assert_panics(|| { + manager.authorize_principal( + phoenix, + CallContext::from_principal(phoenix), + None, + 0, + ); + }); +} + +#[test] +fn reentrancy_guard_blocks_nested_entry() { + let mut guard = ReentrancyGuard::new(); + guard.enter(); + assert_panics(|| guard.enter()); + guard.exit(); + guard.enter(); + guard.exit(); +} + +#[test] +fn reentrancy_guard_run_resets_after_panic() { + let mut guard = ReentrancyGuard::new(); + assert_panics(|| { + guard.run(|| panic!("boom")); + }); + assert!(!guard.entered()); + guard.enter(); + guard.exit(); +} + +#[test] +fn timelock_schedules_executes_and_rejects_invalid_states() { + let op = [8u8; 32]; + let mut timelock = Timelock::new(5); + + let ready_at = timelock.schedule(op, 10, vec![1, 2, 3]); + assert_eq!(ready_at, 15); + assert!(!timelock.is_ready(op, 14)); + assert_panics(|| { + timelock.execute(op, 14); + }); + assert_eq!(timelock.execute(op, 15), vec![1, 2, 3]); + assert_panics(|| { + timelock.execute(op, 16); + }); + + let other = [9u8; 32]; + timelock.schedule(other, 20, vec![]); + timelock.cancel(other); + assert!(timelock.get(other).is_none()); +} + +#[test] +fn timelock_controller_gates_schedule_execute_cancel_and_policy() { + let admin = p(1); + let proposer = p(2); + let executor = p(3); + let canceller = p(4); + let stranger = p(5); + let controller_principal = p(6); + let op = [10u8; 32]; + + let mut controller = + TimelockController::new(controller_principal, admin, 5); + assert_eq!(controller.self_principal(), controller_principal); + assert!(controller.has_role(TIMELOCK_ADMIN_ROLE, admin)); + controller.grant_role(admin, PROPOSER_ROLE, proposer); + controller.grant_role(admin, EXECUTOR_ROLE, executor); + controller.grant_role(admin, CANCELLER_ROLE, canceller); + + assert_panics(|| { + controller.schedule(stranger, op, 10, vec![]); + }); + assert_eq!(controller.schedule(proposer, op, 10, vec![1]), 15); + assert_panics(|| { + controller.execute(executor, op, 14); + }); + assert_eq!(controller.execute(executor, op, 15), vec![1]); + assert_panics(|| { + controller.execute(executor, op, 16); + }); + + let other = [11u8; 32]; + controller.schedule(admin, other, 20, vec![2]); + assert_panics(|| { + controller.cancel(stranger, other); + }); + controller.cancel(canceller, other); + assert!(controller.get(other).is_none()); + + assert_panics(|| controller.set_min_delay(stranger, 9)); + assert_panics(|| controller.set_min_delay(admin, 9)); + let delay_change = [12u8; 32]; + assert_eq!( + controller.schedule_min_delay_change(proposer, delay_change, 20, 9), + 25 + ); + assert_panics(|| { + controller.execute_min_delay_change(executor, delay_change, 24); + }); + assert_eq!( + controller.execute_min_delay_change(executor, delay_change, 25), + 9 + ); + assert_eq!(controller.timelock().min_delay(), 9); +} + +#[test] +fn pausable_tracks_state_and_assertions() { + let mut pausable = Pausable::new(); + pausable.assert_not_paused(); + assert_panics(|| pausable.assert_paused()); + pausable.pause(); + pausable.assert_paused(); + assert_panics(|| pausable.assert_not_paused()); + pausable.unpause(); + pausable.assert_not_paused(); +} + +#[test] +fn drc20_rejects_use_before_init_and_double_init() { + let owner = p(1); + let mut token = Drc20::new(); + + assert_panics(|| { + token.balance_of(BalanceOf20 { account: owner }); + }); + assert_panics(|| { + token.mint(owner, 1); + }); + + token.init(Init20 { + name: "Dusk Token".into(), + symbol: "DUSKX".into(), + decimals: 9, + initial_balances: vec![], + }); + assert_eq!(token.total_supply(), 0); + assert_panics(|| { + token.init(Init20 { + name: "Again".into(), + symbol: "AGAIN".into(), + decimals: 9, + initial_balances: vec![], + }); + }); + + let mut bad = Drc20::new(); + assert_panics(|| { + bad.init(Init20 { + name: "Bad".into(), + symbol: "BAD".into(), + decimals: 9, + initial_balances: vec![InitBalance { + account: p(0), + amount: 1, + }], + }); + }); + assert_panics(|| { + bad.total_supply(); + }); + bad.init(Init20 { + name: "Recovered".into(), + symbol: "GOOD".into(), + decimals: 9, + initial_balances: vec![], + }); + assert_eq!(bad.total_supply(), 0); + + let mut overflowing = Drc20::new(); + assert_panics(|| { + overflowing.init(Init20 { + name: "Overflow".into(), + symbol: "OVR".into(), + decimals: 9, + initial_balances: vec![ + InitBalance { + account: owner, + amount: u64::MAX, + }, + InitBalance { + account: p(2), + amount: 1, + }, + ], + }); + }); + assert_panics(|| { + overflowing.total_supply(); + }); +} + +#[test] +fn drc20_supports_transfer_allowance_mint_and_burn() { + let owner = p(1); + let receiver = p(2); + let spender = p(3); + + let mut token = Drc20::new(); + let mint_events = token.init(Init20 { + name: "Dusk Token".into(), + symbol: "DUSKX".into(), + decimals: 9, + initial_balances: vec![InitBalance { + account: owner, + amount: 100, + }], + }); + assert_eq!(mint_events.len(), 1); + assert_eq!(token.total_supply(), 100); + assert_eq!(token.balance_of(BalanceOf20 { account: owner }), 100); + + let transfer = token.transfer( + owner, + Transfer20 { + to: receiver, + amount: 40, + }, + ); + assert_eq!(transfer.from, owner); + assert_eq!(token.balance_of(BalanceOf20 { account: owner }), 60); + assert_eq!(token.balance_of(BalanceOf20 { account: receiver }), 40); + + let approval = token.approve( + receiver, + ApproveCall { + spender, + amount: 15, + }, + ); + assert_eq!(approval.owner, receiver); + assert_eq!( + token.allowance(Allowance { + owner: receiver, + spender, + }), + 15 + ); + let increased = token.increase_allowance( + receiver, + IncreaseAllowanceCall { + spender, + added_amount: 5, + }, + ); + assert_eq!(increased.amount, 20); + let decreased = token.decrease_allowance( + receiver, + DecreaseAllowanceCall { + spender, + subtracted_amount: 5, + }, + ); + assert_eq!(decreased.amount, 15); + + token.transfer_from( + spender, + TransferFromCall { + owner: receiver, + to: owner, + amount: 10, + }, + ); + assert_eq!( + token.allowance(Allowance { + owner: receiver, + spender, + }), + 5 + ); + assert_eq!(token.balance_of(BalanceOf20 { account: owner }), 70); + + token.mint(receiver, 5); + assert_eq!(token.total_supply(), 105); + token.burn(receiver, 5); + assert_eq!(token.total_supply(), 100); + + assert_panics(|| { + token.transfer( + receiver, + Transfer20 { + to: owner, + amount: 1_000, + }, + ); + }); + assert_panics(|| { + token.decrease_allowance( + receiver, + DecreaseAllowanceCall { + spender, + subtracted_amount: 100, + }, + ); + }); + assert_panics(|| { + token.transfer_from( + spender, + TransferFromCall { + owner: receiver, + to: owner, + amount: 10, + }, + ); + }); +} + +#[test] +fn drc20_supply_cap_and_voting_units_cover_policy_hooks() { + let owner = p(1); + let receiver = p(2); + + let mut cap = SupplyCap::new(100); + cap.assert_mint(90, 10); + assert_eq!(cap.remaining(40), 60); + assert_panics(|| cap.assert_mint(90, 11)); + assert_panics(|| cap.set_cap(80, 79)); + cap.set_cap(80, 120); + assert_eq!(cap.cap(), 120); + + let mut votes = VotingUnits::new(); + votes.move_units(None, Some(owner), 100, 10); + assert_eq!(votes.latest_votes(owner), 100); + assert_eq!(votes.latest_total_supply(), 100); + assert_eq!(votes.past_votes(owner, 9), 0); + assert_eq!(votes.past_votes(owner, 10), 100); + + votes.move_units(Some(owner), Some(receiver), 40, 11); + assert_eq!(votes.latest_votes(owner), 60); + assert_eq!(votes.latest_votes(receiver), 40); + assert_eq!(votes.latest_total_supply(), 100); + assert_eq!(votes.past_votes(owner, 10), 100); + assert_eq!(votes.past_votes(owner, 11), 60); + + votes.move_units(Some(receiver), None, 5, 12); + assert_eq!(votes.latest_votes(receiver), 35); + assert_eq!(votes.latest_total_supply(), 95); + assert_eq!(votes.past_total_supply(11), 100); + assert_eq!(votes.past_total_supply(12), 95); + + assert_panics(|| votes.move_units(Some(receiver), None, 100, 13)); + assert_panics(|| votes.write_votes(owner, 9, 1)); +} + +#[test] +fn drc721_rejects_use_before_init_and_double_init() { + let owner = p(1); + let mut token = Drc721::new(); + + assert_panics(|| { + token.owner_of(OwnerOf { token_id: 1 }); + }); + assert_panics(|| { + token.mint(owner, 1); + }); + + token.init(Init721 { + name: "Dusk NFT".into(), + symbol: "DNFT".into(), + base_uri: String::new(), + initial_tokens: vec![], + }); + assert_eq!(token.total_supply(), 0); + assert_panics(|| { + token.init(Init721 { + name: "Again".into(), + symbol: "AGAIN".into(), + base_uri: String::new(), + initial_tokens: vec![], + }); + }); + + let mut bad = Drc721::new(); + assert_panics(|| { + bad.init(Init721 { + name: "Bad".into(), + symbol: "BAD".into(), + base_uri: String::new(), + initial_tokens: vec![InitToken { + account: p(0), + token_id: 1, + }], + }); + }); + assert_panics(|| { + bad.total_supply(); + }); + bad.init(Init721 { + name: "Recovered".into(), + symbol: "GOOD".into(), + base_uri: String::new(), + initial_tokens: vec![], + }); + assert_eq!(bad.total_supply(), 0); + + let mut duplicate = Drc721::new(); + assert_panics(|| { + duplicate.init(Init721 { + name: "Duplicate".into(), + symbol: "DUP".into(), + base_uri: String::new(), + initial_tokens: vec![ + InitToken { + account: owner, + token_id: 1, + }, + InitToken { + account: p(2), + token_id: 1, + }, + ], + }); + }); + assert_panics(|| { + duplicate.total_supply(); + }); +} + +#[test] +fn drc721_supports_approval_operator_transfer_and_burn() { + let owner = p(1); + let receiver = p(2); + let approved = p(3); + let operator = p(4); + + let mut token = Drc721::new(); + let events = token.init(Init721 { + name: "Dusk NFT".into(), + symbol: "DNFT".into(), + base_uri: "ipfs://collection/".into(), + initial_tokens: vec![InitToken { + account: owner, + token_id: 1, + }], + }); + assert_eq!(events.len(), 1); + assert_eq!(token.total_supply(), 1); + assert_eq!(token.owner_of(OwnerOf { token_id: 1 }), owner); + assert_eq!(token.balance_of(BalanceOf721 { account: owner }), 1); + assert_eq!(token.token_by_index(TokenByIndex { index: 0 }), 1); + assert_eq!( + token.token_of_owner_by_index(TokenOfOwnerByIndex { owner, index: 0 }), + 1 + ); + assert_eq!(token.tokens_of(TokensOf { owner }), vec![1]); + + token.approve( + owner, + Approve721 { + approved, + token_id: 1, + }, + ); + token.transfer_from( + approved, + Transfer721 { + from: owner, + to: receiver, + token_id: 1, + }, + ); + assert_eq!(token.owner_of(OwnerOf { token_id: 1 }), receiver); + assert_eq!(token.balance_of(BalanceOf721 { account: owner }), 0); + assert_eq!(token.balance_of(BalanceOf721 { account: receiver }), 1); + assert_eq!(token.tokens_of(TokensOf { owner }), Vec::::new()); + assert_eq!(token.tokens_of(TokensOf { owner: receiver }), vec![1]); + + token.set_approval_for_all( + receiver, + SetApprovalForAllCall { + operator, + approved: true, + }, + ); + assert!(token.is_approved_for_all(IsApprovedForAll { + owner: receiver, + operator, + })); + + token.mint(owner, 2); + token.burn(owner, 2); + assert_eq!(token.total_supply(), 1); + + assert_panics(|| { + token.transfer_from( + owner, + Transfer721 { + from: receiver, + to: owner, + token_id: 1, + }, + ); + }); + assert_panics(|| { + token.owner_of(OwnerOf { token_id: 2 }); + }); +} + +#[test] +fn drc721_royalty_registry_quotes_default_and_token_overrides() { + let default_receiver = p(7); + let token_receiver = p(8); + let mut royalties = RoyaltyRegistry::new(); + + let empty = royalties.royalty_info(1, 10_000); + assert_eq!( + empty.receiver, + dusk_contract_standards::token::drc721::ZERO_PRINCIPAL + ); + assert_eq!(empty.amount, 0); + + royalties.set_default_royalty(RoyaltyInfo { + receiver: default_receiver, + basis_points: 500, + }); + assert_eq!(royalties.royalty_info(1, 10_000).amount, 500); + assert_eq!(royalties.royalty_info(1, 10_000).receiver, default_receiver); + + royalties.set_token_royalty( + 1, + RoyaltyInfo { + receiver: token_receiver, + basis_points: 1_250, + }, + ); + let quote = royalties.royalty_info(1, 8_000); + assert_eq!(quote.receiver, token_receiver); + assert_eq!(quote.amount, 1_000); + + royalties.clear_token_royalty(1); + assert_eq!(royalties.royalty_info(1, 10_000).receiver, default_receiver); + royalties.clear_default_royalty(); + assert_eq!(royalties.royalty_info(1, 10_000).amount, 0); + + assert_panics(|| { + royalties.set_default_royalty(RoyaltyInfo { + receiver: dusk_contract_standards::token::drc721::ZERO_PRINCIPAL, + basis_points: 1, + }); + }); + assert_panics(|| { + royalties.set_default_royalty(RoyaltyInfo { + receiver: default_receiver, + basis_points: 10_001, + }); + }); +} + +#[test] +fn proxy_upgrade_admin_enforces_delay_and_rollback_window() { + let admin = p(1); + let stranger = p(2); + let implementation_a = c(10); + let implementation_b = c(11); + + assert_panics(|| { + UpgradeAdmin::new(p(0), implementation_a, 0, 0); + }); + assert_panics(|| { + UpgradeAdmin::new(admin, c(0), 0, 0); + }); + + let mut upgrades = UpgradeAdmin::new(admin, implementation_a, 5, 10); + assert_eq!(upgrades.implementation(), implementation_a); + assert_panics(|| { + upgrades.prepare(stranger, implementation_b, 0, vec![]); + }); + + assert_eq!( + upgrades.prepare(admin, implementation_b, 100, vec![1, 2, 3]), + UpgradePrepared { + implementation: implementation_b, + eta: 105, + } + ); + assert_panics(|| { + upgrades.activate(admin, 104); + }); + let (migration, activated) = upgrades.activate_with_event(admin, 105); + assert_eq!(migration, vec![1, 2, 3]); + assert_eq!( + activated, + UpgradeActivated { + previous_implementation: implementation_a, + implementation: implementation_b, + rollback_deadline: 115, + } + ); + assert_eq!(upgrades.implementation(), implementation_b); + assert_eq!(upgrades.rollback_deadline(), 115); + + assert_eq!( + upgrades.rollback_with_event(admin, 110), + UpgradeRolledBack { + from_implementation: implementation_b, + restored_implementation: implementation_a, + } + ); + assert_eq!(upgrades.implementation(), implementation_a); + + upgrades.prepare(admin, implementation_b, 200, vec![]); + assert_eq!( + upgrades.cancel_pending(admin), + UpgradeCancelled { + implementation: implementation_b, + } + ); + assert_panics(|| { + upgrades.activate(admin, 205); + }); + + upgrades.prepare(admin, implementation_b, 200, vec![]); + upgrades.activate(admin, 205); + assert_panics(|| upgrades.rollback(admin, 216)); + upgrades.finalize_rollback_window(admin, 216); + assert_eq!(upgrades.rollback_deadline(), 0); + + let mut delay_overflow = UpgradeAdmin::new(admin, implementation_a, 1, 0); + assert_panics(|| { + delay_overflow.prepare(admin, implementation_b, u64::MAX, vec![]); + }); + assert_eq!(delay_overflow.implementation(), implementation_a); + + let mut rollback_overflow = + UpgradeAdmin::new(admin, implementation_a, 0, 1); + rollback_overflow.prepare(admin, implementation_b, u64::MAX, vec![]); + assert_panics(|| { + rollback_overflow.activate(admin, u64::MAX); + }); + assert_eq!(rollback_overflow.implementation(), implementation_a); + assert!(rollback_overflow.pending().is_some()); +} + +#[test] +fn proxy_state_store_keeps_plain_and_namespaced_state_separate() { + let namespace_a = [1u8; 32]; + let namespace_b = [2u8; 32]; + let key = [3u8; 32]; + let word = [4u8; 32]; + let other_word = [5u8; 32]; + + let mut store = StateStore::new(); + store.set_word(key, word); + store.set_namespaced_word(namespace_a, key, other_word); + store.set_namespaced_bytes(namespace_a, key, vec![1, 2, 3]); + + assert_eq!(store.get_word(key), word); + assert_eq!(store.get_namespaced_word(namespace_a, key), other_word); + assert_eq!(store.get_namespaced_word(namespace_b, key), [0u8; 32]); + assert_eq!(store.get_namespaced_bytes(namespace_a, key), vec![1, 2, 3]); + assert_eq!( + store.get_namespaced_bytes(namespace_b, key), + Vec::::new() + ); + + store.delete_namespaced(namespace_a, key); + assert_eq!(store.get_namespaced_word(namespace_a, key), [0u8; 32]); + assert_eq!(store.get_word(key), word); + + store.delete(key); + assert_eq!(store.get_word(key), [0u8; 32]); +} From 3f467e5477bc5c05271c4f2b8349eceb668cb06d Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sat, 25 Apr 2026 17:10:19 +0200 Subject: [PATCH 03/35] standards: add authorization counter Forge reference --- .../examples/authorization_counter/Cargo.toml | 31 ++++ .../examples/authorization_counter/Makefile | 34 ++++ .../examples/authorization_counter/src/lib.rs | 161 ++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 standards/examples/authorization_counter/Cargo.toml create mode 100644 standards/examples/authorization_counter/Makefile create mode 100644 standards/examples/authorization_counter/src/lib.rs diff --git a/standards/examples/authorization_counter/Cargo.toml b/standards/examples/authorization_counter/Cargo.toml new file mode 100644 index 00000000..674f3669 --- /dev/null +++ b/standards/examples/authorization_counter/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "authorization-counter" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[target.'cfg(target_family = "wasm")'.dependencies] +dusk-core = { workspace = true } +dusk-data-driver = { workspace = true, optional = true } +dusk-forge = { workspace = true } +dusk-contract-standards = { path = "../../dusk-contract-standards" } +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, default-features = false, features = [ + "alloc", +], optional = true } + +[features] +contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] +serde = ["dep:serde", "dusk-contract-standards/serde"] +data-driver = [ + "serde", + "dusk-core/serde", + "dep:dusk-data-driver", + "dusk-data-driver/wasm-export", + "dep:serde_json", +] +data-driver-js = ["data-driver", "dusk-data-driver/alloc"] diff --git a/standards/examples/authorization_counter/Makefile b/standards/examples/authorization_counter/Makefile new file mode 100644 index 00000000..1cc96d25 --- /dev/null +++ b/standards/examples/authorization_counter/Makefile @@ -0,0 +1,34 @@ +TARGET_DIR ?= ../../../target +DD_TARGET_DIR ?= ../../../target/data-driver + +all: wasm wasm-dd clippy + +wasm: + @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ + CARGO_TARGET_DIR=$(TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + -Z build-std=core,alloc \ + --target wasm32-unknown-unknown \ + --features contract \ + -p authorization-counter + +test: + @cargo test -p dusk-contract-standards + +wasm-dd: + @CARGO_TARGET_DIR=$(DD_TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + --target wasm32-unknown-unknown \ + --features data-driver-js \ + -p authorization-counter + +clippy: + @cargo clippy -p authorization-counter -Z build-std=core,alloc --release --target wasm32-unknown-unknown --features contract -- -D warnings + +doc: + +.PHONY: all wasm wasm-dd test clippy doc diff --git a/standards/examples/authorization_counter/src/lib.rs b/standards/examples/authorization_counter/src/lib.rs new file mode 100644 index 00000000..531f3a2d --- /dev/null +++ b/standards/examples/authorization_counter/src/lib.rs @@ -0,0 +1,161 @@ +//! Example counter controlled by replay-protected Dusk authorizations. + +#![no_std] +#![cfg(target_family = "wasm")] + +extern crate alloc; +extern crate self as authorization_counter_types; + +use bytecheck::CheckBytes; +use dusk_contract_standards::auth::{ + MoonlightAuthorization, PhoenixSignatureAuthorization, +}; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use rkyv::{Archive, Deserialize, Serialize}; + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetValueByMoonlight { + pub authorization: MoonlightAuthorization, + pub amount: u64, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetValueByPhoenix { + pub authorization: PhoenixSignatureAuthorization, + pub amount: u64, +} + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct NonceQuery { + pub principal: Principal, + pub domain: NonceDomain, +} + +#[dusk_forge::contract] +mod authorization_counter { + use alloc::vec::Vec; + + use authorization_counter_types::{ + NonceQuery, SetValueByMoonlight, SetValueByPhoenix, + }; + use dusk_contract_standards::auth::AuthorizationManager; + use dusk_contract_standards::core::{NonceDomain, Principal}; + use dusk_core::abi::{self, ContractId}; + + const SET_VALUE_DOMAIN: NonceDomain = [3u8; 32]; + const SET_VALUE_ACTION: [u8; 32] = [4u8; 32]; + + pub struct AuthorizationCounter { + authorizations: AuthorizationManager, + value: u64, + last_authorizer: Option, + initialized: bool, + } + + impl AuthorizationCounter { + pub const fn new() -> Self { + Self { + authorizations: AuthorizationManager::new(), + value: 0, + last_authorizer: None, + initialized: false, + } + } + + pub fn init(&mut self) { + if self.initialized { + panic!("AuthorizationCounter: already initialized"); + } + self.initialized = true; + } + + pub fn value(&self) -> u64 { + self.value + } + + pub const fn last_authorizer(&self) -> Option { + self.last_authorizer + } + + pub fn nonce(&self, query: NonceQuery) -> u64 { + self.authorizations.nonce(query.principal, query.domain) + } + + pub fn set_value_by_moonlight(&mut self, args: SetValueByMoonlight) { + self.assert_action( + args.authorization.action.contract, + args.authorization.action.domain, + args.authorization.action.action_id, + args.authorization.action.payload_hash, + args.amount, + ); + let principal = self + .authorizations + .authorize_moonlight(&args.authorization, now()); + self.set_value(principal, args.amount); + } + + pub fn set_value_by_phoenix(&mut self, args: SetValueByPhoenix) { + self.assert_action( + args.authorization.action.contract, + args.authorization.action.domain, + args.authorization.action.action_id, + args.authorization.action.payload_hash, + args.amount, + ); + let principal = self + .authorizations + .authorize_phoenix(&args.authorization, now()); + self.set_value(principal, args.amount); + } + + fn assert_action( + &self, + contract: ContractId, + domain: NonceDomain, + action_id: [u8; 32], + payload_hash: [u8; 32], + amount: u64, + ) { + if !self.initialized { + panic!("AuthorizationCounter: not initialized"); + } + if contract != abi::self_id() { + panic!("AuthorizationCounter: wrong contract"); + } + if domain != SET_VALUE_DOMAIN || action_id != SET_VALUE_ACTION { + panic!("AuthorizationCounter: wrong action"); + } + if payload_hash != amount_hash(amount) { + panic!("AuthorizationCounter: wrong payload"); + } + } + + fn set_value(&mut self, principal: Principal, amount: u64) { + self.value = amount; + self.last_authorizer = Some(principal); + } + } + + impl Default for AuthorizationCounter { + fn default() -> Self { + Self::new() + } + } + + fn amount_hash(amount: u64) -> [u8; 32] { + abi::keccak256(Vec::from(amount.to_be_bytes())) + } + + fn now() -> u64 { + abi::block_height() + } +} From 82a6e391535494c44f860496136b4fc1ca99d71f Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sat, 25 Apr 2026 17:10:26 +0200 Subject: [PATCH 04/35] standards: add DRC20 roles pausable Forge reference --- .../examples/drc20_roles_pausable/Cargo.toml | 31 ++ .../examples/drc20_roles_pausable/Makefile | 34 ++ .../examples/drc20_roles_pausable/src/lib.rs | 497 ++++++++++++++++++ 3 files changed, 562 insertions(+) create mode 100644 standards/examples/drc20_roles_pausable/Cargo.toml create mode 100644 standards/examples/drc20_roles_pausable/Makefile create mode 100644 standards/examples/drc20_roles_pausable/src/lib.rs diff --git a/standards/examples/drc20_roles_pausable/Cargo.toml b/standards/examples/drc20_roles_pausable/Cargo.toml new file mode 100644 index 00000000..0f33de7c --- /dev/null +++ b/standards/examples/drc20_roles_pausable/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "drc20-roles-pausable" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[target.'cfg(target_family = "wasm")'.dependencies] +dusk-core = { workspace = true } +dusk-data-driver = { workspace = true, optional = true } +dusk-forge = { workspace = true } +dusk-contract-standards = { path = "../../dusk-contract-standards" } +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, default-features = false, features = [ + "alloc", +], optional = true } + +[features] +contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] +serde = ["dep:serde", "dusk-contract-standards/serde"] +data-driver = [ + "serde", + "dusk-core/serde", + "dep:dusk-data-driver", + "dusk-data-driver/wasm-export", + "dep:serde_json", +] +data-driver-js = ["data-driver", "dusk-data-driver/alloc"] diff --git a/standards/examples/drc20_roles_pausable/Makefile b/standards/examples/drc20_roles_pausable/Makefile new file mode 100644 index 00000000..3357eb79 --- /dev/null +++ b/standards/examples/drc20_roles_pausable/Makefile @@ -0,0 +1,34 @@ +TARGET_DIR ?= ../../../target +DD_TARGET_DIR ?= ../../../target/data-driver + +all: wasm wasm-dd clippy + +wasm: + @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ + CARGO_TARGET_DIR=$(TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + -Z build-std=core,alloc \ + --target wasm32-unknown-unknown \ + --features contract \ + -p drc20-roles-pausable + +test: + @cargo test -p dusk-contract-standards + +wasm-dd: + @CARGO_TARGET_DIR=$(DD_TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + --target wasm32-unknown-unknown \ + --features data-driver-js \ + -p drc20-roles-pausable + +clippy: + @cargo clippy -p drc20-roles-pausable -Z build-std=core,alloc --release --target wasm32-unknown-unknown --features contract -- -D warnings + +doc: + +.PHONY: all wasm wasm-dd test clippy doc diff --git a/standards/examples/drc20_roles_pausable/src/lib.rs b/standards/examples/drc20_roles_pausable/src/lib.rs new file mode 100644 index 00000000..18172e5a --- /dev/null +++ b/standards/examples/drc20_roles_pausable/src/lib.rs @@ -0,0 +1,497 @@ +//! Example DRC20 composed from reusable Dusk contract standards modules. + +#![no_std] +#![cfg(target_family = "wasm")] + +extern crate alloc; +extern crate self as drc20_roles_pausable_types; + +use bytecheck::CheckBytes; +use dusk_contract_standards::access::Role; +use dusk_contract_standards::auth::SignedAuthorization; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use dusk_contract_standards::token::drc20::Init as TokenInit; +use rkyv::{Archive, Deserialize, Serialize}; + +pub const TOKEN_ADMIN_DOMAIN: NonceDomain = [11u8; 32]; +pub const SIGNED_APPROVE_DOMAIN: NonceDomain = [12u8; 32]; +pub const MINT_ACTION: [u8; 32] = [13u8; 32]; +pub const PAUSE_ACTION: [u8; 32] = [14u8; 32]; +pub const UNPAUSE_ACTION: [u8; 32] = [15u8; 32]; +pub const GRANT_ROLE_ACTION: [u8; 32] = [16u8; 32]; +pub const REVOKE_ROLE_ACTION: [u8; 32] = [17u8; 32]; +pub const SIGNED_APPROVE_ACTION: [u8; 32] = [18u8; 32]; + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Init { + pub admin: Principal, + pub token: TokenInit, + /// Optional max supply. A value of zero means uncapped. + pub cap: u64, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MintCall { + pub to: Principal, + pub amount: u64, + pub authorization: Option, +} + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoleQuery { + pub role: Role, + pub account: Principal, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoleCall { + pub role: Role, + pub account: Principal, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct AdminCall { + pub authorization: Option, +} + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct VotesQuery { + pub account: Principal, + pub timepoint: u64, +} + +#[dusk_forge::contract] +mod drc20_roles_pausable { + use alloc::string::String; + use alloc::vec::Vec; + + use drc20_roles_pausable_types::{ + AdminCall, Init, MintCall, RoleCall, RoleQuery, VotesQuery, + GRANT_ROLE_ACTION, MINT_ACTION, PAUSE_ACTION, REVOKE_ROLE_ACTION, + SIGNED_APPROVE_ACTION, SIGNED_APPROVE_DOMAIN, TOKEN_ADMIN_DOMAIN, + UNPAUSE_ACTION, + }; + use dusk_contract_standards::access::{AccessControl, Pausable, Role}; + use dusk_contract_standards::auth::{ + ActionEnvelope, AuthorizationManager, SignedAuthorization, + }; + use dusk_contract_standards::core::{ + error, CallContext, NonceQuery, Principal, + }; + use dusk_contract_standards::security::ReentrancyGuard; + use dusk_contract_standards::token::drc20::events::{ + Approval as Drc20Approval, Transfer as Drc20Transfer, APPROVAL_TOPIC, + TRANSFER_TOPIC, + }; + use dusk_contract_standards::token::drc20::{ + Allowance, ApproveCall, BalanceOf, DecreaseAllowanceCall, Drc20, + IncreaseAllowanceCall, SignedApproveCall, SupplyCap, TransferCall, + TransferFromCall, VotingUnits, + }; + use dusk_core::abi; + + const MINTER_ROLE: Role = [1u8; 32]; + const PAUSER_ROLE: Role = [2u8; 32]; + + pub struct Drc20RolesPausable { + token: Drc20, + access: AccessControl, + authorizations: AuthorizationManager, + pausable: Pausable, + guard: ReentrancyGuard, + cap: Option, + votes: VotingUnits, + } + + impl Drc20RolesPausable { + pub const fn new() -> Self { + Self { + token: Drc20::new(), + access: AccessControl::new(), + authorizations: AuthorizationManager::new(), + pausable: Pausable::new(), + guard: ReentrancyGuard::new(), + cap: None, + votes: VotingUnits::new(), + } + } + + pub fn init(&mut self, args: Init) { + if args.admin.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let mut initial_supply = 0u64; + for balance in &args.token.initial_balances { + if balance.account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + initial_supply = initial_supply + .checked_add(balance.amount) + .expect(error::OVERFLOW); + } + if args.cap != 0 { + SupplyCap::new(args.cap).assert_mint(0, initial_supply); + } + + let initial_balances = args.token.initial_balances.clone(); + let events = self.token.init(args.token); + self.access.init_admin(args.admin); + self.access.grant_role(args.admin, MINTER_ROLE, args.admin); + self.access.grant_role(args.admin, PAUSER_ROLE, args.admin); + for event in events { + Self::emit_transfer(event); + } + if args.cap != 0 { + let cap = SupplyCap::new(args.cap); + self.cap = Some(cap); + } + for balance in initial_balances { + self.votes.move_units( + None, + Some(balance.account), + balance.amount, + now(), + ); + } + } + + pub fn name(&self) -> String { + self.token.name() + } + + pub fn symbol(&self) -> String { + self.token.symbol() + } + + pub fn decimals(&self) -> u8 { + self.token.decimals() + } + + pub fn total_supply(&self) -> u64 { + self.token.total_supply() + } + + pub fn balance_of(&self, args: BalanceOf) -> u64 { + self.token.balance_of(args) + } + + pub fn allowance(&self, args: Allowance) -> u64 { + self.token.allowance(args) + } + + pub fn nonce(&self, args: NonceQuery) -> u64 { + self.authorizations.nonce(args.principal, args.domain) + } + + pub fn cap(&self) -> u64 { + self.cap.map(|cap| cap.cap()).unwrap_or(0) + } + + pub fn remaining_mintable(&self) -> u64 { + self.cap + .map(|cap| cap.remaining(self.token.total_supply())) + .unwrap_or(u64::MAX) + } + + pub fn latest_votes(&self, account: Principal) -> u64 { + self.votes.latest_votes(account) + } + + pub fn past_votes(&self, args: VotesQuery) -> u64 { + self.votes.past_votes(args.account, args.timepoint) + } + + pub fn latest_total_votes(&self) -> u64 { + self.votes.latest_total_supply() + } + + pub fn past_total_supply(&self, timepoint: u64) -> u64 { + self.votes.past_total_supply(timepoint) + } + + pub fn has_role(&self, args: RoleQuery) -> bool { + self.access.has_role(args.role, args.account) + } + + pub fn transfer(&mut self, args: TransferCall) { + self.pausable.assert_not_paused(); + let caller = caller(); + let event = { + let guard = &mut self.guard; + let token = &mut self.token; + let votes = &mut self.votes; + guard.run(|| { + let event = token.transfer(caller, args); + votes.move_units( + Some(event.from), + Some(event.to), + event.amount, + now(), + ); + event + }) + }; + Self::emit_transfer(event); + } + + pub fn approve(&mut self, args: ApproveCall) { + let caller = caller(); + let event = self.token.approve(caller, args); + Self::emit_approval(event); + } + + pub fn approve_by_authorization(&mut self, args: SignedApproveCall) { + let principal = self.authorizations.authorize_signed_action( + &args.authorization, + ActionEnvelope::new( + abi::self_id(), + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + approve_payload_hash(args.owner, args.spender, args.amount), + ), + now(), + ); + if principal != args.owner { + panic!("{}", error::UNAUTHORIZED); + } + let event = self.token.approve_for( + args.owner, + ApproveCall { + spender: args.spender, + amount: args.amount, + }, + ); + Self::emit_approval(event); + } + + pub fn increase_allowance(&mut self, args: IncreaseAllowanceCall) { + let caller = caller(); + let event = self.token.increase_allowance(caller, args); + Self::emit_approval(event); + } + + pub fn decrease_allowance(&mut self, args: DecreaseAllowanceCall) { + let caller = caller(); + let event = self.token.decrease_allowance(caller, args); + Self::emit_approval(event); + } + + pub fn transfer_from(&mut self, args: TransferFromCall) { + self.pausable.assert_not_paused(); + let caller = caller(); + let event = { + let guard = &mut self.guard; + let token = &mut self.token; + let votes = &mut self.votes; + guard.run(|| { + let event = token.transfer_from(caller, args); + votes.move_units( + Some(event.from), + Some(event.to), + event.amount, + now(), + ); + event + }) + }; + Self::emit_transfer(event); + } + + pub fn mint(&mut self, args: MintCall) { + self.pausable.assert_not_paused(); + self.authorize_role_action( + MINTER_ROLE, + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + mint_payload_hash(args.to, args.amount), + ), + ); + if let Some(cap) = self.cap { + cap.assert_mint(self.token.total_supply(), args.amount); + } + let event = self.token.mint(args.to, args.amount); + self.votes + .move_units(None, Some(event.to), event.amount, now()); + Self::emit_transfer(event); + } + + pub fn burn(&mut self, amount: u64) { + self.pausable.assert_not_paused(); + let caller = caller(); + let event = self.token.burn(caller, amount); + self.votes + .move_units(Some(event.from), None, event.amount, now()); + Self::emit_transfer(event); + } + + pub fn pause(&mut self, args: AdminCall) { + self.authorize_role_action( + PAUSER_ROLE, + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + TOKEN_ADMIN_DOMAIN, + PAUSE_ACTION, + empty_payload_hash(b"drc20.pause"), + ), + ); + self.pausable.pause(); + } + + pub fn unpause(&mut self, args: AdminCall) { + self.authorize_role_action( + PAUSER_ROLE, + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + TOKEN_ADMIN_DOMAIN, + UNPAUSE_ACTION, + empty_payload_hash(b"drc20.unpause"), + ), + ); + self.pausable.unpause(); + } + + pub fn paused(&self) -> bool { + self.pausable.paused() + } + + pub fn grant_role(&mut self, args: RoleCall) { + let admin = self.access.get_role_admin(args.role); + let caller = self.authorize_role_action( + admin, + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + TOKEN_ADMIN_DOMAIN, + GRANT_ROLE_ACTION, + role_payload_hash(args.role, args.account), + ), + ); + self.access.grant_role(caller, args.role, args.account); + } + + pub fn revoke_role(&mut self, args: RoleCall) { + let admin = self.access.get_role_admin(args.role); + let caller = self.authorize_role_action( + admin, + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + TOKEN_ADMIN_DOMAIN, + REVOKE_ROLE_ACTION, + role_payload_hash(args.role, args.account), + ), + ); + self.access.revoke_role(caller, args.role, args.account); + } + + fn authorize_role_action( + &mut self, + role: Role, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + self.access.authorize_role_action( + role, + &mut self.authorizations, + CallContext::current(), + authorization, + envelope, + now(), + ) + } + + fn emit_transfer(event: Drc20Transfer) { + abi::emit( + TRANSFER_TOPIC, + Drc20Transfer { + from: event.from, + to: event.to, + amount: event.amount, + }, + ); + } + + fn emit_approval(event: Drc20Approval) { + abi::emit( + APPROVAL_TOPIC, + Drc20Approval { + owner: event.owner, + spender: event.spender, + amount: event.amount, + }, + ); + } + } + + impl Default for Drc20RolesPausable { + fn default() -> Self { + Self::new() + } + } + + fn caller() -> Principal { + CallContext::current().require_principal(error::UNAUTHORIZED) + } + + fn empty_payload_hash(tag: &[u8]) -> [u8; 32] { + abi::keccak256(Vec::from(tag)) + } + + fn mint_payload_hash(to: Principal, amount: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.mint"[..]); + push_principal(&mut bytes, to); + bytes.extend_from_slice(&amount.to_be_bytes()); + abi::keccak256(bytes) + } + + fn approve_payload_hash( + owner: Principal, + spender: Principal, + amount: u64, + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, spender); + bytes.extend_from_slice(&amount.to_be_bytes()); + abi::keccak256(bytes) + } + + fn role_payload_hash(role: Role, account: Principal) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.role"[..]); + bytes.extend_from_slice(&role); + push_principal(&mut bytes, account); + abi::keccak256(bytes) + } + + fn push_principal(bytes: &mut Vec, principal: Principal) { + let principal = principal.to_bytes(); + bytes.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&principal); + } + + fn now() -> u64 { + abi::block_height() + } +} From 4c3f1eda3ed1677872d1aa6007f10a46b028a892 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sat, 25 Apr 2026 17:10:33 +0200 Subject: [PATCH 05/35] standards: add DRC721 collection Forge reference --- .../examples/drc721_collection/Cargo.toml | 31 ++ standards/examples/drc721_collection/Makefile | 34 ++ .../examples/drc721_collection/src/lib.rs | 438 ++++++++++++++++++ 3 files changed, 503 insertions(+) create mode 100644 standards/examples/drc721_collection/Cargo.toml create mode 100644 standards/examples/drc721_collection/Makefile create mode 100644 standards/examples/drc721_collection/src/lib.rs diff --git a/standards/examples/drc721_collection/Cargo.toml b/standards/examples/drc721_collection/Cargo.toml new file mode 100644 index 00000000..bd0c0e00 --- /dev/null +++ b/standards/examples/drc721_collection/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "drc721-collection" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[target.'cfg(target_family = "wasm")'.dependencies] +dusk-core = { workspace = true } +dusk-data-driver = { workspace = true, optional = true } +dusk-forge = { workspace = true } +dusk-contract-standards = { path = "../../dusk-contract-standards" } +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, default-features = false, features = [ + "alloc", +], optional = true } + +[features] +contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] +serde = ["dep:serde", "dusk-contract-standards/serde"] +data-driver = [ + "serde", + "dusk-core/serde", + "dep:dusk-data-driver", + "dusk-data-driver/wasm-export", + "dep:serde_json", +] +data-driver-js = ["data-driver", "dusk-data-driver/alloc"] diff --git a/standards/examples/drc721_collection/Makefile b/standards/examples/drc721_collection/Makefile new file mode 100644 index 00000000..02fc3d66 --- /dev/null +++ b/standards/examples/drc721_collection/Makefile @@ -0,0 +1,34 @@ +TARGET_DIR ?= ../../../target +DD_TARGET_DIR ?= ../../../target/data-driver + +all: wasm wasm-dd clippy + +wasm: + @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ + CARGO_TARGET_DIR=$(TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + -Z build-std=core,alloc \ + --target wasm32-unknown-unknown \ + --features contract \ + -p drc721-collection + +test: + @cargo test -p dusk-contract-standards + +wasm-dd: + @CARGO_TARGET_DIR=$(DD_TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + --target wasm32-unknown-unknown \ + --features data-driver-js \ + -p drc721-collection + +clippy: + @cargo clippy -p drc721-collection -Z build-std=core,alloc --release --target wasm32-unknown-unknown --features contract -- -D warnings + +doc: + +.PHONY: all wasm wasm-dd test clippy doc diff --git a/standards/examples/drc721_collection/src/lib.rs b/standards/examples/drc721_collection/src/lib.rs new file mode 100644 index 00000000..d41f4376 --- /dev/null +++ b/standards/examples/drc721_collection/src/lib.rs @@ -0,0 +1,438 @@ +//! Example DRC721 composed from reusable Dusk contract standards modules. + +#![no_std] +#![cfg(target_family = "wasm")] + +extern crate alloc; +extern crate self as drc721_collection_types; + +use bytecheck::CheckBytes; +use dusk_contract_standards::auth::SignedAuthorization; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use dusk_contract_standards::token::drc721::{Init as TokenInit, RoyaltyInfo}; +use rkyv::{Archive, Deserialize, Serialize}; + +pub const NFT_ADMIN_DOMAIN: NonceDomain = [21u8; 32]; +pub const MINT_ACTION: [u8; 32] = [22u8; 32]; +pub const PAUSE_ACTION: [u8; 32] = [23u8; 32]; +pub const UNPAUSE_ACTION: [u8; 32] = [24u8; 32]; +pub const SET_DEFAULT_ROYALTY_ACTION: [u8; 32] = [25u8; 32]; +pub const CLEAR_DEFAULT_ROYALTY_ACTION: [u8; 32] = [26u8; 32]; +pub const SET_TOKEN_ROYALTY_ACTION: [u8; 32] = [27u8; 32]; +pub const CLEAR_TOKEN_ROYALTY_ACTION: [u8; 32] = [28u8; 32]; + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Init { + pub owner: Principal, + pub token: TokenInit, + pub default_royalty: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MintCall { + pub to: Principal, + pub token_id: u64, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct AdminCall { + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetDefaultRoyaltyCall { + pub info: RoyaltyInfo, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetTokenRoyaltyCall { + pub token_id: u64, + pub info: RoyaltyInfo, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ClearTokenRoyaltyCall { + pub token_id: u64, + pub authorization: Option, +} + +#[dusk_forge::contract] +mod drc721_collection { + use alloc::collections::BTreeSet; + use alloc::string::String; + use alloc::vec::Vec; + + use drc721_collection_types::{ + AdminCall, ClearTokenRoyaltyCall, Init, MintCall, + SetDefaultRoyaltyCall, SetTokenRoyaltyCall, + CLEAR_DEFAULT_ROYALTY_ACTION, CLEAR_TOKEN_ROYALTY_ACTION, MINT_ACTION, + NFT_ADMIN_DOMAIN, PAUSE_ACTION, SET_DEFAULT_ROYALTY_ACTION, + SET_TOKEN_ROYALTY_ACTION, UNPAUSE_ACTION, + }; + use dusk_contract_standards::access::{Ownable, Pausable}; + use dusk_contract_standards::auth::{ + ActionEnvelope, AuthorizationManager, SignedAuthorization, + }; + use dusk_contract_standards::core::{ + error, CallContext, NonceQuery, Principal, + }; + use dusk_contract_standards::security::ReentrancyGuard; + use dusk_contract_standards::token::drc721::events::{ + Approval as Drc721Approval, ApprovalForAll as Drc721ApprovalForAll, + Transfer as Drc721Transfer, APPROVAL_FOR_ALL_TOPIC, APPROVAL_TOPIC, + TRANSFER_TOPIC, + }; + use dusk_contract_standards::token::drc721::{ + ApproveCall, BalanceOf, Drc721, GetApproved, IsApprovedForAll, OwnerOf, + RoyaltyInfo, RoyaltyQuery, RoyaltyQuote, RoyaltyRegistry, + SetApprovalForAllCall, TokenByIndex, TokenOfOwnerByIndex, TokenUri, + TokensOf, TransferFromCall, + }; + use dusk_core::abi; + + pub struct Drc721Collection { + token: Drc721, + ownable: Ownable, + authorizations: AuthorizationManager, + pausable: Pausable, + guard: ReentrancyGuard, + royalties: RoyaltyRegistry, + } + + impl Drc721Collection { + pub const fn new() -> Self { + Self { + token: Drc721::new(), + ownable: Ownable::new(), + authorizations: AuthorizationManager::new(), + pausable: Pausable::new(), + guard: ReentrancyGuard::new(), + royalties: RoyaltyRegistry::new(), + } + } + + pub fn init(&mut self, args: Init) { + if args.owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let mut token_ids = BTreeSet::new(); + for token in &args.token.initial_tokens { + if token.account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if !token_ids.insert(token.token_id) { + panic!("DRC721: token already minted"); + } + } + let mut royalties = RoyaltyRegistry::new(); + if let Some(info) = args.default_royalty { + royalties.set_default_royalty(info); + } + + self.ownable.init(args.owner); + for event in self.token.init(args.token) { + Self::emit_transfer(event); + } + self.royalties = royalties; + } + + pub fn name(&self) -> String { + self.token.name() + } + + pub fn symbol(&self) -> String { + self.token.symbol() + } + + pub fn base_uri(&self) -> String { + self.token.base_uri() + } + + pub fn token_uri(&self, args: TokenUri) -> String { + self.token.token_uri(args) + } + + pub fn token_by_index(&self, args: TokenByIndex) -> u64 { + self.token.token_by_index(args) + } + + pub fn token_of_owner_by_index( + &self, + args: TokenOfOwnerByIndex, + ) -> u64 { + self.token.token_of_owner_by_index(args) + } + + pub fn tokens_of(&self, args: TokensOf) -> alloc::vec::Vec { + self.token.tokens_of(args) + } + + pub fn total_supply(&self) -> u64 { + self.token.total_supply() + } + + pub fn balance_of(&self, args: BalanceOf) -> u64 { + self.token.balance_of(args) + } + + pub fn owner_of(&self, args: OwnerOf) -> Principal { + self.token.owner_of(args) + } + + pub fn get_approved(&self, args: GetApproved) -> Principal { + self.token.get_approved(args) + } + + pub fn is_approved_for_all(&self, args: IsApprovedForAll) -> bool { + self.token.is_approved_for_all(args) + } + + pub fn nonce(&self, args: NonceQuery) -> u64 { + self.authorizations.nonce(args.principal, args.domain) + } + + pub fn royalty_info(&self, args: RoyaltyQuery) -> RoyaltyQuote { + self.royalties.royalty_info(args.token_id, args.sale_price) + } + + pub fn approve(&mut self, args: ApproveCall) { + let event = self.token.approve(caller(), args); + Self::emit_approval(event); + } + + pub fn set_approval_for_all(&mut self, args: SetApprovalForAllCall) { + let event = self.token.set_approval_for_all(caller(), args); + Self::emit_approval_for_all(event); + } + + pub fn transfer_from(&mut self, args: TransferFromCall) { + self.pausable.assert_not_paused(); + let caller = caller(); + let event = { + let guard = &mut self.guard; + let token = &mut self.token; + guard.run(|| token.transfer_from(caller, args)) + }; + Self::emit_transfer(event); + } + + pub fn mint(&mut self, args: MintCall) { + self.pausable.assert_not_paused(); + self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + NFT_ADMIN_DOMAIN, + MINT_ACTION, + mint_payload_hash(args.to, args.token_id), + ), + ); + let event = self.token.mint(args.to, args.token_id); + Self::emit_transfer(event); + } + + pub fn burn(&mut self, token_id: u64) { + self.pausable.assert_not_paused(); + let event = self.token.burn(caller(), token_id); + Self::emit_transfer(event); + } + + pub fn pause(&mut self, args: AdminCall) { + self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + NFT_ADMIN_DOMAIN, + PAUSE_ACTION, + empty_payload_hash(b"drc721.pause"), + ), + ); + self.pausable.pause(); + } + + pub fn unpause(&mut self, args: AdminCall) { + self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + NFT_ADMIN_DOMAIN, + UNPAUSE_ACTION, + empty_payload_hash(b"drc721.unpause"), + ), + ); + self.pausable.unpause(); + } + + pub fn paused(&self) -> bool { + self.pausable.paused() + } + + pub fn set_default_royalty(&mut self, args: SetDefaultRoyaltyCall) { + self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + NFT_ADMIN_DOMAIN, + SET_DEFAULT_ROYALTY_ACTION, + royalty_payload_hash(None, args.info), + ), + ); + self.royalties.set_default_royalty(args.info); + } + + pub fn clear_default_royalty(&mut self, args: AdminCall) { + self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + NFT_ADMIN_DOMAIN, + CLEAR_DEFAULT_ROYALTY_ACTION, + empty_payload_hash(b"drc721.clear_default_royalty"), + ), + ); + self.royalties.clear_default_royalty(); + } + + pub fn set_token_royalty(&mut self, args: SetTokenRoyaltyCall) { + self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + NFT_ADMIN_DOMAIN, + SET_TOKEN_ROYALTY_ACTION, + royalty_payload_hash(Some(args.token_id), args.info), + ), + ); + self.royalties.set_token_royalty(args.token_id, args.info); + } + + pub fn clear_token_royalty(&mut self, args: ClearTokenRoyaltyCall) { + self.authorize_owner_action( + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + NFT_ADMIN_DOMAIN, + CLEAR_TOKEN_ROYALTY_ACTION, + token_payload_hash(args.token_id), + ), + ); + self.royalties.clear_token_royalty(args.token_id); + } + + fn authorize_owner_action( + &mut self, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + self.ownable.authorize_owner_action( + &mut self.authorizations, + CallContext::current(), + authorization, + envelope, + now(), + ) + } + + fn emit_transfer(event: Drc721Transfer) { + abi::emit( + TRANSFER_TOPIC, + Drc721Transfer { + from: event.from, + to: event.to, + token_id: event.token_id, + }, + ); + } + + fn emit_approval(event: Drc721Approval) { + abi::emit( + APPROVAL_TOPIC, + Drc721Approval { + owner: event.owner, + approved: event.approved, + token_id: event.token_id, + }, + ); + } + + fn emit_approval_for_all(event: Drc721ApprovalForAll) { + abi::emit( + APPROVAL_FOR_ALL_TOPIC, + Drc721ApprovalForAll { + owner: event.owner, + operator: event.operator, + approved: event.approved, + }, + ); + } + } + + impl Default for Drc721Collection { + fn default() -> Self { + Self::new() + } + } + + fn caller() -> Principal { + CallContext::current().require_principal(error::UNAUTHORIZED) + } + + fn empty_payload_hash(tag: &[u8]) -> [u8; 32] { + abi::keccak256(Vec::from(tag)) + } + + fn mint_payload_hash(to: Principal, token_id: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.mint"[..]); + push_principal(&mut bytes, to); + bytes.extend_from_slice(&token_id.to_be_bytes()); + abi::keccak256(bytes) + } + + fn royalty_payload_hash( + token_id: Option, + info: RoyaltyInfo, + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.royalty"[..]); + match token_id { + Some(token_id) => { + bytes.push(1); + bytes.extend_from_slice(&token_id.to_be_bytes()); + } + None => bytes.push(0), + } + push_principal(&mut bytes, info.receiver); + bytes.extend_from_slice(&info.basis_points.to_be_bytes()); + abi::keccak256(bytes) + } + + fn token_payload_hash(token_id: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.token"[..]); + bytes.extend_from_slice(&token_id.to_be_bytes()); + abi::keccak256(bytes) + } + + fn push_principal(bytes: &mut Vec, principal: Principal) { + let principal = principal.to_bytes(); + bytes.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&principal); + } + + fn now() -> u64 { + abi::block_height() + } +} From b5b1e1af6655e7d54c3a3ea62fcfca82c8b5c408 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sat, 25 Apr 2026 17:10:41 +0200 Subject: [PATCH 06/35] standards: add proxy counter Forge reference --- standards/examples/proxy_counter/Cargo.toml | 31 ++ standards/examples/proxy_counter/Makefile | 34 ++ standards/examples/proxy_counter/src/lib.rs | 346 ++++++++++++++++++++ 3 files changed, 411 insertions(+) create mode 100644 standards/examples/proxy_counter/Cargo.toml create mode 100644 standards/examples/proxy_counter/Makefile create mode 100644 standards/examples/proxy_counter/src/lib.rs diff --git a/standards/examples/proxy_counter/Cargo.toml b/standards/examples/proxy_counter/Cargo.toml new file mode 100644 index 00000000..8402c9f5 --- /dev/null +++ b/standards/examples/proxy_counter/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "proxy-counter" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[target.'cfg(target_family = "wasm")'.dependencies] +dusk-core = { workspace = true } +dusk-data-driver = { workspace = true, optional = true } +dusk-forge = { workspace = true } +dusk-contract-standards = { path = "../../dusk-contract-standards" } +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, default-features = false, features = [ + "alloc", +], optional = true } + +[features] +contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] +serde = ["dep:serde", "dusk-contract-standards/serde"] +data-driver = [ + "serde", + "dusk-core/serde", + "dep:dusk-data-driver", + "dusk-data-driver/wasm-export", + "dep:serde_json", +] +data-driver-js = ["data-driver", "dusk-data-driver/alloc"] diff --git a/standards/examples/proxy_counter/Makefile b/standards/examples/proxy_counter/Makefile new file mode 100644 index 00000000..68427ebe --- /dev/null +++ b/standards/examples/proxy_counter/Makefile @@ -0,0 +1,34 @@ +TARGET_DIR ?= ../../../target +DD_TARGET_DIR ?= ../../../target/data-driver + +all: wasm wasm-dd clippy + +wasm: + @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ + CARGO_TARGET_DIR=$(TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + -Z build-std=core,alloc \ + --target wasm32-unknown-unknown \ + --features contract \ + -p proxy-counter + +test: + @cargo test -p dusk-contract-standards + +wasm-dd: + @CARGO_TARGET_DIR=$(DD_TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + --target wasm32-unknown-unknown \ + --features data-driver-js \ + -p proxy-counter + +clippy: + @cargo clippy -p proxy-counter -Z build-std=core,alloc --release --target wasm32-unknown-unknown --features contract -- -D warnings + +doc: + +.PHONY: all wasm wasm-dd test clippy doc diff --git a/standards/examples/proxy_counter/src/lib.rs b/standards/examples/proxy_counter/src/lib.rs new file mode 100644 index 00000000..a13beb49 --- /dev/null +++ b/standards/examples/proxy_counter/src/lib.rs @@ -0,0 +1,346 @@ +//! Dusk-native proxy/state-store example. + +#![no_std] +#![cfg(target_family = "wasm")] + +extern crate alloc; +extern crate self as proxy_counter_types; + +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use dusk_contract_standards::auth::SignedAuthorization; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use dusk_core::abi::ContractId; +use rkyv::{Archive, Deserialize, Serialize}; + +pub const PROXY_ADMIN_DOMAIN: NonceDomain = [31u8; 32]; +pub const SET_VALUE_ACTION: [u8; 32] = [32u8; 32]; +pub const PREPARE_UPGRADE_ACTION: [u8; 32] = [33u8; 32]; +pub const ACTIVATE_UPGRADE_ACTION: [u8; 32] = [34u8; 32]; +pub const CANCEL_UPGRADE_ACTION: [u8; 32] = [35u8; 32]; +pub const ROLLBACK_ACTION: [u8; 32] = [36u8; 32]; +pub const FINALIZE_ROLLBACK_ACTION: [u8; 32] = [37u8; 32]; + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Init { + pub admin: Principal, + pub implementation: ContractId, + pub upgrade_delay: u64, + pub rollback_window: u64, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct PrepareUpgrade { + pub implementation: ContractId, + pub migrate_data: Vec, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetValue { + pub value: u64, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct AdminCall { + pub authorization: Option, +} + +#[dusk_forge::contract] +mod proxy_counter { + use alloc::vec::Vec; + + use dusk_contract_standards::auth::{ + ActionEnvelope, AuthorizationManager, SignedAuthorization, + }; + use dusk_contract_standards::core::{ + error, CallContext, NonceQuery, Principal, + }; + use dusk_contract_standards::proxy::{ + RollbackFinalized, StateStore, UpgradeActivated, UpgradeAdmin, + UpgradeCancelled, UpgradePrepared, UpgradeRolledBack, + ROLLBACK_FINALIZED_TOPIC, UPGRADE_ACTIVATED_TOPIC, + UPGRADE_CANCELLED_TOPIC, UPGRADE_PREPARED_TOPIC, + UPGRADE_ROLLED_BACK_TOPIC, + }; + use dusk_core::abi::{self, ContractId}; + use proxy_counter_types::{ + AdminCall, Init, PrepareUpgrade, SetValue, ACTIVATE_UPGRADE_ACTION, + CANCEL_UPGRADE_ACTION, FINALIZE_ROLLBACK_ACTION, + PREPARE_UPGRADE_ACTION, PROXY_ADMIN_DOMAIN, ROLLBACK_ACTION, + SET_VALUE_ACTION, + }; + + const COUNTER_KEY: [u8; 32] = [7u8; 32]; + + pub struct ProxyCounter { + store: StateStore, + admin: Option, + authorizations: AuthorizationManager, + } + + impl ProxyCounter { + pub const fn new() -> Self { + Self { + store: StateStore::new(), + admin: None, + authorizations: AuthorizationManager::new(), + } + } + + pub fn init(&mut self, args: Init) { + if self.admin.is_some() { + panic!("{}", error::ALREADY_INITIALIZED); + } + self.admin = Some(UpgradeAdmin::new( + args.admin, + args.implementation, + args.upgrade_delay, + args.rollback_window, + )); + } + + pub fn implementation(&self) -> ContractId { + self.admin().implementation() + } + + pub fn rollback_deadline(&self) -> u64 { + self.admin().rollback_deadline() + } + + pub fn nonce(&self, args: NonceQuery) -> u64 { + self.authorizations.nonce(args.principal, args.domain) + } + + pub fn value(&self) -> u64 { + u64::from_be_bytes( + self.store.get_word(COUNTER_KEY)[24..32].try_into().unwrap(), + ) + } + + pub fn increment(&mut self) { + let next = self.value().checked_add(1).expect(error::OVERFLOW); + self.write_value(next); + } + + pub fn set_value(&mut self, args: SetValue) { + self.authorize_admin_action( + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + PROXY_ADMIN_DOMAIN, + SET_VALUE_ACTION, + value_payload_hash(args.value), + ), + ); + self.write_value(args.value); + } + + pub fn prepare_upgrade(&mut self, args: PrepareUpgrade) { + let caller = self.authorize_admin_action( + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + PROXY_ADMIN_DOMAIN, + PREPARE_UPGRADE_ACTION, + prepare_payload_hash( + args.implementation, + &args.migrate_data, + ), + ), + ); + let event = self.admin_mut().prepare( + caller, + args.implementation, + now(), + args.migrate_data, + ); + Self::emit_upgrade_prepared(event); + } + + pub fn activate_upgrade(&mut self, args: AdminCall) -> Vec { + let caller = self.authorize_admin_action( + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + PROXY_ADMIN_DOMAIN, + ACTIVATE_UPGRADE_ACTION, + empty_payload_hash(b"proxy.activate_upgrade"), + ), + ); + let (migrate_data, event) = + self.admin_mut().activate_with_event(caller, now()); + Self::emit_upgrade_activated(event); + migrate_data + } + + pub fn cancel_pending_upgrade(&mut self, args: AdminCall) { + let caller = self.authorize_admin_action( + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + PROXY_ADMIN_DOMAIN, + CANCEL_UPGRADE_ACTION, + empty_payload_hash(b"proxy.cancel_upgrade"), + ), + ); + let event = self.admin_mut().cancel_pending(caller); + Self::emit_upgrade_cancelled(event); + } + + pub fn rollback(&mut self, args: AdminCall) { + let caller = self.authorize_admin_action( + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + PROXY_ADMIN_DOMAIN, + ROLLBACK_ACTION, + empty_payload_hash(b"proxy.rollback"), + ), + ); + let event = self.admin_mut().rollback_with_event(caller, now()); + Self::emit_upgrade_rolled_back(event); + } + + pub fn finalize_rollback_window(&mut self, args: AdminCall) { + let caller = self.authorize_admin_action( + args.authorization.as_ref(), + ActionEnvelope::new( + abi::self_id(), + PROXY_ADMIN_DOMAIN, + FINALIZE_ROLLBACK_ACTION, + empty_payload_hash(b"proxy.finalize_rollback"), + ), + ); + let event = self + .admin_mut() + .finalize_rollback_window_with_event(caller, now()); + Self::emit_rollback_finalized(event); + } + + fn write_value(&mut self, value: u64) { + let mut word = [0u8; 32]; + word[24..32].copy_from_slice(&value.to_be_bytes()); + self.store.set_word(COUNTER_KEY, word); + } + + fn authorize_admin_action( + &mut self, + authorization: Option<&SignedAuthorization>, + envelope: ActionEnvelope, + ) -> Principal { + let admin = self.admin().admin(); + self.authorizations.authorize_principal_action( + admin, + CallContext::current(), + authorization, + envelope, + now(), + ) + } + + fn emit_upgrade_prepared(event: UpgradePrepared) { + abi::emit( + UPGRADE_PREPARED_TOPIC, + UpgradePrepared { + implementation: event.implementation, + eta: event.eta, + }, + ); + } + + fn emit_upgrade_activated(event: UpgradeActivated) { + abi::emit( + UPGRADE_ACTIVATED_TOPIC, + UpgradeActivated { + previous_implementation: event.previous_implementation, + implementation: event.implementation, + rollback_deadline: event.rollback_deadline, + }, + ); + } + + fn emit_upgrade_cancelled(event: UpgradeCancelled) { + abi::emit( + UPGRADE_CANCELLED_TOPIC, + UpgradeCancelled { + implementation: event.implementation, + }, + ); + } + + fn emit_upgrade_rolled_back(event: UpgradeRolledBack) { + abi::emit( + UPGRADE_ROLLED_BACK_TOPIC, + UpgradeRolledBack { + from_implementation: event.from_implementation, + restored_implementation: event.restored_implementation, + }, + ); + } + + fn emit_rollback_finalized(event: RollbackFinalized) { + abi::emit( + ROLLBACK_FINALIZED_TOPIC, + RollbackFinalized { + implementation: event.implementation, + }, + ); + } + + fn admin(&self) -> &UpgradeAdmin { + self.admin + .as_ref() + .unwrap_or_else(|| panic!("{}", error::NOT_INITIALIZED)) + } + + fn admin_mut(&mut self) -> &mut UpgradeAdmin { + self.admin + .as_mut() + .unwrap_or_else(|| panic!("{}", error::NOT_INITIALIZED)) + } + } + + impl Default for ProxyCounter { + fn default() -> Self { + Self::new() + } + } + + fn now() -> u64 { + abi::block_height() + } + + fn empty_payload_hash(tag: &[u8]) -> [u8; 32] { + abi::keccak256(Vec::from(tag)) + } + + fn value_payload_hash(value: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"proxy.value"[..]); + bytes.extend_from_slice(&value.to_be_bytes()); + abi::keccak256(bytes) + } + + fn prepare_payload_hash( + implementation: ContractId, + migrate_data: &[u8], + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"proxy.prepare"[..]); + bytes.extend_from_slice(&implementation.to_bytes()); + bytes.extend_from_slice(&(migrate_data.len() as u32).to_be_bytes()); + bytes.extend_from_slice(migrate_data); + abi::keccak256(bytes) + } +} From 1918096991b0a4c0a396099aaf6224269e6b0712 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sat, 25 Apr 2026 17:10:49 +0200 Subject: [PATCH 07/35] standards: add client signing and VM validation --- .../examples/build_signed_authorizations.rs | 104 ++ .../examples/encode_local_smoke_args.rs | 657 +++++++++ .../tests/examples_vm.rs | 1306 +++++++++++++++++ 3 files changed, 2067 insertions(+) create mode 100644 standards/dusk-contract-standards/examples/build_signed_authorizations.rs create mode 100644 standards/dusk-contract-standards/examples/encode_local_smoke_args.rs create mode 100644 standards/dusk-contract-standards/tests/examples_vm.rs diff --git a/standards/dusk-contract-standards/examples/build_signed_authorizations.rs b/standards/dusk-contract-standards/examples/build_signed_authorizations.rs new file mode 100644 index 00000000..66d76346 --- /dev/null +++ b/standards/dusk-contract-standards/examples/build_signed_authorizations.rs @@ -0,0 +1,104 @@ +use std::vec::Vec; + +use dusk_contract_standards::auth::{ + AuthorizedAction, MoonlightAuthorization, PhoenixSignatureAuthorization, + SignedAuthorization, +}; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use dusk_core::abi::ContractId; +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; +use dusk_core::signatures::schnorr::{ + PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, +}; +use dusk_core::JubJubScalar; +use dusk_vm::host_queries; +use rand::rngs::StdRng; +use rand::SeedableRng; + +const SIGNED_APPROVE_DOMAIN: NonceDomain = [12u8; 32]; +const SIGNED_APPROVE_ACTION: [u8; 32] = [18u8; 32]; +const EXAMPLE_EXPIRES_AT: u64 = 1_000; + +fn main() { + let contract = ContractId::from_bytes([42u8; 32]); + let spender = Principal::Contract(ContractId::from_bytes([7u8; 32])); + + let moonlight_sk = moonlight_secret(11); + let moonlight_pk = BlsPublicKey::from(&moonlight_sk); + let moonlight_owner = Principal::moonlight(&moonlight_pk); + let moonlight_action = + drc20_signed_approve_action(contract, moonlight_owner, spender, 100, 0); + let moonlight = SignedAuthorization::Moonlight(MoonlightAuthorization { + action: moonlight_action, + public_key: moonlight_pk, + signature: moonlight_sk.sign(&moonlight_action.message_bytes()), + }); + moonlight.assert_action( + contract, + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + drc20_approve_payload_hash(moonlight_owner, spender, 100), + ); + + let mut rng = StdRng::seed_from_u64(99); + let phoenix_sk = SchnorrSecretKey::from(JubJubScalar::from(12u64)); + let phoenix_pk = SchnorrPublicKey::from(&phoenix_sk); + let phoenix_owner = Principal::phoenix_public_key(&phoenix_pk); + let phoenix_action = + drc20_signed_approve_action(contract, phoenix_owner, spender, 100, 0); + let phoenix = SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action: phoenix_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, phoenix_action.message_hash()), + replay_key: None, + }); + phoenix.assert_action( + contract, + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + drc20_approve_payload_hash(phoenix_owner, spender, 100), + ); +} + +fn drc20_signed_approve_action( + contract: ContractId, + owner: Principal, + spender: Principal, + amount: u64, + nonce: u64, +) -> AuthorizedAction { + AuthorizedAction { + contract, + domain: SIGNED_APPROVE_DOMAIN, + action_id: SIGNED_APPROVE_ACTION, + nonce, + expires_at: EXAMPLE_EXPIRES_AT, + principal: owner, + payload_hash: drc20_approve_payload_hash(owner, spender, amount), + } +} + +fn drc20_approve_payload_hash( + owner: Principal, + spender: Principal, + amount: u64, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, spender); + bytes.extend_from_slice(&amount.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn moonlight_secret(seed: u64) -> BlsSecretKey { + let mut rng = StdRng::seed_from_u64(seed); + BlsSecretKey::random(&mut rng) +} + +fn push_principal(bytes: &mut Vec, principal: Principal) { + let principal = principal.to_bytes(); + bytes.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&principal); +} diff --git a/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs new file mode 100644 index 00000000..0bdb493d --- /dev/null +++ b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs @@ -0,0 +1,657 @@ +use std::env; +use std::error::Error; +use std::string::String; +use std::vec::Vec; + +use dusk_contract_standards::auth::{ + AuthorizedAction, MoonlightAuthorization, PhoenixSignatureAuthorization, + SignedAuthorization, +}; +use dusk_contract_standards::core::Principal; +use dusk_contract_standards::token::drc20::{ + Init as Drc20TokenInit, InitBalance as Drc20InitBalance, +}; +use dusk_contract_standards::token::drc721::{ + Init as Drc721TokenInit, InitToken as Drc721InitToken, +}; +use dusk_core::abi::{ContractId, StandardBufSerializer, ARGBUF_LEN}; +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; +use dusk_core::signatures::schnorr::{ + PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, +}; +use dusk_core::JubJubScalar; +use dusk_vm::host_queries; +use rand::rngs::StdRng; +use rand::SeedableRng; +use rkyv::ser::serializers::{ + BufferScratch, BufferSerializer, CompositeSerializer, +}; +use rkyv::ser::Serializer; +use rkyv::{Archive, Deserialize, Infallible, Serialize}; + +const SCRATCH_BUF_BYTES: usize = 1024; +const SET_VALUE_DOMAIN: [u8; 32] = [3u8; 32]; +const SET_VALUE_ACTION: [u8; 32] = [4u8; 32]; +const TOKEN_ADMIN_DOMAIN: [u8; 32] = [11u8; 32]; +const MINT_ACTION: [u8; 32] = [13u8; 32]; +const PAUSE_ACTION: [u8; 32] = [14u8; 32]; +const UNPAUSE_ACTION: [u8; 32] = [15u8; 32]; +const NFT_ADMIN_DOMAIN: [u8; 32] = [21u8; 32]; +const NFT_MINT_ACTION: [u8; 32] = [22u8; 32]; +const NFT_PAUSE_ACTION: [u8; 32] = [23u8; 32]; +const NFT_UNPAUSE_ACTION: [u8; 32] = [24u8; 32]; +const PROXY_ADMIN_DOMAIN: [u8; 32] = [31u8; 32]; +const SET_PROXY_VALUE_ACTION: [u8; 32] = [32u8; 32]; + +#[derive(Archive, Serialize, Deserialize)] +struct Drc20ExampleInit { + admin: Principal, + token: Drc20TokenInit, + cap: u64, +} + +#[derive(Archive, Serialize, Deserialize)] +struct Drc721ExampleInit { + owner: Principal, + token: Drc721TokenInit, + default_royalty: + Option, +} + +#[derive(Archive, Serialize, Deserialize)] +struct ProxyCounterInit { + admin: Principal, + implementation: ContractId, + upgrade_delay: u64, + rollback_window: u64, +} + +#[derive(Archive, Serialize, Deserialize)] +struct SetValueByMoonlight { + authorization: MoonlightAuthorization, + amount: u64, +} + +#[derive(Archive, Serialize, Deserialize)] +struct SetValueByPhoenix { + authorization: PhoenixSignatureAuthorization, + amount: u64, +} + +#[derive(Archive, Serialize, Deserialize)] +struct NonceQuery { + principal: Principal, + domain: [u8; 32], +} + +#[derive(Archive, Serialize, Deserialize)] +struct Drc20MintCall { + to: Principal, + amount: u64, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize)] +struct Drc20AdminCall { + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize)] +struct Drc721MintCall { + to: Principal, + token_id: u64, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize)] +struct Drc721AdminCall { + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize)] +struct ProxySetValue { + value: u64, + authorization: Option, +} + +struct AdminPhoenixAction { + contract: ContractId, + admin: Principal, + domain: [u8; 32], + action_id: [u8; 32], + nonce: u64, + expires_at: u64, + payload_hash: [u8; 32], +} + +fn main() -> Result<(), Box> { + let args = env::args().collect::>(); + let Some(command) = args.get(1).map(String::as_str) else { + eprintln!( + "usage: encode_local_smoke_args " + ); + std::process::exit(2); + }; + + let admin = phoenix_principal(1); + let bytes = match command { + "drc20-init" => encode(&Drc20ExampleInit { + admin, + token: Drc20TokenInit { + name: String::from("Local Dusk Token"), + symbol: String::from("LDUSK"), + decimals: 9, + initial_balances: vec![Drc20InitBalance { + account: admin, + amount: 1_000, + }], + }, + cap: 10_000_000, + })?, + "drc721-init" => encode(&Drc721ExampleInit { + owner: admin, + token: Drc721TokenInit { + name: String::from("Local Dusk Collection"), + symbol: String::from("LDNFT"), + base_uri: String::from("ipfs://local/"), + initial_tokens: vec![Drc721InitToken { + account: admin, + token_id: 1, + }], + }, + default_royalty: None, + })?, + "proxy-init" => encode(&ProxyCounterInit { + admin, + implementation: ContractId::from_bytes([9u8; 32]), + upgrade_delay: 0, + rollback_window: 10, + })?, + "unit" => encode(&())?, + "u64" => encode(&parse_u64_arg(&args, 2, "value")?)?, + "nonce" => encode(&NonceQuery { + principal: principal_arg(&args, 3)?, + domain: domain_arg(&args, 2)?, + })?, + "auth-counter-phoenix" => encode(&counter_phoenix_call(&args)?)?, + "auth-counter-moonlight" => encode(&counter_moonlight_call(&args)?)?, + "drc20-mint" => encode(&drc20_mint_call(&args, admin)?)?, + "drc20-admin" => encode(&drc20_admin_call(&args, admin)?)?, + "drc721-mint" => encode(&drc721_mint_call(&args, admin)?)?, + "drc721-admin" => encode(&drc721_admin_call(&args, admin)?)?, + "proxy-set" => encode(&proxy_set_call(&args, admin)?)?, + _ => { + eprintln!("unknown command: {command}"); + std::process::exit(2); + } + }; + + print_hex(&bytes); + Ok(()) +} + +fn counter_phoenix_call( + args: &[String], +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let amount = parse_u64_arg(args, 5, "amount")?; + let variant = variant_arg(args); + let secret = SchnorrSecretKey::from(JubJubScalar::from(88u64)); + let public = SchnorrPublicKey::from(&secret); + let principal = Principal::phoenix_public_key(&public); + let action = counter_action( + contract_for_variant(contract, variant), + principal, + nonce, + expires_at, + action_for_variant(SET_VALUE_ACTION, variant), + payload_amount_for_variant(amount, variant), + ); + let mut rng = StdRng::seed_from_u64(12_345 + nonce); + Ok(SetValueByPhoenix { + authorization: phoenix_auth(&mut rng, &secret, public, action), + amount, + }) +} + +fn counter_moonlight_call( + args: &[String], +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let amount = parse_u64_arg(args, 5, "amount")?; + let variant = variant_arg(args); + let secret = moonlight_secret(7); + let public = BlsPublicKey::from(&secret); + let principal = Principal::moonlight(&public); + let action = counter_action( + contract_for_variant(contract, variant), + principal, + nonce, + expires_at, + action_for_variant(SET_VALUE_ACTION, variant), + payload_amount_for_variant(amount, variant), + ); + Ok(SetValueByMoonlight { + authorization: moonlight_auth(&secret, public, action), + amount, + }) +} + +fn drc20_mint_call( + args: &[String], + admin: Principal, +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let amount = parse_u64_arg(args, 5, "amount")?; + let variant = variant_arg(args); + let secret = SchnorrSecretKey::from(JubJubScalar::from(1u64)); + let public = SchnorrPublicKey::from(&secret); + let payload_amount = payload_amount_for_variant(amount, variant); + let action = authorized_action( + contract_for_variant(contract, variant), + admin, + TOKEN_ADMIN_DOMAIN, + action_for_variant(MINT_ACTION, variant), + nonce, + expires_at, + mint_payload_hash(admin, payload_amount), + ); + let mut rng = StdRng::seed_from_u64(22_222 + nonce); + Ok(Drc20MintCall { + to: admin, + amount, + authorization: Some(SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, &secret, public, action, + ))), + }) +} + +fn drc20_admin_call( + args: &[String], + admin: Principal, +) -> Result> { + let action_name = arg(args, 2, "action")?; + let contract = contract_arg(args, 3)?; + let nonce = parse_u64_arg(args, 4, "nonce")?; + let expires_at = parse_u64_arg(args, 5, "expires_at")?; + let variant = variant_arg(args); + let (action_id, tag) = match action_name { + "pause" => (PAUSE_ACTION, b"drc20.pause".as_slice()), + "unpause" => (UNPAUSE_ACTION, b"drc20.unpause".as_slice()), + _ => { + return Err( + format!("unknown drc20 admin action: {action_name}").into() + ) + } + }; + Ok(Drc20AdminCall { + authorization: Some(admin_phoenix_authorization( + AdminPhoenixAction { + contract, + admin, + domain: TOKEN_ADMIN_DOMAIN, + action_id: action_for_variant(action_id, variant), + nonce, + expires_at, + payload_hash: empty_payload_hash(tag), + }, + variant, + )), + }) +} + +fn drc721_mint_call( + args: &[String], + admin: Principal, +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let token_id = parse_u64_arg(args, 5, "token_id")?; + let variant = variant_arg(args); + let payload_token_id = payload_amount_for_variant(token_id, variant); + Ok(Drc721MintCall { + to: admin, + token_id, + authorization: Some(admin_phoenix_authorization( + AdminPhoenixAction { + contract, + admin, + domain: NFT_ADMIN_DOMAIN, + action_id: action_for_variant(NFT_MINT_ACTION, variant), + nonce, + expires_at, + payload_hash: nft_mint_payload_hash(admin, payload_token_id), + }, + variant, + )), + }) +} + +fn drc721_admin_call( + args: &[String], + admin: Principal, +) -> Result> { + let action_name = arg(args, 2, "action")?; + let contract = contract_arg(args, 3)?; + let nonce = parse_u64_arg(args, 4, "nonce")?; + let expires_at = parse_u64_arg(args, 5, "expires_at")?; + let variant = variant_arg(args); + let (action_id, tag) = match action_name { + "pause" => (NFT_PAUSE_ACTION, b"drc721.pause".as_slice()), + "unpause" => (NFT_UNPAUSE_ACTION, b"drc721.unpause".as_slice()), + _ => { + return Err( + format!("unknown drc721 admin action: {action_name}").into() + ) + } + }; + Ok(Drc721AdminCall { + authorization: Some(admin_phoenix_authorization( + AdminPhoenixAction { + contract, + admin, + domain: NFT_ADMIN_DOMAIN, + action_id: action_for_variant(action_id, variant), + nonce, + expires_at, + payload_hash: empty_payload_hash(tag), + }, + variant, + )), + }) +} + +fn proxy_set_call( + args: &[String], + admin: Principal, +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let value = parse_u64_arg(args, 5, "value")?; + let variant = variant_arg(args); + let payload_value = payload_amount_for_variant(value, variant); + Ok(ProxySetValue { + value, + authorization: Some(admin_phoenix_authorization( + AdminPhoenixAction { + contract, + admin, + domain: PROXY_ADMIN_DOMAIN, + action_id: action_for_variant(SET_PROXY_VALUE_ACTION, variant), + nonce, + expires_at, + payload_hash: proxy_value_payload_hash(payload_value), + }, + variant, + )), + }) +} + +fn admin_phoenix_authorization( + args: AdminPhoenixAction, + variant: &str, +) -> SignedAuthorization { + let secret = SchnorrSecretKey::from(JubJubScalar::from(1u64)); + let public = SchnorrPublicKey::from(&secret); + let action = authorized_action( + contract_for_variant(args.contract, variant), + args.admin, + args.domain, + args.action_id, + args.nonce, + args.expires_at, + args.payload_hash, + ); + let mut rng = StdRng::seed_from_u64(33_333 + args.nonce); + SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, &secret, public, action, + )) +} + +fn counter_action( + contract: ContractId, + principal: Principal, + nonce: u64, + expires_at: u64, + action_id: [u8; 32], + amount: u64, +) -> AuthorizedAction { + authorized_action( + contract, + principal, + SET_VALUE_DOMAIN, + action_id, + nonce, + expires_at, + amount_hash(amount), + ) +} + +fn authorized_action( + contract: ContractId, + principal: Principal, + domain: [u8; 32], + action_id: [u8; 32], + nonce: u64, + expires_at: u64, + payload_hash: [u8; 32], +) -> AuthorizedAction { + AuthorizedAction { + contract, + domain, + action_id, + nonce, + expires_at, + principal, + payload_hash, + } +} + +fn phoenix_auth( + rng: &mut StdRng, + secret_key: &SchnorrSecretKey, + public_key: SchnorrPublicKey, + action: AuthorizedAction, +) -> PhoenixSignatureAuthorization { + PhoenixSignatureAuthorization { + action, + public_key, + signature: secret_key.sign(rng, action.message_hash()), + replay_key: None, + } +} + +fn moonlight_auth( + secret_key: &BlsSecretKey, + public_key: BlsPublicKey, + action: AuthorizedAction, +) -> MoonlightAuthorization { + MoonlightAuthorization { + action, + public_key, + signature: secret_key.sign(&action.message_bytes()), + } +} + +fn amount_hash(amount: u64) -> [u8; 32] { + host_queries::keccak256(Vec::from(amount.to_be_bytes())) +} + +fn mint_payload_hash(to: Principal, amount: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.mint"[..]); + push_principal(&mut bytes, to); + bytes.extend_from_slice(&amount.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn nft_mint_payload_hash(to: Principal, token_id: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.mint"[..]); + push_principal(&mut bytes, to); + bytes.extend_from_slice(&token_id.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn proxy_value_payload_hash(value: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"proxy.value"[..]); + bytes.extend_from_slice(&value.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn empty_payload_hash(tag: &[u8]) -> [u8; 32] { + host_queries::keccak256(Vec::from(tag)) +} + +fn push_principal(bytes: &mut Vec, principal: Principal) { + let principal = principal.to_bytes(); + bytes.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&principal); +} + +fn phoenix_principal(seed: u64) -> Principal { + let secret = SchnorrSecretKey::from(JubJubScalar::from(seed)); + let public = SchnorrPublicKey::from(&secret); + Principal::phoenix_public_key(&public) +} + +fn moonlight_principal(seed: u64) -> Principal { + let secret = moonlight_secret(seed); + let public = BlsPublicKey::from(&secret); + Principal::moonlight(&public) +} + +fn moonlight_secret(seed: u64) -> BlsSecretKey { + let mut rng = StdRng::seed_from_u64(seed); + BlsSecretKey::random(&mut rng) +} + +fn contract_for_variant(contract: ContractId, variant: &str) -> ContractId { + if variant == "wrong-contract" { + ContractId::from_bytes([0xee; 32]) + } else { + contract + } +} + +fn action_for_variant(action_id: [u8; 32], variant: &str) -> [u8; 32] { + if variant == "wrong-action" { + [0xaa; 32] + } else { + action_id + } +} + +fn payload_amount_for_variant(amount: u64, variant: &str) -> u64 { + if variant == "bad-payload" { + amount.saturating_add(1) + } else { + amount + } +} + +fn variant_arg(args: &[String]) -> &str { + args.get(6).map(String::as_str).unwrap_or("valid") +} + +fn arg<'a>( + args: &'a [String], + index: usize, + name: &str, +) -> Result<&'a str, Box> { + args.get(index) + .map(String::as_str) + .ok_or_else(|| format!("missing {name}").into()) +} + +fn parse_u64_arg( + args: &[String], + index: usize, + name: &str, +) -> Result> { + arg(args, index, name)? + .parse::() + .map_err(|e| format!("invalid {name}: {e}").into()) +} + +fn contract_arg( + args: &[String], + index: usize, +) -> Result> { + Ok(ContractId::from_bytes(parse_hex_32(arg( + args, index, "contract", + )?)?)) +} + +fn principal_arg( + args: &[String], + index: usize, +) -> Result> { + let kind = arg(args, index, "principal kind")?; + let seed = parse_u64_arg(args, index + 1, "principal seed")?; + match kind { + "phoenix" => Ok(phoenix_principal(seed)), + "moonlight" => Ok(moonlight_principal(seed)), + _ => Err(format!("unknown principal kind: {kind}").into()), + } +} + +fn domain_arg( + args: &[String], + index: usize, +) -> Result<[u8; 32], Box> { + match arg(args, index, "domain")? { + "counter" => Ok(SET_VALUE_DOMAIN), + "drc20-admin" => Ok(TOKEN_ADMIN_DOMAIN), + "drc721-admin" => Ok(NFT_ADMIN_DOMAIN), + "proxy-admin" => Ok(PROXY_ADMIN_DOMAIN), + other => Err(format!("unknown domain: {other}").into()), + } +} + +fn parse_hex_32(hex: &str) -> Result<[u8; 32], Box> { + let hex = hex.strip_prefix("0x").unwrap_or(hex); + if hex.len() != 64 { + return Err(format!("expected 64 hex chars, got {}", hex.len()).into()); + } + let mut bytes = [0u8; 32]; + for i in 0..32 { + bytes[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16) + .map_err(|e| format!("invalid contract hex: {e}"))?; + } + Ok(bytes) +} + +fn encode(args: &A) -> Result, Box> +where + A: for<'a> Serialize>, +{ + let mut buf = vec![0u8; ARGBUF_LEN]; + let mut scratch = [0u8; SCRATCH_BUF_BYTES]; + let pos = { + let ser = BufferSerializer::new(buf.as_mut_slice()); + let scratch = BufferScratch::new(&mut scratch); + let mut serializer = CompositeSerializer::new(ser, scratch, Infallible); + serializer + .serialize_value(args) + .map_err(|e| format!("serialize args failed: {e:?}"))?; + serializer.pos() + }; + buf.truncate(pos); + Ok(buf) +} + +fn print_hex(bytes: &[u8]) { + for byte in bytes { + print!("{byte:02x}"); + } + println!(); +} diff --git a/standards/dusk-contract-standards/tests/examples_vm.rs b/standards/dusk-contract-standards/tests/examples_vm.rs new file mode 100644 index 00000000..2a779e40 --- /dev/null +++ b/standards/dusk-contract-standards/tests/examples_vm.rs @@ -0,0 +1,1306 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use bytecheck::CheckBytes; +use dusk_contract_standards::auth::{ + AuthorizedAction, MoonlightAuthorization, PhoenixSignatureAuthorization, + SignedAuthorization, +}; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use dusk_contract_standards::token::drc20::{ + Allowance, BalanceOf as BalanceOf20, Init as Init20, InitBalance, + SignedApproveCall, +}; +use dusk_contract_standards::token::drc721::{ + Init as Init721, InitToken, OwnerOf, TokensOf, +}; +use dusk_core::abi::{ContractId, Metadata}; +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; +use dusk_core::signatures::schnorr::{ + PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, +}; +use dusk_core::JubJubScalar; +use dusk_vm::host_queries; +use dusk_vm::{ContractData, Session, VM}; +use rand::rngs::StdRng; +use rand::SeedableRng; +use rkyv::{Archive, Deserialize, Serialize}; + +const CHAIN_ID: u8 = 0xD5; +const GAS_LIMIT: u64 = 1_000_000_000; +const OWNER_BYTES: [u8; 32] = [1u8; 32]; +const SET_VALUE_DOMAIN: NonceDomain = [3u8; 32]; +const SET_VALUE_ACTION: [u8; 32] = [4u8; 32]; +const TOKEN_ADMIN_DOMAIN: NonceDomain = [11u8; 32]; +const SIGNED_APPROVE_DOMAIN: NonceDomain = [12u8; 32]; +const MINT_ACTION: [u8; 32] = [13u8; 32]; +const PAUSE_ACTION: [u8; 32] = [14u8; 32]; +const UNPAUSE_ACTION: [u8; 32] = [15u8; 32]; +const SIGNED_APPROVE_ACTION: [u8; 32] = [18u8; 32]; +const NFT_ADMIN_DOMAIN: NonceDomain = [21u8; 32]; +const NFT_MINT_ACTION: [u8; 32] = [22u8; 32]; +const NFT_PAUSE_ACTION: [u8; 32] = [23u8; 32]; +const NFT_UNPAUSE_ACTION: [u8; 32] = [24u8; 32]; +const PROXY_ADMIN_DOMAIN: NonceDomain = [31u8; 32]; +const SET_PROXY_VALUE_ACTION: [u8; 32] = [32u8; 32]; + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[archive_attr(derive(CheckBytes))] +struct SetValueByMoonlight { + authorization: MoonlightAuthorization, + amount: u64, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct SetValueByPhoenix { + authorization: PhoenixSignatureAuthorization, + amount: u64, +} + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[archive_attr(derive(CheckBytes))] +struct NonceQuery { + principal: Principal, + domain: NonceDomain, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[archive_attr(derive(CheckBytes))] +struct Drc20ExampleInit { + admin: Principal, + token: Init20, + cap: u64, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct Drc20MintCall { + to: Principal, + amount: u64, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct Drc20AdminCall { + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[archive_attr(derive(CheckBytes))] +struct Drc721ExampleInit { + owner: Principal, + token: Init721, + default_royalty: + Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct Drc721MintCall { + to: Principal, + token_id: u64, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct Drc721AdminCall { + authorization: Option, +} + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[archive_attr(derive(CheckBytes))] +struct ProxyCounterInit { + admin: Principal, + implementation: ContractId, + upgrade_delay: u64, + rollback_window: u64, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct ProxySetValue { + value: u64, + authorization: Option, +} + +#[test] +#[ignore = "requires release Wasm examples; run after the wasm build"] +fn wasm_examples_deploy_and_answer_queries() { + let _hard_fork_guard = + host_queries::set_hard_fork(host_queries::HardFork::Aegis); + let vm = VM::ephemeral().expect("create ephemeral VM"); + let mut session = vm.genesis_session(CHAIN_ID); + + let admin = phoenix_principal(7); + let auth_counter = + deploy(&mut session, "authorization_counter.wasm", [19u8; 32], &()); + let auth_value: u64 = session + .call(auth_counter, "value", &(), GAS_LIMIT) + .expect("query authorization counter value") + .data; + assert_eq!(auth_value, 0); + exercise_authorization_counter_signed_calls(&mut session, auth_counter); + + let drc20 = deploy( + &mut session, + "drc20_roles_pausable.wasm", + [20u8; 32], + &Drc20ExampleInit { + admin, + token: Init20 { + name: "VM Dusk Token".into(), + symbol: "VDUSK".into(), + decimals: 9, + initial_balances: vec![InitBalance { + account: admin, + amount: 1_000, + }], + }, + cap: 10_000, + }, + ); + let drc20_supply: u64 = session + .call(drc20, "total_supply", &(), GAS_LIMIT) + .expect("query drc20 total supply") + .data; + assert_eq!(drc20_supply, 1_000); + let drc20_balance: u64 = session + .call( + drc20, + "balance_of", + &BalanceOf20 { account: admin }, + GAS_LIMIT, + ) + .expect("query drc20 balance") + .data; + assert_eq!(drc20_balance, 1_000); + assert!(session + .call::<_, ()>( + drc20, + "mint", + &Drc20MintCall { + to: admin, + amount: 1, + authorization: None, + }, + GAS_LIMIT, + ) + .is_err()); + assert!(session + .call::<_, ()>( + drc20, + "pause", + &Drc20AdminCall { + authorization: None, + }, + GAS_LIMIT, + ) + .is_err()); + exercise_drc20_signed_calls(&mut session, drc20, admin); + + let drc721 = deploy( + &mut session, + "drc721_collection.wasm", + [21u8; 32], + &Drc721ExampleInit { + owner: admin, + token: Init721 { + name: "VM Dusk Collection".into(), + symbol: "VDNFT".into(), + base_uri: "ipfs://vm/".into(), + initial_tokens: vec![InitToken { + account: admin, + token_id: 42, + }], + }, + default_royalty: None, + }, + ); + let drc721_supply: u64 = session + .call(drc721, "total_supply", &(), GAS_LIMIT) + .expect("query drc721 total supply") + .data; + assert_eq!(drc721_supply, 1); + let owner: Principal = session + .call(drc721, "owner_of", &OwnerOf { token_id: 42 }, GAS_LIMIT) + .expect("query drc721 owner") + .data; + assert_eq!(owner, admin); + let tokens: Vec = session + .call(drc721, "tokens_of", &TokensOf { owner: admin }, GAS_LIMIT) + .expect("query drc721 tokens") + .data; + assert_eq!(tokens, vec![42]); + assert!(session + .call::<_, ()>( + drc721, + "mint", + &Drc721MintCall { + to: admin, + token_id: 43, + authorization: None, + }, + GAS_LIMIT, + ) + .is_err()); + assert!(session + .call::<_, ()>( + drc721, + "pause", + &Drc721AdminCall { + authorization: None, + }, + GAS_LIMIT, + ) + .is_err()); + exercise_drc721_signed_calls(&mut session, drc721, admin); + + let proxy = deploy( + &mut session, + "proxy_counter.wasm", + [22u8; 32], + &ProxyCounterInit { + admin, + implementation: ContractId::from_bytes([99u8; 32]), + upgrade_delay: 0, + rollback_window: 10, + }, + ); + let value: u64 = session + .call(proxy, "value", &(), GAS_LIMIT) + .expect("query proxy value") + .data; + assert_eq!(value, 0); + let implementation: ContractId = session + .call(proxy, "implementation", &(), GAS_LIMIT) + .expect("query proxy implementation") + .data; + assert_eq!(implementation, ContractId::from_bytes([99u8; 32])); + assert!(session + .call::<_, ()>( + proxy, + "set_value", + &ProxySetValue { + value: 7, + authorization: None, + }, + GAS_LIMIT, + ) + .is_err()); + exercise_proxy_signed_admin_call(&mut session, proxy, admin); +} + +fn exercise_drc20_signed_calls( + session: &mut Session, + contract: ContractId, + admin: Principal, +) { + let mut rng = StdRng::seed_from_u64(2222); + let admin_sk = SchnorrSecretKey::from(JubJubScalar::from(7u64)); + let admin_pk = SchnorrPublicKey::from(&admin_sk); + assert_eq!(admin, Principal::phoenix_public_key(&admin_pk)); + + let mint_action = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 0, + 0, + mint_payload_hash(admin, 5), + ); + let mint_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + mint_action, + )); + session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 5, + authorization: Some(mint_auth.clone()), + }, + GAS_LIMIT, + ) + .expect("mint by signed Phoenix admin"); + let supply: u64 = session + .call(contract, "total_supply", &(), GAS_LIMIT) + .expect("query drc20 supply") + .data; + assert_eq!(supply, 1_005); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 5, + authorization: Some(mint_auth), + }, + GAS_LIMIT, + ) + .is_err()); + + let bad_mint_action = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 1, + 0, + mint_payload_hash(admin, 9), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + bad_mint_action, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + + let spender_sk = moonlight_secret(30); + let spender_pk = BlsPublicKey::from(&spender_sk); + let spender = Principal::moonlight(&spender_pk); + + let approve_action = authorized_action( + contract, + admin, + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + 0, + 0, + approve_payload_hash(admin, spender, 33), + ); + let approve_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + approve_action, + )); + session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedApproveCall { + owner: admin, + spender, + amount: 33, + authorization: approve_auth.clone(), + }, + GAS_LIMIT, + ) + .expect("approve by Phoenix authorization"); + let allowance: u64 = session + .call( + contract, + "allowance", + &Allowance { + owner: admin, + spender, + }, + GAS_LIMIT, + ) + .expect("query signed allowance") + .data; + assert_eq!(allowance, 33); + assert_contract_nonce(session, contract, admin, SIGNED_APPROVE_DOMAIN, 1); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedApproveCall { + owner: admin, + spender, + amount: 33, + authorization: approve_auth, + }, + GAS_LIMIT, + ) + .is_err()); + + let moonlight_owner_sk = moonlight_secret(31); + let moonlight_owner_pk = BlsPublicKey::from(&moonlight_owner_sk); + let moonlight_owner = Principal::moonlight(&moonlight_owner_pk); + let moonlight_action = authorized_action( + contract, + moonlight_owner, + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + 0, + 0, + approve_payload_hash(moonlight_owner, admin, 44), + ); + session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedApproveCall { + owner: moonlight_owner, + spender: admin, + amount: 44, + authorization: SignedAuthorization::Moonlight(moonlight_auth( + &moonlight_owner_sk, + moonlight_owner_pk, + moonlight_action, + )), + }, + GAS_LIMIT, + ) + .expect("approve by Moonlight authorization"); + assert_contract_nonce( + session, + contract, + moonlight_owner, + SIGNED_APPROVE_DOMAIN, + 1, + ); + + let pause_action = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + PAUSE_ACTION, + 1, + 0, + empty_payload_hash(b"drc20.pause"), + ); + session + .call::<_, ()>( + contract, + "pause", + &Drc20AdminCall { + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, pause_action), + )), + }, + GAS_LIMIT, + ) + .expect("pause by signed Phoenix admin"); + let paused: bool = session + .call(contract, "paused", &(), GAS_LIMIT) + .expect("query drc20 paused") + .data; + assert!(paused); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 2); + + let paused_mint = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 2, + 0, + mint_payload_hash(admin, 6), + ), + )); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 6, + authorization: Some(paused_mint), + }, + GAS_LIMIT, + ) + .is_err()); + assert!(session + .call::<_, ()>(contract, "burn", &1u64, GAS_LIMIT) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 2); + + let unpause_action = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + UNPAUSE_ACTION, + 2, + 0, + empty_payload_hash(b"drc20.unpause"), + ); + session + .call::<_, ()>( + contract, + "unpause", + &Drc20AdminCall { + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, unpause_action), + )), + }, + GAS_LIMIT, + ) + .expect("unpause by signed Phoenix admin"); + let paused: bool = session + .call(contract, "paused", &(), GAS_LIMIT) + .expect("query drc20 unpaused") + .data; + assert!(!paused); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 3); +} + +fn exercise_drc721_signed_calls( + session: &mut Session, + contract: ContractId, + admin: Principal, +) { + let mut rng = StdRng::seed_from_u64(4444); + let admin_sk = SchnorrSecretKey::from(JubJubScalar::from(7u64)); + let admin_pk = SchnorrPublicKey::from(&admin_sk); + assert_eq!(admin, Principal::phoenix_public_key(&admin_pk)); + + let mint_action = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_MINT_ACTION, + 0, + 0, + nft_mint_payload_hash(admin, 43), + ); + session + .call::<_, ()>( + contract, + "mint", + &Drc721MintCall { + to: admin, + token_id: 43, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, mint_action), + )), + }, + GAS_LIMIT, + ) + .expect("mint NFT by signed Phoenix owner"); + let owner: Principal = session + .call(contract, "owner_of", &OwnerOf { token_id: 43 }, GAS_LIMIT) + .expect("query signed NFT mint owner") + .data; + assert_eq!(owner, admin); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); + + let pause_action = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_PAUSE_ACTION, + 1, + 0, + empty_payload_hash(b"drc721.pause"), + ); + session + .call::<_, ()>( + contract, + "pause", + &Drc721AdminCall { + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, pause_action), + )), + }, + GAS_LIMIT, + ) + .expect("pause NFT by signed Phoenix owner"); + let paused: bool = session + .call(contract, "paused", &(), GAS_LIMIT) + .expect("query drc721 paused") + .data; + assert!(paused); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 2); + + let paused_mint = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_MINT_ACTION, + 2, + 0, + nft_mint_payload_hash(admin, 44), + ), + )); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc721MintCall { + to: admin, + token_id: 44, + authorization: Some(paused_mint), + }, + GAS_LIMIT, + ) + .is_err()); + assert!(session + .call::<_, ()>(contract, "burn", &43u64, GAS_LIMIT) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 2); + + let unpause_action = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_UNPAUSE_ACTION, + 2, + 0, + empty_payload_hash(b"drc721.unpause"), + ); + session + .call::<_, ()>( + contract, + "unpause", + &Drc721AdminCall { + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, unpause_action), + )), + }, + GAS_LIMIT, + ) + .expect("unpause NFT by signed Phoenix owner"); + let paused: bool = session + .call(contract, "paused", &(), GAS_LIMIT) + .expect("query drc721 unpaused") + .data; + assert!(!paused); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 3); +} + +fn exercise_proxy_signed_admin_call( + session: &mut Session, + contract: ContractId, + admin: Principal, +) { + let mut rng = StdRng::seed_from_u64(3333); + let admin_sk = SchnorrSecretKey::from(JubJubScalar::from(7u64)); + let admin_pk = SchnorrPublicKey::from(&admin_sk); + let action = authorized_action( + contract, + admin, + PROXY_ADMIN_DOMAIN, + SET_PROXY_VALUE_ACTION, + 0, + 0, + proxy_value_payload_hash(7), + ); + let auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, &admin_sk, admin_pk, action, + )); + session + .call::<_, ()>( + contract, + "set_value", + &ProxySetValue { + value: 7, + authorization: Some(auth.clone()), + }, + GAS_LIMIT, + ) + .expect("set proxy value by signed admin"); + let value: u64 = session + .call(contract, "value", &(), GAS_LIMIT) + .expect("query proxy value after signed set") + .data; + assert_eq!(value, 7); + assert_contract_nonce(session, contract, admin, PROXY_ADMIN_DOMAIN, 1); + assert!(session + .call::<_, ()>( + contract, + "set_value", + &ProxySetValue { + value: 7, + authorization: Some(auth), + }, + GAS_LIMIT, + ) + .is_err()); +} + +fn exercise_authorization_counter_signed_calls( + session: &mut Session, + contract: ContractId, +) { + let moonlight_sk = moonlight_secret(7); + let moonlight_pk = BlsPublicKey::from(&moonlight_sk); + let moonlight = Principal::moonlight(&moonlight_pk); + + let moonlight_action = + counter_action(contract, moonlight, 0, 0, SET_VALUE_ACTION, 11); + let moonlight_auth_0 = + moonlight_auth(&moonlight_sk, moonlight_pk, moonlight_action); + session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth_0.clone(), + amount: 11, + }, + GAS_LIMIT, + ) + .expect("set counter by Moonlight signature"); + assert_counter(session, contract, 11, Some(moonlight), moonlight, 1); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth_0, + amount: 11, + }, + GAS_LIMIT, + ) + .is_err()); + assert_counter(session, contract, 11, Some(moonlight), moonlight, 1); + + let bad_payload_action = + counter_action(contract, moonlight, 1, 0, SET_VALUE_ACTION, 12); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth( + &moonlight_sk, + moonlight_pk, + bad_payload_action, + ), + amount: 13, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, moonlight, 1); + + let valid_action = + counter_action(contract, moonlight, 1, 0, SET_VALUE_ACTION, 12); + let wrong_moonlight_sk = moonlight_secret(8); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: MoonlightAuthorization { + action: valid_action, + public_key: moonlight_pk, + signature: wrong_moonlight_sk + .sign(&valid_action.message_bytes()), + }, + amount: 12, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, moonlight, 1); + + let wrong_contract_action = counter_action( + ContractId::from_bytes([99u8; 32]), + moonlight, + 1, + 0, + SET_VALUE_ACTION, + 12, + ); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth( + &moonlight_sk, + moonlight_pk, + wrong_contract_action, + ), + amount: 12, + }, + GAS_LIMIT, + ) + .is_err()); + + let wrong_action = counter_action(contract, moonlight, 1, 0, [9u8; 32], 12); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth( + &moonlight_sk, + moonlight_pk, + wrong_action, + ), + amount: 12, + }, + GAS_LIMIT, + ) + .is_err()); + + session + .set_meta(Metadata::BLOCK_HEIGHT, 10u64) + .expect("set VM block height"); + let expired_action = + counter_action(contract, moonlight, 1, 9, SET_VALUE_ACTION, 12); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth( + &moonlight_sk, + moonlight_pk, + expired_action, + ), + amount: 12, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, moonlight, 1); + + session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth( + &moonlight_sk, + moonlight_pk, + valid_action, + ), + amount: 12, + }, + GAS_LIMIT, + ) + .expect("set counter by second Moonlight signature"); + assert_counter(session, contract, 12, Some(moonlight), moonlight, 2); + + let mut rng = StdRng::seed_from_u64(1234); + let phoenix_sk = SchnorrSecretKey::from(JubJubScalar::from(88u64)); + let phoenix_pk = SchnorrPublicKey::from(&phoenix_sk); + let phoenix = Principal::phoenix_public_key(&phoenix_pk); + + let phoenix_action = + counter_action(contract, phoenix, 0, 0, SET_VALUE_ACTION, 21); + let phoenix_auth_0 = + phoenix_auth(&mut rng, &phoenix_sk, phoenix_pk, phoenix_action); + session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth_0.clone(), + amount: 21, + }, + GAS_LIMIT, + ) + .expect("set counter by Phoenix signature"); + assert_counter(session, contract, 21, Some(phoenix), phoenix, 1); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth_0, + amount: 21, + }, + GAS_LIMIT, + ) + .is_err()); + assert_counter(session, contract, 21, Some(phoenix), phoenix, 1); + + let bad_payload_action = + counter_action(contract, phoenix, 1, 0, SET_VALUE_ACTION, 22); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &phoenix_sk, + phoenix_pk, + bad_payload_action, + ), + amount: 23, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, phoenix, 1); + + let valid_phoenix_action = + counter_action(contract, phoenix, 1, 0, SET_VALUE_ACTION, 22); + let wrong_phoenix_sk = SchnorrSecretKey::from(JubJubScalar::from(89u64)); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: PhoenixSignatureAuthorization { + action: valid_phoenix_action, + public_key: phoenix_pk, + signature: wrong_phoenix_sk + .sign(&mut rng, valid_phoenix_action.message_hash()), + replay_key: None, + }, + amount: 22, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, phoenix, 1); + + let wrong_phoenix_pk = SchnorrPublicKey::from(&wrong_phoenix_sk); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &wrong_phoenix_sk, + wrong_phoenix_pk, + valid_phoenix_action, + ), + amount: 22, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, phoenix, 1); + + let wrong_contract_action = counter_action( + ContractId::from_bytes([98u8; 32]), + phoenix, + 1, + 0, + SET_VALUE_ACTION, + 22, + ); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &phoenix_sk, + phoenix_pk, + wrong_contract_action, + ), + amount: 22, + }, + GAS_LIMIT, + ) + .is_err()); + + let wrong_action = counter_action(contract, phoenix, 1, 0, [8u8; 32], 22); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &phoenix_sk, + phoenix_pk, + wrong_action, + ), + amount: 22, + }, + GAS_LIMIT, + ) + .is_err()); + + let expired_action = + counter_action(contract, phoenix, 1, 9, SET_VALUE_ACTION, 22); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &phoenix_sk, + phoenix_pk, + expired_action, + ), + amount: 22, + }, + GAS_LIMIT, + ) + .is_err()); + assert_nonce(session, contract, phoenix, 1); + + session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &phoenix_sk, + phoenix_pk, + valid_phoenix_action, + ), + amount: 22, + }, + GAS_LIMIT, + ) + .expect("set counter by second Phoenix signature"); + assert_counter(session, contract, 22, Some(phoenix), phoenix, 2); +} + +fn counter_action( + contract: ContractId, + principal: Principal, + nonce: u64, + expires_at: u64, + action_id: [u8; 32], + amount: u64, +) -> AuthorizedAction { + authorized_action( + contract, + principal, + SET_VALUE_DOMAIN, + action_id, + nonce, + expires_at, + amount_hash(amount), + ) +} + +fn authorized_action( + contract: ContractId, + principal: Principal, + domain: NonceDomain, + action_id: [u8; 32], + nonce: u64, + expires_at: u64, + payload_hash: [u8; 32], +) -> AuthorizedAction { + AuthorizedAction { + contract, + domain, + action_id, + nonce, + expires_at, + principal, + payload_hash, + } +} + +fn moonlight_auth( + secret_key: &BlsSecretKey, + public_key: BlsPublicKey, + action: AuthorizedAction, +) -> MoonlightAuthorization { + let signature = secret_key.sign(&action.message_bytes()); + assert!(host_queries::verify_bls( + action.message_bytes(), + public_key, + signature + )); + MoonlightAuthorization { + action, + public_key, + signature, + } +} + +fn moonlight_secret(seed: u64) -> BlsSecretKey { + let mut rng = StdRng::seed_from_u64(seed); + BlsSecretKey::random(&mut rng) +} + +fn phoenix_auth( + rng: &mut StdRng, + secret_key: &SchnorrSecretKey, + public_key: SchnorrPublicKey, + action: AuthorizedAction, +) -> PhoenixSignatureAuthorization { + PhoenixSignatureAuthorization { + action, + public_key, + signature: secret_key.sign(rng, action.message_hash()), + replay_key: None, + } +} + +fn assert_counter( + session: &mut Session, + contract: ContractId, + value: u64, + last_authorizer: Option, + principal: Principal, + nonce: u64, +) { + let actual_value: u64 = session + .call(contract, "value", &(), GAS_LIMIT) + .expect("query authorization counter value") + .data; + assert_eq!(actual_value, value); + let actual_authorizer: Option = session + .call(contract, "last_authorizer", &(), GAS_LIMIT) + .expect("query authorization counter last authorizer") + .data; + assert_eq!(actual_authorizer, last_authorizer); + assert_nonce(session, contract, principal, nonce); +} + +fn assert_nonce( + session: &mut Session, + contract: ContractId, + principal: Principal, + nonce: u64, +) { + assert_contract_nonce( + session, + contract, + principal, + SET_VALUE_DOMAIN, + nonce, + ); +} + +fn assert_contract_nonce( + session: &mut Session, + contract: ContractId, + principal: Principal, + domain: NonceDomain, + nonce: u64, +) { + let actual_nonce: u64 = session + .call( + contract, + "nonce", + &NonceQuery { principal, domain }, + GAS_LIMIT, + ) + .expect("query contract nonce") + .data; + assert_eq!(actual_nonce, nonce); +} + +fn amount_hash(amount: u64) -> [u8; 32] { + host_queries::keccak256(Vec::from(amount.to_be_bytes())) +} + +fn mint_payload_hash(to: Principal, amount: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.mint"[..]); + push_principal(&mut bytes, to); + bytes.extend_from_slice(&amount.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn nft_mint_payload_hash(to: Principal, token_id: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.mint"[..]); + push_principal(&mut bytes, to); + bytes.extend_from_slice(&token_id.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn empty_payload_hash(tag: &[u8]) -> [u8; 32] { + host_queries::keccak256(Vec::from(tag)) +} + +fn approve_payload_hash( + owner: Principal, + spender: Principal, + amount: u64, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc20.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, spender); + bytes.extend_from_slice(&amount.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn proxy_value_payload_hash(value: u64) -> [u8; 32] { + let mut bytes = Vec::from(&b"proxy.value"[..]); + bytes.extend_from_slice(&value.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn push_principal(bytes: &mut Vec, principal: Principal) { + let principal = principal.to_bytes(); + bytes.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&principal); +} + +fn phoenix_principal(seed: u64) -> Principal { + let secret = SchnorrSecretKey::from(JubJubScalar::from(seed)); + let public = SchnorrPublicKey::from(&secret); + Principal::phoenix_public_key(&public) +} + +fn deploy( + session: &mut Session, + wasm_name: &str, + contract_id: [u8; 32], + init: &A, +) -> ContractId +where + A: for<'a> Serialize>, +{ + let id = ContractId::from_bytes(contract_id); + let wasm = fs::read(wasm_path(wasm_name)) + .unwrap_or_else(|err| panic!("read {wasm_name}: {err}")); + session + .deploy( + &wasm, + ContractData::builder() + .owner(OWNER_BYTES) + .contract_id(id) + .init_arg(init), + GAS_LIMIT, + ) + .unwrap_or_else(|err| panic!("deploy {wasm_name}: {err}")) +} + +fn wasm_path(name: &str) -> PathBuf { + workspace_root() + .join("target") + .join("wasm32-unknown-unknown") + .join("release") + .join(name) +} + +fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("workspace root") + .to_path_buf() +} From 5603b96d0302cd2f083057871907d9e64abe6ba4 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sat, 25 Apr 2026 17:11:00 +0200 Subject: [PATCH 08/35] scripts: add standards local smoke validation --- .../dusk-contract-standards-local-smoke.sh | 440 ++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100755 scripts/dusk-contract-standards-local-smoke.sh diff --git a/scripts/dusk-contract-standards-local-smoke.sh b/scripts/dusk-contract-standards-local-smoke.sh new file mode 100755 index 00000000..01423047 --- /dev/null +++ b/scripts/dusk-contract-standards-local-smoke.sh @@ -0,0 +1,440 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +RUSK_URL="${RUSK_URL:-http://localhost:8080}" +RUSK_WALLET_BIN="${RUSK_WALLET_BIN:-$(command -v rusk-wallet || true)}" +WALLET_DIR="${WALLET_DIR:-${ROOT_DIR}/target/dusk-contract-standards-wallet}" +WALLET_PASSWORD="${WALLET_PASSWORD:-password}" +DEPLOY_NONCE_BASE="${DEPLOY_NONCE_BASE:-100}" + +cd "$ROOT_DIR" + +echo "Running standards unit tests" +cargo test -p dusk-contract-standards + +echo "Building example contracts" +cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ + -p authorization-counter \ + -p drc20-roles-pausable \ + -p drc721-collection \ + -p proxy-counter \ + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,proxy-counter/contract + +echo "Building Forge data-drivers" +CARGO_TARGET_DIR="${ROOT_DIR}/target/data-driver" \ +cargo build --release --target wasm32-unknown-unknown \ + -p authorization-counter \ + -p drc20-roles-pausable \ + -p drc721-collection \ + -p proxy-counter \ + --features authorization-counter/data-driver-js,drc20-roles-pausable/data-driver-js,drc721-collection/data-driver-js,proxy-counter/data-driver-js + +echo "Running VM example deployment tests" +cargo test -p dusk-contract-standards --test examples_vm -- --ignored + +if [[ -z "$RUSK_WALLET_BIN" ]]; then + cat >&2 <<'MSG' +rusk-wallet was not found. + +Set RUSK_WALLET_BIN=/path/to/rusk-wallet and RUSK_URL=http://host:port for a running local node, +then rerun this script to deploy the standards examples. +MSG + exit 2 +fi + +if ! curl -fsS \ + -H 'Content-Type: text/plain' \ + --data '{ block(height: -1) { header { height } } }' \ + "${RUSK_URL}/on/graphql/query" >/dev/null; then + cat >&2 </dev/null 2>&1; then + echo "xxd was not found; it is required to submit binary query arguments." >&2 + exit 2 +fi + +mkdir -p "$WALLET_DIR" + +encode_args() { + cargo run -q -p dusk-contract-standards --example encode_local_smoke_args -- "$@" +} + +deploy_contract() { + local name="$1" + local wasm="$2" + local nonce="$3" + local init_args="$4" + local output + local contract_id + local status + + echo "Deploying ${name}" >&2 + set +e + output="$(RUSK_WALLET_PWD="$WALLET_PASSWORD" "$RUSK_WALLET_BIN" \ + --wallet-dir "$WALLET_DIR" \ + --state "$RUSK_URL" \ + --prover "$RUSK_URL" \ + --password "$WALLET_PASSWORD" \ + contract-deploy \ + --code "$wasm" \ + --deploy-nonce "$nonce" \ + --init-args "$init_args" 2>&1)" + status=$? + set -e + printf '%s\n' "$output" >&2 + if [[ "$status" -ne 0 ]]; then + echo "Deployment failed for ${name}" >&2 + exit "$status" + fi + + contract_id="$( + printf '%s\n' "$output" | + awk ' + /Deploying/ { + for (i = 1; i <= NF; i++) { + token = $i + gsub(/[^0-9a-fA-F]/, "", token) + if (length(token) == 64) { + print tolower(token) + exit + } + } + } + ' + )" + + if [[ -z "$contract_id" ]]; then + echo "Unable to parse deployed contract id for ${name}" >&2 + exit 1 + fi + + echo "$contract_id" +} + +query_contract() { + local name="$1" + local contract_id="$2" + local fn_name="$3" + local args_hex="$4" + local tmp_dir + local bytes + + tmp_dir="$(mktemp -d)" + printf '%s' "$args_hex" | xxd -r -p >"${tmp_dir}/args.bin" + curl -fsS \ + -H 'Content-Type: application/octet-stream' \ + --data-binary @"${tmp_dir}/args.bin" \ + "${RUSK_URL}/on/contracts:${contract_id}/${fn_name}" \ + -o "${tmp_dir}/response.bin" + bytes="$(wc -c <"${tmp_dir}/response.bin")" + rm -rf "$tmp_dir" + + if [[ "$bytes" -eq 0 ]]; then + echo "${name}.${fn_name} returned an empty response" >&2 + exit 1 + fi + + echo "Queried ${name}.${fn_name} (${bytes} bytes)" +} + +query_hex() { + local contract_id="$1" + local fn_name="$2" + local args_hex="$3" + local tmp_dir + + tmp_dir="$(mktemp -d)" + printf '%s' "$args_hex" | xxd -r -p >"${tmp_dir}/args.bin" + curl -fsS \ + -H 'Content-Type: application/octet-stream' \ + --data-binary @"${tmp_dir}/args.bin" \ + "${RUSK_URL}/on/contracts:${contract_id}/${fn_name}" \ + -o "${tmp_dir}/response.bin" + xxd -p -c 256 "${tmp_dir}/response.bin" + rm -rf "$tmp_dir" +} + +query_u64() { + local contract_id="$1" + local fn_name="$2" + local args_hex="$3" + + printf '%s' "$(query_hex "$contract_id" "$fn_name" "$args_hex")" | + xxd -r -p | + od -An -t u8 | + tr -d ' \n' +} + +query_bool() { + local contract_id="$1" + local fn_name="$2" + local args_hex="$3" + local value + + value="$(query_hex "$contract_id" "$fn_name" "$args_hex")" + case "$value" in + 00) echo "false" ;; + 01) echo "true" ;; + *) + echo "${contract_id}.${fn_name} returned invalid bool hex: ${value}" >&2 + exit 1 + ;; + esac +} + +assert_eq() { + local label="$1" + local actual="$2" + local expected="$3" + + if [[ "$actual" != "$expected" ]]; then + echo "Assertion failed for ${label}: got ${actual}, expected ${expected}" >&2 + exit 1 + fi + + echo "Asserted ${label} = ${expected}" +} + +call_contract() { + local contract_id="$1" + local fn_name="$2" + local args_hex="$3" + local output_file="$4" + local status + + set +e + RUSK_WALLET_PWD="$WALLET_PASSWORD" "$RUSK_WALLET_BIN" \ + --wallet-dir "$WALLET_DIR" \ + --state "$RUSK_URL" \ + --prover "$RUSK_URL" \ + --password "$WALLET_PASSWORD" \ + contract-call \ + --contract-id "$contract_id" \ + --fn-name "$fn_name" \ + --fn-args "$args_hex" >"$output_file" 2>&1 + status=$? + set -e + + return "$status" +} + +expect_call_ok() { + local label="$1" + local contract_id="$2" + local fn_name="$3" + local args_hex="$4" + local output_file + + output_file="$(mktemp)" + if ! call_contract "$contract_id" "$fn_name" "$args_hex" "$output_file"; then + echo "Expected ${label} to succeed, but wallet command failed" >&2 + cat "$output_file" >&2 + rm -f "$output_file" + exit 1 + fi + if grep -q 'Transaction error:' "$output_file"; then + echo "Expected ${label} to succeed, but transaction reverted" >&2 + cat "$output_file" >&2 + rm -f "$output_file" + exit 1 + fi + rm -f "$output_file" + echo "Accepted ${label}" +} + +expect_call_rejected() { + local label="$1" + local contract_id="$2" + local fn_name="$3" + local args_hex="$4" + local output_file + + output_file="$(mktemp)" + if call_contract "$contract_id" "$fn_name" "$args_hex" "$output_file"; then + if ! grep -q 'Transaction error:' "$output_file"; then + echo "Expected ${label} to be rejected, but it succeeded" >&2 + cat "$output_file" >&2 + rm -f "$output_file" + exit 1 + fi + fi + rm -f "$output_file" + echo "Rejected ${label}" +} + +run_signed_invariants() { + local auth_id="$1" + local drc20_id="$2" + local drc721_id="$3" + local proxy_id="$4" + local expires_at="${SIGNED_EXPIRES_AT:-100000}" + local unit_args + local phoenix_call + local moonlight_call + local proxy_call + + unit_args="$(encode_args unit)" + + assert_eq "authorization counter initial value" \ + "$(query_u64 "$auth_id" value "$unit_args")" 0 + assert_eq "DRC20 initial supply" \ + "$(query_u64 "$drc20_id" total_supply "$unit_args")" 1000 + assert_eq "DRC721 initial supply" \ + "$(query_u64 "$drc721_id" total_supply "$unit_args")" 1 + assert_eq "proxy initial value" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 0 + + phoenix_call="$(encode_args auth-counter-phoenix "$auth_id" 0 "$expires_at" 21)" + expect_call_ok "Phoenix counter set" "$auth_id" set_value_by_phoenix "$phoenix_call" + assert_eq "Phoenix counter value" \ + "$(query_u64 "$auth_id" value "$unit_args")" 21 + assert_eq "Phoenix counter nonce after success" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter phoenix 88)")" 1 + expect_call_rejected "Phoenix counter replay" "$auth_id" set_value_by_phoenix "$phoenix_call" + assert_eq "Phoenix counter nonce after replay" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter phoenix 88)")" 1 + expect_call_rejected "Phoenix counter bad payload" \ + "$auth_id" set_value_by_phoenix \ + "$(encode_args auth-counter-phoenix "$auth_id" 1 "$expires_at" 22 bad-payload)" + assert_eq "Phoenix counter nonce after bad payload" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter phoenix 88)")" 1 + expect_call_rejected "Phoenix counter expired action" \ + "$auth_id" set_value_by_phoenix \ + "$(encode_args auth-counter-phoenix "$auth_id" 1 1 22)" + assert_eq "Phoenix counter nonce after expired action" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter phoenix 88)")" 1 + expect_call_ok "Phoenix counter second set" \ + "$auth_id" set_value_by_phoenix \ + "$(encode_args auth-counter-phoenix "$auth_id" 1 "$expires_at" 22)" + assert_eq "Phoenix counter nonce after second success" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter phoenix 88)")" 2 + + moonlight_call="$(encode_args auth-counter-moonlight "$auth_id" 0 "$expires_at" 31)" + expect_call_ok "Moonlight counter set" "$auth_id" set_value_by_moonlight "$moonlight_call" + assert_eq "Moonlight counter value" \ + "$(query_u64 "$auth_id" value "$unit_args")" 31 + assert_eq "Moonlight counter nonce after success" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter moonlight 7)")" 1 + expect_call_rejected "Moonlight counter replay" "$auth_id" set_value_by_moonlight "$moonlight_call" + assert_eq "Moonlight counter nonce after replay" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter moonlight 7)")" 1 + expect_call_rejected "Moonlight counter wrong action" \ + "$auth_id" set_value_by_moonlight \ + "$(encode_args auth-counter-moonlight "$auth_id" 1 "$expires_at" 32 wrong-action)" + assert_eq "Moonlight counter nonce after wrong action" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter moonlight 7)")" 1 + expect_call_ok "Moonlight counter second set" \ + "$auth_id" set_value_by_moonlight \ + "$(encode_args auth-counter-moonlight "$auth_id" 1 "$expires_at" 32)" + assert_eq "Moonlight counter nonce after second success" \ + "$(query_u64 "$auth_id" nonce "$(encode_args nonce counter moonlight 7)")" 2 + + expect_call_ok "DRC20 signed mint" \ + "$drc20_id" mint "$(encode_args drc20-mint "$drc20_id" 0 "$expires_at" 5)" + assert_eq "DRC20 supply after mint" \ + "$(query_u64 "$drc20_id" total_supply "$unit_args")" 1005 + assert_eq "DRC20 admin nonce after mint" \ + "$(query_u64 "$drc20_id" nonce "$(encode_args nonce drc20-admin phoenix 1)")" 1 + expect_call_ok "DRC20 signed pause" \ + "$drc20_id" pause "$(encode_args drc20-admin pause "$drc20_id" 1 "$expires_at")" + assert_eq "DRC20 paused" \ + "$(query_bool "$drc20_id" paused "$unit_args")" true + assert_eq "DRC20 admin nonce after pause" \ + "$(query_u64 "$drc20_id" nonce "$(encode_args nonce drc20-admin phoenix 1)")" 2 + expect_call_rejected "DRC20 paused mint" \ + "$drc20_id" mint "$(encode_args drc20-mint "$drc20_id" 2 "$expires_at" 6)" + assert_eq "DRC20 supply after paused mint" \ + "$(query_u64 "$drc20_id" total_supply "$unit_args")" 1005 + assert_eq "DRC20 admin nonce after paused mint" \ + "$(query_u64 "$drc20_id" nonce "$(encode_args nonce drc20-admin phoenix 1)")" 2 + expect_call_rejected "DRC20 paused burn" \ + "$drc20_id" burn "$(encode_args u64 1)" + expect_call_ok "DRC20 signed unpause" \ + "$drc20_id" unpause "$(encode_args drc20-admin unpause "$drc20_id" 2 "$expires_at")" + assert_eq "DRC20 unpaused" \ + "$(query_bool "$drc20_id" paused "$unit_args")" false + assert_eq "DRC20 admin nonce after unpause" \ + "$(query_u64 "$drc20_id" nonce "$(encode_args nonce drc20-admin phoenix 1)")" 3 + + expect_call_ok "DRC721 signed mint" \ + "$drc721_id" mint "$(encode_args drc721-mint "$drc721_id" 0 "$expires_at" 2)" + assert_eq "DRC721 supply after mint" \ + "$(query_u64 "$drc721_id" total_supply "$unit_args")" 2 + assert_eq "DRC721 admin nonce after mint" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-admin phoenix 1)")" 1 + expect_call_ok "DRC721 signed pause" \ + "$drc721_id" pause "$(encode_args drc721-admin pause "$drc721_id" 1 "$expires_at")" + assert_eq "DRC721 paused" \ + "$(query_bool "$drc721_id" paused "$unit_args")" true + assert_eq "DRC721 admin nonce after pause" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-admin phoenix 1)")" 2 + expect_call_rejected "DRC721 paused mint" \ + "$drc721_id" mint "$(encode_args drc721-mint "$drc721_id" 2 "$expires_at" 3)" + assert_eq "DRC721 supply after paused mint" \ + "$(query_u64 "$drc721_id" total_supply "$unit_args")" 2 + assert_eq "DRC721 admin nonce after paused mint" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-admin phoenix 1)")" 2 + expect_call_rejected "DRC721 paused burn" \ + "$drc721_id" burn "$(encode_args u64 2)" + expect_call_ok "DRC721 signed unpause" \ + "$drc721_id" unpause "$(encode_args drc721-admin unpause "$drc721_id" 2 "$expires_at")" + assert_eq "DRC721 unpaused" \ + "$(query_bool "$drc721_id" paused "$unit_args")" false + assert_eq "DRC721 admin nonce after unpause" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-admin phoenix 1)")" 3 + + proxy_call="$(encode_args proxy-set "$proxy_id" 0 "$expires_at" 7)" + expect_call_ok "proxy signed set value" "$proxy_id" set_value "$proxy_call" + assert_eq "proxy value after signed set" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 7 + assert_eq "proxy admin nonce after signed set" \ + "$(query_u64 "$proxy_id" nonce "$(encode_args nonce proxy-admin phoenix 1)")" 1 + expect_call_rejected "proxy replay" "$proxy_id" set_value "$proxy_call" + assert_eq "proxy value after replay" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 7 + assert_eq "proxy admin nonce after replay" \ + "$(query_u64 "$proxy_id" nonce "$(encode_args nonce proxy-admin phoenix 1)")" 1 + + echo "Local signed invariant checks completed" +} + +unit_args="$(encode_args unit)" + +auth_id="$(deploy_contract \ + "authorization counter" \ + "${ROOT_DIR}/target/wasm32-unknown-unknown/release/authorization_counter.wasm" \ + "$((DEPLOY_NONCE_BASE + 0))" \ + "$unit_args")" +query_contract "authorization counter" "$auth_id" "value" "$unit_args" + +drc20_id="$(deploy_contract \ + "DRC20 roles/pausable" \ + "${ROOT_DIR}/target/wasm32-unknown-unknown/release/drc20_roles_pausable.wasm" \ + "$((DEPLOY_NONCE_BASE + 1))" \ + "$(encode_args drc20-init)")" +query_contract "DRC20 roles/pausable" "$drc20_id" "total_supply" "$unit_args" + +drc721_id="$(deploy_contract \ + "DRC721 collection" \ + "${ROOT_DIR}/target/wasm32-unknown-unknown/release/drc721_collection.wasm" \ + "$((DEPLOY_NONCE_BASE + 2))" \ + "$(encode_args drc721-init)")" +query_contract "DRC721 collection" "$drc721_id" "total_supply" "$unit_args" + +proxy_id="$(deploy_contract \ + "proxy counter" \ + "${ROOT_DIR}/target/wasm32-unknown-unknown/release/proxy_counter.wasm" \ + "$((DEPLOY_NONCE_BASE + 3))" \ + "$(encode_args proxy-init)")" +query_contract "proxy counter" "$proxy_id" "value" "$unit_args" + +run_signed_invariants "$auth_id" "$drc20_id" "$drc721_id" "$proxy_id" + +echo "Local standards smoke completed" From e5026d58de8af0454a8892a0af790e7d4dde8117 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sat, 25 Apr 2026 17:11:06 +0200 Subject: [PATCH 09/35] docs: document Dusk contract standards --- README.md | 15 ++ docs/dusk-contract-standards-security.md | 59 ++++++ docs/dusk-contract-standards.md | 247 +++++++++++++++++++++++ 3 files changed, 321 insertions(+) create mode 100644 docs/dusk-contract-standards-security.md create mode 100644 docs/dusk-contract-standards.md diff --git a/README.md b/README.md index f85cb2c9..318683b9 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ This repository contains smart contracts for the Dusk ecosystem: - Genesis protocol contracts: part of the genesis state and provide core functionality to the Dusk protocol. +- Contract standards: reusable Dusk-native primitives and examples for access + control, tokens, replay and nonce protection, timelocks, pausing, reentrancy + guards, and upgrade policy. - Test contracts: small contracts used for integration tests and host function coverage. @@ -45,3 +48,15 @@ The on-chain ContractId for the stake contract is: on behalf of a contract via the transfer and stake contracts. - [`host_fn`](tests/host_fn): wraps host functions (hashing, signature/proof verification, chain metadata, etc.) for testing. + +## Contract Standards + +- [`dusk-contract-standards`](standards/dusk-contract-standards): reusable + primitives. +- [`standards/examples`](standards/examples): authorization, DRC20, DRC721, + and proxy-style compositions built from those primitives. + +See [docs/dusk-contract-standards.md](docs/dusk-contract-standards.md) for the +design notes and validation commands, and +[docs/dusk-contract-standards-security.md](docs/dusk-contract-standards-security.md) +for the security model. diff --git a/docs/dusk-contract-standards-security.md b/docs/dusk-contract-standards-security.md new file mode 100644 index 00000000..ef688b77 --- /dev/null +++ b/docs/dusk-contract-standards-security.md @@ -0,0 +1,59 @@ +# Dusk Contract Standards Security Notes + +## Replay Domains + +Every signed action must bind `contract`, `domain`, `action_id`, `nonce`, +`principal`, and `payload_hash`. Use separate nonce domains for unrelated +flows: token approvals, role admin, proxy upgrades, voting, and application +actions should not share a nonce stream unless that coupling is deliberate. + +## Expiry and Nonces + +Nonces stop replay after use. Expiry limits how long an unused signature can +float around in clients, relayers, or mempools. Reference client examples use +finite expiry. Use `expires_at = 0` only for offline workflows where a +permanently valid unused signature is acceptable. + +## Phoenix Authorization + +Phoenix does not provide a clean runtime caller identity. Treat Phoenix admin +or owner actions as explicit Schnorr authorizations over an `AuthorizedAction`. +This proves control of the Phoenix signing key represented by the principal; it +does not prove that a specific Phoenix note exists or was spent. + +## Moonlight and Contract Callers + +Moonlight root calls and inter-contract calls can use `CallContext::current()`. +Signed Moonlight authorization is still useful for relayed calls or workflows +where the submitter is not the owner. Contract principals should be accepted +only from observed inter-contract caller context. + +## Payload Binding + +Do not verify a signature without first checking that the action envelope +matches the call being submitted. Public methods should prefer the action-bound +helpers (`authorize_signed_action`, `authorize_owner_action`, +`authorize_role_action`, and `authorize_admin_action`) so wrong contract, wrong +domain, wrong action id, and wrong payload hash are rejected before nonce state +is consumed. + +## Pausable References + +The reference pausable DRC20 and DRC721 contracts pause all balance-changing +operations: transfers, minting, and burning. Approvals remain available while +paused so accounts can prepare permissions without moving balances or ownership. + +## Reentrancy and Call Stack + +Use `ReentrancyGuard` around state-changing flows that perform external calls +or may later grow such calls. Dusk call-stack behavior is not EVM delegatecall; +avoid importing EVM proxy or reentrancy assumptions directly. + +## Upgrades and Timelocks + +Upgrade admin signatures should bind the exact target implementation and +migration payload. For production systems, put upgrade preparation or +activation behind a timelock, keep a rollback window, and emit lifecycle events +for indexers and wallets. Timelock controller maintenance, including minimum +delay changes, should go through the timelock itself rather than a direct admin +call. diff --git a/docs/dusk-contract-standards.md b/docs/dusk-contract-standards.md new file mode 100644 index 00000000..c11b84b2 --- /dev/null +++ b/docs/dusk-contract-standards.md @@ -0,0 +1,247 @@ +# Dusk Contract Standards + +This branch adds Dusk-native reusable contract standards rather than a semantic +copy of an EVM library. The goal is to provide secure building blocks that fit +the Dusk execution model, storage model, and client model. + +## Layout + +- `standards/dusk-contract-standards`: reusable primitives, native tests, and + ignored VM tests for the Wasm examples. +- `standards/examples/authorization_counter`: a Forge counter controlled by + replay-protected Moonlight BLS or Phoenix Schnorr signatures. +- `standards/examples/drc20_roles_pausable`: a Forge fungible-token + reference with roles, pause control, cap policy, vote checkpoints, signed + approvals, events, and reentrancy protection. +- `standards/examples/drc721_collection`: a Forge collection composition with + owner control, pause control, approvals, royalties, events, and token + metadata. +- `standards/examples/proxy_counter`: a Forge upgrade-admin and state-store + example showing how proxy-like upgrade policy can be modeled without + pretending Dusk has EVM delegatecall semantics. + +## Design Notes + +Dusk identity is not a single address type. The shared `Principal` abstraction +distinguishes Moonlight public accounts, Phoenix authorization identities, and +contracts. `OwnerSet` lets a composing contract accept a mixed ownership policy +instead of forcing everything into an Ethereum-style address. Contracts can use +`CallContext::current()` for runtime Moonlight and inter-contract calls, while +Phoenix flows should pass an explicit authorization identity plus a nonce or +replay key when the application needs that model. + +`NonceManager` is intentionally domain-separated by `(principal, domain)`. This +lets one contract keep independent monotonic streams for permits, Phoenix +signature authorizations, voting, role delegation, or upgrade approvals. + +`AuthorizationManager` builds on those nonces. Moonlight authorization verifies +BLS signatures over stable action bytes and consumes the matching +principal/domain nonce. Phoenix authorization verifies a Schnorr signature from +the Phoenix public key represented by the principal, then consumes the nonce and +optional replay key. This proves control of the Phoenix signing key without +pretending that Phoenix exposes an address-like runtime caller. + +`SignedAuthorization` is the ABI-facing wrapper for explicit authorizations, +`ActionEnvelope` is the expected contract/domain/action/payload binding, and +`Authorizer` is the reusable per-call adapter. `Ownable`, `OwnerSet`, +`AccessControl`, and `UpgradeAdmin` expose action-bound helper methods that +accept runtime Moonlight/contract callers when available and otherwise verify +the exact signed envelope before consuming nonce/replay state. This keeps +Phoenix out of the `msg.sender` model while still giving contracts one ergonomic +admin path. + +The token modules keep core accounting in reusable Rust state machines. Access +control, pausing, and event emission live in the composing contracts, which is a +better fit for Dusk because call context, host functions, and exposed ABI shape +are contract-specific. + +The examples are Forge contracts. Their ABI argument structs live at crate scope +for schema generation, while the state and exported methods live inside a +`#[dusk_forge::contract]` module. Forge generates the schema, contract `STATE`, +and exported wrappers; the examples do not hand-roll `#[no_mangle]` ABI +functions. + +DRC20 includes optional supply-cap, burnable, pausable, signed approval, and +voting-unit checkpoint patterns. DRC721 includes enumerable queries, burnable, +pausable, and default/token-specific royalty patterns. The reference pausable +tokens pause all balance-changing operations: transfers, minting, and burning. +Approvals remain available while paused. These remain composing contract choices +rather than mandatory behavior baked into every token. + +Proxy support is expressed as an upgrade admin state machine and a namespaced +state store. The example records the active implementation id, delay, migration +payload, rollback window, and emits upgrade lifecycle events. It deliberately +does not expose an EVM-compatible proxy ABI. + +`Timelock` is a standalone scheduling primitive for upgrade/governance flows, +and `TimelockController` composes it with Dusk-native roles for proposer, +executor, canceller, and admin policies. Controller maintenance that changes +the minimum delay is self-governed: it must be scheduled and executed through +the timelock path rather than performed directly by an administrator. + +## Client Signing Flow + +Clients sign an `AuthorizedAction` for the exact contract action they want to +execute. The action includes: + +- `contract`: the target `ContractId`; +- `domain`: a 32-byte nonce stream chosen by the contract; +- `action_id`: a 32-byte id for the exported operation; +- `nonce`: the current nonce for `(principal, domain)`; +- `expires_at`: a contract-defined block/time deadline, or zero for an explicit + permanent unused signature; +- `principal`: the Moonlight or Phoenix principal being authorized; +- `payload_hash`: a contract-defined hash/commitment of the call payload. + +The composing contract defines `payload_hash`. The authorization counter hashes +the `u64` amount with `abi::keccak256(amount.to_be_bytes())`, then checks that +the signed action payload matches the submitted amount before verifying the +signature. Other contracts should use the same pattern: hash only the payload +fields that the signed action is meant to bind, and reject the call before +signature verification if the submitted payload does not match. + +The DRC20 reference exposes `approve_by_authorization`. Its payload hash is: + +1. the ASCII domain tag `drc20.approve`; +2. owner principal length and bytes from `Principal::to_bytes()`; +3. spender principal length and bytes; +4. the big-endian `u64` amount; +5. `keccak256` over the concatenated bytes. + +Admin references follow the same rule. Signed mint, role, royalty, and proxy +calls validate `contract`, `domain`, `action_id`, and `payload_hash` before +consuming the signature nonce. + +Use stable constants for `domain` and `action_id`. `domain` separates nonce +streams such as permits, role-admin actions, upgrade approvals, and voting. +`action_id` separates operations inside the same stream. Reusing a domain is +fine only when a single monotonic nonce stream is intentional. + +Moonlight signs `AuthorizedAction::message_bytes()` with BLS and submits +`SignedAuthorization::Moonlight`. Phoenix signs +`AuthorizedAction::message_hash()` with Schnorr and submits +`SignedAuthorization::Phoenix`. Public contract methods should use the +action-bound helpers such as `authorize_signed_action`, +`authorize_owner_action`, `authorize_role_action`, or `authorize_admin_action` +so the envelope is checked before nonce state is consumed. The Phoenix +signature proves control of the Phoenix signing key represented by +`Principal::Phoenix`; it does not prove that a specific note exists or was +spent. + +See +`standards/dusk-contract-standards/examples/build_signed_authorizations.rs` for +a compact client-side Rust example that builds the DRC20 signed-approval +payload hash, constructs `AuthorizedAction`, and signs it with Moonlight BLS +and Phoenix Schnorr keys. + +Nonce protection rejects replay after a signed action is consumed. Expiry is +optional but recommended for relayed or delayed execution, because an unused +signature with only a nonce can remain valid until that nonce is consumed by +some other action. The signing example uses a finite expiry by default; +`expires_at = 0` remains available for explicit offline workflows that need a +permanent unused authorization. + +## Forge Data-Drivers + +Each reference contract now has two Forge WASM targets: + +- contract WASM: on-chain, built with the `contract` feature and + `-Z build-std=core,alloc`; +- data-driver WASM: off-chain JSON/rkyv conversion, built with + `data-driver-js` into `target/data-driver`. + +Build all reference data-drivers with: + +```sh +CARGO_TARGET_DIR=target/data-driver \ +cargo build --release --target wasm32-unknown-unknown \ + -p authorization-counter \ + -p drc20-roles-pausable \ + -p drc721-collection \ + -p proxy-counter \ + --features authorization-counter/data-driver-js,drc20-roles-pausable/data-driver-js,drc721-collection/data-driver-js,proxy-counter/data-driver-js +``` + +The generated data-drivers export `init`, `get_schema`, `encode_input_fn`, +`decode_output_fn`, and `decode_event`. The example Makefiles expose this as +`make wasm-dd`, and the top-level `make standards-data-drivers` builds all four. + +## Validation + +The native test suite covers positive and negative paths for: + +- ownership and two-step ownership; +- mixed Moonlight/Phoenix/contract owner sets; +- role grants and revokes; +- replay protection; +- domain-separated nonces; +- Moonlight BLS authorization; +- Phoenix Schnorr authorization with replay keys; +- action-bound authorization helpers that reject wrong envelopes before nonce + movement; +- observed-or-signed owner, role, and upgrade-admin authorization; +- timelock scheduling, cancellation, execution, and invalid states; +- role-gated timelock controller flows, including self-governed delay updates; +- reentrancy guard behavior; +- pausing; +- DRC20 transfer, allowance changes, mint, burn, initialization, and failure + cases; +- DRC20 supply cap and voting-unit checkpoints; +- DRC721 approval, enumeration, operator transfer, mint, burn, initialization, + and failure cases; +- DRC721 royalty policy; +- upgrade preparation, activation delay, cancellation, rollback, finalization, + events, and namespaced proxy state. + +The ignored VM test deploys all four Wasm examples, checks positive query +paths, performs real Moonlight and Phoenix signed calls against the +authorization counter, covers replay/wrong-payload/wrong-signature/wrong-target +failures, submits signed DRC20 mint and signed DRC20 approvals, verifies paused +DRC20/DRC721 signed mint rejection without nonce movement, submits signed +DRC721 owner actions, submits a signed proxy admin call, covers +replay/wrong-payload failures for those reference flows, and calls privileged +functions without a runtime caller to validate the negative authorization path +at the VM boundary. + +Run the focused suite with: + +```sh +cargo test -p dusk-contract-standards +``` + +Build the example Wasm contracts with: + +```sh +cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ + -p authorization-counter \ + -p drc20-roles-pausable \ + -p drc721-collection \ + -p proxy-counter \ + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,proxy-counter/contract +``` + +After the Wasm build, run the VM deployment/query test with: + +```sh +cargo test -p dusk-contract-standards --test examples_vm -- --ignored +``` + +Build the Forge data-driver WASM with: + +```sh +make standards-data-drivers +``` + +For a local-node smoke deployment against a running Rusk endpoint: + +```sh +RUSK_URL=http://localhost:8080 \ +RUSK_WALLET_BIN=/path/to/rusk-wallet \ +./scripts/dusk-contract-standards-local-smoke.sh +``` + +The script first runs the native suite, builds Wasm, runs the VM deployment +test, serializes deploy-time init arguments using the Dusk ABI serializer, +deploys the four example contracts with `rusk-wallet`, and queries one +exported function from each deployment. `WALLET_DIR` should point at a funded +local wallet profile. From 9a546d629d083b547dc3a7eda29dfebcca30056f Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sat, 25 Apr 2026 17:11:14 +0200 Subject: [PATCH 10/35] workspace: wire standards to local Aegis crates --- Cargo.lock | 605 ++++++++++++++++++++++++++++++++++---------- Cargo.toml | 31 ++- Makefile | 8 +- rust-toolchain.toml | 2 +- 4 files changed, 510 insertions(+), 136 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d4dbc29f..eea0b687 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler" @@ -118,6 +118,9 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "ark-bn254" @@ -144,8 +147,8 @@ dependencies = [ "ark-std", "blake2", "derivative", - "digest", - "sha2", + "digest 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -176,7 +179,7 @@ dependencies = [ "ark-serialize", "ark-std", "derivative", - "digest", + "digest 0.10.7", "itertools 0.10.5", "num-bigint", "num-traits", @@ -255,7 +258,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ "ark-serialize-derive", "ark-std", - "digest", + "digest 0.10.7", "num-bigint", ] @@ -289,7 +292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -304,12 +307,32 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "authorization-counter" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bincode" version = "1.3.3" @@ -319,6 +342,22 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -343,7 +382,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -370,6 +409,15 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -381,31 +429,31 @@ dependencies = [ [[package]] name = "bls12_381-bls" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38440ec581131c35809ddb8554f63ede8a051d238cfe974ff21c5aa6ee47a36" +checksum = "1c61c05b7bfa0cff7b7f6bf000589de5541fc03b702adadfbcfbb6d782a2d643" dependencies = [ + "bs58", "bytecheck", - "dusk-bls12_381 0.13.0", + "dusk-bls12_381", "dusk-bytes", "ff", - "rand_core", + "rand_core 0.6.4", "rkyv", + "serde", + "sha2 0.9.9", "zeroize", ] [[package]] -name = "bls12_381-bls" -version = "0.5.1" +name = "blst" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6b7c24701ff5167f336478ec29dc176455f5396f62b8f0951cc1fdddf249b" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" dependencies = [ - "bytecheck", - "dusk-bls12_381 0.14.2", - "dusk-bytes", - "ff", - "rand_core", - "rkyv", + "cc", + "glob", + "threadpool", "zeroize", ] @@ -419,6 +467,12 @@ dependencies = [ "rkyv", ] +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + [[package]] name = "bumpalo" version = "3.19.1" @@ -453,6 +507,22 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "c-kzg" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16507ccb84a57d44c40cf9b8e6741eb560bc44122b325dcff2fdbfa4959b2110" +dependencies = [ + "arbitrary", + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] + [[package]] name = "cast" version = "0.3.0" @@ -482,6 +552,16 @@ dependencies = [ "rkyv", ] +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "num-traits", + "serde", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -763,9 +843,9 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crumbles" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e660ac9c1d7549c850b4cbf31c06bd7e452879b55296554d5c85e25a26fefe" +checksum = "6f3476b7dda7a457e938e4b958aa7b8db66eb3f6bc72894454a86f99bab3dea2" dependencies = [ "libc", "rangemap", @@ -784,7 +864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -797,6 +877,15 @@ dependencies = [ "cipher", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "derivative" version = "2.2.0" @@ -819,13 +908,33 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "crypto-common", "subtle", ] @@ -862,21 +971,31 @@ dependencies = [ ] [[package]] -name = "dusk-bls12_381" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2bd68f9bed48c2e64860e1fd1f5dbb7733eb07d0288fbdcc3763678c742139" +name = "drc20-roles-pausable" +version = "0.1.0" dependencies = [ - "blake2b_simd", "bytecheck", - "dusk-bytes", - "ff", - "group", - "pairing", - "rand_core", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", "rkyv", - "subtle", - "zeroize", + "serde", + "serde_json", +] + +[[package]] +name = "drc721-collection" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", ] [[package]] @@ -887,12 +1006,15 @@ checksum = "633aa835dd9e4db7ad5dc94a3f95d44ff5c6856ec3c83c15eb75366cfb5ee68a" dependencies = [ "blake2b_simd", "bytecheck", + "digest 0.9.0", "dusk-bytes", "ff", "group", + "hex", "pairing", - "rand_core", + "rand_core 0.6.4", "rkyv", + "serde", "subtle", "zeroize", ] @@ -906,19 +1028,33 @@ dependencies = [ "derive-hex", ] +[[package]] +name = "dusk-contract-standards" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-core", + "dusk-vm", + "rand 0.8.5", + "rkyv", + "serde", + "serde_with", + "time", +] + [[package]] name = "dusk-core" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7972fd42fb3fe67ea5492909f7715f8fe373af98a02943b7e24b5262fbd4678a" +version = "1.6.0" dependencies = [ "ark-bn254", "ark-groth16", "ark-relations", "ark-serialize", - "bls12_381-bls 0.5.1", + "blake2b_simd", + "bls12_381-bls", "bytecheck", - "dusk-bls12_381 0.14.2", + "c-kzg", + "dusk-bls12_381", "dusk-bytes", "dusk-jubjub", "dusk-plonk", @@ -929,26 +1065,58 @@ dependencies = [ "phoenix-core", "piecrust-uplink", "poseidon-merkle", - "rand", + "rand 0.8.5", "rkyv", - "sha2", + "serde", + "serde_with", + "sha2 0.10.8", +] + +[[package]] +name = "dusk-data-driver" +version = "0.3.2-alpha.1" +dependencies = [ + "bytecheck", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "dusk-forge" +version = "0.2.2" +dependencies = [ + "dusk-forge-contract", + "serde", + "serde_json", +] + +[[package]] +name = "dusk-forge-contract" +version = "0.1.1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] name = "dusk-jubjub" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0e6995ff48af44bbcb810e4a49ed4ddb0800601c9c9722fd00d7f7e0b77c3a" +checksum = "49c388e710446aabbe7c7717109a239cea254594253ceb94e9134976c5b59509" dependencies = [ "bitvec", "blake2b_simd", "bytecheck", - "dusk-bls12_381 0.14.2", + "dusk-bls12_381", "dusk-bytes", "ff", "group", - "rand_core", + "hex", + "rand_core 0.6.4", "rkyv", + "serde", "subtle", "zeroize", ] @@ -966,13 +1134,14 @@ dependencies = [ [[package]] name = "dusk-plonk" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c425aa88fb1fd44c4bb72c0fe6abd37eae9de5b1f42d375d16c95aa1efceb327" +checksum = "949ec126023c47910d93a0fcb35ef20243f75eb7a726fcaa6541d8c44fadb66d" dependencies = [ + "blake2b_simd", "bytecheck", "cfg-if", - "dusk-bls12_381 0.14.2", + "dusk-bls12_381", "dusk-bytes", "dusk-jubjub", "ff", @@ -981,19 +1150,19 @@ dependencies = [ "merlin", "miniz_oxide", "msgpacker", - "rand_core", + "rand_core 0.6.4", "rkyv", - "sha2", + "sha2 0.10.8", "zeroize", ] [[package]] name = "dusk-poseidon" -version = "0.41.0" +version = "0.42.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66925505adc9db3671ebed59f5f2f196306efbe6ede8de9761e7830af5ddb84" +checksum = "fb428dc6d7aac399fd2c5929fd6e0b2c7b83392d3eb8c9a7b29a916d0087063d" dependencies = [ - "dusk-bls12_381 0.14.2", + "dusk-bls12_381", "dusk-jubjub", "dusk-plonk", "dusk-safe", @@ -1010,19 +1179,22 @@ dependencies = [ [[package]] name = "dusk-vm" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9970b6794ad562dadd2949943655defde02fe21090514c0f88729c709c86a31e" +version = "1.6.0" dependencies = [ "blake2b_simd", "blake3", + "blst", + "bytecheck", + "c-kzg", "dusk-bytes", "dusk-core", "dusk-poseidon", "lru", "piecrust", "rkyv", + "secp256k1", "serde", + "sha2 0.10.8", "sha3", "tracing", "wasmparser 0.240.0", @@ -1030,9 +1202,7 @@ dependencies = [ [[package]] name = "dusk-wallet-core" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f68eafa8ccc68368f904889b86bbcf2298cad90c856ce13c0db96649503e2114" +version = "1.6.0" dependencies = [ "blake3", "bytecheck", @@ -1041,10 +1211,10 @@ dependencies = [ "dusk-core", "ff", "hkdf", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "rkyv", - "sha2", + "sha2 0.10.8", "zeroize", ] @@ -1198,7 +1368,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1208,6 +1378,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "funty" version = "2.0.0" @@ -1268,6 +1444,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "group" version = "0.13.0" @@ -1275,7 +1457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1331,17 +1513,20 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", + "foldhash 0.1.5", "serde", ] [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hermit-abi" @@ -1349,11 +1534,29 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] [[package]] name = "hkdf" @@ -1370,7 +1573,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1388,7 +1591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.15.5", "serde", "serde_core", ] @@ -1408,7 +1611,7 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi", + "hermit-abi 0.4.0", "libc", "windows-sys", ] @@ -1458,29 +1661,32 @@ dependencies = [ [[package]] name = "jubjub-elgamal" -version = "0.2.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2b02b5859a6ebd729e4b7f9970963d3f3be4f0aebc700d0b2d392b9fd8201c" +checksum = "22c81bad2a54e37f6eadcdba35f35761b7dba5665af26ea16f29f0d9372fb64f" dependencies = [ + "dusk-bytes", "dusk-jubjub", "dusk-plonk", ] [[package]] name = "jubjub-schnorr" -version = "0.6.0" +version = "0.7.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008885a6d1b73ef18a827ad0faf7cf48d1c5a28d52a9e863b90a421a92799443" +checksum = "97a7208d3397238bd5fce848642b923f7aebed6d58cd472ad72027ba68167167" dependencies = [ + "bs58", "bytecheck", - "dusk-bls12_381 0.14.2", + "dusk-bls12_381", "dusk-bytes", "dusk-jubjub", "dusk-plonk", "dusk-poseidon", "ff", - "rand_core", + "rand_core 0.6.4", "rkyv", + "serde", "zeroize", ] @@ -1535,11 +1741,11 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.12.5" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -1592,7 +1798,7 @@ checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" dependencies = [ "byteorder", "keccak", - "rand_core", + "rand_core 0.6.4", "zeroize", ] @@ -1607,9 +1813,9 @@ dependencies = [ [[package]] name = "msgpacker" -version = "0.4.3" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79f77b4108a99ed7e834b46df33537a644ea94863a98a9140017eda1d08249f6" +checksum = "134a72ec3a6d034e9c767344ce8aebde228eef4e1343fff3ea209993f12920b3" dependencies = [ "msgpacker-derive", ] @@ -1634,6 +1840,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -1652,6 +1864,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi 0.5.2", + "libc", +] + [[package]] name = "object" version = "0.33.0" @@ -1699,11 +1921,11 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "phoenix-circuits" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d42a8482526bf7bcb3a6d34ad171a26f495319a520b9eb8a68837d837e3b0cf9" +checksum = "f5583c3db8adc437f7854319a6a815666b95475797ba82d8cb5f9f5a9f18fe55" dependencies = [ - "dusk-bls12_381 0.14.2", + "dusk-bls12_381", "dusk-bytes", "dusk-jubjub", "dusk-plonk", @@ -1716,33 +1938,37 @@ dependencies = [ [[package]] name = "phoenix-core" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b45ed942d9d166f7a45d16630529d68d68cfde7f37191d4646dfbe4b350b9cc" +checksum = "1930c6dd9d1054cf0cd725325323a5381fe3854708a2cebe38ccab8699579e2e" dependencies = [ "aes-gcm", - "bls12_381-bls 0.4.0", + "base64", + "bs58", "bytecheck", - "dusk-bls12_381 0.14.2", + "dusk-bls12_381", "dusk-bytes", "dusk-jubjub", "dusk-poseidon", "ff", + "hex", "hkdf", "jubjub-elgamal", "jubjub-schnorr", - "rand", + "rand 0.8.5", "rkyv", - "sha2", + "serde", + "serde_with", + "sha2 0.10.8", "subtle", "zeroize", ] [[package]] name = "piecrust" -version = "0.29.0-rc.3" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c38e9e4914aed3e4ce5e5c8d48df1114e675879c20055064e00608893ca2c6" +checksum = "768a69c4aac88337ec2c1b9ac1624d4c52bd50c6ecbdaed70b5854006d56fe01" dependencies = [ "blake3", "bytecheck", @@ -1754,7 +1980,7 @@ dependencies = [ "indexmap", "memmap2", "piecrust-uplink", - "rand", + "rand 0.8.5", "rkyv", "tempfile", "thiserror", @@ -1763,14 +1989,15 @@ dependencies = [ [[package]] name = "piecrust-uplink" -version = "0.19.0-rc.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49671c8d75a8c55fc4aff276729ac2e653596e74934fe7c72b592b6b868ad897" +checksum = "9df8d2d3f5deb052f61c65d01d892c4babe604cd884cdd2fcef7f6da4392b476" dependencies = [ "bytecheck", "dlmalloc", "hex", "rkyv", + "serde", ] [[package]] @@ -1821,12 +2048,12 @@ dependencies = [ [[package]] name = "poseidon-merkle" -version = "0.8.0" +version = "0.9.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6316347b2cf759ff0303ad280008ac89a36ed9f970e4cdfd1585d8ba5ed60134" +checksum = "95e801b94d059823f3dc9d2af14e2a7438863e4b23736e324ebb368991545d52" dependencies = [ "bytecheck", - "dusk-bls12_381 0.14.2", + "dusk-bls12_381", "dusk-bytes", "dusk-merkle", "dusk-plonk", @@ -1834,6 +2061,12 @@ dependencies = [ "rkyv", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -1852,6 +2085,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proxy-counter" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "psm" version = "0.1.23" @@ -1909,8 +2156,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1920,7 +2177,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1932,6 +2199,15 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rangemap" version = "1.7.1" @@ -2053,16 +2329,14 @@ dependencies = [ [[package]] name = "rusk-profile" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f37ff8ed7398a5442f16e26f8acee801ef5fe42d8982b9cb586cb46f432db9d" +version = "1.6.0" dependencies = [ "blake3", "console", "dirs", "hex", "serde", - "sha2", + "sha2 0.10.8", "toml", "tracing", "version_check", @@ -2070,15 +2344,13 @@ dependencies = [ [[package]] name = "rusk-prover" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ec33ea54518b45d9d26ec791ddc6430f96dcf75083e32170c3d87f7082f692" +version = "1.6.0" dependencies = [ "dusk-bytes", "dusk-core", "dusk-plonk", "once_cell", - "rand", + "rand 0.8.5", "rusk-profile", ] @@ -2129,12 +2401,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - [[package]] name = "same-file" version = "1.0.6" @@ -2150,6 +2416,26 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "secp256k1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.4", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] + [[package]] name = "semver" version = "1.0.23" @@ -2188,14 +2474,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -2207,6 +2494,34 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +dependencies = [ + "base64", + "chrono", + "hex", + "serde", + "serde_derive", + "serde_json", + "time", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.8" @@ -2215,7 +2530,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -2224,7 +2539,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest", + "digest 0.10.7", "keccak", ] @@ -2274,7 +2589,7 @@ dependencies = [ "dusk-vm", "dusk-wallet-core", "ff", - "rand", + "rand 0.8.5", "rkyv", "rusk-prover", ] @@ -2371,6 +2686,34 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + [[package]] name = "tinytemplate" version = "1.2.1" @@ -2454,7 +2797,7 @@ dependencies = [ "dusk-core", "dusk-vm", "ff", - "rand", + "rand 0.8.5", "ringbuffer", "rkyv", "rusk-profile", @@ -2817,3 +3160,9 @@ dependencies = [ "quote", "syn 2.0.96", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index d57f8fbe..8f3c09b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,12 @@ [workspace] members = [ + # Dusk-native reusable contract primitives + "standards/dusk-contract-standards", + "standards/examples/authorization_counter", + "standards/examples/drc20_roles_pausable", + "standards/examples/drc721_collection", + "standards/examples/proxy_counter", + # Test contracts "tests/alice", "tests/bob", @@ -15,12 +22,14 @@ resolver = "2" [workspace.dependencies] # Dusk dependencies -dusk-core = "1.4.0" +dusk-core = { path = "../rusk-private/core" } dusk-bytes = "0.1.7" -dusk-vm = "1.4.3" -dusk-wallet-core = "1.4.0" -rusk-profile = "1.4.0" -rusk-prover = "1.3.0" +dusk-data-driver = { path = "../rusk-private/data-drivers/data-driver" } +dusk-forge = { path = "../forge-explicit-emits" } +dusk-vm = { path = "../rusk-private/vm" } +dusk-wallet-core = { path = "../rusk-private/wallet-core" } +rusk-profile = { path = "../rusk-private/rusk-profile" } +rusk-prover = { path = "../rusk-private/rusk-prover" } # Other dependencies bytecheck = { version = "0.6.12", default-features = false } @@ -29,6 +38,18 @@ ff = { version = "0.13", default-features = false } rand = { version = "0.8.5", default-features = false } ringbuffer = "0.15" rkyv = { version = "0.7.39", default-features = false } +serde = { version = "1", default-features = false, features = [ + "derive", + "alloc", +] } +serde_json = { version = "1", default-features = false } +serde_with = { version = "=3.9.0", default-features = false, features = [ + "hex", +] } + +# Keep the data-driver dependency graph compatible with the repository's +# current nightly toolchain. +time = { version = "=0.3.36", default-features = false } [profile.dev.build-override] opt-level = 3 diff --git a/Makefile b/Makefile index c3e7a124..29253be6 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ -SUBDIRS := tests/alice tests/bob tests/charlie genesis/transfer genesis/stake tests/host_fn +STANDARDS_EXAMPLES := standards/examples/authorization_counter standards/examples/drc20_roles_pausable standards/examples/drc721_collection standards/examples/proxy_counter +SUBDIRS := standards/dusk-contract-standards $(STANDARDS_EXAMPLES) tests/alice tests/bob tests/charlie genesis/transfer genesis/stake tests/host_fn all: setup-compiler $(SUBDIRS) ## Build all the contracts @@ -13,6 +14,9 @@ test: wasm ## Run all the tests in the subfolder wasm: setup-compiler ## Generate the WASM for all the contracts $(MAKE) $(SUBDIRS) MAKECMDGOALS=wasm +standards-data-drivers: ## Generate Forge data-driver WASM for standards reference contracts + $(MAKE) $(STANDARDS_EXAMPLES) MAKECMDGOALS=wasm-dd + clippy: setup-compiler ## Run clippy $(MAKE) $(SUBDIRS) MAKECMDGOALS=clippy @@ -29,4 +33,4 @@ doc: $(SUBDIRS) ## Run doc gen $(SUBDIRS): $(MAKE) -C $@ $(MAKECMDGOALS) -.PHONY: all test help $(SUBDIRS) +.PHONY: all test help standards-data-drivers $(SUBDIRS) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index bc770a12..92da0998 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly-2024-07-30" +channel = "nightly-2026-02-27" components = ["rust-src", "rustfmt", "cargo", "clippy"] targets = ["wasm32-unknown-unknown"] From 11a670ebc207650238c6747564504fdae3ac2e6a Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sat, 25 Apr 2026 20:14:37 +0200 Subject: [PATCH 11/35] standards: annotate Forge reference events --- standards/examples/authorization_counter/src/lib.rs | 3 +++ standards/examples/drc20_roles_pausable/src/lib.rs | 13 +++++++++++++ standards/examples/drc721_collection/src/lib.rs | 12 ++++++++++++ standards/examples/proxy_counter/src/lib.rs | 8 ++++++++ 4 files changed, 36 insertions(+) diff --git a/standards/examples/authorization_counter/src/lib.rs b/standards/examples/authorization_counter/src/lib.rs index 531f3a2d..1e851514 100644 --- a/standards/examples/authorization_counter/src/lib.rs +++ b/standards/examples/authorization_counter/src/lib.rs @@ -70,6 +70,7 @@ mod authorization_counter { } } + #[contract(no_event)] pub fn init(&mut self) { if self.initialized { panic!("AuthorizationCounter: already initialized"); @@ -89,6 +90,7 @@ mod authorization_counter { self.authorizations.nonce(query.principal, query.domain) } + #[contract(no_event)] pub fn set_value_by_moonlight(&mut self, args: SetValueByMoonlight) { self.assert_action( args.authorization.action.contract, @@ -103,6 +105,7 @@ mod authorization_counter { self.set_value(principal, args.amount); } + #[contract(no_event)] pub fn set_value_by_phoenix(&mut self, args: SetValueByPhoenix) { self.assert_action( args.authorization.action.contract, diff --git a/standards/examples/drc20_roles_pausable/src/lib.rs b/standards/examples/drc20_roles_pausable/src/lib.rs index 18172e5a..95e6e27b 100644 --- a/standards/examples/drc20_roles_pausable/src/lib.rs +++ b/standards/examples/drc20_roles_pausable/src/lib.rs @@ -133,6 +133,7 @@ mod drc20_roles_pausable { } } + #[contract(emits = [(TRANSFER_TOPIC, Drc20Transfer)])] pub fn init(&mut self, args: Init) { if args.admin.is_zero() { panic!("{}", error::ZERO_PRINCIPAL); @@ -230,6 +231,7 @@ mod drc20_roles_pausable { self.access.has_role(args.role, args.account) } + #[contract(emits = [(TRANSFER_TOPIC, Drc20Transfer)])] pub fn transfer(&mut self, args: TransferCall) { self.pausable.assert_not_paused(); let caller = caller(); @@ -251,12 +253,14 @@ mod drc20_roles_pausable { Self::emit_transfer(event); } + #[contract(emits = [(APPROVAL_TOPIC, Drc20Approval)])] pub fn approve(&mut self, args: ApproveCall) { let caller = caller(); let event = self.token.approve(caller, args); Self::emit_approval(event); } + #[contract(emits = [(APPROVAL_TOPIC, Drc20Approval)])] pub fn approve_by_authorization(&mut self, args: SignedApproveCall) { let principal = self.authorizations.authorize_signed_action( &args.authorization, @@ -281,18 +285,21 @@ mod drc20_roles_pausable { Self::emit_approval(event); } + #[contract(emits = [(APPROVAL_TOPIC, Drc20Approval)])] pub fn increase_allowance(&mut self, args: IncreaseAllowanceCall) { let caller = caller(); let event = self.token.increase_allowance(caller, args); Self::emit_approval(event); } + #[contract(emits = [(APPROVAL_TOPIC, Drc20Approval)])] pub fn decrease_allowance(&mut self, args: DecreaseAllowanceCall) { let caller = caller(); let event = self.token.decrease_allowance(caller, args); Self::emit_approval(event); } + #[contract(emits = [(TRANSFER_TOPIC, Drc20Transfer)])] pub fn transfer_from(&mut self, args: TransferFromCall) { self.pausable.assert_not_paused(); let caller = caller(); @@ -314,6 +321,7 @@ mod drc20_roles_pausable { Self::emit_transfer(event); } + #[contract(emits = [(TRANSFER_TOPIC, Drc20Transfer)])] pub fn mint(&mut self, args: MintCall) { self.pausable.assert_not_paused(); self.authorize_role_action( @@ -335,6 +343,7 @@ mod drc20_roles_pausable { Self::emit_transfer(event); } + #[contract(emits = [(TRANSFER_TOPIC, Drc20Transfer)])] pub fn burn(&mut self, amount: u64) { self.pausable.assert_not_paused(); let caller = caller(); @@ -344,6 +353,7 @@ mod drc20_roles_pausable { Self::emit_transfer(event); } + #[contract(no_event)] pub fn pause(&mut self, args: AdminCall) { self.authorize_role_action( PAUSER_ROLE, @@ -358,6 +368,7 @@ mod drc20_roles_pausable { self.pausable.pause(); } + #[contract(no_event)] pub fn unpause(&mut self, args: AdminCall) { self.authorize_role_action( PAUSER_ROLE, @@ -376,6 +387,7 @@ mod drc20_roles_pausable { self.pausable.paused() } + #[contract(no_event)] pub fn grant_role(&mut self, args: RoleCall) { let admin = self.access.get_role_admin(args.role); let caller = self.authorize_role_action( @@ -391,6 +403,7 @@ mod drc20_roles_pausable { self.access.grant_role(caller, args.role, args.account); } + #[contract(no_event)] pub fn revoke_role(&mut self, args: RoleCall) { let admin = self.access.get_role_admin(args.role); let caller = self.authorize_role_action( diff --git a/standards/examples/drc721_collection/src/lib.rs b/standards/examples/drc721_collection/src/lib.rs index d41f4376..07a784f1 100644 --- a/standards/examples/drc721_collection/src/lib.rs +++ b/standards/examples/drc721_collection/src/lib.rs @@ -126,6 +126,7 @@ mod drc721_collection { } } + #[contract(emits = [(TRANSFER_TOPIC, Drc721Transfer)])] pub fn init(&mut self, args: Init) { if args.owner.is_zero() { panic!("{}", error::ZERO_PRINCIPAL); @@ -210,16 +211,19 @@ mod drc721_collection { self.royalties.royalty_info(args.token_id, args.sale_price) } + #[contract(emits = [(APPROVAL_TOPIC, Drc721Approval)])] pub fn approve(&mut self, args: ApproveCall) { let event = self.token.approve(caller(), args); Self::emit_approval(event); } + #[contract(emits = [(APPROVAL_FOR_ALL_TOPIC, Drc721ApprovalForAll)])] pub fn set_approval_for_all(&mut self, args: SetApprovalForAllCall) { let event = self.token.set_approval_for_all(caller(), args); Self::emit_approval_for_all(event); } + #[contract(emits = [(TRANSFER_TOPIC, Drc721Transfer)])] pub fn transfer_from(&mut self, args: TransferFromCall) { self.pausable.assert_not_paused(); let caller = caller(); @@ -231,6 +235,7 @@ mod drc721_collection { Self::emit_transfer(event); } + #[contract(emits = [(TRANSFER_TOPIC, Drc721Transfer)])] pub fn mint(&mut self, args: MintCall) { self.pausable.assert_not_paused(); self.authorize_owner_action( @@ -246,12 +251,14 @@ mod drc721_collection { Self::emit_transfer(event); } + #[contract(emits = [(TRANSFER_TOPIC, Drc721Transfer)])] pub fn burn(&mut self, token_id: u64) { self.pausable.assert_not_paused(); let event = self.token.burn(caller(), token_id); Self::emit_transfer(event); } + #[contract(no_event)] pub fn pause(&mut self, args: AdminCall) { self.authorize_owner_action( args.authorization.as_ref(), @@ -265,6 +272,7 @@ mod drc721_collection { self.pausable.pause(); } + #[contract(no_event)] pub fn unpause(&mut self, args: AdminCall) { self.authorize_owner_action( args.authorization.as_ref(), @@ -282,6 +290,7 @@ mod drc721_collection { self.pausable.paused() } + #[contract(no_event)] pub fn set_default_royalty(&mut self, args: SetDefaultRoyaltyCall) { self.authorize_owner_action( args.authorization.as_ref(), @@ -295,6 +304,7 @@ mod drc721_collection { self.royalties.set_default_royalty(args.info); } + #[contract(no_event)] pub fn clear_default_royalty(&mut self, args: AdminCall) { self.authorize_owner_action( args.authorization.as_ref(), @@ -308,6 +318,7 @@ mod drc721_collection { self.royalties.clear_default_royalty(); } + #[contract(no_event)] pub fn set_token_royalty(&mut self, args: SetTokenRoyaltyCall) { self.authorize_owner_action( args.authorization.as_ref(), @@ -321,6 +332,7 @@ mod drc721_collection { self.royalties.set_token_royalty(args.token_id, args.info); } + #[contract(no_event)] pub fn clear_token_royalty(&mut self, args: ClearTokenRoyaltyCall) { self.authorize_owner_action( args.authorization.as_ref(), diff --git a/standards/examples/proxy_counter/src/lib.rs b/standards/examples/proxy_counter/src/lib.rs index a13beb49..08eef53f 100644 --- a/standards/examples/proxy_counter/src/lib.rs +++ b/standards/examples/proxy_counter/src/lib.rs @@ -100,6 +100,7 @@ mod proxy_counter { } } + #[contract(no_event)] pub fn init(&mut self, args: Init) { if self.admin.is_some() { panic!("{}", error::ALREADY_INITIALIZED); @@ -130,11 +131,13 @@ mod proxy_counter { ) } + #[contract(no_event)] pub fn increment(&mut self) { let next = self.value().checked_add(1).expect(error::OVERFLOW); self.write_value(next); } + #[contract(no_event)] pub fn set_value(&mut self, args: SetValue) { self.authorize_admin_action( args.authorization.as_ref(), @@ -148,6 +151,7 @@ mod proxy_counter { self.write_value(args.value); } + #[contract(emits = [(UPGRADE_PREPARED_TOPIC, UpgradePrepared)])] pub fn prepare_upgrade(&mut self, args: PrepareUpgrade) { let caller = self.authorize_admin_action( args.authorization.as_ref(), @@ -170,6 +174,7 @@ mod proxy_counter { Self::emit_upgrade_prepared(event); } + #[contract(emits = [(UPGRADE_ACTIVATED_TOPIC, UpgradeActivated)])] pub fn activate_upgrade(&mut self, args: AdminCall) -> Vec { let caller = self.authorize_admin_action( args.authorization.as_ref(), @@ -186,6 +191,7 @@ mod proxy_counter { migrate_data } + #[contract(emits = [(UPGRADE_CANCELLED_TOPIC, UpgradeCancelled)])] pub fn cancel_pending_upgrade(&mut self, args: AdminCall) { let caller = self.authorize_admin_action( args.authorization.as_ref(), @@ -200,6 +206,7 @@ mod proxy_counter { Self::emit_upgrade_cancelled(event); } + #[contract(emits = [(UPGRADE_ROLLED_BACK_TOPIC, UpgradeRolledBack)])] pub fn rollback(&mut self, args: AdminCall) { let caller = self.authorize_admin_action( args.authorization.as_ref(), @@ -214,6 +221,7 @@ mod proxy_counter { Self::emit_upgrade_rolled_back(event); } + #[contract(emits = [(ROLLBACK_FINALIZED_TOPIC, RollbackFinalized)])] pub fn finalize_rollback_window(&mut self, args: AdminCall) { let caller = self.authorize_admin_action( args.authorization.as_ref(), From c6a7e209a8653c3c7456ca3c94c5d78041aea673 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sat, 25 Apr 2026 23:21:24 +0200 Subject: [PATCH 12/35] standards: emit policy mutation events --- docs/dusk-contract-standards-security.md | 2 + docs/dusk-contract-standards.md | 4 +- .../src/access/events.rs | 69 ++++++++++++++ .../dusk-contract-standards/src/access/mod.rs | 1 + .../src/token/drc721/events.rs | 65 +++++++++++++ .../examples/drc20_roles_pausable/src/lib.rs | 66 +++++++++++-- .../examples/drc721_collection/src/lib.rs | 93 ++++++++++++++++--- standards/examples/proxy_counter/src/lib.rs | 51 ++++++++-- 8 files changed, 322 insertions(+), 29 deletions(-) create mode 100644 standards/dusk-contract-standards/src/access/events.rs diff --git a/docs/dusk-contract-standards-security.md b/docs/dusk-contract-standards-security.md index ef688b77..e6388de6 100644 --- a/docs/dusk-contract-standards-security.md +++ b/docs/dusk-contract-standards-security.md @@ -42,6 +42,8 @@ is consumed. The reference pausable DRC20 and DRC721 contracts pause all balance-changing operations: transfers, minting, and burning. Approvals remain available while paused so accounts can prepare permissions without moving balances or ownership. +Pause and unpause operations emit typed events, and role and royalty policy +changes are also observable through typed events. ## Reentrancy and Call Stack diff --git a/docs/dusk-contract-standards.md b/docs/dusk-contract-standards.md index c11b84b2..6e59361b 100644 --- a/docs/dusk-contract-standards.md +++ b/docs/dusk-contract-standards.md @@ -66,7 +66,9 @@ voting-unit checkpoint patterns. DRC721 includes enumerable queries, burnable, pausable, and default/token-specific royalty patterns. The reference pausable tokens pause all balance-changing operations: transfers, minting, and burning. Approvals remain available while paused. These remain composing contract choices -rather than mandatory behavior baked into every token. +rather than mandatory behavior baked into every token. Reference contracts emit +typed pause/unpause, role grant/revoke, royalty change, transfer, approval, and +proxy value/upgrade events instead of leaving policy mutations silent. Proxy support is expressed as an upgrade admin state machine and a namespaced state store. The example records the active implementation id, delay, migration diff --git a/standards/dusk-contract-standards/src/access/events.rs b/standards/dusk-contract-standards/src/access/events.rs new file mode 100644 index 00000000..00e089a5 --- /dev/null +++ b/standards/dusk-contract-standards/src/access/events.rs @@ -0,0 +1,69 @@ +//! Access-control and emergency-stop event payloads. + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::core::Principal; + +use super::Role; + +/// Pause event topic. +pub const PAUSED_TOPIC: &str = "access/paused"; +/// Unpause event topic. +pub const UNPAUSED_TOPIC: &str = "access/unpaused"; +/// Role-grant event topic. +pub const ROLE_GRANTED_TOPIC: &str = "access/role_granted"; +/// Role-revoke event topic. +pub const ROLE_REVOKED_TOPIC: &str = "access/role_revoked"; + +/// Pause event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Paused { + /// Principal that authorized the pause. + pub account: Principal, +} + +/// Unpause event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Unpaused { + /// Principal that authorized the unpause. + pub account: Principal, +} + +/// Role-grant event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoleGranted { + /// Role id. + pub role: Role, + /// Principal receiving the role. + pub account: Principal, + /// Principal that authorized the grant. + pub sender: Principal, +} + +/// Role-revoke event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct RoleRevoked { + /// Role id. + pub role: Role, + /// Principal losing the role. + pub account: Principal, + /// Principal that authorized the revoke. + pub sender: Principal, +} diff --git a/standards/dusk-contract-standards/src/access/mod.rs b/standards/dusk-contract-standards/src/access/mod.rs index 93d4e224..99e5d047 100644 --- a/standards/dusk-contract-standards/src/access/mod.rs +++ b/standards/dusk-contract-standards/src/access/mod.rs @@ -1,6 +1,7 @@ //! Access-control modules. pub mod access_control; +pub mod events; pub mod ownable; pub mod owner_set; pub mod pausable; diff --git a/standards/dusk-contract-standards/src/token/drc721/events.rs b/standards/dusk-contract-standards/src/token/drc721/events.rs index 3e848ed0..9cbf6eaa 100644 --- a/standards/dusk-contract-standards/src/token/drc721/events.rs +++ b/standards/dusk-contract-standards/src/token/drc721/events.rs @@ -11,6 +11,15 @@ pub const TRANSFER_TOPIC: &str = "drc721/transfer"; pub const APPROVAL_TOPIC: &str = "drc721/approval"; /// Approval-for-all event topic. pub const APPROVAL_FOR_ALL_TOPIC: &str = "drc721/approval_for_all"; +/// Default royalty set event topic. +pub const DEFAULT_ROYALTY_SET_TOPIC: &str = "drc721/default_royalty_set"; +/// Default royalty cleared event topic. +pub const DEFAULT_ROYALTY_CLEARED_TOPIC: &str = + "drc721/default_royalty_cleared"; +/// Token royalty set event topic. +pub const TOKEN_ROYALTY_SET_TOPIC: &str = "drc721/token_royalty_set"; +/// Token royalty cleared event topic. +pub const TOKEN_ROYALTY_CLEARED_TOPIC: &str = "drc721/token_royalty_cleared"; /// Transfer event. #[derive( @@ -56,3 +65,59 @@ pub struct ApprovalForAll { /// Approval. pub approved: bool, } + +/// Default royalty set event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct DefaultRoyaltySet { + /// Principal that authorized the royalty change. + pub operator: Principal, + /// Royalty receiver. + pub receiver: Principal, + /// Royalty basis points. + pub basis_points: u16, +} + +/// Default royalty cleared event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct DefaultRoyaltyCleared { + /// Principal that authorized the royalty change. + pub operator: Principal, +} + +/// Token royalty set event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TokenRoyaltySet { + /// Principal that authorized the royalty change. + pub operator: Principal, + /// Token id. + pub token_id: u64, + /// Royalty receiver. + pub receiver: Principal, + /// Royalty basis points. + pub basis_points: u16, +} + +/// Token royalty cleared event. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct TokenRoyaltyCleared { + /// Principal that authorized the royalty change. + pub operator: Principal, + /// Token id. + pub token_id: u64, +} diff --git a/standards/examples/drc20_roles_pausable/src/lib.rs b/standards/examples/drc20_roles_pausable/src/lib.rs index 95e6e27b..94e1679e 100644 --- a/standards/examples/drc20_roles_pausable/src/lib.rs +++ b/standards/examples/drc20_roles_pausable/src/lib.rs @@ -88,6 +88,10 @@ mod drc20_roles_pausable { SIGNED_APPROVE_ACTION, SIGNED_APPROVE_DOMAIN, TOKEN_ADMIN_DOMAIN, UNPAUSE_ACTION, }; + use dusk_contract_standards::access::events::{ + Paused, RoleGranted, RoleRevoked, Unpaused, PAUSED_TOPIC, + ROLE_GRANTED_TOPIC, ROLE_REVOKED_TOPIC, UNPAUSED_TOPIC, + }; use dusk_contract_standards::access::{AccessControl, Pausable, Role}; use dusk_contract_standards::auth::{ ActionEnvelope, AuthorizationManager, SignedAuthorization, @@ -353,9 +357,9 @@ mod drc20_roles_pausable { Self::emit_transfer(event); } - #[contract(no_event)] + #[contract(emits = [(PAUSED_TOPIC, Paused)])] pub fn pause(&mut self, args: AdminCall) { - self.authorize_role_action( + let caller = self.authorize_role_action( PAUSER_ROLE, args.authorization.as_ref(), ActionEnvelope::new( @@ -365,12 +369,14 @@ mod drc20_roles_pausable { empty_payload_hash(b"drc20.pause"), ), ); + self.pausable.assert_not_paused(); self.pausable.pause(); + Self::emit_paused(caller); } - #[contract(no_event)] + #[contract(emits = [(UNPAUSED_TOPIC, Unpaused)])] pub fn unpause(&mut self, args: AdminCall) { - self.authorize_role_action( + let caller = self.authorize_role_action( PAUSER_ROLE, args.authorization.as_ref(), ActionEnvelope::new( @@ -380,14 +386,16 @@ mod drc20_roles_pausable { empty_payload_hash(b"drc20.unpause"), ), ); + self.pausable.assert_paused(); self.pausable.unpause(); + Self::emit_unpaused(caller); } pub fn paused(&self) -> bool { self.pausable.paused() } - #[contract(no_event)] + #[contract(emits = [(ROLE_GRANTED_TOPIC, RoleGranted)])] pub fn grant_role(&mut self, args: RoleCall) { let admin = self.access.get_role_admin(args.role); let caller = self.authorize_role_action( @@ -400,10 +408,14 @@ mod drc20_roles_pausable { role_payload_hash(args.role, args.account), ), ); + let had_role = self.access.has_role(args.role, args.account); self.access.grant_role(caller, args.role, args.account); + if !had_role { + Self::emit_role_granted(args.role, args.account, caller); + } } - #[contract(no_event)] + #[contract(emits = [(ROLE_REVOKED_TOPIC, RoleRevoked)])] pub fn revoke_role(&mut self, args: RoleCall) { let admin = self.access.get_role_admin(args.role); let caller = self.authorize_role_action( @@ -416,7 +428,11 @@ mod drc20_roles_pausable { role_payload_hash(args.role, args.account), ), ); + let had_role = self.access.has_role(args.role, args.account); self.access.revoke_role(caller, args.role, args.account); + if had_role { + Self::emit_role_revoked(args.role, args.account, caller); + } } fn authorize_role_action( @@ -456,6 +472,44 @@ mod drc20_roles_pausable { }, ); } + + fn emit_paused(account: Principal) { + abi::emit(PAUSED_TOPIC, Paused { account }); + } + + fn emit_unpaused(account: Principal) { + abi::emit(UNPAUSED_TOPIC, Unpaused { account }); + } + + fn emit_role_granted( + role: Role, + account: Principal, + sender: Principal, + ) { + abi::emit( + ROLE_GRANTED_TOPIC, + RoleGranted { + role, + account, + sender, + }, + ); + } + + fn emit_role_revoked( + role: Role, + account: Principal, + sender: Principal, + ) { + abi::emit( + ROLE_REVOKED_TOPIC, + RoleRevoked { + role, + account, + sender, + }, + ); + } } impl Default for Drc20RolesPausable { diff --git a/standards/examples/drc721_collection/src/lib.rs b/standards/examples/drc721_collection/src/lib.rs index 07a784f1..9e51de31 100644 --- a/standards/examples/drc721_collection/src/lib.rs +++ b/standards/examples/drc721_collection/src/lib.rs @@ -84,6 +84,9 @@ mod drc721_collection { NFT_ADMIN_DOMAIN, PAUSE_ACTION, SET_DEFAULT_ROYALTY_ACTION, SET_TOKEN_ROYALTY_ACTION, UNPAUSE_ACTION, }; + use dusk_contract_standards::access::events::{ + Paused, Unpaused, PAUSED_TOPIC, UNPAUSED_TOPIC, + }; use dusk_contract_standards::access::{Ownable, Pausable}; use dusk_contract_standards::auth::{ ActionEnvelope, AuthorizationManager, SignedAuthorization, @@ -94,8 +97,11 @@ mod drc721_collection { use dusk_contract_standards::security::ReentrancyGuard; use dusk_contract_standards::token::drc721::events::{ Approval as Drc721Approval, ApprovalForAll as Drc721ApprovalForAll, - Transfer as Drc721Transfer, APPROVAL_FOR_ALL_TOPIC, APPROVAL_TOPIC, - TRANSFER_TOPIC, + DefaultRoyaltyCleared, DefaultRoyaltySet, TokenRoyaltyCleared, + TokenRoyaltySet, Transfer as Drc721Transfer, APPROVAL_FOR_ALL_TOPIC, + APPROVAL_TOPIC, DEFAULT_ROYALTY_CLEARED_TOPIC, + DEFAULT_ROYALTY_SET_TOPIC, TOKEN_ROYALTY_CLEARED_TOPIC, + TOKEN_ROYALTY_SET_TOPIC, TRANSFER_TOPIC, }; use dusk_contract_standards::token::drc721::{ ApproveCall, BalanceOf, Drc721, GetApproved, IsApprovedForAll, OwnerOf, @@ -258,9 +264,9 @@ mod drc721_collection { Self::emit_transfer(event); } - #[contract(no_event)] + #[contract(emits = [(PAUSED_TOPIC, Paused)])] pub fn pause(&mut self, args: AdminCall) { - self.authorize_owner_action( + let caller = self.authorize_owner_action( args.authorization.as_ref(), ActionEnvelope::new( abi::self_id(), @@ -269,12 +275,14 @@ mod drc721_collection { empty_payload_hash(b"drc721.pause"), ), ); + self.pausable.assert_not_paused(); self.pausable.pause(); + Self::emit_paused(caller); } - #[contract(no_event)] + #[contract(emits = [(UNPAUSED_TOPIC, Unpaused)])] pub fn unpause(&mut self, args: AdminCall) { - self.authorize_owner_action( + let caller = self.authorize_owner_action( args.authorization.as_ref(), ActionEnvelope::new( abi::self_id(), @@ -283,16 +291,18 @@ mod drc721_collection { empty_payload_hash(b"drc721.unpause"), ), ); + self.pausable.assert_paused(); self.pausable.unpause(); + Self::emit_unpaused(caller); } pub fn paused(&self) -> bool { self.pausable.paused() } - #[contract(no_event)] + #[contract(emits = [(DEFAULT_ROYALTY_SET_TOPIC, DefaultRoyaltySet)])] pub fn set_default_royalty(&mut self, args: SetDefaultRoyaltyCall) { - self.authorize_owner_action( + let caller = self.authorize_owner_action( args.authorization.as_ref(), ActionEnvelope::new( abi::self_id(), @@ -302,11 +312,14 @@ mod drc721_collection { ), ); self.royalties.set_default_royalty(args.info); + Self::emit_default_royalty_set(caller, args.info); } - #[contract(no_event)] + #[contract( + emits = [(DEFAULT_ROYALTY_CLEARED_TOPIC, DefaultRoyaltyCleared)] + )] pub fn clear_default_royalty(&mut self, args: AdminCall) { - self.authorize_owner_action( + let caller = self.authorize_owner_action( args.authorization.as_ref(), ActionEnvelope::new( abi::self_id(), @@ -316,11 +329,12 @@ mod drc721_collection { ), ); self.royalties.clear_default_royalty(); + Self::emit_default_royalty_cleared(caller); } - #[contract(no_event)] + #[contract(emits = [(TOKEN_ROYALTY_SET_TOPIC, TokenRoyaltySet)])] pub fn set_token_royalty(&mut self, args: SetTokenRoyaltyCall) { - self.authorize_owner_action( + let caller = self.authorize_owner_action( args.authorization.as_ref(), ActionEnvelope::new( abi::self_id(), @@ -330,11 +344,12 @@ mod drc721_collection { ), ); self.royalties.set_token_royalty(args.token_id, args.info); + Self::emit_token_royalty_set(caller, args.token_id, args.info); } - #[contract(no_event)] + #[contract(emits = [(TOKEN_ROYALTY_CLEARED_TOPIC, TokenRoyaltyCleared)])] pub fn clear_token_royalty(&mut self, args: ClearTokenRoyaltyCall) { - self.authorize_owner_action( + let caller = self.authorize_owner_action( args.authorization.as_ref(), ActionEnvelope::new( abi::self_id(), @@ -344,6 +359,7 @@ mod drc721_collection { ), ); self.royalties.clear_token_royalty(args.token_id); + Self::emit_token_royalty_cleared(caller, args.token_id); } fn authorize_owner_action( @@ -392,6 +408,55 @@ mod drc721_collection { }, ); } + + fn emit_paused(account: Principal) { + abi::emit(PAUSED_TOPIC, Paused { account }); + } + + fn emit_unpaused(account: Principal) { + abi::emit(UNPAUSED_TOPIC, Unpaused { account }); + } + + fn emit_default_royalty_set(operator: Principal, info: RoyaltyInfo) { + abi::emit( + DEFAULT_ROYALTY_SET_TOPIC, + DefaultRoyaltySet { + operator, + receiver: info.receiver, + basis_points: info.basis_points, + }, + ); + } + + fn emit_default_royalty_cleared(operator: Principal) { + abi::emit( + DEFAULT_ROYALTY_CLEARED_TOPIC, + DefaultRoyaltyCleared { operator }, + ); + } + + fn emit_token_royalty_set( + operator: Principal, + token_id: u64, + info: RoyaltyInfo, + ) { + abi::emit( + TOKEN_ROYALTY_SET_TOPIC, + TokenRoyaltySet { + operator, + token_id, + receiver: info.receiver, + basis_points: info.basis_points, + }, + ); + } + + fn emit_token_royalty_cleared(operator: Principal, token_id: u64) { + abi::emit( + TOKEN_ROYALTY_CLEARED_TOPIC, + TokenRoyaltyCleared { operator, token_id }, + ); + } } impl Default for Drc721Collection { diff --git a/standards/examples/proxy_counter/src/lib.rs b/standards/examples/proxy_counter/src/lib.rs index 08eef53f..2757f00d 100644 --- a/standards/examples/proxy_counter/src/lib.rs +++ b/standards/examples/proxy_counter/src/lib.rs @@ -21,6 +21,7 @@ pub const ACTIVATE_UPGRADE_ACTION: [u8; 32] = [34u8; 32]; pub const CANCEL_UPGRADE_ACTION: [u8; 32] = [35u8; 32]; pub const ROLLBACK_ACTION: [u8; 32] = [36u8; 32]; pub const FINALIZE_ROLLBACK_ACTION: [u8; 32] = [37u8; 32]; +pub const VALUE_CHANGED_TOPIC: &str = "proxy_counter/value_changed"; #[derive( Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, @@ -58,6 +59,17 @@ pub struct AdminCall { pub authorization: Option, } +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct ValueChanged { + pub previous: u64, + pub value: u64, + pub sender: Option, +} + #[dusk_forge::contract] mod proxy_counter { use alloc::vec::Vec; @@ -77,10 +89,10 @@ mod proxy_counter { }; use dusk_core::abi::{self, ContractId}; use proxy_counter_types::{ - AdminCall, Init, PrepareUpgrade, SetValue, ACTIVATE_UPGRADE_ACTION, - CANCEL_UPGRADE_ACTION, FINALIZE_ROLLBACK_ACTION, - PREPARE_UPGRADE_ACTION, PROXY_ADMIN_DOMAIN, ROLLBACK_ACTION, - SET_VALUE_ACTION, + AdminCall, Init, PrepareUpgrade, SetValue, ValueChanged, + ACTIVATE_UPGRADE_ACTION, CANCEL_UPGRADE_ACTION, + FINALIZE_ROLLBACK_ACTION, PREPARE_UPGRADE_ACTION, PROXY_ADMIN_DOMAIN, + ROLLBACK_ACTION, SET_VALUE_ACTION, VALUE_CHANGED_TOPIC, }; const COUNTER_KEY: [u8; 32] = [7u8; 32]; @@ -131,15 +143,22 @@ mod proxy_counter { ) } - #[contract(no_event)] + #[contract(emits = [(VALUE_CHANGED_TOPIC, ValueChanged)])] pub fn increment(&mut self) { - let next = self.value().checked_add(1).expect(error::OVERFLOW); + let previous = self.value(); + let next = previous.checked_add(1).expect(error::OVERFLOW); self.write_value(next); + Self::emit_value_changed( + previous, + next, + CallContext::current().principal, + ); } - #[contract(no_event)] + #[contract(emits = [(VALUE_CHANGED_TOPIC, ValueChanged)])] pub fn set_value(&mut self, args: SetValue) { - self.authorize_admin_action( + let previous = self.value(); + let caller = self.authorize_admin_action( args.authorization.as_ref(), ActionEnvelope::new( abi::self_id(), @@ -149,6 +168,7 @@ mod proxy_counter { ), ); self.write_value(args.value); + Self::emit_value_changed(previous, args.value, Some(caller)); } #[contract(emits = [(UPGRADE_PREPARED_TOPIC, UpgradePrepared)])] @@ -308,6 +328,21 @@ mod proxy_counter { ); } + fn emit_value_changed( + previous: u64, + value: u64, + sender: Option, + ) { + abi::emit( + VALUE_CHANGED_TOPIC, + ValueChanged { + previous, + value, + sender, + }, + ); + } + fn admin(&self) -> &UpgradeAdmin { self.admin .as_ref() From 2ad527c95eb6e3d91785b983bac09a4c3bffff20 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sat, 25 Apr 2026 23:29:34 +0200 Subject: [PATCH 13/35] standards: add DRC721 signed approvals --- docs/dusk-contract-standards-security.md | 2 + docs/dusk-contract-standards.md | 32 ++- .../dusk-contract-standards-local-smoke.sh | 29 +++ .../examples/build_signed_authorizations.rs | 54 +++++ .../examples/encode_local_smoke_args.rs | 144 ++++++++++- .../src/token/drc721/mod.rs | 5 +- .../src/token/drc721/types.rs | 31 +++ .../tests/examples_vm.rs | 223 +++++++++++++++++- .../examples/drc721_collection/src/lib.rs | 107 ++++++++- 9 files changed, 606 insertions(+), 21 deletions(-) diff --git a/docs/dusk-contract-standards-security.md b/docs/dusk-contract-standards-security.md index e6388de6..bda90a6e 100644 --- a/docs/dusk-contract-standards-security.md +++ b/docs/dusk-contract-standards-security.md @@ -44,6 +44,8 @@ operations: transfers, minting, and burning. Approvals remain available while paused so accounts can prepare permissions without moving balances or ownership. Pause and unpause operations emit typed events, and role and royalty policy changes are also observable through typed events. +Signed DRC721 token/operator approvals follow the same nonce-bound action +model as DRC20 signed approvals and remain available while paused. ## Reentrancy and Call Stack diff --git a/docs/dusk-contract-standards.md b/docs/dusk-contract-standards.md index 6e59361b..e3750d47 100644 --- a/docs/dusk-contract-standards.md +++ b/docs/dusk-contract-standards.md @@ -63,12 +63,13 @@ functions. DRC20 includes optional supply-cap, burnable, pausable, signed approval, and voting-unit checkpoint patterns. DRC721 includes enumerable queries, burnable, -pausable, and default/token-specific royalty patterns. The reference pausable -tokens pause all balance-changing operations: transfers, minting, and burning. -Approvals remain available while paused. These remain composing contract choices -rather than mandatory behavior baked into every token. Reference contracts emit -typed pause/unpause, role grant/revoke, royalty change, transfer, approval, and -proxy value/upgrade events instead of leaving policy mutations silent. +pausable, signed token/operator approvals, and default/token-specific royalty +patterns. The reference pausable tokens pause all balance-changing operations: +transfers, minting, and burning. Approvals remain available while paused. These +remain composing contract choices rather than mandatory behavior baked into +every token. Reference contracts emit typed pause/unpause, role grant/revoke, +royalty change, transfer, approval, and proxy value/upgrade events instead of +leaving policy mutations silent. Proxy support is expressed as an upgrade admin state machine and a namespaced state store. The example records the active implementation id, delay, migration @@ -114,6 +115,13 @@ Admin references follow the same rule. Signed mint, role, royalty, and proxy calls validate `contract`, `domain`, `action_id`, and `payload_hash` before consuming the signature nonce. +The DRC721 reference exposes `approve_by_authorization` and +`set_approval_for_all_by_authorization`. Token approval hashes use the ASCII +domain tag `drc721.approve`, owner principal bytes, approved principal bytes, +and the big-endian token id. Operator approval hashes use +`drc721.approval_for_all`, owner principal bytes, operator principal bytes, and +a single `0` or `1` approval byte. + Use stable constants for `domain` and `action_id`. `domain` separates nonce streams such as permits, role-admin actions, upgrade approvals, and voting. `action_id` separates operations inside the same stream. Reusing a domain is @@ -133,8 +141,8 @@ spent. See `standards/dusk-contract-standards/examples/build_signed_authorizations.rs` for a compact client-side Rust example that builds the DRC20 signed-approval -payload hash, constructs `AuthorizedAction`, and signs it with Moonlight BLS -and Phoenix Schnorr keys. +and DRC721 signed-approval payload hashes, constructs `AuthorizedAction`, and +signs them with Moonlight BLS and Phoenix Schnorr keys. Nonce protection rejects replay after a signed action is consumed. Expiry is optional but recommended for relayed or delayed execution, because an unused @@ -200,10 +208,10 @@ paths, performs real Moonlight and Phoenix signed calls against the authorization counter, covers replay/wrong-payload/wrong-signature/wrong-target failures, submits signed DRC20 mint and signed DRC20 approvals, verifies paused DRC20/DRC721 signed mint rejection without nonce movement, submits signed -DRC721 owner actions, submits a signed proxy admin call, covers -replay/wrong-payload failures for those reference flows, and calls privileged -functions without a runtime caller to validate the negative authorization path -at the VM boundary. +DRC721 owner actions and signed DRC721 approvals, submits a signed proxy admin +call, covers replay/wrong-payload failures for those reference flows, and calls +privileged functions without a runtime caller to validate the negative +authorization path at the VM boundary. Run the focused suite with: diff --git a/scripts/dusk-contract-standards-local-smoke.sh b/scripts/dusk-contract-standards-local-smoke.sh index 01423047..c61a6503 100755 --- a/scripts/dusk-contract-standards-local-smoke.sh +++ b/scripts/dusk-contract-standards-local-smoke.sh @@ -369,12 +369,41 @@ run_signed_invariants() { "$(query_u64 "$drc721_id" total_supply "$unit_args")" 2 assert_eq "DRC721 admin nonce after mint" \ "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-admin phoenix 1)")" 1 + expect_call_ok "DRC721 signed token approval" \ + "$drc721_id" approve_by_authorization \ + "$(encode_args drc721-approve "$drc721_id" 0 "$expires_at" 2)" + assert_eq "DRC721 approval nonce after token approval" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-approval phoenix 1)")" 1 + expect_call_rejected "DRC721 signed token approval replay" \ + "$drc721_id" approve_by_authorization \ + "$(encode_args drc721-approve "$drc721_id" 0 "$expires_at" 2)" + assert_eq "DRC721 approval nonce after token approval replay" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-approval phoenix 1)")" 1 + expect_call_rejected "DRC721 signed token approval bad payload" \ + "$drc721_id" approve_by_authorization \ + "$(encode_args drc721-approve "$drc721_id" 1 "$expires_at" 2 bad-payload)" + assert_eq "DRC721 approval nonce after bad payload" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-approval phoenix 1)")" 1 + expect_call_ok "DRC721 signed operator approval" \ + "$drc721_id" set_approval_for_all_by_authorization \ + "$(encode_args drc721-operator-approval "$drc721_id" 1 "$expires_at" true)" + assert_eq "DRC721 signed operator approval state" \ + "$(query_bool "$drc721_id" is_approved_for_all "$(encode_args drc721-operator-query)")" true + assert_eq "DRC721 approval nonce after operator approval" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-approval phoenix 1)")" 2 expect_call_ok "DRC721 signed pause" \ "$drc721_id" pause "$(encode_args drc721-admin pause "$drc721_id" 1 "$expires_at")" assert_eq "DRC721 paused" \ "$(query_bool "$drc721_id" paused "$unit_args")" true assert_eq "DRC721 admin nonce after pause" \ "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-admin phoenix 1)")" 2 + expect_call_ok "DRC721 signed operator approval while paused" \ + "$drc721_id" set_approval_for_all_by_authorization \ + "$(encode_args drc721-operator-approval "$drc721_id" 2 "$expires_at" false)" + assert_eq "DRC721 signed operator approval state while paused" \ + "$(query_bool "$drc721_id" is_approved_for_all "$(encode_args drc721-operator-query)")" false + assert_eq "DRC721 approval nonce after paused operator approval" \ + "$(query_u64 "$drc721_id" nonce "$(encode_args nonce drc721-approval phoenix 1)")" 3 expect_call_rejected "DRC721 paused mint" \ "$drc721_id" mint "$(encode_args drc721-mint "$drc721_id" 2 "$expires_at" 3)" assert_eq "DRC721 supply after paused mint" \ diff --git a/standards/dusk-contract-standards/examples/build_signed_authorizations.rs b/standards/dusk-contract-standards/examples/build_signed_authorizations.rs index 66d76346..48e83e0a 100644 --- a/standards/dusk-contract-standards/examples/build_signed_authorizations.rs +++ b/standards/dusk-contract-standards/examples/build_signed_authorizations.rs @@ -19,6 +19,8 @@ use rand::SeedableRng; const SIGNED_APPROVE_DOMAIN: NonceDomain = [12u8; 32]; const SIGNED_APPROVE_ACTION: [u8; 32] = [18u8; 32]; +const NFT_SIGNED_APPROVE_DOMAIN: NonceDomain = [29u8; 32]; +const NFT_SIGNED_APPROVE_ACTION: [u8; 32] = [30u8; 32]; const EXAMPLE_EXPIRES_AT: u64 = 1_000; fn main() { @@ -60,6 +62,28 @@ fn main() { SIGNED_APPROVE_ACTION, drc20_approve_payload_hash(phoenix_owner, spender, 100), ); + + let nft_approved = Principal::Contract(ContractId::from_bytes([8u8; 32])); + let nft_action = drc721_signed_approve_action( + contract, + phoenix_owner, + nft_approved, + 42, + 1, + ); + let nft_approval = + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action: nft_action, + public_key: phoenix_pk, + signature: phoenix_sk.sign(&mut rng, nft_action.message_hash()), + replay_key: None, + }); + nft_approval.assert_action( + contract, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVE_ACTION, + drc721_approve_payload_hash(phoenix_owner, nft_approved, 42), + ); } fn drc20_signed_approve_action( @@ -92,6 +116,36 @@ fn drc20_approve_payload_hash( host_queries::keccak256(bytes) } +fn drc721_signed_approve_action( + contract: ContractId, + owner: Principal, + approved: Principal, + token_id: u64, + nonce: u64, +) -> AuthorizedAction { + AuthorizedAction { + contract, + domain: NFT_SIGNED_APPROVE_DOMAIN, + action_id: NFT_SIGNED_APPROVE_ACTION, + nonce, + expires_at: EXAMPLE_EXPIRES_AT, + principal: owner, + payload_hash: drc721_approve_payload_hash(owner, approved, token_id), + } +} + +fn drc721_approve_payload_hash( + owner: Principal, + approved: Principal, + token_id: u64, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, approved); + bytes.extend_from_slice(&token_id.to_be_bytes()); + host_queries::keccak256(bytes) +} + fn moonlight_secret(seed: u64) -> BlsSecretKey { let mut rng = StdRng::seed_from_u64(seed); BlsSecretKey::random(&mut rng) diff --git a/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs index 0bdb493d..7e624920 100644 --- a/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs +++ b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs @@ -12,7 +12,9 @@ use dusk_contract_standards::token::drc20::{ Init as Drc20TokenInit, InitBalance as Drc20InitBalance, }; use dusk_contract_standards::token::drc721::{ - Init as Drc721TokenInit, InitToken as Drc721InitToken, + Init as Drc721TokenInit, InitToken as Drc721InitToken, IsApprovedForAll, + SignedApproveCall as Drc721SignedApproveCall, + SignedSetApprovalForAllCall as Drc721SignedSetApprovalForAllCall, }; use dusk_core::abi::{ContractId, StandardBufSerializer, ARGBUF_LEN}; use dusk_core::signatures::bls::{ @@ -42,6 +44,9 @@ const NFT_ADMIN_DOMAIN: [u8; 32] = [21u8; 32]; const NFT_MINT_ACTION: [u8; 32] = [22u8; 32]; const NFT_PAUSE_ACTION: [u8; 32] = [23u8; 32]; const NFT_UNPAUSE_ACTION: [u8; 32] = [24u8; 32]; +const NFT_SIGNED_APPROVE_DOMAIN: [u8; 32] = [29u8; 32]; +const NFT_SIGNED_APPROVE_ACTION: [u8; 32] = [30u8; 32]; +const NFT_SIGNED_APPROVAL_FOR_ALL_ACTION: [u8; 32] = [31u8; 32]; const PROXY_ADMIN_DOMAIN: [u8; 32] = [31u8; 32]; const SET_PROXY_VALUE_ACTION: [u8; 32] = [32u8; 32]; @@ -130,7 +135,7 @@ fn main() -> Result<(), Box> { let args = env::args().collect::>(); let Some(command) = args.get(1).map(String::as_str) else { eprintln!( - "usage: encode_local_smoke_args " + "usage: encode_local_smoke_args " ); std::process::exit(2); }; @@ -181,6 +186,14 @@ fn main() -> Result<(), Box> { "drc20-admin" => encode(&drc20_admin_call(&args, admin)?)?, "drc721-mint" => encode(&drc721_mint_call(&args, admin)?)?, "drc721-admin" => encode(&drc721_admin_call(&args, admin)?)?, + "drc721-approve" => encode(&drc721_approve_call(&args, admin)?)?, + "drc721-operator-approval" => { + encode(&drc721_operator_approval_call(&args, admin)?)? + } + "drc721-operator-query" => encode(&IsApprovedForAll { + owner: admin, + operator: drc721_operator_principal(), + })?, "proxy-set" => encode(&proxy_set_call(&args, admin)?)?, _ => { eprintln!("unknown command: {command}"); @@ -370,6 +383,80 @@ fn drc721_admin_call( }) } +fn drc721_approve_call( + args: &[String], + admin: Principal, +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let token_id = parse_u64_arg(args, 5, "token_id")?; + let variant = variant_arg(args); + let approved = drc721_approved_principal(); + let payload_token_id = payload_amount_for_variant(token_id, variant); + Ok(Drc721SignedApproveCall { + owner: admin, + approved, + token_id, + authorization: admin_phoenix_authorization( + AdminPhoenixAction { + contract, + admin, + domain: NFT_SIGNED_APPROVE_DOMAIN, + action_id: action_for_variant( + NFT_SIGNED_APPROVE_ACTION, + variant, + ), + nonce, + expires_at, + payload_hash: nft_approve_payload_hash( + admin, + approved, + payload_token_id, + ), + }, + variant, + ), + }) +} + +fn drc721_operator_approval_call( + args: &[String], + admin: Principal, +) -> Result> { + let contract = contract_arg(args, 2)?; + let nonce = parse_u64_arg(args, 3, "nonce")?; + let expires_at = parse_u64_arg(args, 4, "expires_at")?; + let approved = parse_bool_arg(args, 5, "approved")?; + let variant = variant_arg(args); + let operator = drc721_operator_principal(); + let payload_approved = payload_bool_for_variant(approved, variant); + Ok(Drc721SignedSetApprovalForAllCall { + owner: admin, + operator, + approved, + authorization: admin_phoenix_authorization( + AdminPhoenixAction { + contract, + admin, + domain: NFT_SIGNED_APPROVE_DOMAIN, + action_id: action_for_variant( + NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + variant, + ), + nonce, + expires_at, + payload_hash: nft_approval_for_all_payload_hash( + admin, + operator, + payload_approved, + ), + }, + variant, + ), + }) +} + fn proxy_set_call( args: &[String], admin: Principal, @@ -501,6 +588,30 @@ fn nft_mint_payload_hash(to: Principal, token_id: u64) -> [u8; 32] { host_queries::keccak256(bytes) } +fn nft_approve_payload_hash( + owner: Principal, + approved: Principal, + token_id: u64, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, approved); + bytes.extend_from_slice(&token_id.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn nft_approval_for_all_payload_hash( + owner: Principal, + operator: Principal, + approved: bool, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approval_for_all"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, operator); + bytes.push(u8::from(approved)); + host_queries::keccak256(bytes) +} + fn proxy_value_payload_hash(value: u64) -> [u8; 32] { let mut bytes = Vec::from(&b"proxy.value"[..]); bytes.extend_from_slice(&value.to_be_bytes()); @@ -558,6 +669,14 @@ fn payload_amount_for_variant(amount: u64, variant: &str) -> u64 { } } +fn payload_bool_for_variant(value: bool, variant: &str) -> bool { + if variant == "bad-payload" { + !value + } else { + value + } +} + fn variant_arg(args: &[String]) -> &str { args.get(6).map(String::as_str).unwrap_or("valid") } @@ -604,6 +723,18 @@ fn principal_arg( } } +fn parse_bool_arg( + args: &[String], + index: usize, + name: &str, +) -> Result> { + match arg(args, index, name)? { + "true" => Ok(true), + "false" => Ok(false), + value => Err(format!("invalid {name}: {value}").into()), + } +} + fn domain_arg( args: &[String], index: usize, @@ -612,11 +743,20 @@ fn domain_arg( "counter" => Ok(SET_VALUE_DOMAIN), "drc20-admin" => Ok(TOKEN_ADMIN_DOMAIN), "drc721-admin" => Ok(NFT_ADMIN_DOMAIN), + "drc721-approval" => Ok(NFT_SIGNED_APPROVE_DOMAIN), "proxy-admin" => Ok(PROXY_ADMIN_DOMAIN), other => Err(format!("unknown domain: {other}").into()), } } +fn drc721_approved_principal() -> Principal { + Principal::Contract(ContractId::from_bytes([55u8; 32])) +} + +fn drc721_operator_principal() -> Principal { + Principal::Contract(ContractId::from_bytes([56u8; 32])) +} + fn parse_hex_32(hex: &str) -> Result<[u8; 32], Box> { let hex = hex.strip_prefix("0x").unwrap_or(hex); if hex.len() != 64 { diff --git a/standards/dusk-contract-standards/src/token/drc721/mod.rs b/standards/dusk-contract-standards/src/token/drc721/mod.rs index 3db04986..c7d6823e 100644 --- a/standards/dusk-contract-standards/src/token/drc721/mod.rs +++ b/standards/dusk-contract-standards/src/token/drc721/mod.rs @@ -14,8 +14,9 @@ pub use royalty::{ }; pub use types::{ ApproveCall, BalanceOf, GetApproved, Init, InitToken, IsApprovedForAll, - OwnerOf, SetApprovalForAllCall, TokenByIndex, TokenOfOwnerByIndex, - TokenUri, TokensOf, TransferFromCall, + OwnerOf, SetApprovalForAllCall, SignedApproveCall, + SignedSetApprovalForAllCall, TokenByIndex, TokenOfOwnerByIndex, TokenUri, + TokensOf, TransferFromCall, }; /// Reserved zero principal. diff --git a/standards/dusk-contract-standards/src/token/drc721/types.rs b/standards/dusk-contract-standards/src/token/drc721/types.rs index 04ba2903..d031da6f 100644 --- a/standards/dusk-contract-standards/src/token/drc721/types.rs +++ b/standards/dusk-contract-standards/src/token/drc721/types.rs @@ -6,6 +6,7 @@ use alloc::vec::Vec; use bytecheck::CheckBytes; use rkyv::{Archive, Deserialize, Serialize}; +use crate::auth::SignedAuthorization; use crate::core::Principal; /// Initial token. @@ -154,6 +155,36 @@ pub struct SetApprovalForAllCall { pub approved: bool, } +/// Signed token approval call for Moonlight/Phoenix owner authorization. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SignedApproveCall { + /// Token owner. Must match the signed action principal and current owner. + pub owner: Principal, + /// Approved account. + pub approved: Principal, + /// Token id. + pub token_id: u64, + /// Replay-protected authorization for this approval payload. + pub authorization: SignedAuthorization, +} + +/// Signed operator approval call for Moonlight/Phoenix owner authorization. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SignedSetApprovalForAllCall { + /// Token owner. Must match the signed action principal. + pub owner: Principal, + /// Operator. + pub operator: Principal, + /// Approval. + pub approved: bool, + /// Replay-protected authorization for this approval payload. + pub authorization: SignedAuthorization, +} + /// Transfer call. #[derive( Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, diff --git a/standards/dusk-contract-standards/tests/examples_vm.rs b/standards/dusk-contract-standards/tests/examples_vm.rs index 2a779e40..bbbbcfaa 100644 --- a/standards/dusk-contract-standards/tests/examples_vm.rs +++ b/standards/dusk-contract-standards/tests/examples_vm.rs @@ -12,7 +12,9 @@ use dusk_contract_standards::token::drc20::{ SignedApproveCall, }; use dusk_contract_standards::token::drc721::{ - Init as Init721, InitToken, OwnerOf, TokensOf, + GetApproved, Init as Init721, InitToken, IsApprovedForAll, OwnerOf, + SignedApproveCall as SignedNftApproveCall, SignedSetApprovalForAllCall, + TokensOf, }; use dusk_core::abi::{ContractId, Metadata}; use dusk_core::signatures::bls::{ @@ -43,6 +45,9 @@ const NFT_ADMIN_DOMAIN: NonceDomain = [21u8; 32]; const NFT_MINT_ACTION: [u8; 32] = [22u8; 32]; const NFT_PAUSE_ACTION: [u8; 32] = [23u8; 32]; const NFT_UNPAUSE_ACTION: [u8; 32] = [24u8; 32]; +const NFT_SIGNED_APPROVE_DOMAIN: NonceDomain = [29u8; 32]; +const NFT_SIGNED_APPROVE_ACTION: [u8; 32] = [30u8; 32]; +const NFT_SIGNED_APPROVAL_FOR_ALL_ACTION: [u8; 32] = [31u8; 32]; const PROXY_ADMIN_DOMAIN: NonceDomain = [31u8; 32]; const SET_PROXY_VALUE_ACTION: [u8; 32] = [32u8; 32]; @@ -610,6 +615,150 @@ fn exercise_drc721_signed_calls( assert_eq!(owner, admin); assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); + let approved = Principal::Contract(ContractId::from_bytes([55u8; 32])); + let approve_action = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVE_ACTION, + 0, + 0, + nft_approve_payload_hash(admin, approved, 43), + ); + let approve_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + approve_action, + )); + session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: approve_auth.clone(), + }, + GAS_LIMIT, + ) + .expect("approve NFT by signed Phoenix owner"); + let actual_approved: Principal = session + .call( + contract, + "get_approved", + &GetApproved { token_id: 43 }, + GAS_LIMIT, + ) + .expect("query signed NFT approval") + .data; + assert_eq!(actual_approved, approved); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 1, + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: approve_auth, + }, + GAS_LIMIT, + ) + .is_err()); + + let bad_payload_action = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVE_ACTION, + 1, + 0, + nft_approve_payload_hash(admin, approved, 44), + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + bad_payload_action, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 1, + ); + + let operator = Principal::Contract(ContractId::from_bytes([56u8; 32])); + let operator_action = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + 1, + 0, + nft_approval_for_all_payload_hash(admin, operator, true), + ); + session + .call::<_, ()>( + contract, + "set_approval_for_all_by_authorization", + &SignedSetApprovalForAllCall { + owner: admin, + operator, + approved: true, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + operator_action, + )), + }, + GAS_LIMIT, + ) + .expect("set NFT operator approval by signed Phoenix owner"); + let operator_approved: bool = session + .call( + contract, + "is_approved_for_all", + &IsApprovedForAll { + owner: admin, + operator, + }, + GAS_LIMIT, + ) + .expect("query signed NFT operator approval") + .data; + assert!(operator_approved); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 2, + ); + let pause_action = authorized_action( contract, admin, @@ -638,6 +787,54 @@ fn exercise_drc721_signed_calls( assert!(paused); assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 2); + let paused_operator_action = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + 2, + 0, + nft_approval_for_all_payload_hash(admin, operator, false), + ); + session + .call::<_, ()>( + contract, + "set_approval_for_all_by_authorization", + &SignedSetApprovalForAllCall { + owner: admin, + operator, + approved: false, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + paused_operator_action, + )), + }, + GAS_LIMIT, + ) + .expect("signed NFT operator approval remains available while paused"); + let operator_approved: bool = session + .call( + contract, + "is_approved_for_all", + &IsApprovedForAll { + owner: admin, + operator, + }, + GAS_LIMIT, + ) + .expect("query paused signed NFT operator approval") + .data; + assert!(!operator_approved); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 3, + ); + let paused_mint = SignedAuthorization::Phoenix(phoenix_auth( &mut rng, &admin_sk, @@ -1231,6 +1428,30 @@ fn nft_mint_payload_hash(to: Principal, token_id: u64) -> [u8; 32] { host_queries::keccak256(bytes) } +fn nft_approve_payload_hash( + owner: Principal, + approved: Principal, + token_id: u64, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, approved); + bytes.extend_from_slice(&token_id.to_be_bytes()); + host_queries::keccak256(bytes) +} + +fn nft_approval_for_all_payload_hash( + owner: Principal, + operator: Principal, + approved: bool, +) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approval_for_all"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, operator); + bytes.push(u8::from(approved)); + host_queries::keccak256(bytes) +} + fn empty_payload_hash(tag: &[u8]) -> [u8; 32] { host_queries::keccak256(Vec::from(tag)) } diff --git a/standards/examples/drc721_collection/src/lib.rs b/standards/examples/drc721_collection/src/lib.rs index 9e51de31..be37cbda 100644 --- a/standards/examples/drc721_collection/src/lib.rs +++ b/standards/examples/drc721_collection/src/lib.rs @@ -20,6 +20,9 @@ pub const SET_DEFAULT_ROYALTY_ACTION: [u8; 32] = [25u8; 32]; pub const CLEAR_DEFAULT_ROYALTY_ACTION: [u8; 32] = [26u8; 32]; pub const SET_TOKEN_ROYALTY_ACTION: [u8; 32] = [27u8; 32]; pub const CLEAR_TOKEN_ROYALTY_ACTION: [u8; 32] = [28u8; 32]; +pub const NFT_SIGNED_APPROVE_DOMAIN: NonceDomain = [29u8; 32]; +pub const NFT_SIGNED_APPROVE_ACTION: [u8; 32] = [30u8; 32]; +pub const NFT_SIGNED_APPROVAL_FOR_ALL_ACTION: [u8; 32] = [31u8; 32]; #[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -81,8 +84,9 @@ mod drc721_collection { AdminCall, ClearTokenRoyaltyCall, Init, MintCall, SetDefaultRoyaltyCall, SetTokenRoyaltyCall, CLEAR_DEFAULT_ROYALTY_ACTION, CLEAR_TOKEN_ROYALTY_ACTION, MINT_ACTION, - NFT_ADMIN_DOMAIN, PAUSE_ACTION, SET_DEFAULT_ROYALTY_ACTION, - SET_TOKEN_ROYALTY_ACTION, UNPAUSE_ACTION, + NFT_ADMIN_DOMAIN, NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + NFT_SIGNED_APPROVE_ACTION, NFT_SIGNED_APPROVE_DOMAIN, PAUSE_ACTION, + SET_DEFAULT_ROYALTY_ACTION, SET_TOKEN_ROYALTY_ACTION, UNPAUSE_ACTION, }; use dusk_contract_standards::access::events::{ Paused, Unpaused, PAUSED_TOPIC, UNPAUSED_TOPIC, @@ -106,8 +110,9 @@ mod drc721_collection { use dusk_contract_standards::token::drc721::{ ApproveCall, BalanceOf, Drc721, GetApproved, IsApprovedForAll, OwnerOf, RoyaltyInfo, RoyaltyQuery, RoyaltyQuote, RoyaltyRegistry, - SetApprovalForAllCall, TokenByIndex, TokenOfOwnerByIndex, TokenUri, - TokensOf, TransferFromCall, + SetApprovalForAllCall, SignedApproveCall, SignedSetApprovalForAllCall, + TokenByIndex, TokenOfOwnerByIndex, TokenUri, TokensOf, + TransferFromCall, }; use dusk_core::abi; @@ -229,6 +234,76 @@ mod drc721_collection { Self::emit_approval_for_all(event); } + #[contract(emits = [(APPROVAL_TOPIC, Drc721Approval)])] + pub fn approve_by_authorization(&mut self, args: SignedApproveCall) { + let owner = self.token.owner_of(OwnerOf { + token_id: args.token_id, + }); + if args.owner != owner || args.approved == owner { + panic!("{}", error::UNAUTHORIZED); + } + let principal = self.authorizations.authorize_signed_action( + &args.authorization, + ActionEnvelope::new( + abi::self_id(), + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVE_ACTION, + approve_payload_hash( + args.owner, + args.approved, + args.token_id, + ), + ), + now(), + ); + if principal != args.owner { + panic!("{}", error::UNAUTHORIZED); + } + let event = self.token.approve( + args.owner, + ApproveCall { + approved: args.approved, + token_id: args.token_id, + }, + ); + Self::emit_approval(event); + } + + #[contract(emits = [(APPROVAL_FOR_ALL_TOPIC, Drc721ApprovalForAll)])] + pub fn set_approval_for_all_by_authorization( + &mut self, + args: SignedSetApprovalForAllCall, + ) { + if args.operator.is_zero() || args.operator == args.owner { + panic!("{}", error::UNAUTHORIZED); + } + let principal = self.authorizations.authorize_signed_action( + &args.authorization, + ActionEnvelope::new( + abi::self_id(), + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + approval_for_all_payload_hash( + args.owner, + args.operator, + args.approved, + ), + ), + now(), + ); + if principal != args.owner { + panic!("{}", error::UNAUTHORIZED); + } + let event = self.token.set_approval_for_all( + args.owner, + SetApprovalForAllCall { + operator: args.operator, + approved: args.approved, + }, + ); + Self::emit_approval_for_all(event); + } + #[contract(emits = [(TRANSFER_TOPIC, Drc721Transfer)])] pub fn transfer_from(&mut self, args: TransferFromCall) { self.pausable.assert_not_paused(); @@ -480,6 +555,30 @@ mod drc721_collection { abi::keccak256(bytes) } + fn approve_payload_hash( + owner: Principal, + approved: Principal, + token_id: u64, + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approve"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, approved); + bytes.extend_from_slice(&token_id.to_be_bytes()); + abi::keccak256(bytes) + } + + fn approval_for_all_payload_hash( + owner: Principal, + operator: Principal, + approved: bool, + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.approval_for_all"[..]); + push_principal(&mut bytes, owner); + push_principal(&mut bytes, operator); + bytes.push(u8::from(approved)); + abi::keccak256(bytes) + } + fn royalty_payload_hash( token_id: Option, info: RoyaltyInfo, From 0678c7bc439e01b3c19b871cbb9938e2d508e68f Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sun, 26 Apr 2026 00:11:49 +0200 Subject: [PATCH 14/35] standards: harden signed invariant coverage --- .../tests/examples_vm.rs | 946 +++++++++++++++++- .../examples/drc20_roles_pausable/src/lib.rs | 16 +- .../examples/drc721_collection/src/lib.rs | 20 +- 3 files changed, 933 insertions(+), 49 deletions(-) diff --git a/standards/dusk-contract-standards/tests/examples_vm.rs b/standards/dusk-contract-standards/tests/examples_vm.rs index bbbbcfaa..31f5fbe4 100644 --- a/standards/dusk-contract-standards/tests/examples_vm.rs +++ b/standards/dusk-contract-standards/tests/examples_vm.rs @@ -9,12 +9,14 @@ use dusk_contract_standards::auth::{ use dusk_contract_standards::core::{NonceDomain, Principal}; use dusk_contract_standards::token::drc20::{ Allowance, BalanceOf as BalanceOf20, Init as Init20, InitBalance, - SignedApproveCall, + SignedApproveCall, TransferCall as TransferCall20, + TransferFromCall as TransferFromCall20, }; use dusk_contract_standards::token::drc721::{ GetApproved, Init as Init721, InitToken, IsApprovedForAll, OwnerOf, - SignedApproveCall as SignedNftApproveCall, SignedSetApprovalForAllCall, - TokensOf, + RoyaltyInfo, SignedApproveCall as SignedNftApproveCall, + SignedSetApprovalForAllCall, TokensOf, + TransferFromCall as TransferFromCall721, }; use dusk_core::abi::{ContractId, Metadata}; use dusk_core::signatures::bls::{ @@ -45,6 +47,7 @@ const NFT_ADMIN_DOMAIN: NonceDomain = [21u8; 32]; const NFT_MINT_ACTION: [u8; 32] = [22u8; 32]; const NFT_PAUSE_ACTION: [u8; 32] = [23u8; 32]; const NFT_UNPAUSE_ACTION: [u8; 32] = [24u8; 32]; +const NFT_SET_DEFAULT_ROYALTY_ACTION: [u8; 32] = [25u8; 32]; const NFT_SIGNED_APPROVE_DOMAIN: NonceDomain = [29u8; 32]; const NFT_SIGNED_APPROVE_ACTION: [u8; 32] = [30u8; 32]; const NFT_SIGNED_APPROVAL_FOR_ALL_ACTION: [u8; 32] = [31u8; 32]; @@ -119,6 +122,13 @@ struct Drc721AdminCall { authorization: Option, } +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct Drc721SetDefaultRoyaltyCall { + info: RoyaltyInfo, + authorization: Option, +} + #[derive( Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, )] @@ -390,6 +400,152 @@ fn exercise_drc20_signed_calls( .is_err()); assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + let wrong_contract_mint = authorized_action( + ContractId::from_bytes([77u8; 32]), + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 1, + 0, + mint_payload_hash(admin, 8), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + wrong_contract_mint, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + + let wrong_action_mint = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + [0xab; 32], + 1, + 0, + mint_payload_hash(admin, 8), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + wrong_action_mint, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + + let expired_mint = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 1, + 9, + mint_payload_hash(admin, 8), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, expired_mint), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + + let cap_exceeded_mint = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 1, + 0, + mint_payload_hash(admin, 10_000), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: admin, + amount: 10_000, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + cap_exceeded_mint, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + + let zero = Principal::Contract(ContractId::from_bytes([0u8; 32])); + let zero_recipient_mint = authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + MINT_ACTION, + 1, + 0, + mint_payload_hash(zero, 1), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc20MintCall { + to: zero, + amount: 1, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + zero_recipient_mint, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 1); + let spender_sk = moonlight_secret(30); let spender_pk = BlsPublicKey::from(&spender_sk); let spender = Principal::moonlight(&spender_pk); @@ -450,6 +606,77 @@ fn exercise_drc20_signed_calls( ) .is_err()); + let bad_approve_payload = authorized_action( + contract, + admin, + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + 1, + 0, + approve_payload_hash(admin, spender, 34), + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedApproveCall { + owner: admin, + spender, + amount: 35, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + bad_approve_payload, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, SIGNED_APPROVE_DOMAIN, 1); + let allowance: u64 = session + .call( + contract, + "allowance", + &Allowance { + owner: admin, + spender, + }, + GAS_LIMIT, + ) + .expect("query signed allowance after bad payload") + .data; + assert_eq!(allowance, 33); + + let expired_approve = authorized_action( + contract, + admin, + SIGNED_APPROVE_DOMAIN, + SIGNED_APPROVE_ACTION, + 1, + 9, + approve_payload_hash(admin, spender, 34), + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedApproveCall { + owner: admin, + spender, + amount: 34, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + expired_approve, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, SIGNED_APPROVE_DOMAIN, 1); + let moonlight_owner_sk = moonlight_secret(31); let moonlight_owner_pk = BlsPublicKey::from(&moonlight_owner_sk); let moonlight_owner = Principal::moonlight(&moonlight_owner_pk); @@ -515,6 +742,56 @@ fn exercise_drc20_signed_calls( assert!(paused); assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 2); + let duplicate_pause = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + PAUSE_ACTION, + 2, + 0, + empty_payload_hash(b"drc20.pause"), + ), + )); + assert!(session + .call::<_, ()>( + contract, + "pause", + &Drc20AdminCall { + authorization: Some(duplicate_pause), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 2); + + assert!(session + .call::<_, ()>( + contract, + "transfer", + &TransferCall20 { + to: admin, + amount: 1, + }, + GAS_LIMIT, + ) + .is_err()); + assert!(session + .call::<_, ()>( + contract, + "transfer_from", + &TransferFromCall20 { + owner: admin, + to: admin, + amount: 1, + }, + GAS_LIMIT, + ) + .is_err()); + let paused_mint = SignedAuthorization::Phoenix(phoenix_auth( &mut rng, &admin_sk, @@ -573,6 +850,32 @@ fn exercise_drc20_signed_calls( .data; assert!(!paused); assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 3); + + let duplicate_unpause = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + authorized_action( + contract, + admin, + TOKEN_ADMIN_DOMAIN, + UNPAUSE_ACTION, + 3, + 0, + empty_payload_hash(b"drc20.unpause"), + ), + )); + assert!(session + .call::<_, ()>( + contract, + "unpause", + &Drc20AdminCall { + authorization: Some(duplicate_unpause), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 3); } fn exercise_drc721_signed_calls( @@ -615,8 +918,124 @@ fn exercise_drc721_signed_calls( assert_eq!(owner, admin); assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); - let approved = Principal::Contract(ContractId::from_bytes([55u8; 32])); - let approve_action = authorized_action( + let wrong_contract_mint = authorized_action( + ContractId::from_bytes([78u8; 32]), + admin, + NFT_ADMIN_DOMAIN, + NFT_MINT_ACTION, + 1, + 0, + nft_mint_payload_hash(admin, 44), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc721MintCall { + to: admin, + token_id: 44, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + wrong_contract_mint, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); + + let wrong_action_mint = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + [0xac; 32], + 1, + 0, + nft_mint_payload_hash(admin, 44), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc721MintCall { + to: admin, + token_id: 44, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + wrong_action_mint + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); + + let expired_mint = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_MINT_ACTION, + 1, + 9, + nft_mint_payload_hash(admin, 44), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc721MintCall { + to: admin, + token_id: 44, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, expired_mint), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); + + let zero = Principal::Contract(ContractId::from_bytes([0u8; 32])); + let zero_recipient_mint = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_MINT_ACTION, + 1, + 0, + nft_mint_payload_hash(zero, 44), + ); + assert!(session + .call::<_, ()>( + contract, + "mint", + &Drc721MintCall { + to: zero, + token_id: 44, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + zero_recipient_mint, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); + + let approved = Principal::Contract(ContractId::from_bytes([55u8; 32])); + let approve_action = authorized_action( contract, admin, NFT_SIGNED_APPROVE_DOMAIN, @@ -647,56 +1066,266 @@ fn exercise_drc721_signed_calls( let actual_approved: Principal = session .call( contract, - "get_approved", - &GetApproved { token_id: 43 }, + "get_approved", + &GetApproved { token_id: 43 }, + GAS_LIMIT, + ) + .expect("query signed NFT approval") + .data; + assert_eq!(actual_approved, approved); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 1, + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: approve_auth, + }, + GAS_LIMIT, + ) + .is_err()); + + let bad_payload_action = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVE_ACTION, + 1, + 0, + nft_approve_payload_hash(admin, approved, 44), + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + bad_payload_action, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 1, + ); + + let wrong_contract_approval = authorized_action( + ContractId::from_bytes([79u8; 32]), + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVE_ACTION, + 1, + 0, + nft_approve_payload_hash(admin, approved, 43), + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + wrong_contract_approval, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 1, + ); + + let wrong_action_approval = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + [0xad; 32], + 1, + 0, + nft_approve_payload_hash(admin, approved, 43), + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + wrong_action_approval, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 1, + ); + + let expired_approval = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVE_ACTION, + 1, + 9, + nft_approve_payload_hash(admin, approved, 43), + ); + assert!(session + .call::<_, ()>( + contract, + "approve_by_authorization", + &SignedNftApproveCall { + owner: admin, + approved, + token_id: 43, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + expired_approval, + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 1, + ); + + let operator = Principal::Contract(ContractId::from_bytes([56u8; 32])); + let operator_action = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + 1, + 0, + nft_approval_for_all_payload_hash(admin, operator, true), + ); + let operator_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + operator_action, + )); + session + .call::<_, ()>( + contract, + "set_approval_for_all_by_authorization", + &SignedSetApprovalForAllCall { + owner: admin, + operator, + approved: true, + authorization: operator_auth.clone(), + }, + GAS_LIMIT, + ) + .expect("set NFT operator approval by signed Phoenix owner"); + let operator_approved: bool = session + .call( + contract, + "is_approved_for_all", + &IsApprovedForAll { + owner: admin, + operator, + }, GAS_LIMIT, ) - .expect("query signed NFT approval") + .expect("query signed NFT operator approval") .data; - assert_eq!(actual_approved, approved); + assert!(operator_approved); assert_contract_nonce( session, contract, admin, NFT_SIGNED_APPROVE_DOMAIN, - 1, + 2, ); assert!(session .call::<_, ()>( contract, - "approve_by_authorization", - &SignedNftApproveCall { + "set_approval_for_all_by_authorization", + &SignedSetApprovalForAllCall { owner: admin, - approved, - token_id: 43, - authorization: approve_auth, + operator, + approved: true, + authorization: operator_auth, }, GAS_LIMIT, ) .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 2, + ); - let bad_payload_action = authorized_action( + let bad_operator_payload = authorized_action( contract, admin, NFT_SIGNED_APPROVE_DOMAIN, - NFT_SIGNED_APPROVE_ACTION, - 1, + NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + 2, 0, - nft_approve_payload_hash(admin, approved, 44), + nft_approval_for_all_payload_hash(admin, operator, false), ); assert!(session .call::<_, ()>( contract, - "approve_by_authorization", - &SignedNftApproveCall { + "set_approval_for_all_by_authorization", + &SignedSetApprovalForAllCall { owner: admin, - approved, - token_id: 43, + operator, + approved: true, authorization: SignedAuthorization::Phoenix(phoenix_auth( &mut rng, &admin_sk, admin_pk, - bad_payload_action, + bad_operator_payload, )), }, GAS_LIMIT, @@ -707,50 +1336,71 @@ fn exercise_drc721_signed_calls( contract, admin, NFT_SIGNED_APPROVE_DOMAIN, - 1, + 2, ); - let operator = Principal::Contract(ContractId::from_bytes([56u8; 32])); - let operator_action = authorized_action( + let wrong_action_operator = authorized_action( contract, admin, NFT_SIGNED_APPROVE_DOMAIN, - NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, - 1, + [0xae; 32], + 2, 0, - nft_approval_for_all_payload_hash(admin, operator, true), + nft_approval_for_all_payload_hash(admin, operator, false), ); - session + assert!(session .call::<_, ()>( contract, "set_approval_for_all_by_authorization", &SignedSetApprovalForAllCall { owner: admin, operator, - approved: true, + approved: false, authorization: SignedAuthorization::Phoenix(phoenix_auth( &mut rng, &admin_sk, admin_pk, - operator_action, + wrong_action_operator, )), }, GAS_LIMIT, ) - .expect("set NFT operator approval by signed Phoenix owner"); - let operator_approved: bool = session - .call( + .is_err()); + assert_contract_nonce( + session, + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + 2, + ); + + let expired_operator = authorized_action( + contract, + admin, + NFT_SIGNED_APPROVE_DOMAIN, + NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, + 2, + 9, + nft_approval_for_all_payload_hash(admin, operator, false), + ); + assert!(session + .call::<_, ()>( contract, - "is_approved_for_all", - &IsApprovedForAll { + "set_approval_for_all_by_authorization", + &SignedSetApprovalForAllCall { owner: admin, operator, + approved: false, + authorization: SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + expired_operator, + )), }, GAS_LIMIT, ) - .expect("query signed NFT operator approval") - .data; - assert!(operator_approved); + .is_err()); assert_contract_nonce( session, contract, @@ -759,6 +1409,39 @@ fn exercise_drc721_signed_calls( 2, ); + let invalid_royalty = RoyaltyInfo { + receiver: admin, + basis_points: 10_001, + }; + let invalid_royalty_action = authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_SET_DEFAULT_ROYALTY_ACTION, + 1, + 0, + royalty_payload_hash(None, invalid_royalty), + ); + assert!(session + .call::<_, ()>( + contract, + "set_default_royalty", + &Drc721SetDefaultRoyaltyCall { + info: invalid_royalty, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + invalid_royalty_action, + ), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 1); + let pause_action = authorized_action( contract, admin, @@ -787,6 +1470,45 @@ fn exercise_drc721_signed_calls( assert!(paused); assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 2); + let duplicate_pause = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_PAUSE_ACTION, + 2, + 0, + empty_payload_hash(b"drc721.pause"), + ), + )); + assert!(session + .call::<_, ()>( + contract, + "pause", + &Drc721AdminCall { + authorization: Some(duplicate_pause), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 2); + + assert!(session + .call::<_, ()>( + contract, + "transfer_from", + &TransferFromCall721 { + from: admin, + to: admin, + token_id: 43, + }, + GAS_LIMIT, + ) + .is_err()); + let paused_operator_action = authorized_action( contract, admin, @@ -893,6 +1615,32 @@ fn exercise_drc721_signed_calls( .data; assert!(!paused); assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 3); + + let duplicate_unpause = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &admin_sk, + admin_pk, + authorized_action( + contract, + admin, + NFT_ADMIN_DOMAIN, + NFT_UNPAUSE_ACTION, + 3, + 0, + empty_payload_hash(b"drc721.unpause"), + ), + )); + assert!(session + .call::<_, ()>( + contract, + "unpause", + &Drc721AdminCall { + authorization: Some(duplicate_unpause), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, NFT_ADMIN_DOMAIN, 3); } fn exercise_proxy_signed_admin_call( @@ -943,6 +1691,108 @@ fn exercise_proxy_signed_admin_call( GAS_LIMIT, ) .is_err()); + assert_contract_nonce(session, contract, admin, PROXY_ADMIN_DOMAIN, 1); + let value: u64 = session + .call(contract, "value", &(), GAS_LIMIT) + .expect("query proxy value after replay") + .data; + assert_eq!(value, 7); + + let wrong_payload = authorized_action( + contract, + admin, + PROXY_ADMIN_DOMAIN, + SET_PROXY_VALUE_ACTION, + 1, + 0, + proxy_value_payload_hash(8), + ); + assert!(session + .call::<_, ()>( + contract, + "set_value", + &ProxySetValue { + value: 9, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, wrong_payload), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, PROXY_ADMIN_DOMAIN, 1); + + let wrong_contract = authorized_action( + ContractId::from_bytes([80u8; 32]), + admin, + PROXY_ADMIN_DOMAIN, + SET_PROXY_VALUE_ACTION, + 1, + 0, + proxy_value_payload_hash(8), + ); + assert!(session + .call::<_, ()>( + contract, + "set_value", + &ProxySetValue { + value: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, wrong_contract), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, PROXY_ADMIN_DOMAIN, 1); + + let wrong_action = authorized_action( + contract, + admin, + PROXY_ADMIN_DOMAIN, + [0xaf; 32], + 1, + 0, + proxy_value_payload_hash(8), + ); + assert!(session + .call::<_, ()>( + contract, + "set_value", + &ProxySetValue { + value: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, wrong_action), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, PROXY_ADMIN_DOMAIN, 1); + + let expired = authorized_action( + contract, + admin, + PROXY_ADMIN_DOMAIN, + SET_PROXY_VALUE_ACTION, + 1, + 9, + proxy_value_payload_hash(8), + ); + assert!(session + .call::<_, ()>( + contract, + "set_value", + &ProxySetValue { + value: 8, + authorization: Some(SignedAuthorization::Phoenix( + phoenix_auth(&mut rng, &admin_sk, admin_pk, expired), + )), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, contract, admin, PROXY_ADMIN_DOMAIN, 1); } fn exercise_authorization_counter_signed_calls( @@ -1452,6 +2302,20 @@ fn nft_approval_for_all_payload_hash( host_queries::keccak256(bytes) } +fn royalty_payload_hash(token_id: Option, info: RoyaltyInfo) -> [u8; 32] { + let mut bytes = Vec::from(&b"drc721.royalty"[..]); + match token_id { + Some(token_id) => { + bytes.push(1); + bytes.extend_from_slice(&token_id.to_be_bytes()); + } + None => bytes.push(0), + } + push_principal(&mut bytes, info.receiver); + bytes.extend_from_slice(&info.basis_points.to_be_bytes()); + host_queries::keccak256(bytes) +} + fn empty_payload_hash(tag: &[u8]) -> [u8; 32] { host_queries::keccak256(Vec::from(tag)) } diff --git a/standards/examples/drc20_roles_pausable/src/lib.rs b/standards/examples/drc20_roles_pausable/src/lib.rs index 94e1679e..b41bcd7a 100644 --- a/standards/examples/drc20_roles_pausable/src/lib.rs +++ b/standards/examples/drc20_roles_pausable/src/lib.rs @@ -328,6 +328,12 @@ mod drc20_roles_pausable { #[contract(emits = [(TRANSFER_TOPIC, Drc20Transfer)])] pub fn mint(&mut self, args: MintCall) { self.pausable.assert_not_paused(); + if args.to.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if let Some(cap) = self.cap { + cap.assert_mint(self.token.total_supply(), args.amount); + } self.authorize_role_action( MINTER_ROLE, args.authorization.as_ref(), @@ -338,9 +344,6 @@ mod drc20_roles_pausable { mint_payload_hash(args.to, args.amount), ), ); - if let Some(cap) = self.cap { - cap.assert_mint(self.token.total_supply(), args.amount); - } let event = self.token.mint(args.to, args.amount); self.votes .move_units(None, Some(event.to), event.amount, now()); @@ -359,6 +362,7 @@ mod drc20_roles_pausable { #[contract(emits = [(PAUSED_TOPIC, Paused)])] pub fn pause(&mut self, args: AdminCall) { + self.pausable.assert_not_paused(); let caller = self.authorize_role_action( PAUSER_ROLE, args.authorization.as_ref(), @@ -369,13 +373,13 @@ mod drc20_roles_pausable { empty_payload_hash(b"drc20.pause"), ), ); - self.pausable.assert_not_paused(); self.pausable.pause(); Self::emit_paused(caller); } #[contract(emits = [(UNPAUSED_TOPIC, Unpaused)])] pub fn unpause(&mut self, args: AdminCall) { + self.pausable.assert_paused(); let caller = self.authorize_role_action( PAUSER_ROLE, args.authorization.as_ref(), @@ -386,7 +390,6 @@ mod drc20_roles_pausable { empty_payload_hash(b"drc20.unpause"), ), ); - self.pausable.assert_paused(); self.pausable.unpause(); Self::emit_unpaused(caller); } @@ -397,6 +400,9 @@ mod drc20_roles_pausable { #[contract(emits = [(ROLE_GRANTED_TOPIC, RoleGranted)])] pub fn grant_role(&mut self, args: RoleCall) { + if args.account.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } let admin = self.access.get_role_admin(args.role); let caller = self.authorize_role_action( admin, diff --git a/standards/examples/drc721_collection/src/lib.rs b/standards/examples/drc721_collection/src/lib.rs index be37cbda..e4c7bbc5 100644 --- a/standards/examples/drc721_collection/src/lib.rs +++ b/standards/examples/drc721_collection/src/lib.rs @@ -112,7 +112,7 @@ mod drc721_collection { RoyaltyInfo, RoyaltyQuery, RoyaltyQuote, RoyaltyRegistry, SetApprovalForAllCall, SignedApproveCall, SignedSetApprovalForAllCall, TokenByIndex, TokenOfOwnerByIndex, TokenUri, TokensOf, - TransferFromCall, + TransferFromCall, MAX_BASIS_POINTS, }; use dusk_core::abi; @@ -319,6 +319,9 @@ mod drc721_collection { #[contract(emits = [(TRANSFER_TOPIC, Drc721Transfer)])] pub fn mint(&mut self, args: MintCall) { self.pausable.assert_not_paused(); + if args.to.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } self.authorize_owner_action( args.authorization.as_ref(), ActionEnvelope::new( @@ -341,6 +344,7 @@ mod drc721_collection { #[contract(emits = [(PAUSED_TOPIC, Paused)])] pub fn pause(&mut self, args: AdminCall) { + self.pausable.assert_not_paused(); let caller = self.authorize_owner_action( args.authorization.as_ref(), ActionEnvelope::new( @@ -350,13 +354,13 @@ mod drc721_collection { empty_payload_hash(b"drc721.pause"), ), ); - self.pausable.assert_not_paused(); self.pausable.pause(); Self::emit_paused(caller); } #[contract(emits = [(UNPAUSED_TOPIC, Unpaused)])] pub fn unpause(&mut self, args: AdminCall) { + self.pausable.assert_paused(); let caller = self.authorize_owner_action( args.authorization.as_ref(), ActionEnvelope::new( @@ -366,7 +370,6 @@ mod drc721_collection { empty_payload_hash(b"drc721.unpause"), ), ); - self.pausable.assert_paused(); self.pausable.unpause(); Self::emit_unpaused(caller); } @@ -377,6 +380,7 @@ mod drc721_collection { #[contract(emits = [(DEFAULT_ROYALTY_SET_TOPIC, DefaultRoyaltySet)])] pub fn set_default_royalty(&mut self, args: SetDefaultRoyaltyCall) { + validate_royalty(args.info); let caller = self.authorize_owner_action( args.authorization.as_ref(), ActionEnvelope::new( @@ -409,6 +413,7 @@ mod drc721_collection { #[contract(emits = [(TOKEN_ROYALTY_SET_TOPIC, TokenRoyaltySet)])] pub fn set_token_royalty(&mut self, args: SetTokenRoyaltyCall) { + validate_royalty(args.info); let caller = self.authorize_owner_action( args.authorization.as_ref(), ActionEnvelope::new( @@ -602,6 +607,15 @@ mod drc721_collection { abi::keccak256(bytes) } + fn validate_royalty(info: RoyaltyInfo) { + if info.receiver.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if info.basis_points > MAX_BASIS_POINTS { + panic!("DRC721: royalty exceeds sale price"); + } + } + fn push_principal(bytes: &mut Vec, principal: Principal) { let principal = principal.to_bytes(); bytes.extend_from_slice(&(principal.len() as u16).to_be_bytes()); From 277f1502b86facba5236440c3b2148e3e7f5f873 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sun, 26 Apr 2026 02:43:19 +0200 Subject: [PATCH 15/35] standards: harden authorization and token invariants --- .../src/access/access_control.rs | 15 +- .../dusk-contract-standards/src/auth/mod.rs | 146 +++++++++++++++-- .../src/token/drc20/mod.rs | 64 ++++++-- .../src/token/drc721/mod.rs | 3 + .../tests/primitives.rs | 155 ++++++++++++++++++ 5 files changed, 342 insertions(+), 41 deletions(-) diff --git a/standards/dusk-contract-standards/src/access/access_control.rs b/standards/dusk-contract-standards/src/access/access_control.rs index 84358491..d9f2b45a 100644 --- a/standards/dusk-contract-standards/src/access/access_control.rs +++ b/standards/dusk-contract-standards/src/access/access_control.rs @@ -101,9 +101,9 @@ impl AccessControl { let Some(authorization) = authorization else { panic!("{}", error::UNAUTHORIZED); }; - let principal = authorizer.require_signed(authorization); - self.assert_role(role, principal); - principal + authorizer.require_signed_if(authorization, |principal| { + self.has_role(role, principal) + }) } /// Authorizes any principal with `role` through runtime context or an @@ -143,10 +143,11 @@ impl AccessControl { let Some(authorization) = authorization else { panic!("{}", error::UNAUTHORIZED); }; - let principal = - authorizer.require_signed_action(authorization, envelope); - self.assert_role(role, principal); - principal + authorizer.require_signed_action_if( + authorization, + envelope, + |principal| self.has_role(role, principal), + ) } /// Returns a role's admin role. diff --git a/standards/dusk-contract-standards/src/auth/mod.rs b/standards/dusk-contract-standards/src/auth/mod.rs index 72268949..615c3c90 100644 --- a/standards/dusk-contract-standards/src/auth/mod.rs +++ b/standards/dusk-contract-standards/src/auth/mod.rs @@ -294,6 +294,22 @@ impl<'a> Authorizer<'a> { .authorize_signed(authorization, self.now) } + /// Verifies a signed authorization, applies an authorization predicate, + /// and only then consumes nonce/replay state. + pub fn require_signed_if( + &mut self, + authorization: &SignedAuthorization, + is_authorized: impl FnOnce(Principal) -> bool, + ) -> Principal { + let principal = + self.authorizations.verify_signed(authorization, self.now); + if !is_authorized(principal) { + panic!("{}", error::UNAUTHORIZED); + } + self.authorizations.consume_verified(authorization); + principal + } + /// Verifies a signed authorization for an exact call envelope and consumes /// its nonce/replay state. pub fn require_signed_action( @@ -307,6 +323,26 @@ impl<'a> Authorizer<'a> { self.now, ) } + + /// Verifies a signed authorization for an exact call envelope, applies an + /// authorization predicate, and only then consumes nonce/replay state. + pub fn require_signed_action_if( + &mut self, + authorization: &SignedAuthorization, + envelope: ActionEnvelope, + is_authorized: impl FnOnce(Principal) -> bool, + ) -> Principal { + let principal = self.authorizations.verify_signed_action( + authorization, + envelope, + self.now, + ); + if !is_authorized(principal) { + panic!("{}", error::UNAUTHORIZED); + } + self.authorizations.consume_verified(authorization); + principal + } } /// Authorization manager with nonce and replay-key state. @@ -360,10 +396,11 @@ impl AuthorizationManager { let Some(authorization) = authorization else { panic!("{}", error::UNAUTHORIZED); }; - let principal = self.authorize_signed(authorization, now); + let principal = self.verify_signed(authorization, now); if principal != expected { panic!("{}", error::UNAUTHORIZED); } + self.consume_verified(authorization); principal } @@ -389,11 +426,11 @@ impl AuthorizationManager { let Some(authorization) = authorization else { panic!("{}", error::UNAUTHORIZED); }; - let principal = - self.authorize_signed_action(authorization, envelope, now); + let principal = self.verify_signed_action(authorization, envelope, now); if principal != expected { panic!("{}", error::UNAUTHORIZED); } + self.consume_verified(authorization); principal } @@ -406,27 +443,51 @@ impl AuthorizationManager { &mut self, authorization: &SignedAuthorization, now: u64, + ) -> Principal { + let principal = self.verify_signed(authorization, now); + self.consume_verified(authorization); + principal + } + + /// Verifies a signed authorization for an exact call envelope and consumes + /// its nonce/replay state. + pub fn authorize_signed_action( + &mut self, + authorization: &SignedAuthorization, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + let principal = self.verify_signed_action(authorization, envelope, now); + self.consume_verified(authorization); + principal + } + + /// Verifies a signed authorization without consuming nonce/replay state. + fn verify_signed( + &self, + authorization: &SignedAuthorization, + now: u64, ) -> Principal { match authorization { SignedAuthorization::Moonlight(auth) => { - self.authorize_moonlight(auth, now) + self.verify_moonlight(auth, now) } SignedAuthorization::Phoenix(auth) => { - self.authorize_phoenix(auth, now) + self.verify_phoenix(auth, now) } } } - /// Verifies a signed authorization for an exact call envelope and consumes - /// its nonce/replay state. - pub fn authorize_signed_action( - &mut self, + /// Verifies a signed authorization for an exact call envelope without + /// consuming nonce/replay state. + fn verify_signed_action( + &self, authorization: &SignedAuthorization, envelope: ActionEnvelope, now: u64, ) -> Principal { authorization.assert_envelope(envelope); - self.authorize_signed(authorization, now) + self.verify_signed(authorization, now) } /// Verifies and consumes a Moonlight BLS authorization. @@ -434,6 +495,17 @@ impl AuthorizationManager { &mut self, auth: &MoonlightAuthorization, now: u64, + ) -> Principal { + let principal = self.verify_moonlight(auth, now); + self.consume_action(principal, auth.action.domain, auth.action.nonce); + principal + } + + /// Verifies a Moonlight BLS authorization without consuming nonce state. + fn verify_moonlight( + &self, + auth: &MoonlightAuthorization, + now: u64, ) -> Principal { assert_live(&auth.action, now); let principal = Principal::moonlight(&auth.public_key); @@ -444,8 +516,7 @@ impl AuthorizationManager { if !verify_bls(&message, &auth.public_key, &auth.signature) { panic!("{}", error::UNAUTHORIZED); } - self.nonces - .consume(principal, auth.action.domain, auth.action.nonce); + self.assert_nonce(principal, auth.action.domain, auth.action.nonce); principal } @@ -454,6 +525,21 @@ impl AuthorizationManager { &mut self, auth: &PhoenixSignatureAuthorization, now: u64, + ) -> Principal { + let principal = self.verify_phoenix(auth, now); + self.consume_action(principal, auth.action.domain, auth.action.nonce); + if let Some(key) = auth.replay_key { + self.replays.consume(principal, key); + } + principal + } + + /// Verifies a Phoenix Schnorr authorization without consuming nonce/replay + /// state. + fn verify_phoenix( + &self, + auth: &PhoenixSignatureAuthorization, + now: u64, ) -> Principal { assert_live(&auth.action, now); let principal = Principal::phoenix_public_key(&auth.public_key); @@ -472,11 +558,7 @@ impl AuthorizationManager { panic!("{}", error::REPLAY); } } - self.nonces - .consume(principal, auth.action.domain, auth.action.nonce); - if let Some(key) = auth.replay_key { - self.replays.consume(principal, key); - } + self.assert_nonce(principal, auth.action.domain, auth.action.nonce); principal } @@ -505,6 +587,36 @@ impl AuthorizationManager { ) { self.replays.import_entries(entries); } + + fn assert_nonce( + &self, + principal: Principal, + domain: NonceDomain, + expected: u64, + ) { + if self.nonces.current(principal, domain) != expected { + panic!("{}", error::INVALID_NONCE); + } + } + + fn consume_verified(&mut self, authorization: &SignedAuthorization) { + let action = authorization.action(); + self.consume_action(action.principal, action.domain, action.nonce); + if let SignedAuthorization::Phoenix(auth) = authorization { + if let Some(key) = auth.replay_key { + self.replays.consume(action.principal, key); + } + } + } + + fn consume_action( + &mut self, + principal: Principal, + domain: NonceDomain, + nonce: u64, + ) { + self.nonces.consume(principal, domain, nonce); + } } fn assert_live(action: &AuthorizedAction, now: u64) { diff --git a/standards/dusk-contract-standards/src/token/drc20/mod.rs b/standards/dusk-contract-standards/src/token/drc20/mod.rs index b72be207..4342d80b 100644 --- a/standards/dusk-contract-standards/src/token/drc20/mod.rs +++ b/standards/dusk-contract-standards/src/token/drc20/mod.rs @@ -147,7 +147,7 @@ impl Drc20 { args: ApproveCall, ) -> Approval { self.assert_initialized(); - if args.spender.is_zero() { + if owner.is_zero() || args.spender.is_zero() { panic!("{}", error::ZERO_PRINCIPAL); } self.allowances.insert((owner, args.spender), args.amount); @@ -165,7 +165,7 @@ impl Drc20 { args: IncreaseAllowanceCall, ) -> Approval { self.assert_initialized(); - if args.spender.is_zero() { + if caller.is_zero() || args.spender.is_zero() { panic!("{}", error::ZERO_PRINCIPAL); } let current = self.allowance(Allowance { @@ -190,6 +190,9 @@ impl Drc20 { args: DecreaseAllowanceCall, ) -> Approval { self.assert_initialized(); + if caller.is_zero() || args.spender.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } let current = self.allowance(Allowance { owner: caller, spender: args.spender, @@ -213,6 +216,10 @@ impl Drc20 { args: TransferFromCall, ) -> Transfer { self.assert_initialized(); + if caller.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + self.assert_can_transfer(args.owner, args.to, args.amount); let current = self.allowance(Allowance { owner: args.owner, spender: caller, @@ -257,16 +264,26 @@ impl Drc20 { if to.is_zero() { panic!("{}", error::ZERO_PRINCIPAL); } - let bal = self.balances.entry(to).or_insert(0); - *bal = bal.checked_add(amount).expect(error::OVERFLOW); - self.supply = self.supply.checked_add(amount).expect(error::OVERFLOW); + let current = self.balances.get(&to).copied().unwrap_or(0); + let next = current.checked_add(amount).expect(error::OVERFLOW); + let supply = self.supply.checked_add(amount).expect(error::OVERFLOW); + if next == 0 { + self.balances.remove(&to); + } else { + self.balances.insert(to, next); + } + self.supply = supply; } fn burn_internal(&mut self, from: Principal, amount: u64) { + if from.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } let current = self.balances.get(&from).copied().unwrap_or(0); if current < amount { panic!("DRC20: balance too low"); } + let supply = self.supply.checked_sub(amount).expect(error::UNDERFLOW); if amount != 0 { let next = current - amount; if next == 0 { @@ -274,8 +291,7 @@ impl Drc20 { } else { self.balances.insert(from, next); } - self.supply = - self.supply.checked_sub(amount).expect(error::UNDERFLOW); + self.supply = supply; } } @@ -285,26 +301,40 @@ impl Drc20 { to: Principal, amount: u64, ) -> Transfer { - if to.is_zero() { - panic!("{}", error::ZERO_PRINCIPAL); - } - let current = self.balances.get(&from).copied().unwrap_or(0); - if current < amount { - panic!("DRC20: balance too low"); - } + self.assert_can_transfer(from, to, amount); if amount != 0 { + if from == to { + return Transfer { from, to, amount }; + } + let current = self.balances.get(&from).copied().unwrap_or(0); let next = current - amount; if next == 0 { self.balances.remove(&from); } else { self.balances.insert(from, next); } - let to_balance = self.balances.entry(to).or_insert(0); - *to_balance = - to_balance.checked_add(amount).expect(error::OVERFLOW); + let to_balance = self.balances.get(&to).copied().unwrap_or(0); + self.balances.insert(to, to_balance + amount); } Transfer { from, to, amount } } + + fn assert_can_transfer(&self, from: Principal, to: Principal, amount: u64) { + if from.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if to.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let current = self.balances.get(&from).copied().unwrap_or(0); + if current < amount { + panic!("DRC20: balance too low"); + } + if from != to { + let to_balance = self.balances.get(&to).copied().unwrap_or(0); + to_balance.checked_add(amount).expect(error::OVERFLOW); + } + } } impl Default for Drc20 { diff --git a/standards/dusk-contract-standards/src/token/drc721/mod.rs b/standards/dusk-contract-standards/src/token/drc721/mod.rs index c7d6823e..0eddf7c9 100644 --- a/standards/dusk-contract-standards/src/token/drc721/mod.rs +++ b/standards/dusk-contract-standards/src/token/drc721/mod.rs @@ -238,6 +238,9 @@ impl Drc721 { args: SetApprovalForAllCall, ) -> ApprovalForAll { self.assert_initialized(); + if caller.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } if args.operator.is_zero() { panic!("{}", error::ZERO_PRINCIPAL); } diff --git a/standards/dusk-contract-standards/tests/primitives.rs b/standards/dusk-contract-standards/tests/primitives.rs index 6f0e2ce6..6db40465 100644 --- a/standards/dusk-contract-standards/tests/primitives.rs +++ b/standards/dusk-contract-standards/tests/primitives.rs @@ -546,6 +546,76 @@ fn observed_or_signed_authorization_wraps_owner_role_and_admin_checks() { }); } +#[test] +fn failed_owner_role_and_admin_signed_checks_do_not_consume_nonce() { + let signer_sk = moonlight_secret(70); + let signer_pk = BlsPublicKey::from(&signer_sk); + let signer = Principal::moonlight(&signer_pk); + let owner = p(71); + let admin = p(72); + let contract = c(73); + let implementation = c(74); + let role = [75u8; 32]; + let domain = [76u8; 32]; + let action_id = [77u8; 32]; + let payload_hash = [78u8; 32]; + let envelope = + ActionEnvelope::new(contract, domain, action_id, payload_hash); + let action = AuthorizedAction { + contract, + domain, + action_id, + nonce: 0, + expires_at: 0, + principal: signer, + payload_hash, + }; + let signed = SignedAuthorization::Moonlight(MoonlightAuthorization { + action, + public_key: signer_pk, + signature: signer_sk.sign(&action.message_bytes()), + }); + + let mut manager = AuthorizationManager::new(); + assert_panics(|| { + manager.authorize_principal_action( + owner, + CallContext::none(), + Some(&signed), + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(signer, domain), 0); + + let mut access = AccessControl::new(); + access.init_admin(admin); + access.grant_role(admin, role, owner); + assert_panics(|| { + access.authorize_role_action( + role, + &mut manager, + CallContext::none(), + Some(&signed), + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(signer, domain), 0); + + let upgrades = UpgradeAdmin::new(admin, implementation, 0, 0); + assert_panics(|| { + upgrades.authorize_admin_action( + &mut manager, + CallContext::none(), + Some(&signed), + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(signer, domain), 0); +} + #[test] fn reentrancy_guard_blocks_nested_entry() { let mut guard = ReentrancyGuard::new(); @@ -845,6 +915,91 @@ fn drc20_supports_transfer_allowance_mint_and_burn() { }); } +#[test] +fn drc20_failed_operations_do_not_leave_partial_state() { + let owner = p(1); + let receiver = p(2); + let spender = p(3); + let zero = p(0); + + let mut token = Drc20::new(); + token.init(Init20 { + name: "Dusk Token".into(), + symbol: "DUSKX".into(), + decimals: 9, + initial_balances: vec![InitBalance { + account: owner, + amount: 10, + }], + }); + token.approve(owner, ApproveCall { spender, amount: 7 }); + assert_panics(|| { + token.transfer_from( + spender, + TransferFromCall { + owner, + to: zero, + amount: 3, + }, + ); + }); + assert_eq!( + token.allowance(Allowance { owner, spender }), + 7, + "zero-recipient transfer_from must not spend allowance" + ); + assert_eq!(token.balance_of(BalanceOf20 { account: owner }), 10); + + let empty_owner = p(4); + token.approve_for(empty_owner, ApproveCall { spender, amount: 7 }); + assert_panics(|| { + token.transfer_from( + spender, + TransferFromCall { + owner: empty_owner, + to: receiver, + amount: 3, + }, + ); + }); + assert_eq!( + token.allowance(Allowance { + owner: empty_owner, + spender, + }), + 7, + "failed transfer_from must not spend allowance before balance checks" + ); + assert_eq!(token.balance_of(BalanceOf20 { account: receiver }), 0); + + let mut max_supply = Drc20::new(); + max_supply.init(Init20 { + name: "Max".into(), + symbol: "MAX".into(), + decimals: 9, + initial_balances: vec![InitBalance { + account: owner, + amount: u64::MAX, + }], + }); + max_supply.transfer( + owner, + Transfer20 { + to: owner, + amount: 1, + }, + ); + assert_eq!( + max_supply.balance_of(BalanceOf20 { account: owner }), + u64::MAX + ); + assert_panics(|| { + max_supply.mint(receiver, 1); + }); + assert_eq!(max_supply.total_supply(), u64::MAX); + assert_eq!(max_supply.balance_of(BalanceOf20 { account: receiver }), 0); +} + #[test] fn drc20_supply_cap_and_voting_units_cover_policy_hooks() { let owner = p(1); From 4f8bb67df06f506ba0484e55467972938417945e Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sun, 26 Apr 2026 02:43:23 +0200 Subject: [PATCH 16/35] standards: add property invariant tests --- Cargo.lock | 85 +- standards/dusk-contract-standards/Cargo.toml | 1 + standards/dusk-contract-standards/src/lib.rs | 2 + .../tests/properties.proptest-regressions | 7 + .../tests/properties.rs | 828 ++++++++++++++++++ 5 files changed, 922 insertions(+), 1 deletion(-) create mode 100644 standards/dusk-contract-standards/tests/properties.proptest-regressions create mode 100644 standards/dusk-contract-standards/tests/properties.rs diff --git a/Cargo.lock b/Cargo.lock index eea0b687..94282cb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,6 +342,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitcoin-io" version = "0.1.4" @@ -1035,6 +1050,7 @@ dependencies = [ "bytecheck", "dusk-core", "dusk-vm", + "proptest", "rand 0.8.5", "rkyv", "serde", @@ -1372,6 +1388,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -1591,7 +1613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -2085,6 +2107,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "proxy-counter" version = "0.1.0" @@ -2128,6 +2169,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.38" @@ -2208,6 +2255,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "rangemap" version = "1.7.1" @@ -2401,6 +2457,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2810,6 +2878,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -2838,6 +2912,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/standards/dusk-contract-standards/Cargo.toml b/standards/dusk-contract-standards/Cargo.toml index 39b35e8f..e1efd7ad 100644 --- a/standards/dusk-contract-standards/Cargo.toml +++ b/standards/dusk-contract-standards/Cargo.toml @@ -20,4 +20,5 @@ serde = ["dep:serde", "dep:serde_with", "dep:time", "dusk-core/serde"] [dev-dependencies] dusk-vm = { workspace = true } +proptest = "1.6.0" rand = { workspace = true, features = ["std_rng"] } diff --git a/standards/dusk-contract-standards/src/lib.rs b/standards/dusk-contract-standards/src/lib.rs index 5017a4ad..74cbb42a 100644 --- a/standards/dusk-contract-standards/src/lib.rs +++ b/standards/dusk-contract-standards/src/lib.rs @@ -15,6 +15,8 @@ extern crate alloc; #[cfg(test)] use dusk_vm as _; #[cfg(test)] +use proptest as _; +#[cfg(test)] use rand as _; #[cfg(feature = "serde")] use serde_with as _; diff --git a/standards/dusk-contract-standards/tests/properties.proptest-regressions b/standards/dusk-contract-standards/tests/properties.proptest-regressions new file mode 100644 index 00000000..eb2ef747 --- /dev/null +++ b/standards/dusk-contract-standards/tests/properties.proptest-regressions @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 6f3227501a81e63efa3a96259de7dd04a69bd503e27f4953f5e298cd8a9b7294 # shrinks to ops = [Mint { to: Phoenix([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]), amount: 98 }, Transfer { from: Phoenix([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]), to: Phoenix([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5]), amount: 1 }] diff --git a/standards/dusk-contract-standards/tests/properties.rs b/standards/dusk-contract-standards/tests/properties.rs new file mode 100644 index 00000000..4d16f2f9 --- /dev/null +++ b/standards/dusk-contract-standards/tests/properties.rs @@ -0,0 +1,828 @@ +use std::collections::BTreeMap; +use std::panic::{catch_unwind, AssertUnwindSafe}; + +use dusk_contract_standards::core::Principal; +use dusk_contract_standards::token::drc20::{ + Allowance, ApproveCall, BalanceOf as BalanceOf20, DecreaseAllowanceCall, + Drc20, IncreaseAllowanceCall, Init as Init20, InitBalance, + TransferCall as Transfer20, TransferFromCall, +}; +use dusk_contract_standards::token::drc721::{ + ApproveCall as Approve721, BalanceOf as BalanceOf721, Drc721, GetApproved, + Init as Init721, InitToken, IsApprovedForAll, OwnerOf, + SetApprovalForAllCall, TokensOf, TransferFromCall as Transfer721, +}; +use dusk_core::abi::ContractId; +use proptest::prelude::*; + +fn principal(index: u8) -> Principal { + if index == 0 { + Principal::Contract(ContractId::from_bytes([0u8; 32])) + } else { + Principal::Phoenix([index; 32]) + } +} + +fn principal_strategy() -> BoxedStrategy { + (0u8..=5).prop_map(principal).boxed() +} + +fn all_principals() -> Vec { + (0u8..=5).map(principal).collect() +} + +#[derive(Clone, Debug)] +enum Drc20Op { + Transfer { + from: Principal, + to: Principal, + amount: u64, + }, + Approve { + owner: Principal, + spender: Principal, + amount: u64, + }, + IncreaseAllowance { + owner: Principal, + spender: Principal, + amount: u64, + }, + DecreaseAllowance { + owner: Principal, + spender: Principal, + amount: u64, + }, + TransferFrom { + spender: Principal, + owner: Principal, + to: Principal, + amount: u64, + }, + Mint { + to: Principal, + amount: u64, + }, + Burn { + from: Principal, + amount: u64, + }, +} + +fn drc20_op_strategy() -> impl Strategy { + let account = principal_strategy(); + let amount = 0u64..=150; + prop_oneof![ + (account.clone(), account.clone(), amount.clone()).prop_map( + |(from, to, amount)| Drc20Op::Transfer { from, to, amount }, + ), + (account.clone(), account.clone(), amount.clone()).prop_map( + |(owner, spender, amount)| Drc20Op::Approve { + owner, + spender, + amount, + }, + ), + (account.clone(), account.clone(), amount.clone()).prop_map( + |(owner, spender, amount)| Drc20Op::IncreaseAllowance { + owner, + spender, + amount, + }, + ), + (account.clone(), account.clone(), amount.clone()).prop_map( + |(owner, spender, amount)| Drc20Op::DecreaseAllowance { + owner, + spender, + amount, + }, + ), + ( + account.clone(), + account.clone(), + account.clone(), + amount.clone() + ) + .prop_map(|(spender, owner, to, amount)| { + Drc20Op::TransferFrom { + spender, + owner, + to, + amount, + } + }), + (account.clone(), amount.clone()) + .prop_map(|(to, amount)| Drc20Op::Mint { to, amount }), + (account, amount) + .prop_map(|(from, amount)| Drc20Op::Burn { from, amount }), + ] +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Drc20Snapshot { + total_supply: u64, + balances: Vec<(Principal, u64)>, + allowances: Vec<(Principal, Principal, u64)>, +} + +#[derive(Clone, Debug)] +struct Drc20Model { + balances: BTreeMap, + allowances: BTreeMap<(Principal, Principal), u64>, + total_supply: u64, +} + +impl Drc20Model { + fn initial() -> Self { + let mut balances = BTreeMap::new(); + balances.insert(principal(1), 100); + balances.insert(principal(2), 50); + Self { + balances, + allowances: BTreeMap::new(), + total_supply: 150, + } + } + + fn snapshot(&self, accounts: &[Principal]) -> Drc20Snapshot { + let mut balances = Vec::new(); + let mut allowances = Vec::new(); + for &account in accounts { + balances.push((account, self.balance(account))); + } + for &owner in accounts { + for &spender in accounts { + allowances.push(( + owner, + spender, + self.allowance(owner, spender), + )); + } + } + Drc20Snapshot { + total_supply: self.total_supply, + balances, + allowances, + } + } + + fn balance(&self, account: Principal) -> u64 { + self.balances.get(&account).copied().unwrap_or(0) + } + + fn allowance(&self, owner: Principal, spender: Principal) -> u64 { + self.allowances.get(&(owner, spender)).copied().unwrap_or(0) + } + + fn set_balance(&mut self, account: Principal, amount: u64) { + if amount == 0 { + self.balances.remove(&account); + } else { + self.balances.insert(account, amount); + } + } + + fn apply(&mut self, op: &Drc20Op) -> bool { + match *op { + Drc20Op::Transfer { from, to, amount } => { + self.transfer(from, to, amount) + } + Drc20Op::Approve { + owner, + spender, + amount, + } => { + if owner.is_zero() || spender.is_zero() { + return false; + } + self.allowances.insert((owner, spender), amount); + true + } + Drc20Op::IncreaseAllowance { + owner, + spender, + amount, + } => { + if owner.is_zero() || spender.is_zero() { + return false; + } + let Some(next) = + self.allowance(owner, spender).checked_add(amount) + else { + return false; + }; + self.allowances.insert((owner, spender), next); + true + } + Drc20Op::DecreaseAllowance { + owner, + spender, + amount, + } => { + if owner.is_zero() || spender.is_zero() { + return false; + } + let current = self.allowance(owner, spender); + if current < amount { + return false; + } + self.allowances.insert((owner, spender), current - amount); + true + } + Drc20Op::TransferFrom { + spender, + owner, + to, + amount, + } => { + if spender.is_zero() || self.allowance(owner, spender) < amount + { + return false; + } + let before = self.clone(); + if !self.transfer(owner, to, amount) { + *self = before; + return false; + } + let current = self.allowance(owner, spender); + self.allowances.insert((owner, spender), current - amount); + true + } + Drc20Op::Mint { to, amount } => { + if to.is_zero() { + return false; + } + let Some(next_balance) = self.balance(to).checked_add(amount) + else { + return false; + }; + let Some(next_supply) = self.total_supply.checked_add(amount) + else { + return false; + }; + self.set_balance(to, next_balance); + self.total_supply = next_supply; + true + } + Drc20Op::Burn { from, amount } => { + if from.is_zero() || self.balance(from) < amount { + return false; + } + self.set_balance(from, self.balance(from) - amount); + self.total_supply -= amount; + true + } + } + } + + fn transfer( + &mut self, + from: Principal, + to: Principal, + amount: u64, + ) -> bool { + if from.is_zero() || to.is_zero() || self.balance(from) < amount { + return false; + } + if from != to && self.balance(to).checked_add(amount).is_none() { + return false; + } + if amount != 0 { + if from == to { + return true; + } + self.set_balance(from, self.balance(from) - amount); + self.set_balance(to, self.balance(to) + amount); + } + true + } +} + +fn drc20_snapshot(token: &Drc20, accounts: &[Principal]) -> Drc20Snapshot { + let mut balances = Vec::new(); + let mut allowances = Vec::new(); + for &account in accounts { + balances.push((account, token.balance_of(BalanceOf20 { account }))); + } + for &owner in accounts { + for &spender in accounts { + allowances.push(( + owner, + spender, + token.allowance(Allowance { owner, spender }), + )); + } + } + Drc20Snapshot { + total_supply: token.total_supply(), + balances, + allowances, + } +} + +fn apply_drc20_token(token: &mut Drc20, op: &Drc20Op) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + Drc20Op::Transfer { from, to, amount } => { + token.transfer(from, Transfer20 { to, amount }); + } + Drc20Op::Approve { + owner, + spender, + amount, + } => { + token.approve(owner, ApproveCall { spender, amount }); + } + Drc20Op::IncreaseAllowance { + owner, + spender, + amount, + } => { + token.increase_allowance( + owner, + IncreaseAllowanceCall { + spender, + added_amount: amount, + }, + ); + } + Drc20Op::DecreaseAllowance { + owner, + spender, + amount, + } => { + token.decrease_allowance( + owner, + DecreaseAllowanceCall { + spender, + subtracted_amount: amount, + }, + ); + } + Drc20Op::TransferFrom { + spender, + owner, + to, + amount, + } => { + token + .transfer_from(spender, TransferFromCall { owner, to, amount }); + } + Drc20Op::Mint { to, amount } => { + token.mint(to, amount); + } + Drc20Op::Burn { from, amount } => { + token.burn(from, amount); + } + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum Drc721Op { + Mint { + to: Principal, + token_id: u64, + }, + Approve { + caller: Principal, + approved: Principal, + token_id: u64, + }, + SetApprovalForAll { + owner: Principal, + operator: Principal, + approved: bool, + }, + Transfer { + caller: Principal, + from: Principal, + to: Principal, + token_id: u64, + }, + Burn { + caller: Principal, + token_id: u64, + }, +} + +fn drc721_op_strategy() -> impl Strategy { + let account = principal_strategy(); + let token_id = 0u64..=8; + prop_oneof![ + (account.clone(), token_id.clone()) + .prop_map(|(to, token_id)| Drc721Op::Mint { to, token_id }), + (account.clone(), account.clone(), token_id.clone()).prop_map( + |(caller, approved, token_id)| Drc721Op::Approve { + caller, + approved, + token_id, + }, + ), + (account.clone(), account.clone(), any::()).prop_map( + |(owner, operator, approved)| Drc721Op::SetApprovalForAll { + owner, + operator, + approved, + }, + ), + ( + account.clone(), + account.clone(), + account.clone(), + token_id.clone() + ) + .prop_map(|(caller, from, to, token_id)| { + Drc721Op::Transfer { + caller, + from, + to, + token_id, + } + }), + (account, token_id) + .prop_map(|(caller, token_id)| Drc721Op::Burn { caller, token_id }), + ] +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Drc721Snapshot { + total_supply: u64, + owners: Vec<(u64, Option)>, + balances: Vec<(Principal, u64)>, + approvals: Vec<(u64, Option)>, + operators: Vec<(Principal, Principal, bool)>, + tokens: Vec<(Principal, Vec)>, +} + +#[derive(Clone, Debug)] +struct Drc721Model { + owners: BTreeMap, + approvals: BTreeMap, + operators: BTreeMap<(Principal, Principal), bool>, +} + +impl Drc721Model { + fn initial() -> Self { + let mut owners = BTreeMap::new(); + owners.insert(1, principal(1)); + owners.insert(2, principal(2)); + Self { + owners, + approvals: BTreeMap::new(), + operators: BTreeMap::new(), + } + } + + fn snapshot( + &self, + accounts: &[Principal], + token_ids: &[u64], + ) -> Drc721Snapshot { + let owners = token_ids + .iter() + .map(|&id| (id, self.owners.get(&id).copied())) + .collect(); + let balances = accounts + .iter() + .copied() + .filter(|account| !account.is_zero()) + .map(|account| (account, self.balance(account))) + .collect(); + let approvals = token_ids + .iter() + .map(|&id| (id, self.approved(id))) + .collect(); + let mut operators = Vec::new(); + for &owner in accounts { + for &operator in accounts { + operators.push(( + owner, + operator, + self.is_operator(owner, operator), + )); + } + } + let tokens = accounts + .iter() + .copied() + .map(|account| (account, self.tokens_of(account))) + .collect(); + Drc721Snapshot { + total_supply: self.owners.len() as u64, + owners, + balances, + approvals, + operators, + tokens, + } + } + + fn balance(&self, account: Principal) -> u64 { + self.owners + .values() + .filter(|&&owner| owner == account) + .count() as u64 + } + + fn approved(&self, token_id: u64) -> Option { + if self.owners.contains_key(&token_id) { + Some( + self.approvals + .get(&token_id) + .copied() + .unwrap_or_else(|| principal(0)), + ) + } else { + None + } + } + + fn is_operator(&self, owner: Principal, operator: Principal) -> bool { + self.operators + .get(&(owner, operator)) + .copied() + .unwrap_or(false) + } + + fn tokens_of(&self, account: Principal) -> Vec { + self.owners + .iter() + .filter_map(|(&token_id, &owner)| { + if owner == account { + Some(token_id) + } else { + None + } + }) + .collect() + } + + fn apply(&mut self, op: &Drc721Op) -> bool { + match *op { + Drc721Op::Mint { to, token_id } => { + if to.is_zero() || self.owners.contains_key(&token_id) { + return false; + } + self.owners.insert(token_id, to); + true + } + Drc721Op::Approve { + caller, + approved, + token_id, + } => { + let Some(&owner) = self.owners.get(&token_id) else { + return false; + }; + if approved == owner { + return false; + } + if caller != owner && !self.is_operator(owner, caller) { + return false; + } + if approved.is_zero() { + self.approvals.remove(&token_id); + } else { + self.approvals.insert(token_id, approved); + } + true + } + Drc721Op::SetApprovalForAll { + owner, + operator, + approved, + } => { + if owner.is_zero() || operator.is_zero() || operator == owner { + return false; + } + self.operators.insert((owner, operator), approved); + true + } + Drc721Op::Transfer { + caller, + from, + to, + token_id, + } => { + if to.is_zero() { + return false; + } + let Some(&owner) = self.owners.get(&token_id) else { + return false; + }; + if owner != from { + return false; + } + let approved = self.approvals.get(&token_id).copied(); + if caller != owner + && approved != Some(caller) + && !self.is_operator(owner, caller) + { + return false; + } + self.approvals.remove(&token_id); + self.owners.insert(token_id, to); + true + } + Drc721Op::Burn { caller, token_id } => { + let Some(&owner) = self.owners.get(&token_id) else { + return false; + }; + let approved = self.approvals.get(&token_id).copied(); + if caller != owner && approved != Some(caller) { + return false; + } + self.owners.remove(&token_id); + self.approvals.remove(&token_id); + true + } + } + } +} + +fn drc721_snapshot( + token: &Drc721, + accounts: &[Principal], + token_ids: &[u64], +) -> Drc721Snapshot { + let owners = token_ids + .iter() + .map(|&token_id| { + let owner = catch_unwind(AssertUnwindSafe(|| { + token.owner_of(OwnerOf { token_id }) + })) + .ok(); + (token_id, owner) + }) + .collect(); + let balances = accounts + .iter() + .copied() + .filter(|account| !account.is_zero()) + .map(|account| (account, token.balance_of(BalanceOf721 { account }))) + .collect(); + let approvals = token_ids + .iter() + .map(|&token_id| { + let approval = catch_unwind(AssertUnwindSafe(|| { + token.get_approved(GetApproved { token_id }) + })) + .ok(); + (token_id, approval) + }) + .collect(); + let mut operators = Vec::new(); + for &owner in accounts { + for &operator in accounts { + operators.push(( + owner, + operator, + token.is_approved_for_all(IsApprovedForAll { owner, operator }), + )); + } + } + let tokens = accounts + .iter() + .copied() + .map(|owner| (owner, token.tokens_of(TokensOf { owner }))) + .collect(); + Drc721Snapshot { + total_supply: token.total_supply(), + owners, + balances, + approvals, + operators, + tokens, + } +} + +fn apply_drc721_token(token: &mut Drc721, op: &Drc721Op) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + Drc721Op::Mint { to, token_id } => { + token.mint(to, token_id); + } + Drc721Op::Approve { + caller, + approved, + token_id, + } => { + token.approve(caller, Approve721 { approved, token_id }); + } + Drc721Op::SetApprovalForAll { + owner, + operator, + approved, + } => { + token.set_approval_for_all( + owner, + SetApprovalForAllCall { operator, approved }, + ); + } + Drc721Op::Transfer { + caller, + from, + to, + token_id, + } => { + token.transfer_from(caller, Transfer721 { from, to, token_id }); + } + Drc721Op::Burn { caller, token_id } => { + token.burn(caller, token_id); + } + })) + .is_ok() +} + +proptest! { + #![proptest_config(ProptestConfig { + cases: 256, + max_shrink_iters: 4096, + ..ProptestConfig::default() + })] + + #[test] + fn drc20_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(drc20_op_strategy(), 0..160), + ) { + let accounts = all_principals(); + let mut token = Drc20::new(); + token.init(Init20 { + name: "Property Token".into(), + symbol: "PROP".into(), + decimals: 9, + initial_balances: vec![ + InitBalance { account: principal(1), amount: 100 }, + InitBalance { account: principal(2), amount: 50 }, + ], + }); + let mut model = Drc20Model::initial(); + prop_assert_eq!(drc20_snapshot(&token, &accounts), model.snapshot(&accounts)); + + for op in ops { + let before = drc20_snapshot(&token, &accounts); + let expected = model.apply(&op); + let actual = apply_drc20_token(&mut token, &op); + prop_assert_eq!(actual, expected, "operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + drc20_snapshot(&token, &accounts), + model.snapshot(&accounts), + "successful operation left an unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + drc20_snapshot(&token, &accounts), + before, + "failed operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn drc721_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(drc721_op_strategy(), 0..140), + ) { + let accounts = all_principals(); + let token_ids: Vec = (0..=8).collect(); + let mut token = Drc721::new(); + token.init(Init721 { + name: "Property NFT".into(), + symbol: "PNFT".into(), + base_uri: "ipfs://property/".into(), + initial_tokens: vec![ + InitToken { account: principal(1), token_id: 1 }, + InitToken { account: principal(2), token_id: 2 }, + ], + }); + let mut model = Drc721Model::initial(); + prop_assert_eq!( + drc721_snapshot(&token, &accounts, &token_ids), + model.snapshot(&accounts, &token_ids), + ); + + for op in ops { + let before = drc721_snapshot(&token, &accounts, &token_ids); + let expected = model.apply(&op); + let actual = apply_drc721_token(&mut token, &op); + prop_assert_eq!(actual, expected, "operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + drc721_snapshot(&token, &accounts, &token_ids), + model.snapshot(&accounts, &token_ids), + "successful operation left an unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + drc721_snapshot(&token, &accounts, &token_ids), + before, + "failed operation mutated state: {:?}", + op, + ); + } + } + } +} From a53bc79f759f99e50ad0ff26f4a54c70f4511194 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sun, 26 Apr 2026 02:43:27 +0200 Subject: [PATCH 17/35] docs: add standards hardening track --- docs/dusk-contract-standards-hardening.md | 71 +++++++++++++++++++++++ docs/dusk-contract-standards.md | 5 ++ 2 files changed, 76 insertions(+) create mode 100644 docs/dusk-contract-standards-hardening.md diff --git a/docs/dusk-contract-standards-hardening.md b/docs/dusk-contract-standards-hardening.md new file mode 100644 index 00000000..6c5d3c61 --- /dev/null +++ b/docs/dusk-contract-standards-hardening.md @@ -0,0 +1,71 @@ +# Dusk Contract Standards Hardening Track + +This branch is a separate hardening track for the Dusk-native standards layer. +It keeps implementation work close to the contracts, but treats security +validation as a first-class deliverable rather than a final smoke test. + +## Current Scope + +The first hardening pass focuses on invariants that should hold regardless of +the composing contract: + +- signed authorization must bind contract, domain, action id, payload hash, + nonce, principal, and expiry before nonce/replay state is consumed; +- owner, role, and upgrade-admin checks must not consume a valid signer nonce + when the signer is not authorized for the policy being checked; +- failed token operations must not leave partial native state behind; +- DRC20 total supply must equal the modeled sum of balances for all touched + accounts; +- DRC20 allowance movement must match the reference model exactly; +- DRC721 total supply must equal the modeled number of live token ids; +- DRC721 owner, approval, operator, balance, and enumerable views must remain + internally consistent after arbitrary operation sequences; +- the reserved zero principal must not be accepted as an actor, recipient, + spender, minter, burner, or operator where that would create ownership or + authorization state. + +## Added Validation + +`standards/dusk-contract-standards/tests/properties.rs` adds property-based +state-machine tests for DRC20 and DRC721. The tests generate random operation +sequences, run them against the real primitive and an independent model, and +assert after each operation that: + +- success/failure matches the model; +- successful calls produce the exact expected state; +- failing calls leave the pre-call state unchanged. + +The saved proptest regression file keeps the self-transfer case that caught a +DRC20 accounting bug during this pass. + +## Commands + +Run the hardening tests with: + +```sh +cargo test -p dusk-contract-standards +cargo test -p dusk-contract-standards --test properties +``` + +Run the full standards validation pass with: + +```sh +cargo fmt +cargo test -p dusk-contract-standards +cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ + -p authorization-counter \ + -p drc20-roles-pausable \ + -p drc721-collection \ + -p proxy-counter \ + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,proxy-counter/contract +cargo test -p dusk-contract-standards --test examples_vm -- --ignored +make standards-data-drivers +cargo clippy -p dusk-contract-standards --all-targets -- -D warnings +``` + +## Next Research Items + +The next hardening layer should add ABI-level fuzzing of Forge call payloads, +longer-running property tests in CI, mutation testing for authorization and +pause paths, and local-node scenario tests that intentionally mix successful +transactions with rejected transactions across block boundaries. diff --git a/docs/dusk-contract-standards.md b/docs/dusk-contract-standards.md index e3750d47..fdf9f1e2 100644 --- a/docs/dusk-contract-standards.md +++ b/docs/dusk-contract-standards.md @@ -203,6 +203,11 @@ The native test suite covers positive and negative paths for: - upgrade preparation, activation delay, cancellation, rollback, finalization, events, and namespaced proxy state. +The hardening branch also adds property-based DRC20 and DRC721 state-machine +tests. They compare arbitrary operation sequences against independent models +and assert that rejected operations leave native state unchanged. See +`docs/dusk-contract-standards-hardening.md` for the current hardening track. + The ignored VM test deploys all four Wasm examples, checks positive query paths, performs real Moonlight and Phoenix signed calls against the authorization counter, covers replay/wrong-payload/wrong-signature/wrong-target From 5d1859fff64f44fefee3e11d1993d04a1e058e0e Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sun, 26 Apr 2026 06:54:52 +0200 Subject: [PATCH 18/35] standards: harden owner-set signed authorization --- .../src/access/owner_set.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/standards/dusk-contract-standards/src/access/owner_set.rs b/standards/dusk-contract-standards/src/access/owner_set.rs index 63ec4d2d..8d5a08e8 100644 --- a/standards/dusk-contract-standards/src/access/owner_set.rs +++ b/standards/dusk-contract-standards/src/access/owner_set.rs @@ -74,9 +74,9 @@ impl OwnerSet { let Some(authorization) = authorization else { panic!("{}", error::UNAUTHORIZED); }; - let principal = authorizer.require_signed(authorization); - self.assert_owner(principal); - principal + authorizer.require_signed_if(authorization, |principal| { + self.is_owner(principal) + }) } /// Authorizes any owner through runtime context or an action-bound signed @@ -113,10 +113,11 @@ impl OwnerSet { let Some(authorization) = authorization else { panic!("{}", error::UNAUTHORIZED); }; - let principal = - authorizer.require_signed_action(authorization, envelope); - self.assert_owner(principal); - principal + authorizer.require_signed_action_if( + authorization, + envelope, + |principal| self.is_owner(principal), + ) } /// Returns all owners in stable order. From c0a3dd0e3666570d80c3bf6aa8b5a955e7cb56ac Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sun, 26 Apr 2026 06:54:56 +0200 Subject: [PATCH 19/35] standards: expand property fuzz coverage --- docs/dusk-contract-standards-hardening.md | 30 +- .../tests/properties.rs | 1048 ++++++++++++++++- 2 files changed, 1072 insertions(+), 6 deletions(-) diff --git a/docs/dusk-contract-standards-hardening.md b/docs/dusk-contract-standards-hardening.md index 6c5d3c61..6fc01c5d 100644 --- a/docs/dusk-contract-standards-hardening.md +++ b/docs/dusk-contract-standards-hardening.md @@ -13,6 +13,7 @@ the composing contract: nonce, principal, and expiry before nonce/replay state is consumed; - owner, role, and upgrade-admin checks must not consume a valid signer nonce when the signer is not authorized for the policy being checked; +- mixed owner-set checks must follow the same no-consume-on-unauthorized rule; - failed token operations must not leave partial native state behind; - DRC20 total supply must equal the modeled sum of balances for all touched accounts; @@ -27,9 +28,18 @@ the composing contract: ## Added Validation `standards/dusk-contract-standards/tests/properties.rs` adds property-based -state-machine tests for DRC20 and DRC721. The tests generate random operation -sequences, run them against the real primitive and an independent model, and -assert after each operation that: +state-machine tests for: + +- action-bound authorization rejection cases; +- mixed owner sets; +- access-control role/admin mutation; +- timelock scheduling, execution, cancellation, and delay mutation; +- upgrade-admin prepare, activate, cancel, rollback, and finalization flows; +- DRC20 accounting and allowance flows; +- DRC721 ownership, approval, operator, balance, and enumerable views. + +The tests generate random operation sequences, run them against the real +primitive and an independent model, and assert after each operation that: - success/failure matches the model; - successful calls produce the exact expected state; @@ -47,6 +57,20 @@ cargo test -p dusk-contract-standards cargo test -p dusk-contract-standards --test properties ``` +Run the long randomized hardening loop with: + +```sh +start=$(date +%s) +end=$((start + 14400)) +while [ "$(date +%s)" -lt "$end" ]; do + PROPTEST_CASES=2048 PROPTEST_MAX_SHRINK_ITERS=8192 \ + cargo test -p dusk-contract-standards --test properties +done +``` + +On April 26, 2026, this loop ran for 14,404 seconds and completed 367 full +property-suite iterations without a failure. + Run the full standards validation pass with: ```sh diff --git a/standards/dusk-contract-standards/tests/properties.rs b/standards/dusk-contract-standards/tests/properties.rs index 4d16f2f9..3e5b7994 100644 --- a/standards/dusk-contract-standards/tests/properties.rs +++ b/standards/dusk-contract-standards/tests/properties.rs @@ -1,7 +1,16 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::panic::{catch_unwind, AssertUnwindSafe}; -use dusk_contract_standards::core::Principal; +use dusk_contract_standards::access::{ + AccessControl, OwnerSet, DEFAULT_ADMIN_ROLE, +}; +use dusk_contract_standards::auth::{ + ActionEnvelope, AuthorizationManager, AuthorizedAction, + MoonlightAuthorization, SignedAuthorization, +}; +use dusk_contract_standards::core::{CallContext, Principal}; +use dusk_contract_standards::governance::Timelock; +use dusk_contract_standards::proxy::UpgradeAdmin; use dusk_contract_standards::token::drc20::{ Allowance, ApproveCall, BalanceOf as BalanceOf20, DecreaseAllowanceCall, Drc20, IncreaseAllowanceCall, Init as Init20, InitBalance, @@ -13,7 +22,12 @@ use dusk_contract_standards::token::drc721::{ SetApprovalForAllCall, TokensOf, TransferFromCall as Transfer721, }; use dusk_core::abi::ContractId; +use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, +}; use proptest::prelude::*; +use rand::rngs::StdRng; +use rand::SeedableRng; fn principal(index: u8) -> Principal { if index == 0 { @@ -31,6 +45,30 @@ fn all_principals() -> Vec { (0u8..=5).map(principal).collect() } +fn contract_id(index: u8) -> ContractId { + ContractId::from_bytes([index; 32]) +} + +fn role(index: u8) -> [u8; 32] { + let mut role = [0u8; 32]; + role[0] = index; + role +} + +fn amount_strategy() -> BoxedStrategy { + prop_oneof![ + 12 => 0u64..=150, + 2 => (0u64..=16).prop_map(|delta| u64::MAX - delta), + 1 => Just(u64::MAX), + ] + .boxed() +} + +fn moonlight_secret(seed: u64) -> BlsSecretKey { + let mut rng = StdRng::seed_from_u64(seed); + BlsSecretKey::random(&mut rng) +} + #[derive(Clone, Debug)] enum Drc20Op { Transfer { @@ -71,7 +109,7 @@ enum Drc20Op { fn drc20_op_strategy() -> impl Strategy { let account = principal_strategy(); - let amount = 0u64..=150; + let amount = amount_strategy(); prop_oneof![ (account.clone(), account.clone(), amount.clone()).prop_map( |(from, to, amount)| Drc20Op::Transfer { from, to, amount }, @@ -733,6 +771,860 @@ fn apply_drc721_token(token: &mut Drc721, op: &Drc721Op) -> bool { .is_ok() } +#[derive(Clone, Copy, Debug)] +enum AuthCase { + Good, + WrongContract, + WrongDomain, + WrongAction, + WrongPayload, + Expired, + FutureNonce, + BadPrincipal, + BadSignature, + WrongExpected, + OwnerSetNonOwner, + RoleNonMember, + AdminMismatch, +} + +fn auth_case_strategy() -> impl Strategy { + prop_oneof![ + Just(AuthCase::Good), + Just(AuthCase::WrongContract), + Just(AuthCase::WrongDomain), + Just(AuthCase::WrongAction), + Just(AuthCase::WrongPayload), + Just(AuthCase::Expired), + Just(AuthCase::FutureNonce), + Just(AuthCase::BadPrincipal), + Just(AuthCase::BadSignature), + Just(AuthCase::WrongExpected), + Just(AuthCase::OwnerSetNonOwner), + Just(AuthCase::RoleNonMember), + Just(AuthCase::AdminMismatch), + ] +} + +fn signed_action( + secret: &BlsSecretKey, + public: BlsPublicKey, + action: AuthorizedAction, +) -> SignedAuthorization { + SignedAuthorization::Moonlight(MoonlightAuthorization { + action, + public_key: public, + signature: secret.sign(&action.message_bytes()), + }) +} + +fn authorization_probe(case: AuthCase) -> (bool, u64) { + let secret = moonlight_secret(31); + let public = BlsPublicKey::from(&secret); + let signer = Principal::moonlight(&public); + let contract = contract_id(32); + let domain = [33u8; 32]; + let action_id = [34u8; 32]; + let payload_hash = [35u8; 32]; + let envelope = + ActionEnvelope::new(contract, domain, action_id, payload_hash); + let mut action = AuthorizedAction { + contract, + domain, + action_id, + nonce: 0, + expires_at: 100, + principal: signer, + payload_hash, + }; + let mut now = 100; + let expected = match case { + AuthCase::Good => signer, + AuthCase::WrongExpected => principal(1), + AuthCase::WrongContract => { + action.contract = contract_id(36); + signer + } + AuthCase::WrongDomain => { + action.domain = [37u8; 32]; + signer + } + AuthCase::WrongAction => { + action.action_id = [38u8; 32]; + signer + } + AuthCase::WrongPayload => { + action.payload_hash = [39u8; 32]; + signer + } + AuthCase::Expired => { + now = 101; + signer + } + AuthCase::FutureNonce => { + action.nonce = 1; + signer + } + AuthCase::BadPrincipal => { + action.principal = principal(2); + signer + } + AuthCase::BadSignature + | AuthCase::OwnerSetNonOwner + | AuthCase::RoleNonMember + | AuthCase::AdminMismatch => signer, + }; + + let signed = if matches!(case, AuthCase::BadSignature) { + let original = action; + action.payload_hash = [42u8; 32]; + SignedAuthorization::Moonlight(MoonlightAuthorization { + action, + public_key: public, + signature: secret.sign(&original.message_bytes()), + }) + } else { + signed_action(&secret, public, action) + }; + + let mut authorizations = AuthorizationManager::new(); + let result = catch_unwind(AssertUnwindSafe(|| match case { + AuthCase::OwnerSetNonOwner => { + let mut owners = OwnerSet::new(); + owners.init([principal(1)]); + owners.authorize_owner_action( + &mut authorizations, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + AuthCase::RoleNonMember => { + let checked_role = role(40); + let mut access = AccessControl::new(); + access.init_admin(principal(1)); + access.grant_role(principal(1), checked_role, principal(2)); + access.authorize_role_action( + checked_role, + &mut authorizations, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + AuthCase::AdminMismatch => { + let upgrade = + UpgradeAdmin::new(principal(1), contract_id(41), 0, 0); + upgrade.authorize_admin_action( + &mut authorizations, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + _ => { + authorizations.authorize_principal_action( + expected, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + })) + .is_ok(); + (result, authorizations.nonce(signer, domain)) +} + +#[derive(Clone, Debug)] +enum OwnerSetOp { + Add { + caller: Principal, + owner: Principal, + }, + Remove { + caller: Principal, + owner: Principal, + }, + Replace { + caller: Principal, + old_owner: Principal, + new_owner: Principal, + }, +} + +fn owner_set_op_strategy() -> impl Strategy { + let account = principal_strategy(); + prop_oneof![ + (account.clone(), account.clone()) + .prop_map(|(caller, owner)| { OwnerSetOp::Add { caller, owner } }), + (account.clone(), account.clone()).prop_map(|(caller, owner)| { + OwnerSetOp::Remove { caller, owner } + }), + (account.clone(), account.clone(), account).prop_map( + |(caller, old_owner, new_owner)| OwnerSetOp::Replace { + caller, + old_owner, + new_owner, + }, + ), + ] +} + +fn apply_owner_set_model( + owners: &mut BTreeSet, + op: &OwnerSetOp, +) -> bool { + match *op { + OwnerSetOp::Add { caller, owner } => { + if !owners.contains(&caller) || owner.is_zero() { + return false; + } + owners.insert(owner); + true + } + OwnerSetOp::Remove { caller, owner } => { + if !owners.contains(&caller) + || !owners.contains(&owner) + || owners.len() == 1 + { + return false; + } + owners.remove(&owner); + true + } + OwnerSetOp::Replace { + caller, + old_owner, + new_owner, + } => { + if !owners.contains(&caller) + || new_owner.is_zero() + || !owners.contains(&old_owner) + || (old_owner != new_owner && owners.contains(&new_owner)) + { + return false; + } + owners.remove(&old_owner); + owners.insert(new_owner); + true + } + } +} + +fn apply_owner_set(owners: &mut OwnerSet, op: &OwnerSetOp) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + OwnerSetOp::Add { caller, owner } => owners.add_owner(caller, owner), + OwnerSetOp::Remove { caller, owner } => { + owners.remove_owner(caller, owner) + } + OwnerSetOp::Replace { + caller, + old_owner, + new_owner, + } => owners.replace_owner(caller, old_owner, new_owner), + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum AccessOp { + Grant { + caller: Principal, + role: [u8; 32], + account: Principal, + }, + Revoke { + caller: Principal, + role: [u8; 32], + account: Principal, + }, + Renounce { + role: [u8; 32], + caller: Principal, + }, + SetRoleAdmin { + caller: Principal, + role: [u8; 32], + admin_role: [u8; 32], + }, +} + +#[derive(Clone, Debug)] +struct RoleModel { + members: BTreeSet, + admin_role: [u8; 32], +} + +#[derive(Clone, Debug)] +struct AccessModel { + roles: BTreeMap<[u8; 32], RoleModel>, +} + +impl AccessModel { + fn new(admin: Principal) -> Self { + let mut members = BTreeSet::new(); + members.insert(admin); + let mut roles = BTreeMap::new(); + roles.insert( + DEFAULT_ADMIN_ROLE, + RoleModel { + members, + admin_role: DEFAULT_ADMIN_ROLE, + }, + ); + Self { roles } + } + + fn has_role(&self, role: [u8; 32], account: Principal) -> bool { + self.roles + .get(&role) + .map(|role| role.members.contains(&account)) + .unwrap_or(false) + } + + fn role_admin(&self, role: [u8; 32]) -> [u8; 32] { + self.roles + .get(&role) + .map(|role| role.admin_role) + .unwrap_or(DEFAULT_ADMIN_ROLE) + } + + fn apply(&mut self, op: &AccessOp) -> bool { + match *op { + AccessOp::Grant { + caller, + role, + account, + } => { + if account.is_zero() + || !self.has_role(self.role_admin(role), caller) + { + return false; + } + self.roles + .entry(role) + .or_insert_with(|| RoleModel { + members: BTreeSet::new(), + admin_role: DEFAULT_ADMIN_ROLE, + }) + .members + .insert(account); + true + } + AccessOp::Revoke { + caller, + role, + account, + } => { + if !self.has_role(self.role_admin(role), caller) { + return false; + } + if let Some(role) = self.roles.get_mut(&role) { + role.members.remove(&account); + } + true + } + AccessOp::Renounce { role, caller } => { + if let Some(role) = self.roles.get_mut(&role) { + role.members.remove(&caller); + } + true + } + AccessOp::SetRoleAdmin { + caller, + role, + admin_role, + } => { + if !self.has_role(self.role_admin(role), caller) { + return false; + } + self.roles + .entry(role) + .or_insert_with(|| RoleModel { + members: BTreeSet::new(), + admin_role: DEFAULT_ADMIN_ROLE, + }) + .admin_role = admin_role; + true + } + } + } + + fn snapshot( + &self, + roles: &[[u8; 32]], + accounts: &[Principal], + ) -> AccessSnapshot { + AccessSnapshot { + admins: roles + .iter() + .copied() + .map(|role| (role, self.role_admin(role))) + .collect(), + memberships: roles + .iter() + .copied() + .flat_map(|role| { + accounts.iter().copied().map(move |account| { + (role, account, self.has_role(role, account)) + }) + }) + .collect(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct AccessSnapshot { + admins: Vec<([u8; 32], [u8; 32])>, + memberships: Vec<([u8; 32], Principal, bool)>, +} + +fn access_op_strategy() -> impl Strategy { + let account = principal_strategy(); + let role_strategy = + (0u8..=3).prop_map( + |i| { + if i == 0 { + DEFAULT_ADMIN_ROLE + } else { + role(i) + } + }, + ); + prop_oneof![ + (account.clone(), role_strategy.clone(), account.clone()).prop_map( + |(caller, role, account)| AccessOp::Grant { + caller, + role, + account, + }, + ), + (account.clone(), role_strategy.clone(), account.clone()).prop_map( + |(caller, role, account)| AccessOp::Revoke { + caller, + role, + account, + }, + ), + (role_strategy.clone(), account.clone()) + .prop_map(|(role, caller)| AccessOp::Renounce { role, caller },), + (account, role_strategy.clone(), role_strategy).prop_map( + |(caller, role, admin_role)| AccessOp::SetRoleAdmin { + caller, + role, + admin_role, + }, + ), + ] +} + +fn access_snapshot( + access: &AccessControl, + roles: &[[u8; 32]], + accounts: &[Principal], +) -> AccessSnapshot { + AccessSnapshot { + admins: roles + .iter() + .copied() + .map(|role| (role, access.get_role_admin(role))) + .collect(), + memberships: roles + .iter() + .copied() + .flat_map(|role| { + accounts.iter().copied().map(move |account| { + (role, account, access.has_role(role, account)) + }) + }) + .collect(), + } +} + +fn apply_access(access: &mut AccessControl, op: &AccessOp) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + AccessOp::Grant { + caller, + role, + account, + } => access.grant_role(caller, role, account), + AccessOp::Revoke { + caller, + role, + account, + } => access.revoke_role(caller, role, account), + AccessOp::Renounce { role, caller } => { + access.renounce_role(role, caller) + } + AccessOp::SetRoleAdmin { + caller, + role, + admin_role, + } => access.set_role_admin(caller, role, admin_role), + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum TimelockOp { + SetDelay { + delay: u64, + }, + Schedule { + id: [u8; 32], + now: u64, + payload: Vec, + }, + Cancel { + id: [u8; 32], + }, + Execute { + id: [u8; 32], + now: u64, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct TimelockSnapshot { + min_delay: u64, + operations: Vec, +} + +type TimelockOperationSnapshot = ([u8; 32], Option<(u64, bool, Vec)>); + +#[derive(Clone, Debug)] +struct TimelockModel { + min_delay: u64, + operations: BTreeMap<[u8; 32], (u64, bool, Vec)>, +} + +impl TimelockModel { + fn new(min_delay: u64) -> Self { + Self { + min_delay, + operations: BTreeMap::new(), + } + } + + fn apply(&mut self, op: &TimelockOp) -> bool { + match op { + TimelockOp::SetDelay { delay } => { + self.min_delay = *delay; + true + } + TimelockOp::Schedule { id, now, payload } => { + if self.operations.contains_key(id) { + return false; + } + let Some(ready_at) = now.checked_add(self.min_delay) else { + return false; + }; + self.operations + .insert(*id, (ready_at, false, payload.clone())); + true + } + TimelockOp::Cancel { id } => { + let Some((_, done, _)) = self.operations.get(id) else { + return false; + }; + if *done { + return false; + } + self.operations.remove(id); + true + } + TimelockOp::Execute { id, now } => { + let Some((ready_at, done, _)) = self.operations.get_mut(id) + else { + return false; + }; + if *done || *now < *ready_at { + return false; + } + *done = true; + true + } + } + } + + fn snapshot(&self, ids: &[[u8; 32]]) -> TimelockSnapshot { + TimelockSnapshot { + min_delay: self.min_delay, + operations: ids + .iter() + .copied() + .map(|id| (id, self.operations.get(&id).cloned())) + .collect(), + } + } +} + +fn timelock_op_strategy() -> impl Strategy { + let id = (0u8..=5).prop_map(|i| [i; 32]); + let now = prop_oneof![0u64..=64, (0u64..=8).prop_map(|d| u64::MAX - d)]; + let payload = prop::collection::vec(0u8..=5, 0..4); + prop_oneof![ + (0u64..=16).prop_map(|delay| TimelockOp::SetDelay { delay }), + (id.clone(), now.clone(), payload).prop_map(|(id, now, payload)| { + TimelockOp::Schedule { id, now, payload } + },), + id.clone().prop_map(|id| TimelockOp::Cancel { id }), + (id, now).prop_map(|(id, now)| TimelockOp::Execute { id, now }), + ] +} + +fn timelock_snapshot( + timelock: &Timelock, + ids: &[[u8; 32]], +) -> TimelockSnapshot { + TimelockSnapshot { + min_delay: timelock.min_delay(), + operations: ids + .iter() + .copied() + .map(|id| { + ( + id, + timelock + .get(id) + .map(|op| (op.ready_at, op.done, op.payload.clone())), + ) + }) + .collect(), + } +} + +fn apply_timelock(timelock: &mut Timelock, op: &TimelockOp) -> bool { + catch_unwind(AssertUnwindSafe(|| match op { + TimelockOp::SetDelay { delay } => timelock.set_min_delay(*delay), + TimelockOp::Schedule { id, now, payload } => { + timelock.schedule(*id, *now, payload.clone()); + } + TimelockOp::Cancel { id } => timelock.cancel(*id), + TimelockOp::Execute { id, now } => { + timelock.execute(*id, *now); + } + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum UpgradeOp { + Prepare { + caller: Principal, + implementation: ContractId, + now: u64, + payload: Vec, + }, + Activate { + caller: Principal, + now: u64, + }, + Cancel { + caller: Principal, + }, + Rollback { + caller: Principal, + now: u64, + }, + Finalize { + caller: Principal, + now: u64, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct UpgradeSnapshot { + implementation: ContractId, + pending: Option<(ContractId, u64, Vec)>, + rollback_deadline: u64, +} + +#[derive(Clone, Debug)] +struct UpgradeModel { + admin: Principal, + implementation: ContractId, + previous: Option, + pending: Option<(ContractId, u64, Vec)>, + delay: u64, + rollback_window: u64, + rollback_deadline: u64, +} + +impl UpgradeModel { + fn new(admin: Principal, implementation: ContractId) -> Self { + Self { + admin, + implementation, + previous: None, + pending: None, + delay: 5, + rollback_window: 10, + rollback_deadline: 0, + } + } + + fn apply(&mut self, op: &UpgradeOp) -> bool { + match op { + UpgradeOp::Prepare { + caller, + implementation, + now, + payload, + } => { + if *caller != self.admin || is_zero_contract_id(*implementation) + { + return false; + } + let Some(eta) = now.checked_add(self.delay) else { + return false; + }; + self.pending = Some((*implementation, eta, payload.clone())); + true + } + UpgradeOp::Activate { caller, now } => { + if *caller != self.admin { + return false; + } + let Some((implementation, eta, _)) = self.pending.clone() + else { + return false; + }; + if *now < eta { + return false; + } + let Some(deadline) = now.checked_add(self.rollback_window) + else { + return false; + }; + self.previous = Some(self.implementation); + self.implementation = implementation; + self.pending = None; + self.rollback_deadline = deadline; + true + } + UpgradeOp::Cancel { caller } => { + if *caller != self.admin || self.pending.is_none() { + return false; + } + self.pending = None; + true + } + UpgradeOp::Rollback { caller, now } => { + if *caller != self.admin + || self.rollback_deadline == 0 + || *now > self.rollback_deadline + { + return false; + } + let Some(previous) = self.previous.take() else { + return false; + }; + self.implementation = previous; + self.pending = None; + self.rollback_deadline = 0; + true + } + UpgradeOp::Finalize { caller, now } => { + if *caller != self.admin + || (self.rollback_deadline != 0 + && *now < self.rollback_deadline) + { + return false; + } + self.previous = None; + self.rollback_deadline = 0; + true + } + } + } + + fn snapshot(&self) -> UpgradeSnapshot { + UpgradeSnapshot { + implementation: self.implementation, + pending: self.pending.clone(), + rollback_deadline: self.rollback_deadline, + } + } +} + +fn upgrade_op_strategy() -> impl Strategy { + let account = principal_strategy(); + let implementation = (0u8..=5).prop_map(contract_id); + let now = prop_oneof![0u64..=32, (0u64..=12).prop_map(|d| u64::MAX - d)]; + let payload = prop::collection::vec(0u8..=9, 0..4); + prop_oneof![ + (account.clone(), implementation, now.clone(), payload).prop_map( + |(caller, implementation, now, payload)| { + UpgradeOp::Prepare { + caller, + implementation, + now, + payload, + } + } + ), + (account.clone(), now.clone()) + .prop_map(|(caller, now)| { UpgradeOp::Activate { caller, now } }), + account + .clone() + .prop_map(|caller| UpgradeOp::Cancel { caller }), + (account.clone(), now.clone()) + .prop_map(|(caller, now)| { UpgradeOp::Rollback { caller, now } }), + (account, now) + .prop_map(|(caller, now)| UpgradeOp::Finalize { caller, now }), + ] +} + +fn upgrade_snapshot(upgrade: &UpgradeAdmin) -> UpgradeSnapshot { + UpgradeSnapshot { + implementation: upgrade.implementation(), + pending: upgrade.pending().map(|pending| { + ( + pending.implementation, + pending.eta, + pending.migrate_data.clone(), + ) + }), + rollback_deadline: upgrade.rollback_deadline(), + } +} + +fn apply_upgrade(upgrade: &mut UpgradeAdmin, op: &UpgradeOp) -> bool { + catch_unwind(AssertUnwindSafe(|| match op { + UpgradeOp::Prepare { + caller, + implementation, + now, + payload, + } => { + upgrade.prepare(*caller, *implementation, *now, payload.clone()); + } + UpgradeOp::Activate { caller, now } => { + upgrade.activate(*caller, *now); + } + UpgradeOp::Cancel { caller } => { + upgrade.cancel_pending(*caller); + } + UpgradeOp::Rollback { caller, now } => { + upgrade.rollback(*caller, *now); + } + UpgradeOp::Finalize { caller, now } => { + upgrade.finalize_rollback_window(*caller, *now); + } + })) + .is_ok() +} + +fn is_zero_contract_id(id: ContractId) -> bool { + id.to_bytes().iter().all(|byte| *byte == 0) +} + proptest! { #![proptest_config(ProptestConfig { cases: 256, @@ -825,4 +1717,154 @@ proptest! { } } } + + #[test] + fn authorization_negative_matrix_preserves_nonce(case in auth_case_strategy()) { + let (ok, nonce) = authorization_probe(case); + let expected_ok = matches!(case, AuthCase::Good); + prop_assert_eq!(ok, expected_ok, "authorization case diverged: {:?}", case); + prop_assert_eq!( + nonce, + if expected_ok { 1 } else { 0 }, + "authorization case moved nonce incorrectly: {:?}", + case, + ); + } + + #[test] + fn owner_set_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(owner_set_op_strategy(), 0..120), + ) { + let mut owners = OwnerSet::new(); + owners.init([principal(1), principal(2)]); + let mut model = BTreeSet::new(); + model.insert(principal(1)); + model.insert(principal(2)); + prop_assert_eq!(owners.owners(), model.iter().copied().collect::>()); + + for op in ops { + let before = owners.owners(); + let expected = apply_owner_set_model(&mut model, &op); + let actual = apply_owner_set(&mut owners, &op); + prop_assert_eq!(actual, expected, "owner-set operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + owners.owners(), + model.iter().copied().collect::>(), + "successful owner-set operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + owners.owners(), + before, + "failed owner-set operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn access_control_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(access_op_strategy(), 0..140), + ) { + let roles = [DEFAULT_ADMIN_ROLE, role(1), role(2), role(3)]; + let accounts = all_principals(); + let admin = principal(1); + let mut access = AccessControl::new(); + access.init_admin(admin); + let mut model = AccessModel::new(admin); + prop_assert_eq!( + access_snapshot(&access, &roles, &accounts), + model.snapshot(&roles, &accounts), + ); + + for op in ops { + let before = access_snapshot(&access, &roles, &accounts); + let expected = model.apply(&op); + let actual = apply_access(&mut access, &op); + prop_assert_eq!(actual, expected, "access-control operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + access_snapshot(&access, &roles, &accounts), + model.snapshot(&roles, &accounts), + "successful access-control operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + access_snapshot(&access, &roles, &accounts), + before, + "failed access-control operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn timelock_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(timelock_op_strategy(), 0..140), + ) { + let ids: Vec<[u8; 32]> = (0u8..=5).map(|i| [i; 32]).collect(); + let mut timelock = Timelock::new(5); + let mut model = TimelockModel::new(5); + prop_assert_eq!(timelock_snapshot(&timelock, &ids), model.snapshot(&ids)); + + for op in ops { + let before = timelock_snapshot(&timelock, &ids); + let expected = model.apply(&op); + let actual = apply_timelock(&mut timelock, &op); + prop_assert_eq!(actual, expected, "timelock operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + timelock_snapshot(&timelock, &ids), + model.snapshot(&ids), + "successful timelock operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + timelock_snapshot(&timelock, &ids), + before, + "failed timelock operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn upgrade_admin_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(upgrade_op_strategy(), 0..120), + ) { + let admin = principal(1); + let implementation = contract_id(1); + let mut upgrade = UpgradeAdmin::new(admin, implementation, 5, 10); + let mut model = UpgradeModel::new(admin, implementation); + prop_assert_eq!(upgrade_snapshot(&upgrade), model.snapshot()); + + for op in ops { + let before = upgrade_snapshot(&upgrade); + let expected = model.apply(&op); + let actual = apply_upgrade(&mut upgrade, &op); + prop_assert_eq!(actual, expected, "upgrade operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + upgrade_snapshot(&upgrade), + model.snapshot(), + "successful upgrade operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + upgrade_snapshot(&upgrade), + before, + "failed upgrade operation mutated state: {:?}", + op, + ); + } + } + } } From 9c7327ecce58a7f9b0db6f1c554f36f2c1244aa4 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sun, 26 Apr 2026 17:12:26 +0200 Subject: [PATCH 20/35] standards: harden atomic state transitions --- .../src/access/owner_set.rs | 9 ++- .../src/governance/controller.rs | 15 ++++- .../src/token/drc20/extensions.rs | 66 ++++++++++++++----- 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/standards/dusk-contract-standards/src/access/owner_set.rs b/standards/dusk-contract-standards/src/access/owner_set.rs index 8d5a08e8..378ba6a6 100644 --- a/standards/dusk-contract-standards/src/access/owner_set.rs +++ b/standards/dusk-contract-standards/src/access/owner_set.rs @@ -28,12 +28,17 @@ impl OwnerSet { if !self.owners.is_empty() { panic!("{}", error::ALREADY_INITIALIZED); } + let mut next = BTreeSet::new(); for owner in owners { - self.add_initial_owner(owner); + if owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + next.insert(owner); } - if self.owners.is_empty() { + if next.is_empty() { panic!("{}", error::INVALID_OWNER); } + self.owners = next; } /// Returns true when `principal` is an owner. diff --git a/standards/dusk-contract-standards/src/governance/controller.rs b/standards/dusk-contract-standards/src/governance/controller.rs index 1f100036..30cd0910 100644 --- a/standards/dusk-contract-standards/src/governance/controller.rs +++ b/standards/dusk-contract-standards/src/governance/controller.rs @@ -142,12 +142,23 @@ impl TimelockController { now: u64, ) -> u64 { self.access.assert_role(EXECUTOR_ROLE, caller); - let payload = self.timelock.execute(id, now); - let bytes: [u8; 8] = payload + let op = self + .timelock + .get(id) + .unwrap_or_else(|| panic!("{}", error::OPERATION_UNKNOWN)); + if op.done { + panic!("{}", error::OPERATION_DONE); + } + if now < op.ready_at { + panic!("{}", error::DELAY_NOT_ELAPSED); + } + let bytes: [u8; 8] = op + .payload .as_slice() .try_into() .unwrap_or_else(|_| panic!("{}", error::INVALID_OPERATION)); let min_delay = u64::from_be_bytes(bytes); + self.timelock.execute(id, now); self.set_min_delay(self.self_principal, min_delay); min_delay } diff --git a/standards/dusk-contract-standards/src/token/drc20/extensions.rs b/standards/dusk-contract-standards/src/token/drc20/extensions.rs index 4b2022ee..f92548a2 100644 --- a/standards/dusk-contract-standards/src/token/drc20/extensions.rs +++ b/standards/dusk-contract-standards/src/token/drc20/extensions.rs @@ -105,6 +105,14 @@ impl Checkpoints { self.checkpoints.push(Checkpoint { key, value }); } + fn assert_can_push(&self, key: u64) { + if let Some(last) = self.checkpoints.last() { + if key < last.key { + panic!("Checkpoints: non-monotonic key"); + } + } + } + /// Returns value at or before `key`. pub fn get_at(&self, key: u64) -> u64 { let mut low = 0usize; @@ -195,36 +203,64 @@ impl VotingUnits { return; } - if let Some(from) = from { + let from_next = if let Some(from) = from { let current = self.latest_votes(from); if current < amount { panic!("VotingUnits: insufficient units"); } - self.write_votes(from, timepoint, current - amount); - } + Some((from, current - amount)) + } else { + None + }; - if let Some(to) = to { + let to_next = if let Some(to) = to { let current = self.latest_votes(to); let next = current.checked_add(amount).expect(error::OVERFLOW); - self.write_votes(to, timepoint, next); - } + Some((to, next)) + } else { + None + }; - match (from, to) { - (None, Some(_)) => { - let next = self - .latest_total_supply() + let total_next = match (from, to) { + (None, Some(_)) => Some( + self.latest_total_supply() .checked_add(amount) - .expect(error::OVERFLOW); - self.total_supply.push(timepoint, next); - } + .expect(error::OVERFLOW), + ), (Some(_), None) => { let current = self.latest_total_supply(); if current < amount { panic!("VotingUnits: total supply underflow"); } - self.total_supply.push(timepoint, current - amount); + Some(current - amount) } - _ => {} + _ => None, + }; + + if let Some((from, _)) = from_next { + self.assert_can_write_votes(from, timepoint); + } + if let Some((to, _)) = to_next { + self.assert_can_write_votes(to, timepoint); + } + if total_next.is_some() { + self.total_supply.assert_can_push(timepoint); + } + + if let Some((from, value)) = from_next { + self.write_votes(from, timepoint, value); + } + if let Some((to, value)) = to_next { + self.write_votes(to, timepoint, value); + } + if let Some(value) = total_next { + self.total_supply.push(timepoint, value); + } + } + + fn assert_can_write_votes(&self, account: Principal, timepoint: u64) { + if let Some(trace) = self.accounts.get(&account) { + trace.assert_can_push(timepoint); } } } From 8a3f23773cee064dc6875ba11aebb4122ec0de18 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sun, 26 Apr 2026 17:12:30 +0200 Subject: [PATCH 21/35] standards: expand invariant hardening coverage --- docs/dusk-contract-standards-hardening.md | 39 +- docs/dusk-contract-standards.md | 10 +- .../tests/properties.rs | 1470 ++++++++++++++++- 3 files changed, 1447 insertions(+), 72 deletions(-) diff --git a/docs/dusk-contract-standards-hardening.md b/docs/dusk-contract-standards-hardening.md index 6fc01c5d..c6d9d26f 100644 --- a/docs/dusk-contract-standards-hardening.md +++ b/docs/dusk-contract-standards-hardening.md @@ -11,6 +11,8 @@ the composing contract: - signed authorization must bind contract, domain, action id, payload hash, nonce, principal, and expiry before nonce/replay state is consumed; +- Phoenix signed authorization must preserve nonce and replay-key state across + all rejected envelope, signature, expiry, policy, and replay-key cases; - owner, role, and upgrade-admin checks must not consume a valid signer nonce when the signer is not authorized for the policy being checked; - mixed owner-set checks must follow the same no-consume-on-unauthorized rule; @@ -21,6 +23,16 @@ the composing contract: - DRC721 total supply must equal the modeled number of live token ids; - DRC721 owner, approval, operator, balance, and enumerable views must remain internally consistent after arbitrary operation sequences; +- nonce import/export and replay-key import/export must be monotonic, + idempotent, and atomic on rejected nonce consumption; +- voting checkpoints must reject non-monotonic writes without partially + updating account or total-supply checkpoints; +- royalty registry mutations and overflow-prone royalty quotes must leave + registry state unchanged on rejection; +- supply-cap changes must reject cap reductions below current supply and + overflow-prone mints without moving the configured cap; +- owner-set initialization and timelock-controller maintenance operations must + validate before mutating durable state; - the reserved zero principal must not be accepted as an actor, recipient, spender, minter, burner, or operator where that would create ownership or authorization state. @@ -31,12 +43,20 @@ the composing contract: state-machine tests for: - action-bound authorization rejection cases; +- Phoenix action-bound authorization rejection and replay-key preservation + cases; - mixed owner sets; - access-control role/admin mutation; - timelock scheduling, execution, cancellation, and delay mutation; +- timelock-controller self-governed delay changes and malformed maintenance + payload rejection; - upgrade-admin prepare, activate, cancel, rollback, and finalization flows; +- nonce/replay state import, consumption, and rejection; +- two-step ownership; +- DRC20 supply caps and voting-unit checkpoints; - DRC20 accounting and allowance flows; -- DRC721 ownership, approval, operator, balance, and enumerable views. +- DRC721 ownership, approval, operator, balance, and enumerable views; +- DRC721 royalty registry mutation and quote behavior. The tests generate random operation sequences, run them against the real primitive and an independent model, and assert after each operation that: @@ -48,6 +68,15 @@ primitive and an independent model, and assert after each operation that: The saved proptest regression file keeps the self-transfer case that caught a DRC20 accounting bug during this pass. +The extended invariant pass also found and fixed: + +- a `VotingUnits::move_units` atomicity bug where an account checkpoint could + be written before a later total-supply checkpoint rejected the operation; +- an `OwnerSet::init` atomicity bug where a later invalid owner could leave + earlier owners in the set; +- a `TimelockController::execute_min_delay_change` atomicity bug where a + malformed payload could mark an operation done before being rejected. + ## Commands Run the hardening tests with: @@ -71,6 +100,14 @@ done On April 26, 2026, this loop ran for 14,404 seconds and completed 367 full property-suite iterations without a failure. +On April 26, 2026, the extended 16-test property suite also passed a +4,096-case focused run: + +```sh +PROPTEST_CASES=4096 PROPTEST_MAX_SHRINK_ITERS=8192 \ + cargo test -p dusk-contract-standards --test properties +``` + Run the full standards validation pass with: ```sh diff --git a/docs/dusk-contract-standards.md b/docs/dusk-contract-standards.md index fdf9f1e2..371c917d 100644 --- a/docs/dusk-contract-standards.md +++ b/docs/dusk-contract-standards.md @@ -203,10 +203,12 @@ The native test suite covers positive and negative paths for: - upgrade preparation, activation delay, cancellation, rollback, finalization, events, and namespaced proxy state. -The hardening branch also adds property-based DRC20 and DRC721 state-machine -tests. They compare arbitrary operation sequences against independent models -and assert that rejected operations leave native state unchanged. See -`docs/dusk-contract-standards-hardening.md` for the current hardening track. +The hardening branch also adds property-based state-machine tests for token, +authorization, ownership, role, timelock, proxy, nonce/replay, checkpoint, +royalty, and cap primitives. They compare arbitrary operation sequences +against independent models and assert that rejected operations leave native +state unchanged. See `docs/dusk-contract-standards-hardening.md` for the +current hardening track. The ignored VM test deploys all four Wasm examples, checks positive query paths, performs real Moonlight and Phoenix signed calls against the diff --git a/standards/dusk-contract-standards/tests/properties.rs b/standards/dusk-contract-standards/tests/properties.rs index 3e5b7994..c45d2400 100644 --- a/standards/dusk-contract-standards/tests/properties.rs +++ b/standards/dusk-contract-standards/tests/properties.rs @@ -2,29 +2,38 @@ use std::collections::{BTreeMap, BTreeSet}; use std::panic::{catch_unwind, AssertUnwindSafe}; use dusk_contract_standards::access::{ - AccessControl, OwnerSet, DEFAULT_ADMIN_ROLE, + AccessControl, Ownable2Step, OwnerSet, DEFAULT_ADMIN_ROLE, }; use dusk_contract_standards::auth::{ ActionEnvelope, AuthorizationManager, AuthorizedAction, - MoonlightAuthorization, SignedAuthorization, + MoonlightAuthorization, PhoenixSignatureAuthorization, SignedAuthorization, +}; +use dusk_contract_standards::core::{ + CallContext, NonceEntry, NonceManager, Principal, ReplayEntry, ReplayGuard, +}; +use dusk_contract_standards::governance::{ + Timelock, TimelockController, EXECUTOR_ROLE, PROPOSER_ROLE, }; -use dusk_contract_standards::core::{CallContext, Principal}; -use dusk_contract_standards::governance::Timelock; use dusk_contract_standards::proxy::UpgradeAdmin; use dusk_contract_standards::token::drc20::{ Allowance, ApproveCall, BalanceOf as BalanceOf20, DecreaseAllowanceCall, - Drc20, IncreaseAllowanceCall, Init as Init20, InitBalance, - TransferCall as Transfer20, TransferFromCall, + Drc20, IncreaseAllowanceCall, Init as Init20, InitBalance, SupplyCap, + TransferCall as Transfer20, TransferFromCall, VotingUnits, }; use dusk_contract_standards::token::drc721::{ ApproveCall as Approve721, BalanceOf as BalanceOf721, Drc721, GetApproved, - Init as Init721, InitToken, IsApprovedForAll, OwnerOf, - SetApprovalForAllCall, TokensOf, TransferFromCall as Transfer721, + Init as Init721, InitToken, IsApprovedForAll, OwnerOf, RoyaltyInfo, + RoyaltyRegistry, SetApprovalForAllCall, TokensOf, + TransferFromCall as Transfer721, MAX_BASIS_POINTS, }; use dusk_core::abi::ContractId; use dusk_core::signatures::bls::{ PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, }; +use dusk_core::signatures::schnorr::{ + PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, +}; +use dusk_core::JubJubScalar; use proptest::prelude::*; use rand::rngs::StdRng; use rand::SeedableRng; @@ -69,6 +78,10 @@ fn moonlight_secret(seed: u64) -> BlsSecretKey { BlsSecretKey::random(&mut rng) } +fn phoenix_secret(seed: u64) -> SchnorrSecretKey { + SchnorrSecretKey::from(JubJubScalar::from(seed)) +} + #[derive(Clone, Debug)] enum Drc20Op { Transfer { @@ -818,6 +831,59 @@ fn signed_action( }) } +#[derive(Clone, Copy, Debug)] +enum PhoenixAuthCase { + Good, + WrongContract, + WrongDomain, + WrongAction, + WrongPayload, + Expired, + FutureNonce, + BadPrincipal, + BadSignature, + WrongExpected, + OwnerSetNonOwner, + RoleNonMember, + AdminMismatch, + ReplayKeyUsed, +} + +fn phoenix_auth_case_strategy() -> impl Strategy { + prop_oneof![ + Just(PhoenixAuthCase::Good), + Just(PhoenixAuthCase::WrongContract), + Just(PhoenixAuthCase::WrongDomain), + Just(PhoenixAuthCase::WrongAction), + Just(PhoenixAuthCase::WrongPayload), + Just(PhoenixAuthCase::Expired), + Just(PhoenixAuthCase::FutureNonce), + Just(PhoenixAuthCase::BadPrincipal), + Just(PhoenixAuthCase::BadSignature), + Just(PhoenixAuthCase::WrongExpected), + Just(PhoenixAuthCase::OwnerSetNonOwner), + Just(PhoenixAuthCase::RoleNonMember), + Just(PhoenixAuthCase::AdminMismatch), + Just(PhoenixAuthCase::ReplayKeyUsed), + ] +} + +fn phoenix_signed_action( + secret: &SchnorrSecretKey, + public: SchnorrPublicKey, + action: AuthorizedAction, + replay_key: Option<[u8; 32]>, + seed: u64, +) -> SignedAuthorization { + let mut rng = StdRng::seed_from_u64(seed); + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action, + public_key: public, + signature: secret.sign(&mut rng, action.message_hash()), + replay_key, + }) +} + fn authorization_probe(case: AuthCase) -> (bool, u64) { let secret = moonlight_secret(31); let public = BlsPublicKey::from(&secret); @@ -939,6 +1005,141 @@ fn authorization_probe(case: AuthCase) -> (bool, u64) { (result, authorizations.nonce(signer, domain)) } +fn phoenix_authorization_probe(case: PhoenixAuthCase) -> (bool, u64, bool) { + let secret = phoenix_secret(43); + let public = SchnorrPublicKey::from(&secret); + let signer = Principal::phoenix_public_key(&public); + let contract = contract_id(44); + let domain = [45u8; 32]; + let action_id = [46u8; 32]; + let payload_hash = [47u8; 32]; + let replay_key = [48u8; 32]; + let envelope = + ActionEnvelope::new(contract, domain, action_id, payload_hash); + let mut action = AuthorizedAction { + contract, + domain, + action_id, + nonce: 0, + expires_at: 100, + principal: signer, + payload_hash, + }; + let mut now = 100; + let expected = match case { + PhoenixAuthCase::Good | PhoenixAuthCase::ReplayKeyUsed => signer, + PhoenixAuthCase::WrongExpected => principal(1), + PhoenixAuthCase::WrongContract => { + action.contract = contract_id(49); + signer + } + PhoenixAuthCase::WrongDomain => { + action.domain = [50u8; 32]; + signer + } + PhoenixAuthCase::WrongAction => { + action.action_id = [51u8; 32]; + signer + } + PhoenixAuthCase::WrongPayload => { + action.payload_hash = [52u8; 32]; + signer + } + PhoenixAuthCase::Expired => { + now = 101; + signer + } + PhoenixAuthCase::FutureNonce => { + action.nonce = 1; + signer + } + PhoenixAuthCase::BadPrincipal => { + action.principal = principal(2); + signer + } + PhoenixAuthCase::BadSignature + | PhoenixAuthCase::OwnerSetNonOwner + | PhoenixAuthCase::RoleNonMember + | PhoenixAuthCase::AdminMismatch => signer, + }; + + let signed = if matches!(case, PhoenixAuthCase::BadSignature) { + let original = action; + action.payload_hash = [53u8; 32]; + let mut rng = StdRng::seed_from_u64(55); + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action, + public_key: public, + signature: secret.sign(&mut rng, original.message_hash()), + replay_key: Some(replay_key), + }) + } else { + phoenix_signed_action(&secret, public, action, Some(replay_key), 56) + }; + + let mut authorizations = AuthorizationManager::new(); + if matches!(case, PhoenixAuthCase::ReplayKeyUsed) { + authorizations.import_replay_entries([ReplayEntry { + principal: signer, + key: replay_key, + }]); + } + + let result = catch_unwind(AssertUnwindSafe(|| match case { + PhoenixAuthCase::OwnerSetNonOwner => { + let mut owners = OwnerSet::new(); + owners.init([principal(1)]); + owners.authorize_owner_action( + &mut authorizations, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + PhoenixAuthCase::RoleNonMember => { + let checked_role = role(57); + let mut access = AccessControl::new(); + access.init_admin(principal(1)); + access.grant_role(principal(1), checked_role, principal(2)); + access.authorize_role_action( + checked_role, + &mut authorizations, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + PhoenixAuthCase::AdminMismatch => { + let upgrade = + UpgradeAdmin::new(principal(1), contract_id(58), 0, 0); + upgrade.authorize_admin_action( + &mut authorizations, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + _ => { + authorizations.authorize_principal_action( + expected, + CallContext::none(), + Some(&signed), + envelope, + now, + ); + } + })) + .is_ok(); + ( + result, + authorizations.nonce(signer, domain), + authorizations.replay_used(signer, replay_key), + ) +} + #[derive(Clone, Debug)] enum OwnerSetOp { Add { @@ -1030,6 +1231,114 @@ fn apply_owner_set(owners: &mut OwnerSet, op: &OwnerSetOp) -> bool { .is_ok() } +#[derive(Clone, Debug)] +enum Ownable2StepOp { + Transfer { + caller: Principal, + new_owner: Principal, + }, + Accept { + caller: Principal, + }, + Renounce { + caller: Principal, + }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Ownable2StepSnapshot { + owner: Option, + pending_owner: Option, +} + +#[derive(Clone, Copy, Debug)] +struct Ownable2StepModel { + owner: Option, + pending_owner: Option, +} + +impl Ownable2StepModel { + fn new(owner: Principal) -> Self { + Self { + owner: Some(owner), + pending_owner: None, + } + } + + fn apply(&mut self, op: &Ownable2StepOp) -> bool { + match *op { + Ownable2StepOp::Transfer { caller, new_owner } => { + if self.owner != Some(caller) || new_owner.is_zero() { + return false; + } + self.pending_owner = Some(new_owner); + true + } + Ownable2StepOp::Accept { caller } => { + if self.pending_owner != Some(caller) { + return false; + } + self.owner = Some(caller); + self.pending_owner = None; + true + } + Ownable2StepOp::Renounce { caller } => { + if self.owner != Some(caller) { + return false; + } + self.owner = None; + self.pending_owner = None; + true + } + } + } + + const fn snapshot(&self) -> Ownable2StepSnapshot { + Ownable2StepSnapshot { + owner: self.owner, + pending_owner: self.pending_owner, + } + } +} + +fn ownable2_step_op_strategy() -> impl Strategy { + let account = principal_strategy(); + prop_oneof![ + (account.clone(), account.clone()).prop_map(|(caller, new_owner)| { + Ownable2StepOp::Transfer { caller, new_owner } + },), + account + .clone() + .prop_map(|caller| Ownable2StepOp::Accept { caller }), + account.prop_map(|caller| Ownable2StepOp::Renounce { caller }), + ] +} + +fn ownable2_step_snapshot(ownable: &Ownable2Step) -> Ownable2StepSnapshot { + Ownable2StepSnapshot { + owner: ownable.owner(), + pending_owner: ownable.pending_owner(), + } +} + +fn apply_ownable2_step( + ownable: &mut Ownable2Step, + op: &Ownable2StepOp, +) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + Ownable2StepOp::Transfer { caller, new_owner } => { + ownable.transfer_ownership(caller, new_owner); + } + Ownable2StepOp::Accept { caller } => { + ownable.accept_ownership(caller); + } + Ownable2StepOp::Renounce { caller } => { + ownable.renounce_ownership(caller); + } + })) + .is_ok() +} + #[derive(Clone, Debug)] enum AccessOp { Grant { @@ -1413,6 +1722,13 @@ fn apply_timelock(timelock: &mut Timelock, op: &TimelockOp) -> bool { .is_ok() } +fn bad_min_delay_payload_strategy() -> impl Strategy> { + prop_oneof![ + prop::collection::vec(any::(), 0..8), + prop::collection::vec(any::(), 9..17), + ] +} + #[derive(Clone, Debug)] enum UpgradeOp { Prepare { @@ -1625,69 +1941,766 @@ fn is_zero_contract_id(id: ContractId) -> bool { id.to_bytes().iter().all(|byte| *byte == 0) } -proptest! { - #![proptest_config(ProptestConfig { - cases: 256, - max_shrink_iters: 4096, - ..ProptestConfig::default() - })] +#[derive(Clone, Debug)] +enum NonceReplayOp { + Consume { + principal: Principal, + domain: [u8; 32], + nonce: u64, + }, + UseNext { + principal: Principal, + domain: [u8; 32], + }, + InvalidateUntil { + principal: Principal, + domain: [u8; 32], + nonce: u64, + }, + ImportNonce { + entries: Vec, + }, + ConsumeReplay { + principal: Principal, + key: [u8; 32], + }, + ImportReplay { + entries: Vec, + }, +} - #[test] - fn drc20_state_matches_model_and_failed_calls_are_atomic( - ops in prop::collection::vec(drc20_op_strategy(), 0..160), - ) { - let accounts = all_principals(); - let mut token = Drc20::new(); - token.init(Init20 { - name: "Property Token".into(), - symbol: "PROP".into(), - decimals: 9, - initial_balances: vec![ - InitBalance { account: principal(1), amount: 100 }, - InitBalance { account: principal(2), amount: 50 }, - ], - }); - let mut model = Drc20Model::initial(); - prop_assert_eq!(drc20_snapshot(&token, &accounts), model.snapshot(&accounts)); +#[derive(Clone, Debug, PartialEq, Eq)] +struct NonceReplaySnapshot { + nonces: Vec, + replay_entries: Vec, +} - for op in ops { - let before = drc20_snapshot(&token, &accounts); - let expected = model.apply(&op); - let actual = apply_drc20_token(&mut token, &op); - prop_assert_eq!(actual, expected, "operation diverged: {:?}", op); - if expected { - prop_assert_eq!( - drc20_snapshot(&token, &accounts), - model.snapshot(&accounts), - "successful operation left an unexpected state: {:?}", - op, - ); - } else { - prop_assert_eq!( - drc20_snapshot(&token, &accounts), - before, - "failed operation mutated state: {:?}", - op, - ); - } - } +#[derive(Clone, Debug, Default)] +struct NonceReplayModel { + nonces: BTreeMap<(Principal, [u8; 32]), u64>, + replay_entries: BTreeSet<(Principal, [u8; 32])>, +} + +impl NonceReplayModel { + fn current(&self, principal: Principal, domain: [u8; 32]) -> u64 { + self.nonces.get(&(principal, domain)).copied().unwrap_or(0) } - #[test] - fn drc721_state_matches_model_and_failed_calls_are_atomic( - ops in prop::collection::vec(drc721_op_strategy(), 0..140), - ) { - let accounts = all_principals(); - let token_ids: Vec = (0..=8).collect(); - let mut token = Drc721::new(); - token.init(Init721 { - name: "Property NFT".into(), - symbol: "PNFT".into(), - base_uri: "ipfs://property/".into(), - initial_tokens: vec![ - InitToken { account: principal(1), token_id: 1 }, - InitToken { account: principal(2), token_id: 2 }, - ], + fn apply(&mut self, op: &NonceReplayOp) -> bool { + match op { + NonceReplayOp::Consume { + principal, + domain, + nonce, + } => { + if self.current(*principal, *domain) != *nonce { + return false; + } + let Some(next) = nonce.checked_add(1) else { + return false; + }; + self.nonces.insert((*principal, *domain), next); + true + } + NonceReplayOp::UseNext { principal, domain } => { + let current = self.current(*principal, *domain); + let Some(next) = current.checked_add(1) else { + return false; + }; + self.nonces.insert((*principal, *domain), next); + true + } + NonceReplayOp::InvalidateUntil { + principal, + domain, + nonce, + } => { + if *nonce < self.current(*principal, *domain) { + return false; + } + self.nonces.insert((*principal, *domain), *nonce); + true + } + NonceReplayOp::ImportNonce { entries } => { + for entry in entries { + let current = self.current(entry.principal, entry.domain); + if entry.nonce > current { + self.nonces.insert( + (entry.principal, entry.domain), + entry.nonce, + ); + } + } + true + } + NonceReplayOp::ConsumeReplay { principal, key } => { + self.replay_entries.insert((*principal, *key)) + } + NonceReplayOp::ImportReplay { entries } => { + for entry in entries { + self.replay_entries.insert((entry.principal, entry.key)); + } + true + } + } + } + + fn snapshot(&self) -> NonceReplaySnapshot { + NonceReplaySnapshot { + nonces: self + .nonces + .iter() + .map(|((principal, domain), nonce)| NonceEntry { + principal: *principal, + domain: *domain, + nonce: *nonce, + }) + .collect(), + replay_entries: self + .replay_entries + .iter() + .map(|(principal, key)| ReplayEntry { + principal: *principal, + key: *key, + }) + .collect(), + } + } +} + +fn nonce_replay_op_strategy() -> impl Strategy { + let principal = principal_strategy(); + let domain = (0u8..=4).prop_map(|i| [i; 32]); + let nonce = prop_oneof![ + 12 => 0u64..=12, + 1 => Just(u64::MAX), + 1 => (0u64..=4).prop_map(|d| u64::MAX - d), + ]; + let nonce_entry = (principal.clone(), domain.clone(), nonce.clone()) + .prop_map(|(principal, domain, nonce)| NonceEntry { + principal, + domain, + nonce, + }); + let replay_entry = (principal.clone(), domain.clone()) + .prop_map(|(principal, key)| ReplayEntry { principal, key }); + prop_oneof![ + (principal.clone(), domain.clone(), nonce.clone()).prop_map( + |(principal, domain, nonce)| NonceReplayOp::Consume { + principal, + domain, + nonce, + } + ), + (principal.clone(), domain.clone()).prop_map(|(principal, domain)| { + NonceReplayOp::UseNext { principal, domain } + },), + (principal.clone(), domain.clone(), nonce).prop_map( + |(principal, domain, nonce)| NonceReplayOp::InvalidateUntil { + principal, + domain, + nonce, + }, + ), + prop::collection::vec(nonce_entry, 0..5) + .prop_map(|entries| NonceReplayOp::ImportNonce { entries }), + (principal, domain).prop_map(|(principal, key)| { + NonceReplayOp::ConsumeReplay { principal, key } + }), + prop::collection::vec(replay_entry, 0..5) + .prop_map(|entries| NonceReplayOp::ImportReplay { entries }), + ] +} + +fn nonce_replay_snapshot( + nonces: &NonceManager, + replays: &ReplayGuard, +) -> NonceReplaySnapshot { + NonceReplaySnapshot { + nonces: nonces.entries(), + replay_entries: replays.entries(), + } +} + +fn apply_nonce_replay( + nonces: &mut NonceManager, + replays: &mut ReplayGuard, + op: &NonceReplayOp, +) -> bool { + catch_unwind(AssertUnwindSafe(|| match op { + NonceReplayOp::Consume { + principal, + domain, + nonce, + } => { + nonces.consume(*principal, *domain, *nonce); + } + NonceReplayOp::UseNext { principal, domain } => { + nonces.use_next(*principal, *domain); + } + NonceReplayOp::InvalidateUntil { + principal, + domain, + nonce, + } => nonces.invalidate_until(*principal, *domain, *nonce), + NonceReplayOp::ImportNonce { entries } => { + nonces.import_entries(entries.iter().copied()); + } + NonceReplayOp::ConsumeReplay { principal, key } => { + replays.consume(*principal, *key); + } + NonceReplayOp::ImportReplay { entries } => { + replays.import_entries(entries.iter().copied()); + } + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum VotingOp { + Move { + from: Option, + to: Option, + amount: u64, + timepoint: u64, + }, + Write { + account: Principal, + timepoint: u64, + value: u64, + }, +} + +#[derive(Clone, Debug, Default)] +struct CheckpointModel { + entries: Vec<(u64, u64)>, +} + +impl CheckpointModel { + fn latest(&self) -> u64 { + self.entries.last().map(|(_, value)| *value).unwrap_or(0) + } + + fn push(&mut self, timepoint: u64, value: u64) -> bool { + if let Some(last) = self.entries.last_mut() { + if timepoint < last.0 { + return false; + } + if timepoint == last.0 { + last.1 = value; + return true; + } + } + self.entries.push((timepoint, value)); + true + } + + fn get_at(&self, timepoint: u64) -> u64 { + self.entries + .iter() + .rev() + .find_map(|(key, value)| (*key <= timepoint).then_some(*value)) + .unwrap_or(0) + } +} + +#[derive(Clone, Debug, Default)] +struct VotingModel { + accounts: BTreeMap, + total_supply: CheckpointModel, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct VotingSnapshot { + latest_votes: Vec<(Principal, u64)>, + past_votes: Vec<(Principal, u64, u64)>, + latest_total: u64, + past_total: Vec<(u64, u64)>, +} + +impl VotingModel { + fn latest_votes(&self, account: Principal) -> u64 { + self.accounts + .get(&account) + .map(CheckpointModel::latest) + .unwrap_or(0) + } + + fn write_votes( + &mut self, + account: Principal, + timepoint: u64, + value: u64, + ) -> bool { + self.accounts + .entry(account) + .or_default() + .push(timepoint, value) + } + + fn apply(&mut self, op: &VotingOp) -> bool { + match *op { + VotingOp::Write { + account, + timepoint, + value, + } => self.write_votes(account, timepoint, value), + VotingOp::Move { + from, + to, + amount, + timepoint, + } => { + if amount == 0 || from == to { + return true; + } + let from_next = if let Some(from) = from { + let current = self.latest_votes(from); + if current < amount { + return false; + } + Some((from, current - amount)) + } else { + None + }; + let to_next = if let Some(to) = to { + let Some(next) = self.latest_votes(to).checked_add(amount) + else { + return false; + }; + Some((to, next)) + } else { + None + }; + let total_next = match (from, to) { + (None, Some(_)) => { + let Some(next) = + self.total_supply.latest().checked_add(amount) + else { + return false; + }; + Some(next) + } + (Some(_), None) => { + let current = self.total_supply.latest(); + if current < amount { + return false; + } + Some(current - amount) + } + _ => None, + }; + + let mut next = self.clone(); + if let Some((account, value)) = from_next { + if !next.write_votes(account, timepoint, value) { + return false; + } + } + if let Some((account, value)) = to_next { + if !next.write_votes(account, timepoint, value) { + return false; + } + } + if let Some(value) = total_next { + if !next.total_supply.push(timepoint, value) { + return false; + } + } + *self = next; + true + } + } + } + + fn snapshot( + &self, + accounts: &[Principal], + timepoints: &[u64], + ) -> VotingSnapshot { + VotingSnapshot { + latest_votes: accounts + .iter() + .copied() + .map(|account| (account, self.latest_votes(account))) + .collect(), + past_votes: accounts + .iter() + .copied() + .flat_map(|account| { + timepoints.iter().copied().map(move |timepoint| { + let value = self + .accounts + .get(&account) + .map(|trace| trace.get_at(timepoint)) + .unwrap_or(0); + (account, timepoint, value) + }) + }) + .collect(), + latest_total: self.total_supply.latest(), + past_total: timepoints + .iter() + .copied() + .map(|timepoint| { + (timepoint, self.total_supply.get_at(timepoint)) + }) + .collect(), + } + } +} + +fn voting_op_strategy() -> impl Strategy { + let account = principal_strategy(); + let account_or_none = + prop_oneof![Just(None), account.clone().prop_map(Some)]; + let amount = amount_strategy(); + let timepoint = prop_oneof![ + 12 => 0u64..=24, + 1 => (0u64..=4).prop_map(|d| u64::MAX - d), + ]; + prop_oneof![ + ( + account_or_none.clone(), + account_or_none, + amount.clone(), + timepoint.clone() + ) + .prop_map(|(from, to, amount, timepoint)| VotingOp::Move { + from, + to, + amount, + timepoint, + }), + (account, timepoint, amount).prop_map(|(account, timepoint, value)| { + VotingOp::Write { + account, + timepoint, + value, + } + },), + ] +} + +fn voting_snapshot( + votes: &VotingUnits, + accounts: &[Principal], + timepoints: &[u64], +) -> VotingSnapshot { + VotingSnapshot { + latest_votes: accounts + .iter() + .copied() + .map(|account| (account, votes.latest_votes(account))) + .collect(), + past_votes: accounts + .iter() + .copied() + .flat_map(|account| { + timepoints.iter().copied().map(move |timepoint| { + (account, timepoint, votes.past_votes(account, timepoint)) + }) + }) + .collect(), + latest_total: votes.latest_total_supply(), + past_total: timepoints + .iter() + .copied() + .map(|timepoint| (timepoint, votes.past_total_supply(timepoint))) + .collect(), + } +} + +fn apply_voting(votes: &mut VotingUnits, op: &VotingOp) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + VotingOp::Move { + from, + to, + amount, + timepoint, + } => votes.move_units(from, to, amount, timepoint), + VotingOp::Write { + account, + timepoint, + value, + } => votes.write_votes(account, timepoint, value), + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum RoyaltyOp { + SetDefault { info: RoyaltyInfo }, + ClearDefault, + SetToken { token_id: u64, info: RoyaltyInfo }, + ClearToken { token_id: u64 }, + Query { token_id: u64, sale_price: u64 }, +} + +#[derive(Clone, Debug, Default)] +struct RoyaltyModel { + default: Option, + tokens: BTreeMap, +} + +type RoyaltyQuoteSnapshot = (u64, u64, Option<(Principal, u64)>); + +#[derive(Clone, Debug, PartialEq, Eq)] +struct RoyaltySnapshot { + default: Option, + royalty_for: Vec<(u64, Option)>, + quotes: Vec, +} + +impl RoyaltyModel { + fn royalty_for(&self, token_id: u64) -> Option { + self.tokens.get(&token_id).copied().or(self.default) + } + + fn quote( + &self, + token_id: u64, + sale_price: u64, + ) -> Option<(Principal, u64)> { + let Some(info) = self.royalty_for(token_id) else { + return Some((principal(0), 0)); + }; + let amount = sale_price.checked_mul(u64::from(info.basis_points))? + / u64::from(MAX_BASIS_POINTS); + Some((info.receiver, amount)) + } + + fn apply(&mut self, op: &RoyaltyOp) -> bool { + match *op { + RoyaltyOp::SetDefault { info } => { + if !valid_royalty(info) { + return false; + } + self.default = Some(info); + true + } + RoyaltyOp::ClearDefault => { + self.default = None; + true + } + RoyaltyOp::SetToken { token_id, info } => { + if !valid_royalty(info) { + return false; + } + self.tokens.insert(token_id, info); + true + } + RoyaltyOp::ClearToken { token_id } => { + self.tokens.remove(&token_id); + true + } + RoyaltyOp::Query { + token_id, + sale_price, + } => self.quote(token_id, sale_price).is_some(), + } + } + + fn snapshot( + &self, + token_ids: &[u64], + sale_prices: &[u64], + ) -> RoyaltySnapshot { + RoyaltySnapshot { + default: self.default, + royalty_for: token_ids + .iter() + .copied() + .map(|token_id| (token_id, self.royalty_for(token_id))) + .collect(), + quotes: token_ids + .iter() + .copied() + .flat_map(|token_id| { + sale_prices.iter().copied().map(move |sale_price| { + (token_id, sale_price, self.quote(token_id, sale_price)) + }) + }) + .collect(), + } + } +} + +fn valid_royalty(info: RoyaltyInfo) -> bool { + !info.receiver.is_zero() && info.basis_points <= MAX_BASIS_POINTS +} + +fn royalty_info_strategy() -> BoxedStrategy { + (principal_strategy(), 0u16..=10_500) + .prop_map(|(receiver, basis_points)| RoyaltyInfo { + receiver, + basis_points, + }) + .boxed() +} + +fn royalty_op_strategy() -> impl Strategy { + let token_id = 0u64..=5; + let sale_price = prop_oneof![ + 12 => 0u64..=100_000, + 2 => (0u64..=16).prop_map(|d| u64::MAX - d), + 1 => Just(u64::MAX), + ]; + let info = royalty_info_strategy(); + prop_oneof![ + info.clone().prop_map(|info| RoyaltyOp::SetDefault { info }), + Just(RoyaltyOp::ClearDefault), + (token_id.clone(), info).prop_map(|(token_id, info)| { + RoyaltyOp::SetToken { token_id, info } + }), + token_id + .clone() + .prop_map(|token_id| RoyaltyOp::ClearToken { token_id }), + (token_id, sale_price).prop_map(|(token_id, sale_price)| { + RoyaltyOp::Query { + token_id, + sale_price, + } + }), + ] +} + +fn royalty_snapshot( + royalties: &RoyaltyRegistry, + token_ids: &[u64], + sale_prices: &[u64], +) -> RoyaltySnapshot { + RoyaltySnapshot { + default: royalties.default_royalty(), + royalty_for: token_ids + .iter() + .copied() + .map(|token_id| (token_id, royalties.royalty_for(token_id))) + .collect(), + quotes: token_ids + .iter() + .copied() + .flat_map(|token_id| { + sale_prices.iter().copied().map(move |sale_price| { + let quote = catch_unwind(AssertUnwindSafe(|| { + royalties.royalty_info(token_id, sale_price) + })) + .ok() + .map(|quote| (quote.receiver, quote.amount)); + (token_id, sale_price, quote) + }) + }) + .collect(), + } +} + +fn apply_royalty(royalties: &mut RoyaltyRegistry, op: &RoyaltyOp) -> bool { + catch_unwind(AssertUnwindSafe(|| match *op { + RoyaltyOp::SetDefault { info } => royalties.set_default_royalty(info), + RoyaltyOp::ClearDefault => royalties.clear_default_royalty(), + RoyaltyOp::SetToken { token_id, info } => { + royalties.set_token_royalty(token_id, info); + } + RoyaltyOp::ClearToken { token_id } => { + royalties.clear_token_royalty(token_id) + } + RoyaltyOp::Query { + token_id, + sale_price, + } => { + royalties.royalty_info(token_id, sale_price); + } + })) + .is_ok() +} + +#[derive(Clone, Debug)] +enum CapOp { + AssertMint { current_supply: u64, amount: u64 }, + SetCap { current_supply: u64, cap: u64 }, +} + +fn cap_op_strategy() -> impl Strategy { + let amount = amount_strategy(); + prop_oneof![ + (amount.clone(), amount.clone()).prop_map( + |(current_supply, amount)| CapOp::AssertMint { + current_supply, + amount, + }, + ), + (amount.clone(), amount).prop_map(|(current_supply, cap)| { + CapOp::SetCap { + current_supply, + cap, + } + }), + ] +} + +proptest! { + #![proptest_config(ProptestConfig { + cases: 256, + max_shrink_iters: 4096, + ..ProptestConfig::default() + })] + + #[test] + fn drc20_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(drc20_op_strategy(), 0..160), + ) { + let accounts = all_principals(); + let mut token = Drc20::new(); + token.init(Init20 { + name: "Property Token".into(), + symbol: "PROP".into(), + decimals: 9, + initial_balances: vec![ + InitBalance { account: principal(1), amount: 100 }, + InitBalance { account: principal(2), amount: 50 }, + ], + }); + let mut model = Drc20Model::initial(); + prop_assert_eq!(drc20_snapshot(&token, &accounts), model.snapshot(&accounts)); + + for op in ops { + let before = drc20_snapshot(&token, &accounts); + let expected = model.apply(&op); + let actual = apply_drc20_token(&mut token, &op); + prop_assert_eq!(actual, expected, "operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + drc20_snapshot(&token, &accounts), + model.snapshot(&accounts), + "successful operation left an unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + drc20_snapshot(&token, &accounts), + before, + "failed operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn drc721_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(drc721_op_strategy(), 0..140), + ) { + let accounts = all_principals(); + let token_ids: Vec = (0..=8).collect(); + let mut token = Drc721::new(); + token.init(Init721 { + name: "Property NFT".into(), + symbol: "PNFT".into(), + base_uri: "ipfs://property/".into(), + initial_tokens: vec![ + InitToken { account: principal(1), token_id: 1 }, + InitToken { account: principal(2), token_id: 2 }, + ], }); let mut model = Drc721Model::initial(); prop_assert_eq!( @@ -1731,6 +2744,27 @@ proptest! { ); } + #[test] + fn phoenix_authorization_negative_matrix_preserves_nonce_and_replay( + case in phoenix_auth_case_strategy(), + ) { + let (ok, nonce, replay_used) = phoenix_authorization_probe(case); + let expected_ok = matches!(case, PhoenixAuthCase::Good); + prop_assert_eq!(ok, expected_ok, "Phoenix authorization case diverged: {:?}", case); + prop_assert_eq!( + nonce, + if expected_ok { 1 } else { 0 }, + "Phoenix authorization case moved nonce incorrectly: {:?}", + case, + ); + prop_assert_eq!( + replay_used, + matches!(case, PhoenixAuthCase::Good | PhoenixAuthCase::ReplayKeyUsed), + "Phoenix authorization case moved replay state incorrectly: {:?}", + case, + ); + } + #[test] fn owner_set_state_matches_model_and_failed_calls_are_atomic( ops in prop::collection::vec(owner_set_op_strategy(), 0..120), @@ -1765,6 +2799,85 @@ proptest! { } } + #[test] + fn owner_set_init_rejects_invalid_inputs_without_partial_state( + initial_owners in prop::collection::vec(principal_strategy(), 0..16), + ) { + let mut expected = BTreeSet::new(); + let mut has_zero = false; + for owner in &initial_owners { + has_zero |= owner.is_zero(); + expected.insert(*owner); + } + let expected_ok = !has_zero && !expected.is_empty(); + + let mut owners = OwnerSet::new(); + let actual_ok = catch_unwind(AssertUnwindSafe(|| { + owners.init(initial_owners.clone()); + })) + .is_ok(); + + prop_assert_eq!(actual_ok, expected_ok); + if expected_ok { + prop_assert_eq!( + owners.owners(), + expected.iter().copied().collect::>(), + ); + prop_assert!( + catch_unwind(AssertUnwindSafe(|| { + owners.init([principal(3)]); + })) + .is_err(), + "successful initialization allowed a second init", + ); + } else { + prop_assert!( + owners.is_empty(), + "failed initialization left partial owner state", + ); + prop_assert!( + catch_unwind(AssertUnwindSafe(|| { + owners.init([principal(1)]); + })) + .is_ok(), + "failed initialization poisoned the owner set", + ); + } + } + + #[test] + fn ownable2_step_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(ownable2_step_op_strategy(), 0..120), + ) { + let owner = principal(1); + let mut ownable = Ownable2Step::new(); + ownable.init(owner); + let mut model = Ownable2StepModel::new(owner); + prop_assert_eq!(ownable2_step_snapshot(&ownable), model.snapshot()); + + for op in ops { + let before = ownable2_step_snapshot(&ownable); + let expected = model.apply(&op); + let actual = apply_ownable2_step(&mut ownable, &op); + prop_assert_eq!(actual, expected, "ownable2-step operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + ownable2_step_snapshot(&ownable), + model.snapshot(), + "successful ownable2-step operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + ownable2_step_snapshot(&ownable), + before, + "failed ownable2-step operation mutated state: {:?}", + op, + ); + } + } + } + #[test] fn access_control_state_matches_model_and_failed_calls_are_atomic( ops in prop::collection::vec(access_op_strategy(), 0..140), @@ -1835,6 +2948,75 @@ proptest! { } } + #[test] + fn timelock_controller_rejects_bad_min_delay_payloads_atomically( + payload in bad_min_delay_payload_strategy(), + now in 0u64..=64, + ) { + let self_principal = principal(2); + let admin = principal(1); + let mut controller = TimelockController::new(self_principal, admin, 5); + let id = [88u8; 32]; + prop_assert!(controller.has_role(PROPOSER_ROLE, admin)); + prop_assert!(controller.has_role(EXECUTOR_ROLE, admin)); + + let ready_at = controller.schedule(admin, id, now, payload); + let before = controller.get(id).cloned(); + let actual = catch_unwind(AssertUnwindSafe(|| { + controller.execute_min_delay_change(admin, id, ready_at); + })) + .is_ok(); + + prop_assert!(!actual, "bad min-delay payload unexpectedly executed"); + prop_assert_eq!(controller.timelock().min_delay(), 5); + prop_assert_eq!( + controller.get(id).cloned(), + before, + "bad min-delay payload mutated scheduled operation state", + ); + } + + #[test] + fn timelock_controller_min_delay_change_flow_preserves_delay_until_ready( + new_delay in any::(), + now in 0u64..=64, + ) { + let self_principal = principal(2); + let admin = principal(1); + let mut controller = TimelockController::new(self_principal, admin, 5); + let id = [89u8; 32]; + let ready_at = controller.schedule_min_delay_change( + admin, + id, + now, + new_delay, + ); + let before = controller.get(id).cloned(); + + let early = catch_unwind(AssertUnwindSafe(|| { + controller.execute_min_delay_change(admin, id, ready_at - 1); + })) + .is_ok(); + prop_assert!(!early, "min-delay change executed before ready_at"); + prop_assert_eq!(controller.timelock().min_delay(), 5); + prop_assert_eq!(controller.get(id).cloned(), before); + + let applied = controller.execute_min_delay_change(admin, id, ready_at); + prop_assert_eq!(applied, new_delay); + prop_assert_eq!(controller.timelock().min_delay(), new_delay); + prop_assert!( + controller.get(id).map(|operation| operation.done).unwrap_or(false), + "executed min-delay change was not marked done", + ); + prop_assert!( + catch_unwind(AssertUnwindSafe(|| { + controller.set_min_delay(admin, 5); + })) + .is_err(), + "admin changed delay directly without timelock self-call", + ); + } + #[test] fn upgrade_admin_state_matches_model_and_failed_calls_are_atomic( ops in prop::collection::vec(upgrade_op_strategy(), 0..120), @@ -1867,4 +3049,158 @@ proptest! { } } } + + #[test] + fn nonce_and_replay_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(nonce_replay_op_strategy(), 0..180), + ) { + let mut nonces = NonceManager::new(); + let mut replays = ReplayGuard::new(); + let mut model = NonceReplayModel::default(); + prop_assert_eq!(nonce_replay_snapshot(&nonces, &replays), model.snapshot()); + + for op in ops { + let before = nonce_replay_snapshot(&nonces, &replays); + let expected = model.apply(&op); + let actual = apply_nonce_replay(&mut nonces, &mut replays, &op); + prop_assert_eq!(actual, expected, "nonce/replay operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + nonce_replay_snapshot(&nonces, &replays), + model.snapshot(), + "successful nonce/replay operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + nonce_replay_snapshot(&nonces, &replays), + before, + "failed nonce/replay operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn voting_units_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(voting_op_strategy(), 0..160), + ) { + let accounts = all_principals(); + let timepoints = [0, 1, 2, 3, 4, 8, 16, 24, u64::MAX]; + let mut votes = VotingUnits::new(); + let mut model = VotingModel::default(); + prop_assert_eq!( + voting_snapshot(&votes, &accounts, &timepoints), + model.snapshot(&accounts, &timepoints), + ); + + for op in ops { + let before = voting_snapshot(&votes, &accounts, &timepoints); + let expected = model.apply(&op); + let actual = apply_voting(&mut votes, &op); + prop_assert_eq!(actual, expected, "voting operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + voting_snapshot(&votes, &accounts, &timepoints), + model.snapshot(&accounts, &timepoints), + "successful voting operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + voting_snapshot(&votes, &accounts, &timepoints), + before, + "failed voting operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn royalty_registry_state_matches_model_and_failed_calls_are_atomic( + ops in prop::collection::vec(royalty_op_strategy(), 0..140), + ) { + let token_ids = [0, 1, 2, 3, 4, 5]; + let sale_prices = [0, 1, 10_000, 100_000, u64::MAX]; + let mut royalties = RoyaltyRegistry::new(); + let mut model = RoyaltyModel::default(); + prop_assert_eq!( + royalty_snapshot(&royalties, &token_ids, &sale_prices), + model.snapshot(&token_ids, &sale_prices), + ); + + for op in ops { + let before = royalty_snapshot(&royalties, &token_ids, &sale_prices); + let expected = model.apply(&op); + let actual = apply_royalty(&mut royalties, &op); + prop_assert_eq!(actual, expected, "royalty operation diverged: {:?}", op); + if expected { + prop_assert_eq!( + royalty_snapshot(&royalties, &token_ids, &sale_prices), + model.snapshot(&token_ids, &sale_prices), + "successful royalty operation left unexpected state: {:?}", + op, + ); + } else { + prop_assert_eq!( + royalty_snapshot(&royalties, &token_ids, &sale_prices), + before, + "failed royalty operation mutated state: {:?}", + op, + ); + } + } + } + + #[test] + fn supply_cap_rejects_overflow_and_cap_reduction( + initial_cap in amount_strategy(), + ops in prop::collection::vec(cap_op_strategy(), 0..120), + ) { + let mut cap = SupplyCap::new(initial_cap); + let mut model_cap = initial_cap; + prop_assert_eq!(cap.cap(), model_cap); + + for op in ops { + let before = cap.cap(); + let expected = match op { + CapOp::AssertMint { current_supply, amount } => { + current_supply + .checked_add(amount) + .map(|next| next <= model_cap) + .unwrap_or(false) + } + CapOp::SetCap { current_supply, cap } => { + if cap < current_supply { + false + } else { + model_cap = cap; + true + } + } + }; + let actual = catch_unwind(AssertUnwindSafe(|| match op { + CapOp::AssertMint { current_supply, amount } => { + cap.assert_mint(current_supply, amount) + } + CapOp::SetCap { current_supply, cap: next_cap } => { + cap.set_cap(current_supply, next_cap) + } + })) + .is_ok(); + prop_assert_eq!(actual, expected, "supply-cap operation diverged: {:?}", op); + if expected { + prop_assert_eq!(cap.cap(), model_cap); + } else { + prop_assert_eq!( + cap.cap(), + before, + "failed supply-cap operation mutated state: {:?}", + op, + ); + } + } + } } From 3d13bcfae9e5eaf2f3376cb7b8e2bcf45af0ced9 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sun, 26 Apr 2026 18:23:36 +0200 Subject: [PATCH 22/35] standards: add data-driver hardening fuzz --- .github/workflows/standards-hardening.yml | 31 + Cargo.lock | 58 ++ Cargo.toml | 6 + Makefile | 18 +- docs/dusk-contract-standards-hardening.md | 31 +- standards/dusk-contract-standards/Cargo.toml | 2 + standards/dusk-contract-standards/src/lib.rs | 4 + .../tests/data_driver_fuzz.rs | 639 ++++++++++++++++++ .../tests/properties.rs | 22 +- 9 files changed, 801 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/standards-hardening.yml create mode 100644 standards/dusk-contract-standards/tests/data_driver_fuzz.rs diff --git a/.github/workflows/standards-hardening.yml b/.github/workflows/standards-hardening.yml new file mode 100644 index 00000000..65423da4 --- /dev/null +++ b/.github/workflows/standards-hardening.yml @@ -0,0 +1,31 @@ +name: Standards Hardening + +on: + workflow_dispatch: + inputs: + property_cases: + description: Property-test cases per invariant + required: false + default: "8192" + data_driver_cases: + description: Forge data-driver fuzz cases + required: false + default: "2048" + schedule: + - cron: "17 2 * * *" + +jobs: + hardening: + name: Standards property and ABI fuzzing + runs-on: core + timeout-minutes: 240 + env: + STANDARDS_PROPTEST_CASES: ${{ github.event.inputs.property_cases || '8192' }} + STANDARDS_PROPTEST_MAX_SHRINK_ITERS: "16384" + STANDARDS_DATA_DRIVER_FUZZ_CASES: ${{ github.event.inputs.data_driver_cases || '2048' }} + STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS: "4096" + steps: + - uses: actions/checkout@v4 + - uses: dsherret/rust-toolchain-file@v1 + - run: rustup target add wasm32-unknown-unknown + - run: make standards-hardening diff --git a/Cargo.lock b/Cargo.lock index 94282cb6..fc31c1c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1049,11 +1049,13 @@ version = "0.1.0" dependencies = [ "bytecheck", "dusk-core", + "dusk-data-driver", "dusk-vm", "proptest", "rand 0.8.5", "rkyv", "serde", + "serde_json", "serde_with", "time", ] @@ -1093,6 +1095,9 @@ name = "dusk-data-driver" version = "0.3.2-alpha.1" dependencies = [ "bytecheck", + "dusk-core", + "dusk-wasmtime", + "parking_lot", "rkyv", "serde", "serde_json", @@ -1755,6 +1760,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -1935,6 +1949,29 @@ dependencies = [ "group", ] +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "paste" version = "1.0.15" @@ -2290,6 +2327,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -2478,6 +2524,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "seahash" version = "4.1.0" @@ -3106,6 +3158,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 8f3c09b3..9453099e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,11 @@ dusk-data-driver = { path = "../rusk-private/data-drivers/data-driver" } dusk-forge = { path = "../forge-explicit-emits" } dusk-vm = { path = "../rusk-private/vm" } dusk-wallet-core = { path = "../rusk-private/wallet-core" } +dusk-wasmtime = { version = "21.0.0-alpha", default-features = false, features = [ + "cranelift", + "runtime", + "parallel-compilation", +] } rusk-profile = { path = "../rusk-private/rusk-profile" } rusk-prover = { path = "../rusk-private/rusk-prover" } @@ -36,6 +41,7 @@ bytecheck = { version = "0.6.12", default-features = false } criterion = "0.5.1" ff = { version = "0.13", default-features = false } rand = { version = "0.8.5", default-features = false } +parking_lot = "0.12.3" ringbuffer = "0.15" rkyv = { version = "0.7.39", default-features = false } serde = { version = "1", default-features = false, features = [ diff --git a/Makefile b/Makefile index 29253be6..30250c9e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,9 @@ STANDARDS_EXAMPLES := standards/examples/authorization_counter standards/examples/drc20_roles_pausable standards/examples/drc721_collection standards/examples/proxy_counter SUBDIRS := standards/dusk-contract-standards $(STANDARDS_EXAMPLES) tests/alice tests/bob tests/charlie genesis/transfer genesis/stake tests/host_fn +STANDARDS_PROPTEST_CASES ?= 8192 +STANDARDS_PROPTEST_MAX_SHRINK_ITERS ?= 16384 +STANDARDS_DATA_DRIVER_FUZZ_CASES ?= 2048 +STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS ?= 4096 all: setup-compiler $(SUBDIRS) ## Build all the contracts @@ -17,6 +21,18 @@ wasm: setup-compiler ## Generate the WASM for all the contracts standards-data-drivers: ## Generate Forge data-driver WASM for standards reference contracts $(MAKE) $(STANDARDS_EXAMPLES) MAKECMDGOALS=wasm-dd +standards-properties: ## Run the longer standards property hardening suite + STANDARDS_PROPTEST_CASES=$(STANDARDS_PROPTEST_CASES) \ + STANDARDS_PROPTEST_MAX_SHRINK_ITERS=$(STANDARDS_PROPTEST_MAX_SHRINK_ITERS) \ + cargo test -p dusk-contract-standards --test properties + +standards-data-driver-fuzz: standards-data-drivers ## Fuzz Forge data-driver JSON/rkyv input codecs + STANDARDS_DATA_DRIVER_FUZZ_CASES=$(STANDARDS_DATA_DRIVER_FUZZ_CASES) \ + STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS=$(STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS) \ + cargo test -p dusk-contract-standards --test data_driver_fuzz -- --ignored + +standards-hardening: standards-properties standards-data-driver-fuzz ## Run long standards hardening checks + clippy: setup-compiler ## Run clippy $(MAKE) $(SUBDIRS) MAKECMDGOALS=clippy @@ -33,4 +49,4 @@ doc: $(SUBDIRS) ## Run doc gen $(SUBDIRS): $(MAKE) -C $@ $(MAKECMDGOALS) -.PHONY: all test help standards-data-drivers $(SUBDIRS) +.PHONY: all test help standards-data-drivers standards-properties standards-data-driver-fuzz standards-hardening $(SUBDIRS) diff --git a/docs/dusk-contract-standards-hardening.md b/docs/dusk-contract-standards-hardening.md index c6d9d26f..7c06eb69 100644 --- a/docs/dusk-contract-standards-hardening.md +++ b/docs/dusk-contract-standards-hardening.md @@ -108,6 +108,28 @@ PROPTEST_CASES=4096 PROPTEST_MAX_SHRINK_ITERS=8192 \ cargo test -p dusk-contract-standards --test properties ``` +The property suite now also honors `STANDARDS_PROPTEST_CASES` and +`STANDARDS_PROPTEST_MAX_SHRINK_ITERS`, which lets CI run longer without +editing the test source: + +```sh +STANDARDS_PROPTEST_CASES=8192 STANDARDS_PROPTEST_MAX_SHRINK_ITERS=16384 \ + cargo test -p dusk-contract-standards --test properties +``` + +Forge data-driver ABI fuzzing covers JSON-to-rkyv input encoding, input +decoding, roundtrips, malformed JSON, bad shapes, unknown functions, and +mutated encoded payloads for the four standards reference contracts: + +```sh +make standards-data-drivers +STANDARDS_DATA_DRIVER_FUZZ_CASES=2048 \ + cargo test -p dusk-contract-standards --test data_driver_fuzz -- --ignored +``` + +The `standards-hardening` workflow runs these longer property and data-driver +fuzz jobs on demand and nightly. + Run the full standards validation pass with: ```sh @@ -121,12 +143,13 @@ cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,proxy-counter/contract cargo test -p dusk-contract-standards --test examples_vm -- --ignored make standards-data-drivers +cargo test -p dusk-contract-standards --test data_driver_fuzz -- --ignored cargo clippy -p dusk-contract-standards --all-targets -- -D warnings ``` ## Next Research Items -The next hardening layer should add ABI-level fuzzing of Forge call payloads, -longer-running property tests in CI, mutation testing for authorization and -pause paths, and local-node scenario tests that intentionally mix successful -transactions with rejected transactions across block boundaries. +The next hardening layer should add mutation testing for authorization and +pause paths, differential tests against independent client encoders, and +local-node scenario tests that intentionally mix successful transactions with +rejected transactions across block boundaries. diff --git a/standards/dusk-contract-standards/Cargo.toml b/standards/dusk-contract-standards/Cargo.toml index e1efd7ad..7394906d 100644 --- a/standards/dusk-contract-standards/Cargo.toml +++ b/standards/dusk-contract-standards/Cargo.toml @@ -19,6 +19,8 @@ contract = [] serde = ["dep:serde", "dep:serde_with", "dep:time", "dusk-core/serde"] [dev-dependencies] +dusk-data-driver = { workspace = true, features = ["reader"] } dusk-vm = { workspace = true } proptest = "1.6.0" rand = { workspace = true, features = ["std_rng"] } +serde_json = { workspace = true, features = ["std"] } diff --git a/standards/dusk-contract-standards/src/lib.rs b/standards/dusk-contract-standards/src/lib.rs index 74cbb42a..43393465 100644 --- a/standards/dusk-contract-standards/src/lib.rs +++ b/standards/dusk-contract-standards/src/lib.rs @@ -12,12 +12,16 @@ extern crate alloc; +#[cfg(test)] +use dusk_data_driver as _; #[cfg(test)] use dusk_vm as _; #[cfg(test)] use proptest as _; #[cfg(test)] use rand as _; +#[cfg(test)] +use serde_json as _; #[cfg(feature = "serde")] use serde_with as _; #[cfg(feature = "serde")] diff --git a/standards/dusk-contract-standards/tests/data_driver_fuzz.rs b/standards/dusk-contract-standards/tests/data_driver_fuzz.rs new file mode 100644 index 00000000..eaf76675 --- /dev/null +++ b/standards/dusk-contract-standards/tests/data_driver_fuzz.rs @@ -0,0 +1,639 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::PathBuf; + +use dusk_data_driver::reader::DriverReader; +use proptest::prelude::*; +use proptest::test_runner::{Config as ProptestConfig, TestRunner}; +use serde_json::{json, Value}; + +const DRIVER_NAMES: [&str; 4] = [ + "authorization_counter", + "drc20_roles_pausable", + "drc721_collection", + "proxy_counter", +]; + +#[derive(Clone, Copy, Debug)] +enum ReferenceContract { + AuthorizationCounter, + Drc20, + Drc721, + ProxyCounter, +} + +#[derive(Clone, Debug)] +struct DriverCase { + contract: ReferenceContract, + selector: u8, + a: u8, + b: u8, + amount: u64, + flag: bool, +} + +struct CallSpec { + driver: &'static str, + function: &'static str, + input: Value, +} + +#[test] +#[ignore = "requires `make standards-data-drivers` before running"] +fn data_driver_schemas_are_loadable() { + for name in DRIVER_NAMES { + let driver = load_driver(name); + let version = driver.get_version().expect("driver version"); + assert!(!version.is_empty(), "{name} returned an empty version"); + + let schema = driver.get_schema().expect("driver schema"); + let functions = schema + .get("functions") + .and_then(Value::as_array) + .unwrap_or_else(|| panic!("{name} schema has no functions array")); + assert!(!functions.is_empty(), "{name} schema has no functions"); + } +} + +#[test] +#[ignore = "requires `make standards-data-drivers` before running"] +fn data_driver_inputs_roundtrip_and_reject_bad_shapes() { + let drivers = load_drivers(); + assert_rejects_bad_inputs(&drivers); + + let mut runner = TestRunner::new(ProptestConfig { + cases: env_usize("STANDARDS_DATA_DRIVER_FUZZ_CASES") + .or_else(|| env_usize("PROPTEST_CASES")) + .unwrap_or(512) as u32, + max_shrink_iters: env_usize("STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS") + .or_else(|| env_usize("PROPTEST_MAX_SHRINK_ITERS")) + .unwrap_or(2048) as u32, + ..ProptestConfig::default() + }); + + runner + .run(&driver_case_strategy(), |case| { + let call = call_for(&case); + let driver = drivers.get(call.driver).expect("loaded driver"); + assert_input_roundtrip(driver, &call)?; + assert_mutated_inputs_are_handled(driver, &call)?; + Ok(()) + }) + .expect("data-driver fuzz cases failed"); +} + +fn load_drivers() -> BTreeMap<&'static str, DriverReader> { + DRIVER_NAMES + .into_iter() + .map(|name| (name, load_driver(name))) + .collect() +} + +fn load_driver(name: &str) -> DriverReader { + let path = driver_path(name); + let wasm = fs::read(&path).unwrap_or_else(|error| { + panic!( + "failed to read {}: {error}; run `make standards-data-drivers`", + path.display() + ) + }); + DriverReader::new(&wasm) + .unwrap_or_else(|error| panic!("failed to load {name}: {error}")) +} + +fn driver_path(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../target/data-driver/wasm32-unknown-unknown/release") + .join(format!("{name}.wasm")) +} + +fn assert_rejects_bad_inputs(drivers: &BTreeMap<&'static str, DriverReader>) { + for (name, driver) in drivers { + assert!( + driver + .encode_input_fn("definitely_missing", "null") + .is_err(), + "{name} accepted an unknown function" + ); + assert!( + driver.encode_input_fn("value", "{").is_err(), + "{name} accepted malformed JSON" + ); + } + + let auth = drivers.get("authorization_counter").unwrap(); + assert!( + auth.encode_input_fn( + "nonce", + &json!({ + "principal": {"kind": "Phoenix", "bytes": [1]}, + "domain": bytes32(0), + }) + .to_string() + ) + .is_err(), + "authorization counter accepted a malformed Phoenix principal" + ); + + let drc20 = drivers.get("drc20_roles_pausable").unwrap(); + assert!( + drc20 + .encode_input_fn( + "balance_of", + &json!({"account": {"kind": "Moonlight", "bytes": bytes32(1)}}) + .to_string(), + ) + .is_err(), + "DRC20 accepted a short Moonlight principal" + ); + assert!( + drc20.encode_input_fn("burn", r#""not a number""#).is_err(), + "DRC20 accepted a string u64" + ); + + let drc721 = drivers.get("drc721_collection").unwrap(); + assert!( + drc721 + .encode_input_fn( + "set_approval_for_all", + &json!({"operator": principal(1), "approved": "yes"}) + .to_string(), + ) + .is_err(), + "DRC721 accepted a string bool" + ); +} + +fn assert_input_roundtrip( + driver: &DriverReader, + call: &CallSpec, +) -> Result<(), TestCaseError> { + let input = call.input.to_string(); + let encoded = + driver + .encode_input_fn(call.function, &input) + .map_err(|error| { + TestCaseError::fail(format!("encode failed: {error}")) + })?; + prop_assert!( + !encoded.is_empty() || call.input.is_null(), + "{}:{} encoded to an empty non-unit payload", + call.driver, + call.function + ); + + let decoded = + driver + .decode_input_fn(call.function, &encoded) + .map_err(|error| { + TestCaseError::fail(format!("decode failed: {error}")) + })?; + let reencoded = driver + .encode_input_fn(call.function, &decoded.to_string()) + .map_err(|error| { + TestCaseError::fail(format!("re-encode failed: {error}")) + })?; + let decoded_again = driver + .decode_input_fn(call.function, &reencoded) + .map_err(|error| { + TestCaseError::fail(format!( + "decode-after-reencode failed: {error}" + )) + })?; + + prop_assert_eq!(decoded_again, decoded); + Ok(()) +} + +fn assert_mutated_inputs_are_handled( + driver: &DriverReader, + call: &CallSpec, +) -> Result<(), TestCaseError> { + let encoded = driver + .encode_input_fn(call.function, &call.input.to_string()) + .map_err(|error| { + TestCaseError::fail(format!("encode failed: {error}")) + })?; + + for mutated in mutated_inputs(&encoded) { + if let Ok(decoded) = driver.decode_input_fn(call.function, &mutated) { + driver + .encode_input_fn(call.function, &decoded.to_string()) + .map_err(|error| { + TestCaseError::fail(format!( + "mutated input decoded to unencodable JSON: {error}" + )) + })?; + } + } + + Ok(()) +} + +fn mutated_inputs(encoded: &[u8]) -> Vec> { + let mut mutations = Vec::new(); + mutations.push(Vec::new()); + + if !encoded.is_empty() { + let mut flipped = encoded.to_vec(); + flipped[0] ^= 0x80; + mutations.push(flipped); + + let mut middle = encoded.to_vec(); + let index = middle.len() / 2; + middle[index] ^= 0x55; + mutations.push(middle); + + let mut truncated = encoded.to_vec(); + truncated.truncate(truncated.len() / 2); + mutations.push(truncated); + } + + let mut appended = encoded.to_vec(); + appended.extend_from_slice(&[0xaa, 0x55, 0x00, 0xff]); + mutations.push(appended); + + mutations +} + +fn driver_case_strategy() -> impl Strategy { + ( + 0u8..4, + any::(), + any::(), + any::(), + any::(), + any::(), + ) + .prop_map(|(contract, selector, a, b, amount, flag)| DriverCase { + contract: match contract { + 0 => ReferenceContract::AuthorizationCounter, + 1 => ReferenceContract::Drc20, + 2 => ReferenceContract::Drc721, + _ => ReferenceContract::ProxyCounter, + }, + selector, + a, + b, + amount, + flag, + }) +} + +fn call_for(case: &DriverCase) -> CallSpec { + match case.contract { + ReferenceContract::AuthorizationCounter => auth_counter_call(case), + ReferenceContract::Drc20 => drc20_call(case), + ReferenceContract::Drc721 => drc721_call(case), + ReferenceContract::ProxyCounter => proxy_call(case), + } +} + +fn auth_counter_call(case: &DriverCase) -> CallSpec { + match case.selector % 4 { + 0 => call("authorization_counter", "init", Value::Null), + 1 => call("authorization_counter", "value", Value::Null), + 2 => call("authorization_counter", "last_authorizer", Value::Null), + _ => call("authorization_counter", "nonce", nonce_query(case.a)), + } +} + +#[allow(clippy::too_many_lines)] +fn drc20_call(case: &DriverCase) -> CallSpec { + match case.selector % 27 { + 0 => call( + "drc20_roles_pausable", + "init", + json!({ + "admin": principal(case.a), + "token": { + "name": "Fuzz Token", + "symbol": "FUZZ", + "decimals": case.a % 19, + "initial_balances": [{ + "account": principal(case.b), + "amount": bounded(case.amount), + }], + }, + "cap": bounded(case.amount).saturating_add(1), + }), + ), + 1 => call("drc20_roles_pausable", "name", Value::Null), + 2 => call("drc20_roles_pausable", "symbol", Value::Null), + 3 => call("drc20_roles_pausable", "decimals", Value::Null), + 4 => call("drc20_roles_pausable", "total_supply", Value::Null), + 5 => call( + "drc20_roles_pausable", + "balance_of", + json!({"account": principal(case.a)}), + ), + 6 => call( + "drc20_roles_pausable", + "allowance", + json!({"owner": principal(case.a), "spender": principal(case.b)}), + ), + 7 => call("drc20_roles_pausable", "nonce", nonce_query(case.a)), + 8 => call("drc20_roles_pausable", "cap", Value::Null), + 9 => call("drc20_roles_pausable", "remaining_mintable", Value::Null), + 10 => call("drc20_roles_pausable", "latest_votes", principal(case.a)), + 11 => call( + "drc20_roles_pausable", + "past_votes", + json!({"account": principal(case.a), "timepoint": case.amount}), + ), + 12 => call("drc20_roles_pausable", "latest_total_votes", Value::Null), + 13 => call( + "drc20_roles_pausable", + "past_total_supply", + json!(case.amount), + ), + 14 => call( + "drc20_roles_pausable", + "has_role", + json!({"role": bytes32(case.a), "account": principal(case.b)}), + ), + 15 => call( + "drc20_roles_pausable", + "transfer", + json!({"to": principal(case.a), "amount": case.amount}), + ), + 16 => call( + "drc20_roles_pausable", + "approve", + json!({"spender": principal(case.a), "amount": case.amount}), + ), + 17 => call( + "drc20_roles_pausable", + "increase_allowance", + json!({"spender": principal(case.a), "added_amount": case.amount}), + ), + 18 => call( + "drc20_roles_pausable", + "decrease_allowance", + json!({ + "spender": principal(case.a), + "subtracted_amount": case.amount, + }), + ), + 19 => call( + "drc20_roles_pausable", + "transfer_from", + json!({ + "owner": principal(case.a), + "to": principal(case.b), + "amount": case.amount, + }), + ), + 20 => call( + "drc20_roles_pausable", + "mint", + json!({ + "to": principal(case.a), + "amount": case.amount, + "authorization": null, + }), + ), + 21 => call("drc20_roles_pausable", "burn", json!(case.amount)), + 22 => call( + "drc20_roles_pausable", + "pause", + json!({"authorization": null}), + ), + 23 => call( + "drc20_roles_pausable", + "unpause", + json!({"authorization": null}), + ), + 24 => call("drc20_roles_pausable", "paused", Value::Null), + 25 => call( + "drc20_roles_pausable", + "grant_role", + json!({ + "role": bytes32(case.a), + "account": principal(case.b), + "authorization": null, + }), + ), + _ => call( + "drc20_roles_pausable", + "revoke_role", + json!({ + "role": bytes32(case.a), + "account": principal(case.b), + "authorization": null, + }), + ), + } +} + +#[allow(clippy::too_many_lines)] +fn drc721_call(case: &DriverCase) -> CallSpec { + match case.selector % 27 { + 0 => call( + "drc721_collection", + "init", + json!({ + "owner": principal(case.a), + "token": { + "name": "Fuzz Collection", + "symbol": "FUZ", + "base_uri": "ipfs://fuzz/", + "initial_tokens": [{ + "account": principal(case.b), + "token_id": bounded(case.amount), + }], + }, + "default_royalty": royalty(case.a, case.amount), + }), + ), + 1 => call("drc721_collection", "name", Value::Null), + 2 => call("drc721_collection", "symbol", Value::Null), + 3 => call("drc721_collection", "base_uri", Value::Null), + 4 => call( + "drc721_collection", + "token_uri", + json!({"token_id": case.amount}), + ), + 5 => call( + "drc721_collection", + "token_by_index", + json!({"index": bounded(case.amount)}), + ), + 6 => call( + "drc721_collection", + "token_of_owner_by_index", + json!({"owner": principal(case.a), "index": bounded(case.amount)}), + ), + 7 => call( + "drc721_collection", + "tokens_of", + json!({"owner": principal(case.a)}), + ), + 8 => call("drc721_collection", "total_supply", Value::Null), + 9 => call( + "drc721_collection", + "balance_of", + json!({"account": principal(case.a)}), + ), + 10 => call( + "drc721_collection", + "owner_of", + json!({"token_id": case.amount}), + ), + 11 => call( + "drc721_collection", + "get_approved", + json!({"token_id": case.amount}), + ), + 12 => call( + "drc721_collection", + "is_approved_for_all", + json!({"owner": principal(case.a), "operator": principal(case.b)}), + ), + 13 => call("drc721_collection", "nonce", nonce_query(case.a)), + 14 => call( + "drc721_collection", + "royalty_info", + json!({"token_id": case.amount, "sale_price": bounded(case.amount)}), + ), + 15 => call( + "drc721_collection", + "approve", + json!({"approved": principal(case.a), "token_id": case.amount}), + ), + 16 => call( + "drc721_collection", + "set_approval_for_all", + json!({"operator": principal(case.a), "approved": case.flag}), + ), + 17 => call( + "drc721_collection", + "transfer_from", + json!({ + "from": principal(case.a), + "to": principal(case.b), + "token_id": case.amount, + }), + ), + 18 => call( + "drc721_collection", + "mint", + json!({ + "to": principal(case.a), + "token_id": case.amount, + "authorization": null, + }), + ), + 19 => call("drc721_collection", "burn", json!(case.amount)), + 20 => { + call("drc721_collection", "pause", json!({"authorization": null})) + } + 21 => call( + "drc721_collection", + "unpause", + json!({"authorization": null}), + ), + 22 => call("drc721_collection", "paused", Value::Null), + 23 => call( + "drc721_collection", + "set_default_royalty", + json!({"info": royalty(case.a, case.amount), "authorization": null}), + ), + 24 => call( + "drc721_collection", + "clear_default_royalty", + json!({"authorization": null}), + ), + 25 => call( + "drc721_collection", + "set_token_royalty", + json!({ + "token_id": case.amount, + "info": royalty(case.a, case.amount), + "authorization": null, + }), + ), + _ => call( + "drc721_collection", + "clear_token_royalty", + json!({"token_id": case.amount, "authorization": null}), + ), + } +} + +fn proxy_call(case: &DriverCase) -> CallSpec { + match case.selector % 9 { + 0 => call("proxy_counter", "implementation", Value::Null), + 1 => call("proxy_counter", "rollback_deadline", Value::Null), + 2 => call("proxy_counter", "nonce", nonce_query(case.a)), + 3 => call("proxy_counter", "value", Value::Null), + 4 => call("proxy_counter", "increment", Value::Null), + 5 => call( + "proxy_counter", + "set_value", + json!({"value": case.amount, "authorization": null}), + ), + 6 => call( + "proxy_counter", + "activate_upgrade", + json!({"authorization": null}), + ), + 7 => call( + "proxy_counter", + "cancel_pending_upgrade", + json!({"authorization": null}), + ), + _ => call( + "proxy_counter", + "finalize_rollback_window", + json!({"authorization": null}), + ), + } +} + +fn call( + driver: &'static str, + function: &'static str, + input: Value, +) -> CallSpec { + CallSpec { + driver, + function, + input, + } +} + +fn nonce_query(seed: u8) -> Value { + json!({"principal": principal(seed), "domain": bytes32(seed)}) +} + +fn principal(seed: u8) -> Value { + match seed % 3 { + 0 => json!({"kind": "Phoenix", "bytes": bytes32(seed)}), + 1 => json!({"kind": "Contract", "bytes": bytes32(seed)}), + _ => json!({"kind": "Moonlight", "bytes": bytes193(seed)}), + } +} + +fn royalty(seed: u8, amount: u64) -> Value { + json!({ + "receiver": principal(seed), + "basis_points": (amount % 10_001) as u16, + }) +} + +fn bytes32(seed: u8) -> Vec { + (0..32).map(|index| seed.wrapping_add(index)).collect() +} + +fn bytes193(seed: u8) -> Vec { + (0..193).map(|index| seed.wrapping_add(index)).collect() +} + +fn bounded(value: u64) -> u64 { + value % 1_000_000 +} + +fn env_usize(name: &str) -> Option { + std::env::var(name).ok()?.parse().ok() +} diff --git a/standards/dusk-contract-standards/tests/properties.rs b/standards/dusk-contract-standards/tests/properties.rs index c45d2400..ff1fe0b5 100644 --- a/standards/dusk-contract-standards/tests/properties.rs +++ b/standards/dusk-contract-standards/tests/properties.rs @@ -38,6 +38,22 @@ use proptest::prelude::*; use rand::rngs::StdRng; use rand::SeedableRng; +fn standards_proptest_config() -> ProptestConfig { + ProptestConfig { + cases: env_usize("STANDARDS_PROPTEST_CASES") + .or_else(|| env_usize("PROPTEST_CASES")) + .unwrap_or(256) as u32, + max_shrink_iters: env_usize("STANDARDS_PROPTEST_MAX_SHRINK_ITERS") + .or_else(|| env_usize("PROPTEST_MAX_SHRINK_ITERS")) + .unwrap_or(4096) as u32, + ..ProptestConfig::default() + } +} + +fn env_usize(name: &str) -> Option { + std::env::var(name).ok()?.parse().ok() +} + fn principal(index: u8) -> Principal { if index == 0 { Principal::Contract(ContractId::from_bytes([0u8; 32])) @@ -2639,11 +2655,7 @@ fn cap_op_strategy() -> impl Strategy { } proptest! { - #![proptest_config(ProptestConfig { - cases: 256, - max_shrink_iters: 4096, - ..ProptestConfig::default() - })] + #![proptest_config(standards_proptest_config())] #[test] fn drc20_state_matches_model_and_failed_calls_are_atomic( From fa68716e48bece12a9d4adbf40ec3f8fb0531c57 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sun, 26 Apr 2026 18:23:39 +0200 Subject: [PATCH 23/35] standards: restore wallet in local smoke --- docs/dusk-contract-standards.md | 12 ++++++++++++ scripts/dusk-contract-standards-local-smoke.sh | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/docs/dusk-contract-standards.md b/docs/dusk-contract-standards.md index 371c917d..be65c4a9 100644 --- a/docs/dusk-contract-standards.md +++ b/docs/dusk-contract-standards.md @@ -262,3 +262,15 @@ test, serializes deploy-time init arguments using the Dusk ABI serializer, deploys the four example contracts with `rusk-wallet`, and queries one exported function from each deployment. `WALLET_DIR` should point at a funded local wallet profile. + +For an isolated local smoke wallet, set `WALLET_RESTORE_FILE` to a funded +wallet backup. The script restores it into `WALLET_DIR` when the directory does +not already contain `wallet.keystore.json`: + +```sh +RUSK_URL=http://127.0.0.1:18080 \ +RUSK_WALLET_BIN=/path/to/rusk-wallet \ +WALLET_DIR=target/rusk-local-smoke/wallet \ +WALLET_RESTORE_FILE=/path/to/wallet.dat \ +./scripts/dusk-contract-standards-local-smoke.sh +``` diff --git a/scripts/dusk-contract-standards-local-smoke.sh b/scripts/dusk-contract-standards-local-smoke.sh index c61a6503..c0222dd4 100755 --- a/scripts/dusk-contract-standards-local-smoke.sh +++ b/scripts/dusk-contract-standards-local-smoke.sh @@ -6,6 +6,7 @@ RUSK_URL="${RUSK_URL:-http://localhost:8080}" RUSK_WALLET_BIN="${RUSK_WALLET_BIN:-$(command -v rusk-wallet || true)}" WALLET_DIR="${WALLET_DIR:-${ROOT_DIR}/target/dusk-contract-standards-wallet}" WALLET_PASSWORD="${WALLET_PASSWORD:-password}" +WALLET_RESTORE_FILE="${WALLET_RESTORE_FILE:-}" DEPLOY_NONCE_BASE="${DEPLOY_NONCE_BASE:-100}" cd "$ROOT_DIR" @@ -62,6 +63,21 @@ fi mkdir -p "$WALLET_DIR" +if [[ -n "$WALLET_RESTORE_FILE" && ! -f "$WALLET_DIR/wallet.keystore.json" ]]; then + if [[ ! -f "$WALLET_RESTORE_FILE" ]]; then + echo "WALLET_RESTORE_FILE does not exist: ${WALLET_RESTORE_FILE}" >&2 + exit 2 + fi + + echo "Restoring local smoke wallet from ${WALLET_RESTORE_FILE}" + RUSK_WALLET_PWD="$WALLET_PASSWORD" "$RUSK_WALLET_BIN" \ + --wallet-dir "$WALLET_DIR" \ + --state "$RUSK_URL" \ + --prover "$RUSK_URL" \ + --password "$WALLET_PASSWORD" \ + restore -f "$WALLET_RESTORE_FILE" +fi + encode_args() { cargo run -q -p dusk-contract-standards --example encode_local_smoke_args -- "$@" } From 1e9682711e2f243463d53bd3ce456e3907f1dec0 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sun, 26 Apr 2026 20:57:18 +0200 Subject: [PATCH 24/35] standards: add audit readiness packet --- docs/dusk-contract-standards-audit.md | 137 ++++++++++ docs/dusk-contract-standards-hardening.md | 20 +- docs/dusk-contract-standards-security.md | 11 + .../dusk-contract-standards-audit-grade.sh | 56 ++++ .../tests/data_driver_fuzz.rs | 250 ++++++++++++++++++ 5 files changed, 469 insertions(+), 5 deletions(-) create mode 100644 docs/dusk-contract-standards-audit.md create mode 100755 scripts/dusk-contract-standards-audit-grade.sh diff --git a/docs/dusk-contract-standards-audit.md b/docs/dusk-contract-standards-audit.md new file mode 100644 index 00000000..f1136fbd --- /dev/null +++ b/docs/dusk-contract-standards-audit.md @@ -0,0 +1,137 @@ +# Dusk Contract Standards Audit Packet + +This document is the auditor-facing entry point for the Dusk-native standards +layer. It captures the review scope, security assumptions, invariant matrix, +validation commands, and known residual risks for this branch. + +## Scope + +In scope: + +- `standards/dusk-contract-standards/src/**` +- `standards/examples/authorization_counter` +- `standards/examples/drc20_roles_pausable` +- `standards/examples/drc721_collection` +- `standards/examples/proxy_counter` +- `scripts/dusk-contract-standards-local-smoke.sh` +- `standards/dusk-contract-standards/tests/**` + +Out of scope: + +- Dusk protocol consensus, wallet, node, prover, and VM internals. +- Forge macro implementation correctness, except where the generated + data-driver and Wasm artifacts are exercised through this repository's tests. +- Economic policy choices such as exact upgrade delays, role assignment + procedures, and marketplace royalty enforcement. +- Compatibility with Ethereum/OpenZeppelin ABIs where Dusk-native semantics + deliberately diverge. + +## Security Model + +The standards layer assumes: + +- `CallContext::current()` correctly reports observed Moonlight and contract + callers when the runtime exposes such callers. +- Phoenix calls do not expose a stable caller identity. Phoenix authorization + must therefore be an explicit Schnorr signature over an `AuthorizedAction`. +- Dusk signature primitives verify according to their upstream implementations. +- `abi::self_id()`, `abi::block_height()`, `abi::emit`, and contract-call + routing are provided correctly by the host. +- Client encoders use the same schema and byte serialization as the generated + Forge data-drivers. + +The layer does not assume: + +- an Ethereum-like `msg.sender` for Phoenix; +- delegatecall-style proxy storage behavior; +- off-chain signatures are safe without contract/domain/action/payload binding; +- zero principals are valid owners, recipients, spenders, operators, or admins. + +## Invariant Matrix + +| ID | Invariant | Primary Coverage | +| --- | --- | --- | +| AUTH-1 | Signed actions bind contract, domain, action id, payload hash, nonce, principal, and expiry before nonce/replay consumption. | `tests/primitives.rs`, `tests/properties.rs`, VM test, local-node smoke | +| AUTH-2 | Rejected envelope, signature, expiry, role, owner, admin, and replay-key cases do not advance nonce/replay state. | property negative matrices, VM test, local-node smoke | +| AUTH-3 | Phoenix authorization proves control of a Schnorr key principal and never relies on observed caller identity. | primitives, properties, signed auth example, local-node smoke | +| AUTH-4 | Moonlight owners can authorize by observed caller or signed BLS action; contract owners only by observed contract caller. | primitives, reference contracts, VM test | +| ACCESS-1 | Role grants/revokes are admin-gated, reject zero accounts, and emit typed events when state changes. | primitives, DRC20 reference, data-driver event decoding | +| ACCESS-2 | Owner and owner-set initialization reject zero principals atomically. | primitives, properties | +| PAUSE-1 | Reference pausable DRC20/DRC721 pause all balance-changing operations. | primitives, VM test, local-node smoke | +| PAUSE-2 | Approvals remain available while paused and do not move balances or ownership. | VM test, local-node smoke | +| TOKEN20-1 | DRC20 total supply equals the modeled sum of balances after arbitrary operation sequences. | property state-machine model | +| TOKEN20-2 | DRC20 transfer, transfer-from, allowance, mint, burn, cap, and vote checkpoint failures are atomic. | primitives, properties | +| TOKEN721-1 | DRC721 owner, balance, approval, operator, enumerable, and total-supply views remain internally consistent. | property state-machine model | +| TOKEN721-2 | DRC721 mint, transfer, burn, approval, operator, and royalty failures are atomic. | primitives, properties | +| ROYALTY-1 | Royalty receivers cannot be zero, basis points cannot exceed 10,000, and overflow-prone quotes do not mutate state. | primitives, properties | +| NONCE-1 | Nonce domains are independent, monotonic, import/export safe, and atomic on rejection. | primitives, properties | +| REPLAY-1 | Replay keys cannot be reused for the same principal and import/export is monotonic. | primitives, properties | +| PROXY-1 | Upgrade admin rejects zero admin/implementation and binds signed admin actions to exact payloads. | primitives, VM test, local-node smoke | +| PROXY-2 | Prepare, activate, cancel, rollback, and rollback-finalization obey delay/window state transitions and emit events. | primitives, properties, data-driver event decoding | +| TIMELOCK-1 | Timelock scheduling/execution/cancellation follows delay and predecessor constraints. | primitives, properties | +| TIMELOCK-2 | Controller minimum-delay changes are self-governed through scheduled operations, not direct arbitrary admin mutation. | primitives, properties | +| REENTRANCY-1 | Scoped guard rejects nested entry and resets after panics. | primitives | +| ABI-1 | Forge data-driver schemas load for every reference contract. | `tests/data_driver_fuzz.rs` | +| ABI-2 | Function inputs JSON-encode, decode, and re-encode consistently; malformed and mutated payloads do not crash. | `tests/data_driver_fuzz.rs` | +| ABI-3 | Function outputs and typed events decode from rkyv and mutated output/event payloads do not crash readers. | `tests/data_driver_fuzz.rs` | +| NODE-1 | Real local Rusk deployment accepts valid signed calls and rejects replay, bad payload, expired action, pause, and proxy replay cases while preserving expected state. | `scripts/dusk-contract-standards-local-smoke.sh` | + +## Reproducible Validation + +Run the audit-grade validation pass: + +```sh +./scripts/dusk-contract-standards-audit-grade.sh +``` + +Default intensity: + +- `STANDARDS_PROPTEST_CASES=8192` +- `STANDARDS_PROPTEST_MAX_SHRINK_ITERS=16384` +- `STANDARDS_DATA_DRIVER_FUZZ_CASES=4096` +- `STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS=8192` + +For a funded local-node run, start Rusk and provide wallet/node settings: + +```sh +RUN_LOCAL_NODE_SMOKE=1 \ +RUSK_URL=http://127.0.0.1:18080 \ +RUSK_WALLET_BIN=/path/to/rusk-wallet \ +WALLET_DIR=target/rusk-local-smoke/wallet \ +WALLET_RESTORE_FILE=/path/to/wallet.dat \ +./scripts/dusk-contract-standards-audit-grade.sh +``` + +For dependency advisory scanning, install `cargo-audit` and opt in: + +```sh +RUN_CARGO_AUDIT=1 ./scripts/dusk-contract-standards-audit-grade.sh +``` + +## Auditor Checklist + +Reviewers should focus on: + +- whether the Dusk principal model is the right abstraction for Moonlight, + Phoenix, and contract callers; +- whether every public signed path uses action-bound helpers before nonce + consumption; +- whether payload-hash construction is unambiguous and domain separated enough + for downstream wallets; +- whether pause semantics match product/security expectations; +- whether upgrade and timelock policies are sufficient for production systems; +- whether any event is missing for an indexer-relevant state change; +- whether generated data-driver schemas are acceptable as the client/wallet + ABI source of truth. + +## Residual Risks + +The branch is audit-ready, not audit-complete. Remaining risk areas: + +- independent client encoders may still disagree with Forge data-drivers unless + they are tested differentially; +- Dusk runtime host-function behavior is assumed rather than proven here; +- no formal verification is included for arithmetic or state machines; +- dependency advisory scanning is optional unless `cargo-audit` is installed; +- local-node smoke is deterministic and adversarial for defined invariants, but + it is not a long-running network chaos test. diff --git a/docs/dusk-contract-standards-hardening.md b/docs/dusk-contract-standards-hardening.md index 7c06eb69..0283e1aa 100644 --- a/docs/dusk-contract-standards-hardening.md +++ b/docs/dusk-contract-standards-hardening.md @@ -118,8 +118,9 @@ STANDARDS_PROPTEST_CASES=8192 STANDARDS_PROPTEST_MAX_SHRINK_ITERS=16384 \ ``` Forge data-driver ABI fuzzing covers JSON-to-rkyv input encoding, input -decoding, roundtrips, malformed JSON, bad shapes, unknown functions, and -mutated encoded payloads for the four standards reference contracts: +decoding, roundtrips, malformed JSON, bad shapes, unknown functions, output +decoding, typed event decoding, and mutated input/output/event payloads for the +four standards reference contracts: ```sh make standards-data-drivers @@ -130,6 +131,15 @@ STANDARDS_DATA_DRIVER_FUZZ_CASES=2048 \ The `standards-hardening` workflow runs these longer property and data-driver fuzz jobs on demand and nightly. +Run the audit-grade validation script with: + +```sh +./scripts/dusk-contract-standards-audit-grade.sh +``` + +See `docs/dusk-contract-standards-audit.md` for the auditor-facing scope, +threat model, invariant matrix, and residual-risk register. + Run the full standards validation pass with: ```sh @@ -149,7 +159,7 @@ cargo clippy -p dusk-contract-standards --all-targets -- -D warnings ## Next Research Items -The next hardening layer should add mutation testing for authorization and -pause paths, differential tests against independent client encoders, and +The next hardening layer should add differential tests against independent +client encoders, formal or semi-formal arithmetic/state-machine proofs, and local-node scenario tests that intentionally mix successful transactions with -rejected transactions across block boundaries. +rejected transactions across block boundaries over longer runs. diff --git a/docs/dusk-contract-standards-security.md b/docs/dusk-contract-standards-security.md index bda90a6e..c3d57cbe 100644 --- a/docs/dusk-contract-standards-security.md +++ b/docs/dusk-contract-standards-security.md @@ -37,6 +37,11 @@ helpers (`authorize_signed_action`, `authorize_owner_action`, domain, wrong action id, and wrong payload hash are rejected before nonce state is consumed. +Payload hashes must include enough typed structure to prevent ambiguous +concatenation. Reference contracts prefix each payload with an operation tag and +length-prefix variable-size principal encodings before hashing. New standards +should follow that pattern or use a canonical structured encoder. + ## Pausable References The reference pausable DRC20 and DRC721 contracts pause all balance-changing @@ -61,3 +66,9 @@ activation behind a timelock, keep a rollback window, and emit lifecycle events for indexers and wallets. Timelock controller maintenance, including minimum delay changes, should go through the timelock itself rather than a direct admin call. + +## Audit Packet + +`docs/dusk-contract-standards-audit.md` is the auditor-facing packet for this +branch. It lists the exact scope, trust assumptions, invariant matrix, +validation commands, and residual risks. diff --git a/scripts/dusk-contract-standards-audit-grade.sh b/scripts/dusk-contract-standards-audit-grade.sh new file mode 100755 index 00000000..66a4cc7a --- /dev/null +++ b/scripts/dusk-contract-standards-audit-grade.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +STANDARDS_PROPTEST_CASES="${STANDARDS_PROPTEST_CASES:-8192}" +STANDARDS_PROPTEST_MAX_SHRINK_ITERS="${STANDARDS_PROPTEST_MAX_SHRINK_ITERS:-16384}" +STANDARDS_DATA_DRIVER_FUZZ_CASES="${STANDARDS_DATA_DRIVER_FUZZ_CASES:-4096}" +STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS="${STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS:-8192}" +RUN_LOCAL_NODE_SMOKE="${RUN_LOCAL_NODE_SMOKE:-0}" +RUN_CARGO_AUDIT="${RUN_CARGO_AUDIT:-0}" + +cd "$ROOT_DIR" + +echo "Checking formatting" +cargo fmt --check + +echo "Running standards clippy" +cargo clippy -p dusk-contract-standards --all-targets -- -D warnings + +echo "Running native standards tests" +cargo test -p dusk-contract-standards + +echo "Building reference contract Wasm" +cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ + -p authorization-counter \ + -p drc20-roles-pausable \ + -p drc721-collection \ + -p proxy-counter \ + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,proxy-counter/contract + +echo "Running VM reference deployment tests" +cargo test -p dusk-contract-standards --test examples_vm -- --ignored + +echo "Running long property and data-driver hardening" +STANDARDS_PROPTEST_CASES="$STANDARDS_PROPTEST_CASES" \ +STANDARDS_PROPTEST_MAX_SHRINK_ITERS="$STANDARDS_PROPTEST_MAX_SHRINK_ITERS" \ +STANDARDS_DATA_DRIVER_FUZZ_CASES="$STANDARDS_DATA_DRIVER_FUZZ_CASES" \ +STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS="$STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS" \ +make standards-hardening + +if [[ "$RUN_CARGO_AUDIT" == "1" ]]; then + echo "Running cargo audit" + cargo audit +else + echo "Skipping cargo audit; set RUN_CARGO_AUDIT=1 when cargo-audit is installed" +fi + +if [[ "$RUN_LOCAL_NODE_SMOKE" == "1" ]]; then + echo "Running funded local-node smoke" + ./scripts/dusk-contract-standards-local-smoke.sh +else + echo "Skipping funded local-node smoke; set RUN_LOCAL_NODE_SMOKE=1 with RUSK_URL/RUSK_WALLET_BIN/WALLET_DIR" +fi + +echo "Audit-grade standards validation completed" diff --git a/standards/dusk-contract-standards/tests/data_driver_fuzz.rs b/standards/dusk-contract-standards/tests/data_driver_fuzz.rs index eaf76675..5c698e6f 100644 --- a/standards/dusk-contract-standards/tests/data_driver_fuzz.rs +++ b/standards/dusk-contract-standards/tests/data_driver_fuzz.rs @@ -2,9 +2,22 @@ use std::collections::BTreeMap; use std::fs; use std::path::PathBuf; +use dusk_contract_standards::access::events as access_events; +use dusk_contract_standards::core::Principal; +use dusk_contract_standards::proxy::{ + RollbackFinalized, UpgradeActivated, UpgradeCancelled, UpgradePrepared, + UpgradeRolledBack, ROLLBACK_FINALIZED_TOPIC, UPGRADE_ACTIVATED_TOPIC, + UPGRADE_CANCELLED_TOPIC, UPGRADE_PREPARED_TOPIC, UPGRADE_ROLLED_BACK_TOPIC, +}; +use dusk_contract_standards::token::drc20::events as drc20_events; +use dusk_contract_standards::token::drc721::{ + events as drc721_events, RoyaltyQuote, +}; +use dusk_core::abi::ContractId; use dusk_data_driver::reader::DriverReader; use proptest::prelude::*; use proptest::test_runner::{Config as ProptestConfig, TestRunner}; +use rkyv::ser::serializers::AllocSerializer; use serde_json::{json, Value}; const DRIVER_NAMES: [&str; 4] = [ @@ -82,6 +95,160 @@ fn data_driver_inputs_roundtrip_and_reject_bad_shapes() { .expect("data-driver fuzz cases failed"); } +#[test] +#[ignore = "requires `make standards-data-drivers` before running"] +fn data_driver_outputs_and_events_decode() { + let drivers = load_drivers(); + + assert_output_decodes( + drivers.get("authorization_counter").unwrap(), + "value", + &42u64, + ); + assert_output_decodes( + drivers.get("authorization_counter").unwrap(), + "last_authorizer", + &Some(principal_value(1)), + ); + + let drc20 = drivers.get("drc20_roles_pausable").unwrap(); + assert_output_decodes(drc20, "name", &"Audit Token".to_string()); + assert_output_decodes(drc20, "decimals", &9u8); + assert_output_decodes(drc20, "total_supply", &1_000u64); + assert_output_decodes(drc20, "has_role", &true); + assert_output_decodes(drc20, "paused", &false); + assert_event_decodes( + drc20, + drc20_events::TRANSFER_TOPIC, + &drc20_events::Transfer { + from: principal_value(1), + to: principal_value(2), + amount: 7, + }, + ); + assert_event_decodes( + drc20, + drc20_events::APPROVAL_TOPIC, + &drc20_events::Approval { + owner: principal_value(1), + spender: principal_value(2), + amount: 7, + }, + ); + assert_event_decodes( + drc20, + access_events::PAUSED_TOPIC, + &access_events::Paused { + account: principal_value(1), + }, + ); + assert_event_decodes( + drc20, + access_events::ROLE_GRANTED_TOPIC, + &access_events::RoleGranted { + role: [3u8; 32], + account: principal_value(1), + sender: principal_value(2), + }, + ); + + let drc721 = drivers.get("drc721_collection").unwrap(); + assert_output_decodes(drc721, "base_uri", &"ipfs://audit/".to_string()); + assert_output_decodes(drc721, "tokens_of", &vec![1u64, 2, 3]); + assert_output_decodes(drc721, "owner_of", &principal_value(1)); + assert_output_decodes( + drc721, + "royalty_info", + &RoyaltyQuote { + receiver: principal_value(2), + amount: 50, + }, + ); + assert_event_decodes( + drc721, + drc721_events::TRANSFER_TOPIC, + &drc721_events::Transfer { + from: principal_value(1), + to: principal_value(2), + token_id: 9, + }, + ); + assert_event_decodes( + drc721, + drc721_events::APPROVAL_FOR_ALL_TOPIC, + &drc721_events::ApprovalForAll { + owner: principal_value(1), + operator: principal_value(2), + approved: true, + }, + ); + assert_event_decodes( + drc721, + drc721_events::TOKEN_ROYALTY_SET_TOPIC, + &drc721_events::TokenRoyaltySet { + operator: principal_value(1), + token_id: 9, + receiver: principal_value(2), + basis_points: 500, + }, + ); + + let proxy = drivers.get("proxy_counter").unwrap(); + assert_output_decodes(proxy, "implementation", &contract_id(4)); + assert_output_decodes(proxy, "rollback_deadline", &100u64); + assert_output_decodes(proxy, "activate_upgrade", &vec![1u8, 2, 3]); + assert_event_decodes( + proxy, + UPGRADE_PREPARED_TOPIC, + &UpgradePrepared { + implementation: contract_id(4), + eta: 10, + }, + ); + assert_event_decodes( + proxy, + UPGRADE_ACTIVATED_TOPIC, + &UpgradeActivated { + previous_implementation: contract_id(3), + implementation: contract_id(4), + rollback_deadline: 20, + }, + ); + assert_event_decodes( + proxy, + UPGRADE_CANCELLED_TOPIC, + &UpgradeCancelled { + implementation: contract_id(4), + }, + ); + assert_event_decodes( + proxy, + UPGRADE_ROLLED_BACK_TOPIC, + &UpgradeRolledBack { + from_implementation: contract_id(4), + restored_implementation: contract_id(3), + }, + ); + assert_event_decodes( + proxy, + ROLLBACK_FINALIZED_TOPIC, + &RollbackFinalized { + implementation: contract_id(4), + }, + ); + + for (name, driver) in &drivers { + assert!( + driver.decode_output_fn("definitely_missing", &[]).is_err(), + "{name} accepted an unknown output function" + ); + assert!( + driver.decode_event("definitely_missing", &[]).is_err(), + "{name} accepted an unknown event topic" + ); + } +} + fn load_drivers() -> BTreeMap<&'static str, DriverReader> { DRIVER_NAMES .into_iter() @@ -230,6 +397,61 @@ fn assert_mutated_inputs_are_handled( Ok(()) } +fn assert_output_decodes(driver: &DriverReader, function: &str, value: &T) +where + T: rkyv::Serialize>, +{ + let encoded = rkyv_bytes(value); + driver + .decode_output_fn(function, &encoded) + .unwrap_or_else(|error| { + panic!("decode_output_fn({function}) failed: {error}") + }); + assert_mutated_output_decode_is_handled(driver, function, &encoded); +} + +fn assert_event_decodes(driver: &DriverReader, topic: &str, value: &T) +where + T: rkyv::Serialize>, +{ + let encoded = rkyv_bytes(value); + driver + .decode_event(topic, &encoded) + .unwrap_or_else(|error| { + panic!("decode_event({topic}) failed: {error}") + }); + assert_mutated_event_decode_is_handled(driver, topic, &encoded); +} + +fn assert_mutated_output_decode_is_handled( + driver: &DriverReader, + function: &str, + encoded: &[u8], +) { + for mutated in mutated_inputs(encoded) { + let _ = driver.decode_output_fn(function, &mutated); + } +} + +fn assert_mutated_event_decode_is_handled( + driver: &DriverReader, + topic: &str, + encoded: &[u8], +) { + for mutated in mutated_inputs(encoded) { + let _ = driver.decode_event(topic, &mutated); + } +} + +fn rkyv_bytes(value: &T) -> Vec +where + T: rkyv::Serialize>, +{ + rkyv::to_bytes::<_, 4096>(value) + .expect("rkyv serialization") + .to_vec() +} + fn mutated_inputs(encoded: &[u8]) -> Vec> { let mut mutations = Vec::new(); mutations.push(Vec::new()); @@ -622,14 +844,42 @@ fn royalty(seed: u8, amount: u64) -> Value { }) } +fn principal_value(seed: u8) -> Principal { + match seed % 3 { + 0 => Principal::Phoenix(bytes32_array(seed)), + 1 => Principal::Contract(contract_id(seed)), + _ => Principal::Moonlight(bytes193_array(seed)), + } +} + +fn contract_id(seed: u8) -> ContractId { + ContractId::from_bytes(bytes32_array(seed)) +} + fn bytes32(seed: u8) -> Vec { (0..32).map(|index| seed.wrapping_add(index)).collect() } +fn bytes32_array(seed: u8) -> [u8; 32] { + let mut bytes = [0u8; 32]; + for (index, byte) in bytes.iter_mut().enumerate() { + *byte = seed.wrapping_add(index as u8); + } + bytes +} + fn bytes193(seed: u8) -> Vec { (0..193).map(|index| seed.wrapping_add(index)).collect() } +fn bytes193_array(seed: u8) -> [u8; 193] { + let mut bytes = [0u8; 193]; + for (index, byte) in bytes.iter_mut().enumerate() { + *byte = seed.wrapping_add(index as u8); + } + bytes +} + fn bounded(value: u64) -> u64 { value % 1_000_000 } From 49cb3cd11d39c10d5d7e2edab142807019d43553 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sun, 26 Apr 2026 23:01:59 +0200 Subject: [PATCH 25/35] standards: add threshold multisig authorization --- docs/dusk-contract-standards-audit.md | 5 + docs/dusk-contract-standards-hardening.md | 2 + docs/dusk-contract-standards-security.md | 14 + docs/dusk-contract-standards.md | 12 + .../dusk-contract-standards/src/auth/mod.rs | 14 +- .../src/governance/mod.rs | 4 + .../src/governance/multisig.rs | 241 +++++++++++++++ .../tests/primitives.rs | 290 +++++++++++++++++- 8 files changed, 578 insertions(+), 4 deletions(-) create mode 100644 standards/dusk-contract-standards/src/governance/multisig.rs diff --git a/docs/dusk-contract-standards-audit.md b/docs/dusk-contract-standards-audit.md index f1136fbd..2a39556e 100644 --- a/docs/dusk-contract-standards-audit.md +++ b/docs/dusk-contract-standards-audit.md @@ -55,6 +55,9 @@ The layer does not assume: | AUTH-2 | Rejected envelope, signature, expiry, role, owner, admin, and replay-key cases do not advance nonce/replay state. | property negative matrices, VM test, local-node smoke | | AUTH-3 | Phoenix authorization proves control of a Schnorr key principal and never relies on observed caller identity. | primitives, properties, signed auth example, local-node smoke | | AUTH-4 | Moonlight owners can authorize by observed caller or signed BLS action; contract owners only by observed contract caller. | primitives, reference contracts, VM test | +| MULTISIG-1 | Threshold multisig requires distinct owner quorum and rejects duplicate signers before nonce/replay consumption. | primitives | +| MULTISIG-2 | Observed Moonlight/contract owners can count toward quorum; Phoenix owners require signed action approvals. | primitives | +| MULTISIG-3 | Multisig owner and threshold maintenance requires current quorum and rejected changes leave state unchanged. | primitives | | ACCESS-1 | Role grants/revokes are admin-gated, reject zero accounts, and emit typed events when state changes. | primitives, DRC20 reference, data-driver event decoding | | ACCESS-2 | Owner and owner-set initialization reject zero principals atomically. | primitives, properties | | PAUSE-1 | Reference pausable DRC20/DRC721 pause all balance-changing operations. | primitives, VM test, local-node smoke | @@ -116,6 +119,8 @@ Reviewers should focus on: Phoenix, and contract callers; - whether every public signed path uses action-bound helpers before nonce consumption; +- whether single-owner admin paths should be replaced with + `ThresholdMultisig` composition before production deployment; - whether payload-hash construction is unambiguous and domain separated enough for downstream wallets; - whether pause semantics match product/security expectations; diff --git a/docs/dusk-contract-standards-hardening.md b/docs/dusk-contract-standards-hardening.md index 0283e1aa..110d4d1c 100644 --- a/docs/dusk-contract-standards-hardening.md +++ b/docs/dusk-contract-standards-hardening.md @@ -15,6 +15,8 @@ the composing contract: all rejected envelope, signature, expiry, policy, and replay-key cases; - owner, role, and upgrade-admin checks must not consume a valid signer nonce when the signer is not authorized for the policy being checked; +- threshold multisig checks must verify distinct owner quorum before consuming + any signer nonce or replay key; - mixed owner-set checks must follow the same no-consume-on-unauthorized rule; - failed token operations must not leave partial native state behind; - DRC20 total supply must equal the modeled sum of balances for all touched diff --git a/docs/dusk-contract-standards-security.md b/docs/dusk-contract-standards-security.md index c3d57cbe..721f0f0e 100644 --- a/docs/dusk-contract-standards-security.md +++ b/docs/dusk-contract-standards-security.md @@ -67,6 +67,20 @@ for indexers and wallets. Timelock controller maintenance, including minimum delay changes, should go through the timelock itself rather than a direct admin call. +## Threshold Multisig + +Do not default production admin paths to a single owner when compromise of that +owner would give full control. `ThresholdMultisig` lets composing contracts +require a distinct M-of-N owner quorum over Dusk principals. It counts observed +Moonlight/contract callers when the runtime exposes them, requires Phoenix +owners to use signed approvals, rejects duplicate signers, and verifies all +approvals before consuming any nonce/replay state. + +Multisig approvals should be bound to the exact operation through +`ActionEnvelope`. Owner-set changes, threshold changes, proxy upgrades, token +minting, pausing, and role administration should use separate domains or action +ids so approvals cannot be replayed across policy surfaces. + ## Audit Packet `docs/dusk-contract-standards-audit.md` is the auditor-facing packet for this diff --git a/docs/dusk-contract-standards.md b/docs/dusk-contract-standards.md index be65c4a9..32d684f0 100644 --- a/docs/dusk-contract-standards.md +++ b/docs/dusk-contract-standards.md @@ -82,6 +82,15 @@ executor, canceller, and admin policies. Controller maintenance that changes the minimum delay is self-governed: it must be scheduled and executed through the timelock path rather than performed directly by an administrator. +`ThresholdMultisig` is the Dusk-native answer to single-owner admin risk. It +counts distinct Moonlight, Phoenix, and contract principals toward a threshold: +Moonlight and contract owners may be observed through call context, while +Phoenix owners must approve with signed actions. The primitive verifies every +approval against the exact `ActionEnvelope` and checks distinct owner quorum +before consuming any nonce or replay state. This lets proxy admins, token +admins, pausers, and future governance flows require M-of-N approval instead of +trusting one hot wallet. + ## Client Signing Flow Clients sign an `AuthorizedAction` for the exact contract action they want to @@ -190,6 +199,9 @@ The native test suite covers positive and negative paths for: - action-bound authorization helpers that reject wrong envelopes before nonce movement; - observed-or-signed owner, role, and upgrade-admin authorization; +- threshold multisig authorization, duplicate signer rejection, observed + Moonlight/contract approval, Phoenix signed approval, and threshold-gated + owner/threshold maintenance; - timelock scheduling, cancellation, execution, and invalid states; - role-gated timelock controller flows, including self-governed delay updates; - reentrancy guard behavior; diff --git a/standards/dusk-contract-standards/src/auth/mod.rs b/standards/dusk-contract-standards/src/auth/mod.rs index 615c3c90..a019998e 100644 --- a/standards/dusk-contract-standards/src/auth/mod.rs +++ b/standards/dusk-contract-standards/src/auth/mod.rs @@ -480,7 +480,11 @@ impl AuthorizationManager { /// Verifies a signed authorization for an exact call envelope without /// consuming nonce/replay state. - fn verify_signed_action( + /// + /// This is an advanced primitive for higher-level authorization policies + /// such as multisig threshold checks. Callers must only consume the + /// authorization after all policy checks have succeeded. + pub fn verify_signed_action( &self, authorization: &SignedAuthorization, envelope: ActionEnvelope, @@ -599,7 +603,13 @@ impl AuthorizationManager { } } - fn consume_verified(&mut self, authorization: &SignedAuthorization) { + /// Consumes nonce/replay state for an authorization that was already + /// verified against its call envelope. + /// + /// This is intentionally low level. Public contract methods should usually + /// use `authorize_signed_action` or a higher-level helper so verification + /// and consumption remain coupled. + pub fn consume_verified(&mut self, authorization: &SignedAuthorization) { let action = authorization.action(); self.consume_action(action.principal, action.domain, action.nonce); if let SignedAuthorization::Phoenix(auth) = authorization { diff --git a/standards/dusk-contract-standards/src/governance/mod.rs b/standards/dusk-contract-standards/src/governance/mod.rs index 2438b650..83ff5aeb 100644 --- a/standards/dusk-contract-standards/src/governance/mod.rs +++ b/standards/dusk-contract-standards/src/governance/mod.rs @@ -1,10 +1,14 @@ //! Governance and scheduling primitives. pub mod controller; +pub mod multisig; pub mod timelock; pub use controller::{ TimelockController, CANCELLER_ROLE, EXECUTOR_ROLE, PROPOSER_ROLE, TIMELOCK_ADMIN_ROLE, }; +pub use multisig::{ + MultisigApprovals, MultisigConfig, Threshold, ThresholdMultisig, +}; pub use timelock::{OperationId, ScheduledOperation, Timelock}; diff --git a/standards/dusk-contract-standards/src/governance/multisig.rs b/standards/dusk-contract-standards/src/governance/multisig.rs new file mode 100644 index 00000000..50dcace9 --- /dev/null +++ b/standards/dusk-contract-standards/src/governance/multisig.rs @@ -0,0 +1,241 @@ +//! Threshold multisig authorization for Dusk principals. + +use alloc::collections::BTreeSet; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::auth::{ActionEnvelope, AuthorizationManager, SignedAuthorization}; +use crate::core::{error, CallContext, Principal, PrincipalKind}; + +/// Owner threshold type used by multisig policies. +pub type Threshold = u16; + +/// Initialization config for a threshold multisig policy. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigConfig { + /// Unique non-zero owners. + pub owners: Vec, + /// Required number of distinct owners. + pub threshold: Threshold, +} + +/// Signed approvals submitted with a multisig-gated call. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigApprovals { + /// Signed Moonlight/Phoenix approvals for the exact call envelope. + pub approvals: Vec, +} + +/// Dusk-native threshold multisig authorization policy. +/// +/// The primitive is intentionally an action authorizer, not an EVM-style +/// arbitrary calldata executor. Composing contracts bind approvals to their own +/// `ActionEnvelope`, then execute the local state transition after this module +/// returns a threshold of distinct owner signers. +#[derive(Clone, Debug, Default)] +pub struct ThresholdMultisig { + owners: BTreeSet, + threshold: Threshold, +} + +impl ThresholdMultisig { + /// Creates an uninitialized multisig policy. + pub const fn new() -> Self { + Self { + owners: BTreeSet::new(), + threshold: 0, + } + } + + /// Initializes the policy with unique owners and a non-zero threshold. + pub fn init(&mut self, config: MultisigConfig) { + if self.threshold != 0 || !self.owners.is_empty() { + panic!("{}", error::ALREADY_INITIALIZED); + } + let owners = collect_owners(config.owners); + validate_threshold(config.threshold, owners.len()); + self.owners = owners; + self.threshold = config.threshold; + } + + /// Returns all owners in stable order. + pub fn owners(&self) -> Vec { + self.owners.iter().copied().collect() + } + + /// Returns the number of owners. + pub fn len(&self) -> usize { + self.owners.len() + } + + /// Returns true when there are no owners configured. + pub fn is_empty(&self) -> bool { + self.owners.is_empty() + } + + /// Returns the current threshold. + pub const fn threshold(&self) -> Threshold { + self.threshold + } + + /// Returns true when `principal` is an owner. + pub fn is_owner(&self, principal: Principal) -> bool { + self.owners.contains(&principal) + } + + /// Verifies that the observed caller plus supplied signed approvals satisfy + /// the threshold for a concrete call envelope. + /// + /// The method verifies every signed approval and all policy constraints + /// before consuming any nonce/replay state. Rejected threshold, duplicate, + /// unauthorized, expired, wrong-envelope, or bad-signature cases are + /// therefore atomic with respect to signer nonces. + pub fn authorize_action( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + approvals: &[SignedAuthorization], + envelope: ActionEnvelope, + now: u64, + ) -> Vec { + self.assert_initialized(); + + let mut signers = BTreeSet::new(); + if let Some(principal) = context.principal { + match principal.kind() { + PrincipalKind::Moonlight | PrincipalKind::Contract => { + if self.is_owner(principal) { + signers.insert(principal); + } + } + PrincipalKind::Phoenix => {} + } + } + + for approval in approvals { + let principal = + authorizations.verify_signed_action(approval, envelope, now); + if !self.is_owner(principal) || !signers.insert(principal) { + panic!("{}", error::UNAUTHORIZED); + } + } + + self.assert_threshold_count(signers.len()); + + for approval in approvals { + authorizations.consume_verified(approval); + } + + signers.iter().copied().collect() + } + + /// Panics unless `signers` are a distinct threshold of current owners. + pub fn assert_quorum(&self, signers: &[Principal]) { + self.assert_initialized(); + let mut unique = BTreeSet::new(); + for signer in signers { + if !self.is_owner(*signer) || !unique.insert(*signer) { + panic!("{}", error::UNAUTHORIZED); + } + } + self.assert_threshold_count(unique.len()); + } + + /// Adds an owner after a threshold of current owners has authorized it. + pub fn add_owner(&mut self, signers: &[Principal], owner: Principal) { + self.assert_quorum(signers); + if owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if !self.owners.insert(owner) { + panic!("{}", error::INVALID_OWNER); + } + } + + /// Removes an owner and sets the threshold that should apply after removal. + pub fn remove_owner( + &mut self, + signers: &[Principal], + owner: Principal, + new_threshold: Threshold, + ) { + self.assert_quorum(signers); + let mut owners = self.owners.clone(); + if !owners.remove(&owner) { + panic!("{}", error::INVALID_OWNER); + } + validate_threshold(new_threshold, owners.len()); + self.owners = owners; + self.threshold = new_threshold; + } + + /// Replaces one owner with another after threshold authorization. + pub fn replace_owner( + &mut self, + signers: &[Principal], + old_owner: Principal, + new_owner: Principal, + ) { + self.assert_quorum(signers); + if new_owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + let mut owners = self.owners.clone(); + if !owners.remove(&old_owner) || !owners.insert(new_owner) { + panic!("{}", error::INVALID_OWNER); + } + self.owners = owners; + } + + /// Changes the threshold after a threshold of current owners has authorized + /// it. + pub fn set_threshold( + &mut self, + signers: &[Principal], + threshold: Threshold, + ) { + self.assert_quorum(signers); + validate_threshold(threshold, self.owners.len()); + self.threshold = threshold; + } + + fn assert_initialized(&self) { + if self.threshold == 0 || self.owners.is_empty() { + panic!("{}", error::NOT_INITIALIZED); + } + } + + fn assert_threshold_count(&self, count: usize) { + if count < usize::from(self.threshold) { + panic!("{}", error::UNAUTHORIZED); + } + } +} + +fn collect_owners(owners: Vec) -> BTreeSet { + let mut next = BTreeSet::new(); + for owner in owners { + if owner.is_zero() { + panic!("{}", error::ZERO_PRINCIPAL); + } + if !next.insert(owner) { + panic!("{}", error::INVALID_OWNER); + } + } + if next.is_empty() { + panic!("{}", error::INVALID_OWNER); + } + next +} + +fn validate_threshold(threshold: Threshold, owner_count: usize) { + if threshold == 0 || usize::from(threshold) > owner_count { + panic!("{}", error::INVALID_OWNER); + } +} diff --git a/standards/dusk-contract-standards/tests/primitives.rs b/standards/dusk-contract-standards/tests/primitives.rs index 6db40465..7f3e1cbd 100644 --- a/standards/dusk-contract-standards/tests/primitives.rs +++ b/standards/dusk-contract-standards/tests/primitives.rs @@ -13,8 +13,8 @@ use dusk_contract_standards::core::{ ReplayGuard, }; use dusk_contract_standards::governance::{ - Timelock, TimelockController, CANCELLER_ROLE, EXECUTOR_ROLE, PROPOSER_ROLE, - TIMELOCK_ADMIN_ROLE, + MultisigConfig, ThresholdMultisig, Timelock, TimelockController, + CANCELLER_ROLE, EXECUTOR_ROLE, PROPOSER_ROLE, TIMELOCK_ADMIN_ROLE, }; use dusk_contract_standards::proxy::{ StateStore, UpgradeActivated, UpgradeAdmin, UpgradeCancelled, @@ -60,6 +60,63 @@ fn moonlight_secret(seed: u64) -> BlsSecretKey { BlsSecretKey::random(&mut rng) } +#[allow(clippy::too_many_arguments)] +fn signed_moonlight_action( + secret: BlsSecretKey, + public_key: BlsPublicKey, + principal: Principal, + contract: ContractId, + domain: [u8; 32], + action_id: [u8; 32], + payload_hash: [u8; 32], + nonce: u64, +) -> SignedAuthorization { + let action = AuthorizedAction { + contract, + domain, + action_id, + nonce, + expires_at: 0, + principal, + payload_hash, + }; + SignedAuthorization::Moonlight(MoonlightAuthorization { + action, + public_key, + signature: secret.sign(&action.message_bytes()), + }) +} + +#[allow(clippy::too_many_arguments)] +fn signed_phoenix_action( + secret: SchnorrSecretKey, + public_key: SchnorrPublicKey, + principal: Principal, + contract: ContractId, + domain: [u8; 32], + action_id: [u8; 32], + payload_hash: [u8; 32], + nonce: u64, + rng_seed: u64, +) -> SignedAuthorization { + let action = AuthorizedAction { + contract, + domain, + action_id, + nonce, + expires_at: 0, + principal, + payload_hash, + }; + let mut rng = StdRng::seed_from_u64(rng_seed); + SignedAuthorization::Phoenix(PhoenixSignatureAuthorization { + action, + public_key, + signature: secret.sign(&mut rng, action.message_hash()), + replay_key: None, + }) +} + #[test] fn ownable_positive_and_negative_paths() { let owner = p(1); @@ -162,6 +219,235 @@ fn owner_set_supports_mixed_dusk_principals() { assert_eq!(owners.owners(), before); } +#[test] +fn threshold_multisig_requires_distinct_quorum_before_nonce_consumption() { + let owner_a_sk = moonlight_secret(101); + let owner_a_pk = BlsPublicKey::from(&owner_a_sk); + let owner_a = Principal::moonlight(&owner_a_pk); + let owner_b_sk = moonlight_secret(102); + let owner_b_pk = BlsPublicKey::from(&owner_b_sk); + let owner_b = Principal::moonlight(&owner_b_pk); + let outsider_sk = moonlight_secret(103); + let outsider_pk = BlsPublicKey::from(&outsider_sk); + let outsider = Principal::moonlight(&outsider_pk); + + let contract = c(104); + let domain = [105u8; 32]; + let action_id = [106u8; 32]; + let payload_hash = [107u8; 32]; + let envelope = + ActionEnvelope::new(contract, domain, action_id, payload_hash); + + let mut multisig = ThresholdMultisig::new(); + multisig.init(MultisigConfig { + owners: vec![owner_a, owner_b], + threshold: 2, + }); + + let approval_a = signed_moonlight_action( + owner_a_sk, + owner_a_pk, + owner_a, + contract, + domain, + action_id, + payload_hash, + 0, + ); + let approval_b = signed_moonlight_action( + owner_b_sk, + owner_b_pk, + owner_b, + contract, + domain, + action_id, + payload_hash, + 0, + ); + let outsider_approval = signed_moonlight_action( + outsider_sk, + outsider_pk, + outsider, + contract, + domain, + action_id, + payload_hash, + 0, + ); + + let mut manager = AuthorizationManager::new(); + assert_panics(|| { + multisig.authorize_action( + &mut manager, + CallContext::none(), + std::slice::from_ref(&approval_a), + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(owner_a, domain), 0); + + assert_panics(|| { + multisig.authorize_action( + &mut manager, + CallContext::none(), + &[approval_a.clone(), approval_a.clone()], + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(owner_a, domain), 0); + + assert_panics(|| { + multisig.authorize_action( + &mut manager, + CallContext::none(), + &[approval_a.clone(), outsider_approval], + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(owner_a, domain), 0); + assert_eq!(manager.nonce(outsider, domain), 0); + + let wrong_envelope = + ActionEnvelope::new(contract, domain, action_id, [108u8; 32]); + assert_panics(|| { + multisig.authorize_action( + &mut manager, + CallContext::none(), + &[approval_a.clone(), approval_b.clone()], + wrong_envelope, + 0, + ); + }); + assert_eq!(manager.nonce(owner_a, domain), 0); + assert_eq!(manager.nonce(owner_b, domain), 0); + + let signers = multisig.authorize_action( + &mut manager, + CallContext::none(), + &[approval_a.clone(), approval_b.clone()], + envelope, + 0, + ); + assert_eq!(signers.len(), 2); + assert!(signers.contains(&owner_a)); + assert!(signers.contains(&owner_b)); + assert_eq!(manager.nonce(owner_a, domain), 1); + assert_eq!(manager.nonce(owner_b, domain), 1); + + assert_panics(|| { + multisig.authorize_action( + &mut manager, + CallContext::none(), + &[approval_a, approval_b], + envelope, + 0, + ); + }); + assert_eq!(manager.nonce(owner_a, domain), 1); + assert_eq!(manager.nonce(owner_b, domain), 1); +} + +#[test] +fn threshold_multisig_counts_observed_moonlight_and_contract_callers() { + let contract_owner = Principal::contract(c(110)); + let phoenix_sk = SchnorrSecretKey::from(JubJubScalar::from(111u64)); + let phoenix_pk = SchnorrPublicKey::from(&phoenix_sk); + let phoenix_owner = Principal::phoenix_public_key(&phoenix_pk); + let contract = c(112); + let domain = [113u8; 32]; + let action_id = [114u8; 32]; + let payload_hash = [115u8; 32]; + let envelope = + ActionEnvelope::new(contract, domain, action_id, payload_hash); + + let mut multisig = ThresholdMultisig::new(); + multisig.init(MultisigConfig { + owners: vec![contract_owner, phoenix_owner], + threshold: 2, + }); + + let phoenix_approval = signed_phoenix_action( + phoenix_sk, + phoenix_pk, + phoenix_owner, + contract, + domain, + action_id, + payload_hash, + 0, + 116, + ); + + let mut manager = AuthorizationManager::new(); + assert_panics(|| { + multisig.authorize_action( + &mut manager, + CallContext::from_principal(phoenix_owner), + &[], + envelope, + 0, + ); + }); + + let signers = multisig.authorize_action( + &mut manager, + CallContext::from_principal(contract_owner), + &[phoenix_approval], + envelope, + 0, + ); + assert_eq!(signers, vec![phoenix_owner, contract_owner]); + assert_eq!(manager.nonce(phoenix_owner, domain), 1); +} + +#[test] +fn threshold_multisig_owner_management_requires_quorum_and_is_atomic() { + let owner_a = p(120); + let owner_b = p(121); + let owner_c = p(122); + let owner_d = p(123); + let outsider = p(124); + + let mut multisig = ThresholdMultisig::new(); + multisig.init(MultisigConfig { + owners: vec![owner_a, owner_b, owner_c], + threshold: 2, + }); + + assert_panics(|| multisig.set_threshold(&[owner_a], 1)); + assert_eq!(multisig.threshold(), 2); + + assert_panics(|| multisig.add_owner(&[owner_a, outsider], owner_d)); + assert_eq!(multisig.owners(), vec![owner_a, owner_b, owner_c]); + + multisig.add_owner(&[owner_a, owner_b], owner_d); + assert_eq!(multisig.owners(), vec![owner_a, owner_b, owner_c, owner_d]); + + let before = multisig.owners(); + assert_panics(|| { + multisig.replace_owner(&[owner_a, owner_b], owner_c, owner_d) + }); + assert_eq!(multisig.owners(), before); + + assert_panics(|| multisig.remove_owner(&[owner_a, owner_b], owner_d, 4)); + assert_eq!(multisig.owners(), before); + assert_eq!(multisig.threshold(), 2); + + multisig.remove_owner(&[owner_a, owner_b], owner_d, 2); + assert_eq!(multisig.owners(), vec![owner_a, owner_b, owner_c]); + + multisig.replace_owner(&[owner_a, owner_b], owner_c, owner_d); + assert_eq!(multisig.owners(), vec![owner_a, owner_b, owner_d]); + + multisig.set_threshold(&[owner_a, owner_b], 3); + assert_eq!(multisig.threshold(), 3); + assert_panics(|| multisig.set_threshold(&[owner_a, owner_b], 0)); + assert_eq!(multisig.threshold(), 3); +} + #[test] fn replay_guard_rejects_reuse_across_same_principal() { let owner = p(1); From 6872dde69a77275fa8979f497cd54e9898ff815d Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Sun, 26 Apr 2026 23:52:18 +0200 Subject: [PATCH 26/35] standards: add standalone multisig controller --- Cargo.lock | 14 + Cargo.toml | 1 + Makefile | 2 +- docs/dusk-contract-standards-audit.md | 6 +- docs/dusk-contract-standards-hardening.md | 5 +- docs/dusk-contract-standards-security.md | 10 + docs/dusk-contract-standards.md | 31 +- .../dusk-contract-standards-audit-grade.sh | 3 +- .../dusk-contract-standards-local-smoke.sh | 15 +- .../examples/encode_local_smoke_args.rs | 16 +- .../src/core/context.rs | 23 +- .../src/governance/mod.rs | 11 + .../src/governance/multisig.rs | 29 +- .../src/governance/multisig_controller.rs | 597 ++++++++++++++++++ .../tests/data_driver_fuzz.rs | 207 +++++- .../tests/examples_vm.rs | 298 +++++++++ .../tests/primitives.rs | 225 ++++++- .../examples/multisig_controller/Cargo.toml | 31 + .../examples/multisig_controller/Makefile | 34 + .../examples/multisig_controller/src/lib.rs | 495 +++++++++++++++ 20 files changed, 2014 insertions(+), 39 deletions(-) create mode 100644 standards/dusk-contract-standards/src/governance/multisig_controller.rs create mode 100644 standards/examples/multisig_controller/Cargo.toml create mode 100644 standards/examples/multisig_controller/Makefile create mode 100644 standards/examples/multisig_controller/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index fc31c1c4..cf5c05dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1866,6 +1866,20 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "multisig-controller" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "num-bigint" version = "0.4.6" diff --git a/Cargo.toml b/Cargo.toml index 9453099e..80534353 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "standards/examples/authorization_counter", "standards/examples/drc20_roles_pausable", "standards/examples/drc721_collection", + "standards/examples/multisig_controller", "standards/examples/proxy_counter", # Test contracts diff --git a/Makefile b/Makefile index 30250c9e..d46dc276 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -STANDARDS_EXAMPLES := standards/examples/authorization_counter standards/examples/drc20_roles_pausable standards/examples/drc721_collection standards/examples/proxy_counter +STANDARDS_EXAMPLES := standards/examples/authorization_counter standards/examples/drc20_roles_pausable standards/examples/drc721_collection standards/examples/multisig_controller standards/examples/proxy_counter SUBDIRS := standards/dusk-contract-standards $(STANDARDS_EXAMPLES) tests/alice tests/bob tests/charlie genesis/transfer genesis/stake tests/host_fn STANDARDS_PROPTEST_CASES ?= 8192 STANDARDS_PROPTEST_MAX_SHRINK_ITERS ?= 16384 diff --git a/docs/dusk-contract-standards-audit.md b/docs/dusk-contract-standards-audit.md index 2a39556e..197ee0a7 100644 --- a/docs/dusk-contract-standards-audit.md +++ b/docs/dusk-contract-standards-audit.md @@ -12,6 +12,7 @@ In scope: - `standards/examples/authorization_counter` - `standards/examples/drc20_roles_pausable` - `standards/examples/drc721_collection` +- `standards/examples/multisig_controller` - `standards/examples/proxy_counter` - `scripts/dusk-contract-standards-local-smoke.sh` - `standards/dusk-contract-standards/tests/**` @@ -58,6 +59,8 @@ The layer does not assume: | MULTISIG-1 | Threshold multisig requires distinct owner quorum and rejects duplicate signers before nonce/replay consumption. | primitives | | MULTISIG-2 | Observed Moonlight/contract owners can count toward quorum; Phoenix owners require signed action approvals. | primitives | | MULTISIG-3 | Multisig owner and threshold maintenance requires current quorum and rejected changes leave state unchanged. | primitives | +| MULTISIG-4 | Standalone controller proposal/confirmation requires distinct owners, rejects duplicate confirmations without nonce movement, expires stale proposals, tombstones executed ids, and clears stale pending operations after authority-removing changes. | primitives, VM test | +| MULTISIG-5 | A contract-owned proxy/admin path can be controlled by the standalone multisig controller through observed inter-contract caller context. | VM test | | ACCESS-1 | Role grants/revokes are admin-gated, reject zero accounts, and emit typed events when state changes. | primitives, DRC20 reference, data-driver event decoding | | ACCESS-2 | Owner and owner-set initialization reject zero principals atomically. | primitives, properties | | PAUSE-1 | Reference pausable DRC20/DRC721 pause all balance-changing operations. | primitives, VM test, local-node smoke | @@ -120,7 +123,8 @@ Reviewers should focus on: - whether every public signed path uses action-bound helpers before nonce consumption; - whether single-owner admin paths should be replaced with - `ThresholdMultisig` composition before production deployment; + `ThresholdMultisig` composition or standalone `MultisigController` + ownership before production deployment; - whether payload-hash construction is unambiguous and domain separated enough for downstream wallets; - whether pause semantics match product/security expectations; diff --git a/docs/dusk-contract-standards-hardening.md b/docs/dusk-contract-standards-hardening.md index 110d4d1c..5df14a7f 100644 --- a/docs/dusk-contract-standards-hardening.md +++ b/docs/dusk-contract-standards-hardening.md @@ -122,7 +122,7 @@ STANDARDS_PROPTEST_CASES=8192 STANDARDS_PROPTEST_MAX_SHRINK_ITERS=16384 \ Forge data-driver ABI fuzzing covers JSON-to-rkyv input encoding, input decoding, roundtrips, malformed JSON, bad shapes, unknown functions, output decoding, typed event decoding, and mutated input/output/event payloads for the -four standards reference contracts: +five standards reference contracts: ```sh make standards-data-drivers @@ -151,8 +151,9 @@ cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ -p authorization-counter \ -p drc20-roles-pausable \ -p drc721-collection \ + -p multisig-controller \ -p proxy-counter \ - --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,proxy-counter/contract + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,multisig-controller/contract,proxy-counter/contract cargo test -p dusk-contract-standards --test examples_vm -- --ignored make standards-data-drivers cargo test -p dusk-contract-standards --test data_driver_fuzz -- --ignored diff --git a/docs/dusk-contract-standards-security.md b/docs/dusk-contract-standards-security.md index 721f0f0e..c391b486 100644 --- a/docs/dusk-contract-standards-security.md +++ b/docs/dusk-contract-standards-security.md @@ -81,6 +81,16 @@ Multisig approvals should be bound to the exact operation through minting, pausing, and role administration should use separate domains or action ids so approvals cannot be replayed across policy surfaces. +Use the standalone `MultisigController` when a ported contract wants a single +owner/admin principal like an Ethereum contract owned by a Safe. The target +contract should assign ownership or the relevant role to the controller's +contract id. Owners then approve the target call through proposal/confirmation +or signed actions, and the controller performs the call as the observed +contract caller after threshold. Operation ids must bind chain id, controller +id, target call bytes, and salt; failed target execution is observable through +the execution event and should be retried with a new salt only when operators +intend a new attempt. + ## Audit Packet `docs/dusk-contract-standards-audit.md` is the auditor-facing packet for this diff --git a/docs/dusk-contract-standards.md b/docs/dusk-contract-standards.md index 32d684f0..8730a28f 100644 --- a/docs/dusk-contract-standards.md +++ b/docs/dusk-contract-standards.md @@ -16,6 +16,10 @@ the Dusk execution model, storage model, and client model. - `standards/examples/drc721_collection`: a Forge collection composition with owner control, pause control, approvals, royalties, events, and token metadata. +- `standards/examples/multisig_controller`: a Forge standalone multisig + controller that can own/administer other contracts through a contract + principal, with proposals, confirmations, tombstones, typed events, and + Dusk-native signed owner approvals. - `standards/examples/proxy_counter`: a Forge upgrade-admin and state-store example showing how proxy-like upgrade policy can be modeled without pretending Dusk has EVM delegatecall semantics. @@ -91,6 +95,15 @@ before consuming any nonce or replay state. This lets proxy admins, token admins, pausers, and future governance flows require M-of-N approval instead of trusting one hot wallet. +`MultisigController` is the standalone counterpart for DuskEVM/OZ-style ports +where a contract expects one owner/admin principal. The multisig contract is +assigned as that owner, owners propose and confirm a target `ContractCall`, and +the controller performs the call after threshold. Operation ids are bound to +chain id, controller id, target call bytes, and salt; executed operations are +tombstoned to avoid accidental replay. Failed target execution emits a typed +execution event and consumes the proposal, so clients should use a new salt for +an intentional retry. + ## Client Signing Flow Clients sign an `AuthorizedAction` for the exact contract action they want to @@ -177,13 +190,14 @@ cargo build --release --target wasm32-unknown-unknown \ -p authorization-counter \ -p drc20-roles-pausable \ -p drc721-collection \ + -p multisig-controller \ -p proxy-counter \ - --features authorization-counter/data-driver-js,drc20-roles-pausable/data-driver-js,drc721-collection/data-driver-js,proxy-counter/data-driver-js + --features authorization-counter/data-driver-js,drc20-roles-pausable/data-driver-js,drc721-collection/data-driver-js,multisig-controller/data-driver-js,proxy-counter/data-driver-js ``` The generated data-drivers export `init`, `get_schema`, `encode_input_fn`, `decode_output_fn`, and `decode_event`. The example Makefiles expose this as -`make wasm-dd`, and the top-level `make standards-data-drivers` builds all four. +`make wasm-dd`, and the top-level `make standards-data-drivers` builds all five. ## Validation @@ -202,6 +216,9 @@ The native test suite covers positive and negative paths for: - threshold multisig authorization, duplicate signer rejection, observed Moonlight/contract approval, Phoenix signed approval, and threshold-gated owner/threshold maintenance; +- standalone multisig controller proposal, confirmation, duplicate-confirmation + rejection, expiry cleanup, cancellation, tombstoning, authority updates, and + proxy-as-owner VM execution; - timelock scheduling, cancellation, execution, and invalid states; - role-gated timelock controller flows, including self-governed delay updates; - reentrancy guard behavior; @@ -222,13 +239,14 @@ against independent models and assert that rejected operations leave native state unchanged. See `docs/dusk-contract-standards-hardening.md` for the current hardening track. -The ignored VM test deploys all four Wasm examples, checks positive query +The ignored VM test deploys all five Wasm examples, checks positive query paths, performs real Moonlight and Phoenix signed calls against the authorization counter, covers replay/wrong-payload/wrong-signature/wrong-target failures, submits signed DRC20 mint and signed DRC20 approvals, verifies paused DRC20/DRC721 signed mint rejection without nonce movement, submits signed DRC721 owner actions and signed DRC721 approvals, submits a signed proxy admin -call, covers replay/wrong-payload failures for those reference flows, and calls +call, executes a proxy admin call through the standalone multisig controller, +covers replay/wrong-payload failures for those reference flows, and calls privileged functions without a runtime caller to validate the negative authorization path at the VM boundary. @@ -245,8 +263,9 @@ cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ -p authorization-counter \ -p drc20-roles-pausable \ -p drc721-collection \ + -p multisig-controller \ -p proxy-counter \ - --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,proxy-counter/contract + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,multisig-controller/contract,proxy-counter/contract ``` After the Wasm build, run the VM deployment/query test with: @@ -271,7 +290,7 @@ RUSK_WALLET_BIN=/path/to/rusk-wallet \ The script first runs the native suite, builds Wasm, runs the VM deployment test, serializes deploy-time init arguments using the Dusk ABI serializer, -deploys the four example contracts with `rusk-wallet`, and queries one +deploys the five example contracts with `rusk-wallet`, and queries one exported function from each deployment. `WALLET_DIR` should point at a funded local wallet profile. diff --git a/scripts/dusk-contract-standards-audit-grade.sh b/scripts/dusk-contract-standards-audit-grade.sh index 66a4cc7a..be16faf6 100755 --- a/scripts/dusk-contract-standards-audit-grade.sh +++ b/scripts/dusk-contract-standards-audit-grade.sh @@ -26,8 +26,9 @@ cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ -p authorization-counter \ -p drc20-roles-pausable \ -p drc721-collection \ + -p multisig-controller \ -p proxy-counter \ - --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,proxy-counter/contract + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,multisig-controller/contract,proxy-counter/contract echo "Running VM reference deployment tests" cargo test -p dusk-contract-standards --test examples_vm -- --ignored diff --git a/scripts/dusk-contract-standards-local-smoke.sh b/scripts/dusk-contract-standards-local-smoke.sh index c0222dd4..1250fabf 100755 --- a/scripts/dusk-contract-standards-local-smoke.sh +++ b/scripts/dusk-contract-standards-local-smoke.sh @@ -19,8 +19,9 @@ cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ -p authorization-counter \ -p drc20-roles-pausable \ -p drc721-collection \ + -p multisig-controller \ -p proxy-counter \ - --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,proxy-counter/contract + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,multisig-controller/contract,proxy-counter/contract echo "Building Forge data-drivers" CARGO_TARGET_DIR="${ROOT_DIR}/target/data-driver" \ @@ -28,8 +29,9 @@ cargo build --release --target wasm32-unknown-unknown \ -p authorization-counter \ -p drc20-roles-pausable \ -p drc721-collection \ + -p multisig-controller \ -p proxy-counter \ - --features authorization-counter/data-driver-js,drc20-roles-pausable/data-driver-js,drc721-collection/data-driver-js,proxy-counter/data-driver-js + --features authorization-counter/data-driver-js,drc20-roles-pausable/data-driver-js,drc721-collection/data-driver-js,multisig-controller/data-driver-js,proxy-counter/data-driver-js echo "Running VM example deployment tests" cargo test -p dusk-contract-standards --test examples_vm -- --ignored @@ -473,10 +475,17 @@ drc721_id="$(deploy_contract \ "$(encode_args drc721-init)")" query_contract "DRC721 collection" "$drc721_id" "total_supply" "$unit_args" +multisig_id="$(deploy_contract \ + "multisig controller" \ + "${ROOT_DIR}/target/wasm32-unknown-unknown/release/multisig_controller.wasm" \ + "$((DEPLOY_NONCE_BASE + 3))" \ + "$(encode_args multisig-init)")" +query_contract "multisig controller" "$multisig_id" "threshold" "$unit_args" + proxy_id="$(deploy_contract \ "proxy counter" \ "${ROOT_DIR}/target/wasm32-unknown-unknown/release/proxy_counter.wasm" \ - "$((DEPLOY_NONCE_BASE + 3))" \ + "$((DEPLOY_NONCE_BASE + 4))" \ "$(encode_args proxy-init)")" query_contract "proxy counter" "$proxy_id" "value" "$unit_args" diff --git a/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs index 7e624920..aedcd7f3 100644 --- a/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs +++ b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs @@ -8,6 +8,7 @@ use dusk_contract_standards::auth::{ SignedAuthorization, }; use dusk_contract_standards::core::Principal; +use dusk_contract_standards::governance::MultisigControllerConfig; use dusk_contract_standards::token::drc20::{ Init as Drc20TokenInit, InitBalance as Drc20InitBalance, }; @@ -73,6 +74,11 @@ struct ProxyCounterInit { rollback_window: u64, } +#[derive(Archive, Serialize, Deserialize)] +struct MultisigInit { + config: MultisigControllerConfig, +} + #[derive(Archive, Serialize, Deserialize)] struct SetValueByMoonlight { authorization: MoonlightAuthorization, @@ -135,7 +141,7 @@ fn main() -> Result<(), Box> { let args = env::args().collect::>(); let Some(command) = args.get(1).map(String::as_str) else { eprintln!( - "usage: encode_local_smoke_args " + "usage: encode_local_smoke_args " ); std::process::exit(2); }; @@ -174,6 +180,14 @@ fn main() -> Result<(), Box> { upgrade_delay: 0, rollback_window: 10, })?, + "multisig-init" => encode(&MultisigInit { + config: MultisigControllerConfig { + owners: vec![admin, phoenix_principal(2)], + threshold: 2, + proposal_ttl: 10, + tombstone_ttl: 6, + }, + })?, "unit" => encode(&())?, "u64" => encode(&parse_u64_arg(&args, 2, "value")?)?, "nonce" => encode(&NonceQuery { diff --git a/standards/dusk-contract-standards/src/core/context.rs b/standards/dusk-contract-standards/src/core/context.rs index 7007d861..55f7e265 100644 --- a/standards/dusk-contract-standards/src/core/context.rs +++ b/standards/dusk-contract-standards/src/core/context.rs @@ -38,21 +38,16 @@ impl CallContext { pub fn current() -> Self { use dusk_core::abi; - match abi::callstack().len() { - 0 => Self::none(), - 1 => { - let Some(pk) = abi::public_sender() else { - return Self::none(); - }; - Self::from_principal(Principal::moonlight(&pk)) - } - _ => { - let Some(caller) = abi::caller() else { - return Self::none(); - }; - Self::from_principal(Principal::Contract(caller)) - } + if abi::callstack().is_empty() { + return Self::none(); } + if let Some(caller) = abi::caller() { + return Self::from_principal(Principal::Contract(caller)); + } + let Some(pk) = abi::public_sender() else { + return Self::none(); + }; + Self::from_principal(Principal::moonlight(&pk)) } /// Native tests must inject context explicitly. diff --git a/standards/dusk-contract-standards/src/governance/mod.rs b/standards/dusk-contract-standards/src/governance/mod.rs index 83ff5aeb..3198ea15 100644 --- a/standards/dusk-contract-standards/src/governance/mod.rs +++ b/standards/dusk-contract-standards/src/governance/mod.rs @@ -2,6 +2,7 @@ pub mod controller; pub mod multisig; +pub mod multisig_controller; pub mod timelock; pub use controller::{ @@ -11,4 +12,14 @@ pub use controller::{ pub use multisig::{ MultisigApprovals, MultisigConfig, Threshold, ThresholdMultisig, }; +pub use multisig_controller::{ + MultisigAuthorityUpdated, MultisigController, MultisigControllerConfig, + MultisigControllerOutcome, MultisigControllerStatus, + MultisigOperationCancelled, MultisigOperationConfirmed, + MultisigOperationExecuted, MultisigOperationId, MultisigOperationProposed, + MultisigPendingOperation, MultisigTarget, MultisigTimeLimitsUpdated, + MULTISIG_AUTHORITY_UPDATED_TOPIC, MULTISIG_OPERATION_CANCELLED_TOPIC, + MULTISIG_OPERATION_CONFIRMED_TOPIC, MULTISIG_OPERATION_EXECUTED_TOPIC, + MULTISIG_OPERATION_PROPOSED_TOPIC, MULTISIG_TIME_LIMITS_UPDATED_TOPIC, +}; pub use timelock::{OperationId, ScheduledOperation, Timelock}; diff --git a/standards/dusk-contract-standards/src/governance/multisig.rs b/standards/dusk-contract-standards/src/governance/multisig.rs index 50dcace9..85efe8e9 100644 --- a/standards/dusk-contract-standards/src/governance/multisig.rs +++ b/standards/dusk-contract-standards/src/governance/multisig.rs @@ -103,6 +103,30 @@ impl ThresholdMultisig { approvals: &[SignedAuthorization], envelope: ActionEnvelope, now: u64, + ) -> Vec { + let signers = self.verify_action( + authorizations, + context, + approvals, + envelope, + now, + ); + for approval in approvals { + authorizations.consume_verified(approval); + } + signers + } + + /// Verifies that the observed caller plus supplied signed approvals satisfy + /// the threshold for a concrete call envelope without consuming nonce or + /// replay state. + pub fn verify_action( + &self, + authorizations: &AuthorizationManager, + context: CallContext, + approvals: &[SignedAuthorization], + envelope: ActionEnvelope, + now: u64, ) -> Vec { self.assert_initialized(); @@ -127,11 +151,6 @@ impl ThresholdMultisig { } self.assert_threshold_count(signers.len()); - - for approval in approvals { - authorizations.consume_verified(approval); - } - signers.iter().copied().collect() } diff --git a/standards/dusk-contract-standards/src/governance/multisig_controller.rs b/standards/dusk-contract-standards/src/governance/multisig_controller.rs new file mode 100644 index 00000000..0b0b29fb --- /dev/null +++ b/standards/dusk-contract-standards/src/governance/multisig_controller.rs @@ -0,0 +1,597 @@ +//! Standalone multisig controller state machine. + +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use core::mem; + +use bytecheck::CheckBytes; +use dusk_core::transfer::data::ContractCall; +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::auth::{ActionEnvelope, AuthorizationManager, SignedAuthorization}; +use crate::core::{error, CallContext, Principal}; +use crate::governance::{MultisigConfig, Threshold, ThresholdMultisig}; + +/// Multisig operation id. +pub type MultisigOperationId = [u8; 32]; + +/// Event topic emitted when an operation is proposed. +pub const MULTISIG_OPERATION_PROPOSED_TOPIC: &str = + "multisig/operation_proposed"; +/// Event topic emitted when an operation is confirmed. +pub const MULTISIG_OPERATION_CONFIRMED_TOPIC: &str = + "multisig/operation_confirmed"; +/// Event topic emitted when an operation execution is attempted. +pub const MULTISIG_OPERATION_EXECUTED_TOPIC: &str = + "multisig/operation_executed"; +/// Event topic emitted when a pending operation is cancelled. +pub const MULTISIG_OPERATION_CANCELLED_TOPIC: &str = + "multisig/operation_cancelled"; +/// Event topic emitted when owner authority changes. +pub const MULTISIG_AUTHORITY_UPDATED_TOPIC: &str = "multisig/authority_updated"; +/// Event topic emitted when proposal/replay timing changes. +pub const MULTISIG_TIME_LIMITS_UPDATED_TOPIC: &str = + "multisig/time_limits_updated"; + +/// Target call controlled by the multisig. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigTarget { + /// Call executed by the controller after quorum. + pub call: ContractCall, + /// Salt used to intentionally repeat the same logical call. + pub salt: [u8; 32], +} + +/// Initialization config for a standalone multisig controller. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigControllerConfig { + /// Unique non-zero owner principals. + pub owners: Vec, + /// Required distinct confirmations. + pub threshold: Threshold, + /// Number of blocks/heights a proposal remains confirmable. + pub proposal_ttl: u64, + /// Number of blocks/heights an executed operation id remains tombstoned. + pub tombstone_ttl: u64, +} + +/// Pending operation metadata. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigPendingOperation { + /// Target call. + pub target: MultisigTarget, + /// Distinct owner confirmations in stable insertion order. + pub confirmations: Vec, + /// Last block/height at which confirmations are accepted. + pub deadline: u64, +} + +impl MultisigPendingOperation { + /// Returns true when `principal` has already confirmed. + pub fn confirmed_by(&self, principal: Principal) -> bool { + self.confirmations.contains(&principal) + } +} + +/// Lifecycle status for a proposal/confirmation call. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub enum MultisigControllerStatus { + /// A new operation was created. + Proposed, + /// An existing operation received one confirmation. + Confirmed, + /// The operation reached threshold and should be executed by the wrapper. + Ready, +} + +/// Result of proposing or confirming an operation. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigControllerOutcome { + /// Operation id. + pub id: MultisigOperationId, + /// Owner principal that authorized this confirmation. + pub authorizer: Principal, + /// Lifecycle status. + pub status: MultisigControllerStatus, + /// Confirmation count after this call. + pub confirmations: u16, + /// Current threshold. + pub threshold: Threshold, + /// Operation deadline. + pub deadline: u64, + /// Operation to execute when status is + /// [`MultisigControllerStatus::Ready`]. + pub ready_operation: Option, +} + +/// Event payload for proposal creation. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigOperationProposed { + /// Operation id. + pub id: MultisigOperationId, + /// Owner that proposed the operation. + pub authorizer: Principal, + /// Confirmation count after proposal. + pub confirmations: u16, + /// Required confirmation threshold. + pub threshold: Threshold, + /// Operation deadline. + pub deadline: u64, +} + +/// Event payload for operation confirmation. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigOperationConfirmed { + /// Operation id. + pub id: MultisigOperationId, + /// Owner that confirmed the operation. + pub authorizer: Principal, + /// Confirmation count after confirmation. + pub confirmations: u16, + /// Required confirmation threshold. + pub threshold: Threshold, + /// Operation deadline. + pub deadline: u64, +} + +/// Event payload for operation execution attempts. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigOperationExecuted { + /// Operation id. + pub id: MultisigOperationId, + /// Whether the target call returned successfully. + pub success: bool, + /// Raw return data when the target call succeeds. + pub return_data: Vec, + /// Error string when the target call fails. + pub error: Option, +} + +/// Event payload for threshold-cancelled operations. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigOperationCancelled { + /// Operation id. + pub id: MultisigOperationId, + /// Owners that authorized cancellation. + pub signers: Vec, +} + +/// Event payload for authority changes. +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigAuthorityUpdated { + /// Previous owners. + pub previous_owners: Vec, + /// Previous threshold. + pub previous_threshold: Threshold, + /// New owners. + pub owners: Vec, + /// New threshold. + pub threshold: Threshold, + /// Pending operations removed because the authority changed. + pub removed_operations: Vec, +} + +/// Event payload for proposal/replay timing changes. +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct MultisigTimeLimitsUpdated { + /// Previous proposal TTL. + pub previous_proposal_ttl: u64, + /// Previous tombstone TTL. + pub previous_tombstone_ttl: u64, + /// New proposal TTL. + pub proposal_ttl: u64, + /// New tombstone TTL. + pub tombstone_ttl: u64, +} + +/// Standalone multisig controller primitive. +/// +/// This state machine tracks operation proposals and confirmations. It does +/// not call target contracts itself; Forge wrappers perform the runtime call +/// after this primitive returns a ready operation. +#[derive(Clone, Debug)] +pub struct MultisigController { + policy: ThresholdMultisig, + proposal_ttl: u64, + tombstone_ttl: u64, + proposals: BTreeMap, + tombstones: BTreeMap, +} + +impl MultisigController { + /// Creates an uninitialized controller. + pub const fn new() -> Self { + Self { + policy: ThresholdMultisig::new(), + proposal_ttl: 0, + tombstone_ttl: 0, + proposals: BTreeMap::new(), + tombstones: BTreeMap::new(), + } + } + + /// Initializes the controller. + pub fn init(&mut self, config: MultisigControllerConfig) { + if self.is_initialized() { + panic!("{}", error::ALREADY_INITIALIZED); + } + validate_time_limits(config.proposal_ttl, config.tombstone_ttl); + self.policy.init(MultisigConfig { + owners: config.owners, + threshold: config.threshold, + }); + self.proposal_ttl = config.proposal_ttl; + self.tombstone_ttl = config.tombstone_ttl; + } + + /// Returns true when the controller is initialized. + pub fn is_initialized(&self) -> bool { + !self.policy.is_empty() && self.policy.threshold() != 0 + } + + /// Returns owners in stable order. + pub fn owners(&self) -> Vec { + self.policy.owners() + } + + /// Returns the current threshold. + pub const fn threshold(&self) -> Threshold { + self.policy.threshold() + } + + /// Returns the proposal TTL. + pub const fn proposal_ttl(&self) -> u64 { + self.proposal_ttl + } + + /// Returns the tombstone TTL. + pub const fn tombstone_ttl(&self) -> u64 { + self.tombstone_ttl + } + + /// Returns true when `principal` is an owner. + pub fn is_owner(&self, principal: Principal) -> bool { + self.policy.is_owner(principal) + } + + /// Returns a pending proposal. + pub fn proposal( + &self, + id: MultisigOperationId, + ) -> Option { + self.proposals.get(&id).cloned() + } + + /// Returns a tombstone expiry. + pub fn tombstone_expiry(&self, id: MultisigOperationId) -> Option { + self.tombstones.get(&id).copied() + } + + /// Verifies an action-bound threshold of owner approvals. + pub fn authorize_action( + &self, + authorizations: &mut AuthorizationManager, + context: CallContext, + approvals: &[SignedAuthorization], + envelope: ActionEnvelope, + now: u64, + ) -> Vec { + self.policy.authorize_action( + authorizations, + context, + approvals, + envelope, + now, + ) + } + + /// Verifies an action-bound threshold of owner approvals without consuming + /// nonce or replay state. + pub fn verify_action( + &self, + authorizations: &AuthorizationManager, + context: CallContext, + approvals: &[SignedAuthorization], + envelope: ActionEnvelope, + now: u64, + ) -> Vec { + self.policy.verify_action( + authorizations, + context, + approvals, + envelope, + now, + ) + } + + /// Proposes an operation and records the first confirmation. + pub fn propose( + &mut self, + id: MultisigOperationId, + target: MultisigTarget, + authorizer: Principal, + now: u64, + ) -> MultisigControllerOutcome { + self.assert_initialized(); + self.assert_owner(authorizer); + validate_target(&target); + self.prune(now); + self.assert_not_tombstoned(id); + + if self.proposals.contains_key(&id) { + return self.confirm_pending(id, authorizer, now); + } + + let deadline = + now.checked_add(self.proposal_ttl).expect(error::OVERFLOW); + self.proposals.insert( + id, + MultisigPendingOperation { + target, + confirmations: vec![authorizer], + deadline, + }, + ); + + self.finish_if_ready( + id, + authorizer, + MultisigControllerStatus::Proposed, + deadline, + now, + ) + } + + /// Confirms a pending operation. + pub fn confirm( + &mut self, + id: MultisigOperationId, + authorizer: Principal, + now: u64, + ) -> MultisigControllerOutcome { + self.assert_initialized(); + self.assert_owner(authorizer); + self.prune(now); + self.assert_not_tombstoned(id); + self.confirm_pending(id, authorizer, now) + } + + /// Cancels a pending operation after a current owner quorum authorizes it. + pub fn cancel( + &mut self, + id: MultisigOperationId, + signers: &[Principal], + now: u64, + ) -> MultisigOperationCancelled { + self.assert_initialized(); + self.policy.assert_quorum(signers); + self.prune(now); + if self.proposals.remove(&id).is_none() { + panic!("{}", error::OPERATION_UNKNOWN); + } + MultisigOperationCancelled { + id, + signers: signers.to_vec(), + } + } + + /// Updates owners and threshold after a current owner quorum authorizes it. + /// + /// Pending operations are removed when an owner is removed or the threshold + /// changes, because old confirmations may no longer represent the new + /// authority. + pub fn update_authority( + &mut self, + signers: &[Principal], + owners: Vec, + threshold: Threshold, + ) -> MultisigAuthorityUpdated { + self.assert_initialized(); + self.policy.assert_quorum(signers); + + let previous_owners = self.owners(); + let previous_threshold = self.threshold(); + let mut next = ThresholdMultisig::new(); + next.init(MultisigConfig { owners, threshold }); + let next_owners = next.owners(); + + let removed_owner = + previous_owners.iter().any(|owner| !next.is_owner(*owner)); + let threshold_changed = previous_threshold != next.threshold(); + let removed_operations = if removed_owner || threshold_changed { + self.clear_pending() + } else { + Vec::new() + }; + + self.policy = next; + + MultisigAuthorityUpdated { + previous_owners, + previous_threshold, + owners: next_owners, + threshold, + removed_operations, + } + } + + /// Updates time limits after a current owner quorum authorizes it. + pub fn set_time_limits( + &mut self, + signers: &[Principal], + proposal_ttl: u64, + tombstone_ttl: u64, + ) -> MultisigTimeLimitsUpdated { + self.assert_initialized(); + self.policy.assert_quorum(signers); + validate_time_limits(proposal_ttl, tombstone_ttl); + + let event = MultisigTimeLimitsUpdated { + previous_proposal_ttl: self.proposal_ttl, + previous_tombstone_ttl: self.tombstone_ttl, + proposal_ttl, + tombstone_ttl, + }; + self.proposal_ttl = proposal_ttl; + self.tombstone_ttl = tombstone_ttl; + event + } + + fn confirm_pending( + &mut self, + id: MultisigOperationId, + authorizer: Principal, + now: u64, + ) -> MultisigControllerOutcome { + let deadline = { + let operation = self + .proposals + .get_mut(&id) + .unwrap_or_else(|| panic!("{}", error::OPERATION_UNKNOWN)); + if now > operation.deadline { + panic!("{}", error::DELAY_NOT_ELAPSED); + } + if operation.confirmed_by(authorizer) { + panic!("{}", error::UNAUTHORIZED); + } + operation.confirmations.push(authorizer); + operation.deadline + }; + + self.finish_if_ready( + id, + authorizer, + MultisigControllerStatus::Confirmed, + deadline, + now, + ) + } + + fn finish_if_ready( + &mut self, + id: MultisigOperationId, + authorizer: Principal, + status: MultisigControllerStatus, + deadline: u64, + now: u64, + ) -> MultisigControllerOutcome { + let confirmations = self + .proposals + .get(&id) + .unwrap_or_else(|| panic!("{}", error::OPERATION_UNKNOWN)) + .confirmations + .len(); + let mut status = status; + let ready_operation = + if confirmations >= usize::from(self.policy.threshold()) { + status = MultisigControllerStatus::Ready; + let operation = self + .proposals + .remove(&id) + .expect("ready operation must be pending"); + self.tombstones.insert( + id, + now.checked_add(self.tombstone_ttl).expect(error::OVERFLOW), + ); + Some(operation) + } else { + None + }; + + MultisigControllerOutcome { + id, + authorizer, + status, + confirmations: confirmations as u16, + threshold: self.policy.threshold(), + deadline, + ready_operation, + } + } + + fn assert_initialized(&self) { + if !self.is_initialized() { + panic!("{}", error::NOT_INITIALIZED); + } + } + + fn assert_owner(&self, authorizer: Principal) { + if !self.policy.is_owner(authorizer) { + panic!("{}", error::UNAUTHORIZED); + } + } + + fn assert_not_tombstoned(&self, id: MultisigOperationId) { + if self.tombstones.contains_key(&id) { + panic!("{}", error::REPLAY); + } + } + + fn prune(&mut self, now: u64) { + self.proposals + .retain(|_, operation| now <= operation.deadline); + self.tombstones.retain(|_, expiry| now <= *expiry); + } + + fn clear_pending(&mut self) -> Vec { + let proposals = mem::take(&mut self.proposals); + proposals.into_keys().collect() + } +} + +impl Default for MultisigController { + fn default() -> Self { + Self::new() + } +} + +fn validate_time_limits(proposal_ttl: u64, tombstone_ttl: u64) { + if proposal_ttl == 0 || tombstone_ttl == 0 { + panic!("{}", error::INVALID_OPERATION); + } +} + +fn validate_target(target: &MultisigTarget) { + if target + .call + .contract + .to_bytes() + .iter() + .all(|byte| *byte == 0) + || target.call.fn_name.is_empty() + { + panic!("{}", error::INVALID_OPERATION); + } +} diff --git a/standards/dusk-contract-standards/tests/data_driver_fuzz.rs b/standards/dusk-contract-standards/tests/data_driver_fuzz.rs index 5c698e6f..3266e024 100644 --- a/standards/dusk-contract-standards/tests/data_driver_fuzz.rs +++ b/standards/dusk-contract-standards/tests/data_driver_fuzz.rs @@ -4,6 +4,14 @@ use std::path::PathBuf; use dusk_contract_standards::access::events as access_events; use dusk_contract_standards::core::Principal; +use dusk_contract_standards::governance::{ + MultisigAuthorityUpdated, MultisigOperationCancelled, + MultisigOperationConfirmed, MultisigOperationExecuted, + MultisigOperationProposed, MultisigTarget, MultisigTimeLimitsUpdated, + MULTISIG_AUTHORITY_UPDATED_TOPIC, MULTISIG_OPERATION_CANCELLED_TOPIC, + MULTISIG_OPERATION_CONFIRMED_TOPIC, MULTISIG_OPERATION_EXECUTED_TOPIC, + MULTISIG_OPERATION_PROPOSED_TOPIC, MULTISIG_TIME_LIMITS_UPDATED_TOPIC, +}; use dusk_contract_standards::proxy::{ RollbackFinalized, UpgradeActivated, UpgradeCancelled, UpgradePrepared, UpgradeRolledBack, ROLLBACK_FINALIZED_TOPIC, UPGRADE_ACTIVATED_TOPIC, @@ -14,16 +22,18 @@ use dusk_contract_standards::token::drc721::{ events as drc721_events, RoyaltyQuote, }; use dusk_core::abi::ContractId; +use dusk_core::transfer::data::ContractCall; use dusk_data_driver::reader::DriverReader; use proptest::prelude::*; use proptest::test_runner::{Config as ProptestConfig, TestRunner}; use rkyv::ser::serializers::AllocSerializer; use serde_json::{json, Value}; -const DRIVER_NAMES: [&str; 4] = [ +const DRIVER_NAMES: [&str; 5] = [ "authorization_counter", "drc20_roles_pausable", "drc721_collection", + "multisig_controller", "proxy_counter", ]; @@ -32,6 +42,7 @@ enum ReferenceContract { AuthorizationCounter, Drc20, Drc721, + MultisigController, ProxyCounter, } @@ -193,6 +204,93 @@ fn data_driver_outputs_and_events_decode() { }, ); + let multisig = drivers.get("multisig_controller").unwrap(); + assert_output_decodes(multisig, "owners", &vec![principal_value(1)]); + assert_output_decodes(multisig, "threshold", &2u16); + assert_output_decodes(multisig, "proposal_ttl", &10u64); + assert_output_decodes(multisig, "tombstone_ttl", &5u64); + assert_output_decodes(multisig, "operation_id", &[7u8; 32]); + assert_output_decodes( + multisig, + "proposal", + &Some( + dusk_contract_standards::governance::MultisigPendingOperation { + target: multisig_target( + 2, + "set_value", + vec![1, 2, 3], + [4u8; 32], + ), + confirmations: vec![principal_value(1)], + deadline: 100, + }, + ), + ); + assert_output_decodes(multisig, "tombstone_expiry", &Some(120u64)); + assert_output_decodes(multisig, "propose", &vec![1u8, 2, 3]); + assert_output_decodes(multisig, "confirm", &vec![1u8, 2, 3]); + assert_event_decodes( + multisig, + MULTISIG_OPERATION_PROPOSED_TOPIC, + &MultisigOperationProposed { + id: [1u8; 32], + authorizer: principal_value(1), + confirmations: 1, + threshold: 2, + deadline: 10, + }, + ); + assert_event_decodes( + multisig, + MULTISIG_OPERATION_CONFIRMED_TOPIC, + &MultisigOperationConfirmed { + id: [1u8; 32], + authorizer: principal_value(2), + confirmations: 2, + threshold: 2, + deadline: 10, + }, + ); + assert_event_decodes( + multisig, + MULTISIG_OPERATION_EXECUTED_TOPIC, + &MultisigOperationExecuted { + id: [1u8; 32], + success: true, + return_data: vec![9], + error: None, + }, + ); + assert_event_decodes( + multisig, + MULTISIG_OPERATION_CANCELLED_TOPIC, + &MultisigOperationCancelled { + id: [1u8; 32], + signers: vec![principal_value(1), principal_value(2)], + }, + ); + assert_event_decodes( + multisig, + MULTISIG_AUTHORITY_UPDATED_TOPIC, + &MultisigAuthorityUpdated { + previous_owners: vec![principal_value(1)], + previous_threshold: 1, + owners: vec![principal_value(1), principal_value(2)], + threshold: 2, + removed_operations: vec![[3u8; 32]], + }, + ); + assert_event_decodes( + multisig, + MULTISIG_TIME_LIMITS_UPDATED_TOPIC, + &MultisigTimeLimitsUpdated { + previous_proposal_ttl: 10, + previous_tombstone_ttl: 5, + proposal_ttl: 20, + tombstone_ttl: 6, + }, + ); + let proxy = drivers.get("proxy_counter").unwrap(); assert_output_decodes(proxy, "implementation", &contract_id(4)); assert_output_decodes(proxy, "rollback_deadline", &100u64); @@ -480,7 +578,7 @@ fn mutated_inputs(encoded: &[u8]) -> Vec> { fn driver_case_strategy() -> impl Strategy { ( - 0u8..4, + 0u8..5, any::(), any::(), any::(), @@ -492,6 +590,7 @@ fn driver_case_strategy() -> impl Strategy { 0 => ReferenceContract::AuthorizationCounter, 1 => ReferenceContract::Drc20, 2 => ReferenceContract::Drc721, + 3 => ReferenceContract::MultisigController, _ => ReferenceContract::ProxyCounter, }, selector, @@ -507,6 +606,7 @@ fn call_for(case: &DriverCase) -> CallSpec { ReferenceContract::AuthorizationCounter => auth_counter_call(case), ReferenceContract::Drc20 => drc20_call(case), ReferenceContract::Drc721 => drc721_call(case), + ReferenceContract::MultisigController => multisig_call(case), ReferenceContract::ProxyCounter => proxy_call(case), } } @@ -783,6 +883,75 @@ fn drc721_call(case: &DriverCase) -> CallSpec { } } +fn multisig_call(case: &DriverCase) -> CallSpec { + match case.selector % 14 { + 0 => call( + "multisig_controller", + "init", + json!({ + "config": { + "owners": [principal(case.a), principal(case.b)], + "threshold": 1u16, + "proposal_ttl": 10u64, + "tombstone_ttl": 5u64, + } + }), + ), + 1 => call("multisig_controller", "owners", Value::Null), + 2 => call("multisig_controller", "threshold", Value::Null), + 3 => call("multisig_controller", "proposal_ttl", Value::Null), + 4 => call("multisig_controller", "tombstone_ttl", Value::Null), + 5 => call("multisig_controller", "nonce", nonce_query(case.a)), + 6 => call( + "multisig_controller", + "operation_id", + multisig_target_json(case.a, case.b), + ), + 7 => call("multisig_controller", "proposal", json!(bytes32(case.a))), + 8 => call( + "multisig_controller", + "tombstone_expiry", + json!(bytes32(case.a)), + ), + 9 => call( + "multisig_controller", + "propose", + json!({ + "target": multisig_target_json(case.a, case.b), + "authorization": null, + }), + ), + 10 => call( + "multisig_controller", + "confirm", + json!({"id": bytes32(case.a), "authorization": null}), + ), + 11 => call( + "multisig_controller", + "cancel", + json!({"id": bytes32(case.a), "approvals": {"approvals": []}}), + ), + 12 => call( + "multisig_controller", + "update_authority", + json!({ + "owners": [principal(case.a)], + "threshold": 1u16, + "approvals": {"approvals": []}, + }), + ), + _ => call( + "multisig_controller", + "set_time_limits", + json!({ + "proposal_ttl": bounded(case.amount).saturating_add(1), + "tombstone_ttl": 5u64, + "approvals": {"approvals": []}, + }), + ), + } +} + fn proxy_call(case: &DriverCase) -> CallSpec { match case.selector % 9 { 0 => call("proxy_counter", "implementation", Value::Null), @@ -837,6 +1006,30 @@ fn principal(seed: u8) -> Value { } } +fn multisig_target_json(contract_seed: u8, salt_seed: u8) -> Value { + json!({ + "call": { + "contract": hex_bytes(contract_seed, 32), + "fn_name": "set_value", + "fn_args": "", + }, + "salt": bytes32(salt_seed), + }) +} + +fn multisig_target( + contract_seed: u8, + function: &str, + args: Vec, + salt: [u8; 32], +) -> MultisigTarget { + MultisigTarget { + call: ContractCall::new(contract_id(contract_seed), function) + .with_raw_args(args), + salt, + } +} + fn royalty(seed: u8, amount: u64) -> Value { json!({ "receiver": principal(seed), @@ -880,6 +1073,16 @@ fn bytes193_array(seed: u8) -> [u8; 193] { bytes } +fn hex_bytes(seed: u8, len: usize) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(len * 2); + for byte in (0..len).map(|index| seed.wrapping_add(index as u8)) { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + fn bounded(value: u64) -> u64 { value % 1_000_000 } diff --git a/standards/dusk-contract-standards/tests/examples_vm.rs b/standards/dusk-contract-standards/tests/examples_vm.rs index 31f5fbe4..8a0fae35 100644 --- a/standards/dusk-contract-standards/tests/examples_vm.rs +++ b/standards/dusk-contract-standards/tests/examples_vm.rs @@ -7,6 +7,11 @@ use dusk_contract_standards::auth::{ SignedAuthorization, }; use dusk_contract_standards::core::{NonceDomain, Principal}; +use dusk_contract_standards::governance::{ + MultisigControllerConfig, MultisigOperationExecuted, MultisigOperationId, + MultisigPendingOperation, MultisigTarget, + MULTISIG_OPERATION_EXECUTED_TOPIC, +}; use dusk_contract_standards::token::drc20::{ Allowance, BalanceOf as BalanceOf20, Init as Init20, InitBalance, SignedApproveCall, TransferCall as TransferCall20, @@ -25,6 +30,7 @@ use dusk_core::signatures::bls::{ use dusk_core::signatures::schnorr::{ PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, }; +use dusk_core::transfer::data::ContractCall; use dusk_core::JubJubScalar; use dusk_vm::host_queries; use dusk_vm::{ContractData, Session, VM}; @@ -53,6 +59,9 @@ const NFT_SIGNED_APPROVE_ACTION: [u8; 32] = [30u8; 32]; const NFT_SIGNED_APPROVAL_FOR_ALL_ACTION: [u8; 32] = [31u8; 32]; const PROXY_ADMIN_DOMAIN: NonceDomain = [31u8; 32]; const SET_PROXY_VALUE_ACTION: [u8; 32] = [32u8; 32]; +const MULTISIG_CONTROLLER_DOMAIN: NonceDomain = [41u8; 32]; +const MULTISIG_PROPOSE_ACTION: [u8; 32] = [42u8; 32]; +const MULTISIG_CONFIRM_ACTION: [u8; 32] = [43u8; 32]; #[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[archive_attr(derive(CheckBytes))] @@ -147,6 +156,26 @@ struct ProxySetValue { authorization: Option, } +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[archive_attr(derive(CheckBytes))] +struct MultisigInit { + config: MultisigControllerConfig, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct MultisigPropose { + target: MultisigTarget, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[archive_attr(derive(CheckBytes))] +struct MultisigConfirm { + id: MultisigOperationId, + authorization: Option, +} + #[test] #[ignore = "requires release Wasm examples; run after the wasm build"] fn wasm_examples_deploy_and_answer_queries() { @@ -312,6 +341,40 @@ fn wasm_examples_deploy_and_answer_queries() { ) .is_err()); exercise_proxy_signed_admin_call(&mut session, proxy, admin); + + let multisig_owner_a = phoenix_principal(8); + let multisig_owner_b = phoenix_principal(9); + let multisig = deploy( + &mut session, + "multisig_controller.wasm", + [23u8; 32], + &MultisigInit { + config: MultisigControllerConfig { + owners: vec![multisig_owner_a, multisig_owner_b], + threshold: 2, + proposal_ttl: 10, + tombstone_ttl: 6, + }, + }, + ); + let governed_proxy = deploy( + &mut session, + "proxy_counter.wasm", + [24u8; 32], + &ProxyCounterInit { + admin: Principal::contract(multisig), + implementation: ContractId::from_bytes([101u8; 32]), + upgrade_delay: 0, + rollback_window: 10, + }, + ); + exercise_multisig_controller_admin_call( + &mut session, + multisig, + governed_proxy, + multisig_owner_a, + multisig_owner_b, + ); } fn exercise_drc20_signed_calls( @@ -1795,6 +1858,213 @@ fn exercise_proxy_signed_admin_call( assert_contract_nonce(session, contract, admin, PROXY_ADMIN_DOMAIN, 1); } +fn exercise_multisig_controller_admin_call( + session: &mut Session, + multisig: ContractId, + proxy: ContractId, + owner_a: Principal, + owner_b: Principal, +) { + let mut rng = StdRng::seed_from_u64(5555); + let owner_a_sk = SchnorrSecretKey::from(JubJubScalar::from(8u64)); + let owner_a_pk = SchnorrPublicKey::from(&owner_a_sk); + let owner_b_sk = SchnorrSecretKey::from(JubJubScalar::from(9u64)); + let owner_b_pk = SchnorrPublicKey::from(&owner_b_sk); + assert_eq!(owner_a, Principal::phoenix_public_key(&owner_a_pk)); + assert_eq!(owner_b, Principal::phoenix_public_key(&owner_b_pk)); + + let target = proxy_set_value_target(proxy, 77, [1u8; 32]); + let id: MultisigOperationId = session + .call(multisig, "operation_id", &target, GAS_LIMIT) + .expect("compute multisig operation id") + .data; + + let propose_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_a_sk, + owner_a_pk, + authorized_action( + multisig, + owner_a, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_PROPOSE_ACTION, + 0, + 0, + id, + ), + )); + let returned: Vec = session + .call( + multisig, + "propose", + &MultisigPropose { + target: target.clone(), + authorization: Some(propose_auth), + }, + GAS_LIMIT, + ) + .expect("propose multisig operation") + .data; + assert!(returned.is_empty()); + assert_contract_nonce( + session, + multisig, + owner_a, + MULTISIG_CONTROLLER_DOMAIN, + 1, + ); + let pending: Option = session + .call(multisig, "proposal", &id, GAS_LIMIT) + .expect("query pending multisig operation") + .data; + assert_eq!(pending.expect("pending").confirmations, vec![owner_a]); + assert_proxy_value(session, proxy, 0); + + let duplicate_confirm = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_a_sk, + owner_a_pk, + authorized_action( + multisig, + owner_a, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_CONFIRM_ACTION, + 1, + 0, + id, + ), + )); + assert!(session + .call::<_, Vec>( + multisig, + "confirm", + &MultisigConfirm { + id, + authorization: Some(duplicate_confirm), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + multisig, + owner_a, + MULTISIG_CONTROLLER_DOMAIN, + 1, + ); + + let wrong_payload = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_b_sk, + owner_b_pk, + authorized_action( + multisig, + owner_b, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_CONFIRM_ACTION, + 0, + 0, + [0xee; 32], + ), + )); + assert!(session + .call::<_, Vec>( + multisig, + "confirm", + &MultisigConfirm { + id, + authorization: Some(wrong_payload), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + multisig, + owner_b, + MULTISIG_CONTROLLER_DOMAIN, + 0, + ); + + let confirm_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_b_sk, + owner_b_pk, + authorized_action( + multisig, + owner_b, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_CONFIRM_ACTION, + 0, + 0, + id, + ), + )); + let receipt = session + .call::<_, Vec>( + multisig, + "confirm", + &MultisigConfirm { + id, + authorization: Some(confirm_auth.clone()), + }, + GAS_LIMIT, + ) + .expect("confirm and execute multisig operation"); + let execution = receipt + .events + .iter() + .find(|event| event.topic == MULTISIG_OPERATION_EXECUTED_TOPIC) + .map(|event| { + rkyv::from_bytes::(&event.data) + .expect("decode multisig execution event") + }) + .expect("multisig execution event"); + assert!( + execution.success, + "multisig target execution failed: {:?}", + execution.error + ); + assert_proxy_value(session, proxy, 77); + assert_contract_nonce( + session, + multisig, + owner_b, + MULTISIG_CONTROLLER_DOMAIN, + 1, + ); + let pending: Option = session + .call(multisig, "proposal", &id, GAS_LIMIT) + .expect("query executed multisig operation") + .data; + assert!(pending.is_none()); + let tombstone: Option = session + .call(multisig, "tombstone_expiry", &id, GAS_LIMIT) + .expect("query multisig tombstone") + .data; + assert!(tombstone.is_some()); + + assert!(session + .call::<_, Vec>( + multisig, + "confirm", + &MultisigConfirm { + id, + authorization: Some(confirm_auth), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + multisig, + owner_b, + MULTISIG_CONTROLLER_DOMAIN, + 1, + ); + assert_proxy_value(session, proxy, 77); +} + fn exercise_authorization_counter_signed_calls( session: &mut Session, contract: ContractId, @@ -2260,6 +2530,34 @@ fn assert_contract_nonce( assert_eq!(actual_nonce, nonce); } +fn assert_proxy_value( + session: &mut Session, + contract: ContractId, + expected: u64, +) { + let value: u64 = session + .call(contract, "value", &(), GAS_LIMIT) + .expect("query proxy value") + .data; + assert_eq!(value, expected); +} + +fn proxy_set_value_target( + proxy: ContractId, + value: u64, + salt: [u8; 32], +) -> MultisigTarget { + MultisigTarget { + call: ContractCall::new(proxy, "set_value") + .with_args(&ProxySetValue { + value, + authorization: None, + }) + .expect("serialize proxy set_value args"), + salt, + } +} + fn amount_hash(amount: u64) -> [u8; 32] { host_queries::keccak256(Vec::from(amount.to_be_bytes())) } diff --git a/standards/dusk-contract-standards/tests/primitives.rs b/standards/dusk-contract-standards/tests/primitives.rs index 7f3e1cbd..c04b909e 100644 --- a/standards/dusk-contract-standards/tests/primitives.rs +++ b/standards/dusk-contract-standards/tests/primitives.rs @@ -13,8 +13,10 @@ use dusk_contract_standards::core::{ ReplayGuard, }; use dusk_contract_standards::governance::{ - MultisigConfig, ThresholdMultisig, Timelock, TimelockController, - CANCELLER_ROLE, EXECUTOR_ROLE, PROPOSER_ROLE, TIMELOCK_ADMIN_ROLE, + MultisigConfig, MultisigController, MultisigControllerConfig, + MultisigControllerStatus, MultisigTarget, ThresholdMultisig, Timelock, + TimelockController, CANCELLER_ROLE, EXECUTOR_ROLE, PROPOSER_ROLE, + TIMELOCK_ADMIN_ROLE, }; use dusk_contract_standards::proxy::{ StateStore, UpgradeActivated, UpgradeAdmin, UpgradeCancelled, @@ -39,6 +41,7 @@ use dusk_core::signatures::bls::{ use dusk_core::signatures::schnorr::{ PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, }; +use dusk_core::transfer::data::ContractCall; use dusk_core::JubJubScalar; use rand::rngs::StdRng; use rand::SeedableRng; @@ -51,7 +54,20 @@ fn c(byte: u8) -> ContractId { ContractId::from_bytes([byte; 32]) } -fn assert_panics(f: impl FnOnce()) { +fn multisig_target( + contract: u8, + function: &str, + args: impl AsRef<[u8]>, + salt: [u8; 32], +) -> MultisigTarget { + MultisigTarget { + call: ContractCall::new(c(contract), function) + .with_raw_args(args.as_ref().to_vec()), + salt, + } +} + +fn assert_panics(f: impl FnOnce() -> R) { assert!(catch_unwind(AssertUnwindSafe(f)).is_err()); } @@ -448,6 +464,209 @@ fn threshold_multisig_owner_management_requires_quorum_and_is_atomic() { assert_eq!(multisig.threshold(), 3); } +#[test] +fn multisig_controller_proposes_confirms_executes_and_tombstones() { + let owner_a = p(130); + let owner_b = p(131); + let owner_c = p(132); + let outsider = p(133); + let id = [134u8; 32]; + let target = multisig_target(135, "set_value", [1, 2, 3], [136u8; 32]); + + let mut controller = MultisigController::new(); + controller.init(MultisigControllerConfig { + owners: vec![owner_a, owner_b, owner_c], + threshold: 2, + proposal_ttl: 10, + tombstone_ttl: 5, + }); + + assert_panics(|| controller.confirm(id, owner_a, 0)); + assert_panics(|| controller.propose(id, target.clone(), outsider, 0)); + assert!(controller.proposal(id).is_none()); + + let proposed = controller.propose(id, target.clone(), owner_a, 0); + assert_eq!(proposed.status, MultisigControllerStatus::Proposed); + assert_eq!(proposed.confirmations, 1); + assert!(proposed.ready_operation.is_none()); + assert_eq!( + controller.proposal(id).expect("pending").confirmations, + vec![owner_a] + ); + + assert_panics(|| controller.confirm(id, owner_a, 1)); + assert_eq!( + controller + .proposal(id) + .expect("still pending") + .confirmations, + vec![owner_a] + ); + + let ready = controller.confirm(id, owner_b, 1); + assert_eq!(ready.status, MultisigControllerStatus::Ready); + assert_eq!(ready.confirmations, 2); + let operation = ready.ready_operation.expect("ready operation"); + assert_eq!(operation.target, target); + assert_eq!(operation.confirmations, vec![owner_a, owner_b]); + assert!(controller.proposal(id).is_none()); + assert_eq!(controller.tombstone_expiry(id), Some(6)); + + assert_panics(|| { + controller.propose( + id, + multisig_target(135, "set_value", [4], [136u8; 32]), + owner_c, + 2, + ) + }); + assert_eq!(controller.tombstone_expiry(id), Some(6)); + + let reproposed = controller.propose( + id, + multisig_target(135, "set_value", [4], [136u8; 32]), + owner_c, + 7, + ); + assert_eq!(reproposed.status, MultisigControllerStatus::Proposed); + assert_eq!(controller.tombstone_expiry(id), None); +} + +#[test] +fn multisig_controller_expiry_cancel_and_authority_updates_are_atomic() { + let owner_a = p(140); + let owner_b = p(141); + let owner_c = p(142); + let owner_d = p(143); + let outsider = p(144); + let id_a = [145u8; 32]; + let id_b = [146u8; 32]; + + let mut controller = MultisigController::new(); + controller.init(MultisigControllerConfig { + owners: vec![owner_a, owner_b, owner_c], + threshold: 2, + proposal_ttl: 3, + tombstone_ttl: 4, + }); + assert_panics(|| { + controller.init(MultisigControllerConfig { + owners: vec![owner_a, owner_b], + threshold: 1, + proposal_ttl: 1, + tombstone_ttl: 1, + }); + }); + + controller.propose( + id_a, + multisig_target(147, "one", [], [148u8; 32]), + owner_a, + 10, + ); + assert!(controller.proposal(id_a).is_some()); + assert_panics(|| controller.confirm(id_a, owner_b, 14)); + assert!(controller.proposal(id_a).is_none()); + controller.propose( + id_b, + multisig_target(149, "two", [], [150u8; 32]), + owner_a, + 14, + ); + assert!(controller.proposal(id_a).is_none()); + assert!(controller.proposal(id_b).is_some()); + + assert_panics(|| controller.cancel(id_b, &[owner_a], 14)); + assert!(controller.proposal(id_b).is_some()); + let cancelled = controller.cancel(id_b, &[owner_a, owner_b], 14); + assert_eq!(cancelled.signers, vec![owner_a, owner_b]); + assert!(controller.proposal(id_b).is_none()); + + controller.propose( + id_a, + multisig_target(151, "three", [], [152u8; 32]), + owner_a, + 15, + ); + let before = controller.owners(); + assert_panics(|| { + controller.update_authority( + &[owner_a, outsider], + vec![owner_a, owner_d], + 2, + ); + }); + assert_eq!(controller.owners(), before); + assert!(controller.proposal(id_a).is_some()); + + let event = controller.update_authority( + &[owner_a, owner_b], + vec![owner_a, owner_b, owner_c, owner_d], + 2, + ); + assert_eq!(event.previous_owners, before); + assert_eq!(event.owners, vec![owner_a, owner_b, owner_c, owner_d]); + assert!(event.removed_operations.is_empty()); + assert!(controller.proposal(id_a).is_some()); + + let event = controller.update_authority( + &[owner_a, owner_b], + vec![owner_a, owner_d], + 2, + ); + assert_eq!(event.removed_operations, vec![id_a]); + assert!(controller.proposal(id_a).is_none()); + + let before_ttl = controller.proposal_ttl(); + assert_panics(|| controller.set_time_limits(&[owner_a], 0, 4)); + assert_eq!(controller.proposal_ttl(), before_ttl); + let event = controller.set_time_limits(&[owner_a, owner_d], 8, 9); + assert_eq!(event.previous_proposal_ttl, 3); + assert_eq!(event.proposal_ttl, 8); + assert_eq!(controller.tombstone_ttl(), 9); +} + +#[test] +fn multisig_controller_rejects_bad_targets_and_config() { + let owner_a = p(153); + let owner_b = p(154); + let mut controller = MultisigController::new(); + + assert_panics(|| { + controller.init(MultisigControllerConfig { + owners: vec![owner_a, owner_b], + threshold: 2, + proposal_ttl: 0, + tombstone_ttl: 1, + }); + }); + assert!(!controller.is_initialized()); + + controller.init(MultisigControllerConfig { + owners: vec![owner_a, owner_b], + threshold: 2, + proposal_ttl: 1, + tombstone_ttl: 1, + }); + + assert_panics(|| { + controller.propose( + [155u8; 32], + multisig_target(0, "call", [], [156u8; 32]), + owner_a, + 0, + ); + }); + assert_panics(|| { + controller.propose( + [157u8; 32], + multisig_target(158, "", [], [159u8; 32]), + owner_a, + 0, + ); + }); +} + #[test] fn replay_guard_rejects_reuse_across_same_principal() { let owner = p(1); diff --git a/standards/examples/multisig_controller/Cargo.toml b/standards/examples/multisig_controller/Cargo.toml new file mode 100644 index 00000000..199b14f2 --- /dev/null +++ b/standards/examples/multisig_controller/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "multisig-controller" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[target.'cfg(target_family = "wasm")'.dependencies] +dusk-core = { workspace = true } +dusk-data-driver = { workspace = true, optional = true } +dusk-forge = { workspace = true } +dusk-contract-standards = { path = "../../dusk-contract-standards" } +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, default-features = false, features = [ + "alloc", +], optional = true } + +[features] +contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] +serde = ["dep:serde", "dusk-contract-standards/serde"] +data-driver = [ + "serde", + "dusk-core/serde", + "dep:dusk-data-driver", + "dusk-data-driver/wasm-export", + "dep:serde_json", +] +data-driver-js = ["data-driver", "dusk-data-driver/alloc"] diff --git a/standards/examples/multisig_controller/Makefile b/standards/examples/multisig_controller/Makefile new file mode 100644 index 00000000..ba0153af --- /dev/null +++ b/standards/examples/multisig_controller/Makefile @@ -0,0 +1,34 @@ +TARGET_DIR ?= ../../../target +DD_TARGET_DIR ?= ../../../target/data-driver + +all: wasm wasm-dd clippy + +wasm: + @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ + CARGO_TARGET_DIR=$(TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + -Z build-std=core,alloc \ + --target wasm32-unknown-unknown \ + --features contract \ + -p multisig-controller + +test: + @cargo test -p dusk-contract-standards + +wasm-dd: + @CARGO_TARGET_DIR=$(DD_TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + --target wasm32-unknown-unknown \ + --features data-driver-js \ + -p multisig-controller + +clippy: + @cargo clippy -p multisig-controller -Z build-std=core,alloc --release --target wasm32-unknown-unknown --features contract -- -D warnings + +doc: + +.PHONY: all wasm wasm-dd test clippy doc diff --git a/standards/examples/multisig_controller/src/lib.rs b/standards/examples/multisig_controller/src/lib.rs new file mode 100644 index 00000000..70974345 --- /dev/null +++ b/standards/examples/multisig_controller/src/lib.rs @@ -0,0 +1,495 @@ +//! Forge reference multisig controller for Dusk standards contracts. + +#![no_std] +#![cfg(target_family = "wasm")] + +extern crate alloc; +extern crate self as multisig_controller_types; + +use alloc::vec::Vec; +use bytecheck::CheckBytes; +use dusk_contract_standards::auth::SignedAuthorization; +use dusk_contract_standards::core::{NonceDomain, Principal}; +use dusk_contract_standards::governance::{ + MultisigApprovals, MultisigControllerConfig, MultisigOperationId, + MultisigTarget, Threshold, +}; +use rkyv::{Archive, Deserialize, Serialize}; + +pub const MULTISIG_CONTROLLER_DOMAIN: NonceDomain = [41u8; 32]; +pub const PROPOSE_ACTION: [u8; 32] = [42u8; 32]; +pub const CONFIRM_ACTION: [u8; 32] = [43u8; 32]; +pub const CANCEL_ACTION: [u8; 32] = [44u8; 32]; +pub const UPDATE_AUTHORITY_ACTION: [u8; 32] = [45u8; 32]; +pub const SET_TIME_LIMITS_ACTION: [u8; 32] = [46u8; 32]; + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Init { + pub config: MultisigControllerConfig, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Propose { + pub target: MultisigTarget, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Confirm { + pub id: MultisigOperationId, + pub authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct Cancel { + pub id: MultisigOperationId, + pub approvals: MultisigApprovals, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct UpdateAuthority { + pub owners: Vec, + pub threshold: Threshold, + pub approvals: MultisigApprovals, +} + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct SetTimeLimits { + pub proposal_ttl: u64, + pub tombstone_ttl: u64, + pub approvals: MultisigApprovals, +} + +#[derive( + Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[archive_attr(derive(CheckBytes))] +pub struct NonceQuery { + pub principal: Principal, + pub domain: NonceDomain, +} + +#[dusk_forge::contract] +mod multisig_controller { + use alloc::format; + use alloc::vec::Vec; + + use dusk_contract_standards::auth::{ + ActionEnvelope, AuthorizationManager, SignedAuthorization, + }; + use dusk_contract_standards::core::{ + error, CallContext, Principal, PrincipalKind, + }; + use dusk_contract_standards::governance::{ + MultisigAuthorityUpdated, MultisigController, + MultisigControllerOutcome, MultisigControllerStatus, + MultisigOperationCancelled, MultisigOperationConfirmed, + MultisigOperationExecuted, MultisigOperationId, + MultisigOperationProposed, MultisigPendingOperation, MultisigTarget, + MultisigTimeLimitsUpdated, Threshold, MULTISIG_AUTHORITY_UPDATED_TOPIC, + MULTISIG_OPERATION_CANCELLED_TOPIC, MULTISIG_OPERATION_CONFIRMED_TOPIC, + MULTISIG_OPERATION_EXECUTED_TOPIC, MULTISIG_OPERATION_PROPOSED_TOPIC, + MULTISIG_TIME_LIMITS_UPDATED_TOPIC, + }; + use dusk_core::abi; + + use multisig_controller_types::{ + Cancel, Confirm, Init, NonceQuery, Propose, SetTimeLimits, + UpdateAuthority, CANCEL_ACTION, CONFIRM_ACTION, + MULTISIG_CONTROLLER_DOMAIN, PROPOSE_ACTION, SET_TIME_LIMITS_ACTION, + UPDATE_AUTHORITY_ACTION, + }; + + pub struct MultisigControllerContract { + controller: MultisigController, + authorizations: AuthorizationManager, + initialized: bool, + } + + impl MultisigControllerContract { + pub const fn new() -> Self { + Self { + controller: MultisigController::new(), + authorizations: AuthorizationManager::new(), + initialized: false, + } + } + + #[contract(no_event)] + pub fn init(&mut self, args: Init) { + if self.initialized { + panic!("{}", error::ALREADY_INITIALIZED); + } + self.controller.init(args.config); + self.initialized = true; + } + + pub fn owners(&self) -> Vec { + self.controller.owners() + } + + pub fn threshold(&self) -> Threshold { + self.controller.threshold() + } + + pub fn proposal_ttl(&self) -> u64 { + self.controller.proposal_ttl() + } + + pub fn tombstone_ttl(&self) -> u64 { + self.controller.tombstone_ttl() + } + + pub fn nonce(&self, args: NonceQuery) -> u64 { + self.authorizations.nonce(args.principal, args.domain) + } + + pub fn operation_id( + &self, + target: MultisigTarget, + ) -> MultisigOperationId { + operation_id(&target) + } + + pub fn proposal( + &self, + id: MultisigOperationId, + ) -> Option { + self.controller.proposal(id) + } + + pub fn tombstone_expiry(&self, id: MultisigOperationId) -> Option { + self.controller.tombstone_expiry(id) + } + + pub fn propose(&mut self, args: Propose) -> Vec { + self.assert_initialized(); + let Propose { + target, + authorization, + } = args; + let id = operation_id(&target); + let authorizer = + self.verify_owner(PROPOSE_ACTION, id, authorization.as_ref()); + let outcome = + self.controller.propose(id, target, authorizer, now()); + self.consume_authorization(authorization.as_ref()); + match outcome.status { + MultisigControllerStatus::Proposed => abi::emit( + MULTISIG_OPERATION_PROPOSED_TOPIC, + MultisigOperationProposed { + id: outcome.id, + authorizer: outcome.authorizer, + confirmations: outcome.confirmations, + threshold: outcome.threshold, + deadline: outcome.deadline, + }, + ), + MultisigControllerStatus::Confirmed + | MultisigControllerStatus::Ready => abi::emit( + MULTISIG_OPERATION_CONFIRMED_TOPIC, + MultisigOperationConfirmed { + id: outcome.id, + authorizer: outcome.authorizer, + confirmations: outcome.confirmations, + threshold: outcome.threshold, + deadline: outcome.deadline, + }, + ), + } + let execution_id = outcome.id; + let (return_data, execution) = self.execute_if_ready(outcome); + if let Some((success, error)) = execution { + abi::emit( + MULTISIG_OPERATION_EXECUTED_TOPIC, + MultisigOperationExecuted { + id: execution_id, + success, + return_data: return_data.clone(), + error, + }, + ); + } + return_data + } + + pub fn confirm(&mut self, args: Confirm) -> Vec { + self.assert_initialized(); + let authorizer = self.verify_owner( + CONFIRM_ACTION, + args.id, + args.authorization.as_ref(), + ); + let outcome = self.controller.confirm(args.id, authorizer, now()); + self.consume_authorization(args.authorization.as_ref()); + match outcome.status { + MultisigControllerStatus::Proposed => abi::emit( + MULTISIG_OPERATION_PROPOSED_TOPIC, + MultisigOperationProposed { + id: outcome.id, + authorizer: outcome.authorizer, + confirmations: outcome.confirmations, + threshold: outcome.threshold, + deadline: outcome.deadline, + }, + ), + MultisigControllerStatus::Confirmed + | MultisigControllerStatus::Ready => abi::emit( + MULTISIG_OPERATION_CONFIRMED_TOPIC, + MultisigOperationConfirmed { + id: outcome.id, + authorizer: outcome.authorizer, + confirmations: outcome.confirmations, + threshold: outcome.threshold, + deadline: outcome.deadline, + }, + ), + } + let execution_id = outcome.id; + let (return_data, execution) = self.execute_if_ready(outcome); + if let Some((success, error)) = execution { + abi::emit( + MULTISIG_OPERATION_EXECUTED_TOPIC, + MultisigOperationExecuted { + id: execution_id, + success, + return_data: return_data.clone(), + error, + }, + ); + } + return_data + } + + pub fn cancel(&mut self, args: Cancel) { + self.assert_initialized(); + let signers = self.verify_quorum( + CANCEL_ACTION, + args.id, + &args.approvals.approvals, + ); + let event: MultisigOperationCancelled = + self.controller.cancel(args.id, &signers, now()); + self.consume_approvals(&args.approvals.approvals); + abi::emit( + MULTISIG_OPERATION_CANCELLED_TOPIC, + MultisigOperationCancelled { + id: event.id, + signers: event.signers, + }, + ); + } + + pub fn update_authority(&mut self, args: UpdateAuthority) { + self.assert_initialized(); + let payload_hash = + authority_payload_hash(&args.owners, args.threshold); + let signers = self.verify_quorum( + UPDATE_AUTHORITY_ACTION, + payload_hash, + &args.approvals.approvals, + ); + let event: MultisigAuthorityUpdated = self + .controller + .update_authority(&signers, args.owners, args.threshold); + self.consume_approvals(&args.approvals.approvals); + abi::emit( + MULTISIG_AUTHORITY_UPDATED_TOPIC, + MultisigAuthorityUpdated { + previous_owners: event.previous_owners, + previous_threshold: event.previous_threshold, + owners: event.owners, + threshold: event.threshold, + removed_operations: event.removed_operations, + }, + ); + } + + pub fn set_time_limits(&mut self, args: SetTimeLimits) { + self.assert_initialized(); + let payload_hash = + time_limits_payload_hash(args.proposal_ttl, args.tombstone_ttl); + let signers = self.verify_quorum( + SET_TIME_LIMITS_ACTION, + payload_hash, + &args.approvals.approvals, + ); + let event: MultisigTimeLimitsUpdated = + self.controller.set_time_limits( + &signers, + args.proposal_ttl, + args.tombstone_ttl, + ); + self.consume_approvals(&args.approvals.approvals); + abi::emit( + MULTISIG_TIME_LIMITS_UPDATED_TOPIC, + MultisigTimeLimitsUpdated { + previous_proposal_ttl: event.previous_proposal_ttl, + previous_tombstone_ttl: event.previous_tombstone_ttl, + proposal_ttl: event.proposal_ttl, + tombstone_ttl: event.tombstone_ttl, + }, + ); + } + + fn verify_owner( + &self, + action_id: [u8; 32], + payload_hash: [u8; 32], + authorization: Option<&SignedAuthorization>, + ) -> Principal { + if let Some(principal) = CallContext::current().principal { + match principal.kind() { + PrincipalKind::Moonlight | PrincipalKind::Contract => { + if self.controller.is_owner(principal) { + return principal; + } + } + PrincipalKind::Phoenix => {} + } + } + + let Some(authorization) = authorization else { + panic!("{}", error::UNAUTHORIZED); + }; + let envelope = ActionEnvelope::new( + abi::self_id(), + MULTISIG_CONTROLLER_DOMAIN, + action_id, + payload_hash, + ); + let principal = self.authorizations.verify_signed_action( + authorization, + envelope, + now(), + ); + if !self.controller.is_owner(principal) { + panic!("{}", error::UNAUTHORIZED); + } + principal + } + + fn verify_quorum( + &self, + action_id: [u8; 32], + payload_hash: [u8; 32], + approvals: &[SignedAuthorization], + ) -> Vec { + self.controller.verify_action( + &self.authorizations, + CallContext::current(), + approvals, + ActionEnvelope::new( + abi::self_id(), + MULTISIG_CONTROLLER_DOMAIN, + action_id, + payload_hash, + ), + now(), + ) + } + + fn consume_authorization( + &mut self, + authorization: Option<&SignedAuthorization>, + ) { + if let Some(authorization) = authorization { + self.authorizations.consume_verified(authorization); + } + } + + fn consume_approvals(&mut self, approvals: &[SignedAuthorization]) { + for approval in approvals { + self.authorizations.consume_verified(approval); + } + } + + fn execute_if_ready( + &mut self, + outcome: MultisigControllerOutcome, + ) -> (Vec, Option<(bool, Option)>) { + let Some(operation) = outcome.ready_operation else { + return (Vec::new(), None); + }; + + let call = &operation.target.call; + let (success, return_data, error) = match abi::call_raw( + call.contract, + &call.fn_name, + &call.fn_args, + ) { + Ok(data) => (true, data, None), + Err(error) => (false, Vec::new(), Some(format!("{error}"))), + }; + (return_data, Some((success, error))) + } + + fn assert_initialized(&self) { + if !self.initialized { + panic!("{}", error::NOT_INITIALIZED); + } + } + } + + impl Default for MultisigControllerContract { + fn default() -> Self { + Self::new() + } + } + + fn operation_id(target: &MultisigTarget) -> MultisigOperationId { + let mut bytes = + Vec::from(&b"dusk-contract-standards/multisig/op/v1"[..]); + bytes.push(abi::chain_id()); + bytes.extend_from_slice(&abi::self_id().to_bytes()); + bytes.extend_from_slice(&target.call.to_var_bytes()); + bytes.extend_from_slice(&target.salt); + abi::keccak256(bytes) + } + + fn authority_payload_hash( + owners: &[Principal], + threshold: Threshold, + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"multisig.authority"[..]); + bytes.extend_from_slice(&(owners.len() as u32).to_be_bytes()); + for owner in owners { + push_principal(&mut bytes, *owner); + } + bytes.extend_from_slice(&threshold.to_be_bytes()); + abi::keccak256(bytes) + } + + fn time_limits_payload_hash( + proposal_ttl: u64, + tombstone_ttl: u64, + ) -> [u8; 32] { + let mut bytes = Vec::from(&b"multisig.time_limits"[..]); + bytes.extend_from_slice(&proposal_ttl.to_be_bytes()); + bytes.extend_from_slice(&tombstone_ttl.to_be_bytes()); + abi::keccak256(bytes) + } + + fn push_principal(bytes: &mut Vec, principal: Principal) { + let principal = principal.to_bytes(); + bytes.extend_from_slice(&(principal.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&principal); + } + + fn now() -> u64 { + abi::block_height() + } +} From d395a24194475b0fc85f34dc411b68a7b80a6507 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Mon, 27 Apr 2026 00:18:22 +0200 Subject: [PATCH 27/35] standards: harden multisig controller coverage --- docs/dusk-contract-standards-audit.md | 8 +- ...ct-standards-multisig-controller-review.md | 114 +++++++++++ docs/dusk-contract-standards.md | 19 +- .../tests/examples_vm.rs | 184 +++++++++++++++++- .../tests/properties.rs | 164 +++++++++++++++- 5 files changed, 476 insertions(+), 13 deletions(-) create mode 100644 docs/dusk-contract-standards-multisig-controller-review.md diff --git a/docs/dusk-contract-standards-audit.md b/docs/dusk-contract-standards-audit.md index 197ee0a7..3792198b 100644 --- a/docs/dusk-contract-standards-audit.md +++ b/docs/dusk-contract-standards-audit.md @@ -59,8 +59,8 @@ The layer does not assume: | MULTISIG-1 | Threshold multisig requires distinct owner quorum and rejects duplicate signers before nonce/replay consumption. | primitives | | MULTISIG-2 | Observed Moonlight/contract owners can count toward quorum; Phoenix owners require signed action approvals. | primitives | | MULTISIG-3 | Multisig owner and threshold maintenance requires current quorum and rejected changes leave state unchanged. | primitives | -| MULTISIG-4 | Standalone controller proposal/confirmation requires distinct owners, rejects duplicate confirmations without nonce movement, expires stale proposals, tombstones executed ids, and clears stale pending operations after authority-removing changes. | primitives, VM test | -| MULTISIG-5 | A contract-owned proxy/admin path can be controlled by the standalone multisig controller through observed inter-contract caller context. | VM test | +| MULTISIG-4 | Standalone controller proposal/confirmation requires distinct owners, rejects non-owners and duplicate confirmations without nonce movement, expires stale proposals, tombstones executed ids, and clears stale pending operations after authority-removing changes. | primitives, properties, VM test | +| MULTISIG-5 | A contract-owned proxy/admin path can be controlled by a 2-of-3 standalone multisig controller through observed inter-contract caller context, while direct owner calls to the governed target fail. | VM test | | ACCESS-1 | Role grants/revokes are admin-gated, reject zero accounts, and emit typed events when state changes. | primitives, DRC20 reference, data-driver event decoding | | ACCESS-2 | Owner and owner-set initialization reject zero principals atomically. | primitives, properties | | PAUSE-1 | Reference pausable DRC20/DRC721 pause all balance-changing operations. | primitives, VM test, local-node smoke | @@ -114,6 +114,10 @@ For dependency advisory scanning, install `cargo-audit` and opt in: RUN_CARGO_AUDIT=1 ./scripts/dusk-contract-standards-audit-grade.sh ``` +## Focused Reviews + +- `docs/dusk-contract-standards-multisig-controller-review.md` + ## Auditor Checklist Reviewers should focus on: diff --git a/docs/dusk-contract-standards-multisig-controller-review.md b/docs/dusk-contract-standards-multisig-controller-review.md new file mode 100644 index 00000000..db71537f --- /dev/null +++ b/docs/dusk-contract-standards-multisig-controller-review.md @@ -0,0 +1,114 @@ +# Multisig Controller Security Review + +Review date: 2026-04-27 + +## Scope + +Reviewed code: + +- `standards/dusk-contract-standards/src/governance/multisig_controller.rs` +- `standards/examples/multisig_controller` +- `standards/examples/proxy_counter` +- multisig primitive, property, VM, and data-driver tests under + `standards/dusk-contract-standards/tests` + +The review focused on the standalone controller pattern where a target contract +is owned by `Principal::Contract(multisig_controller_id)`, and individual +Moonlight/Phoenix/contract owners approve controller operations. + +## Intended Security Properties + +- The target contract must treat the multisig controller contract as the owner. + Individual multisig owners are not target admins by themselves. +- A proposal with one owner approval must not execute when the threshold is + two. +- Confirmation requires a distinct current owner. Duplicate confirmations, + non-owner confirmations, wrong payload signatures, and replayed signatures + must fail. +- Signed approvals must bind the controller id, nonce domain, action id, + operation id or maintenance payload, signer principal, nonce, expiry, and + payload hash before nonce/replay state is consumed. +- Operation ids must be scoped to chain id, controller contract id, target call + bytes, and salt. +- A ready operation must be removed from pending proposals and tombstoned before + the wrapper attempts target execution. +- Expired proposals are pruned and cannot become ready. +- Authority changes that remove owners or change threshold clear stale pending + operations. + +## Review Results + +No critical or high-severity issues are open from this pass. + +The VM test now deploys `multisig_controller` with three Phoenix owners and +threshold `2`, then deploys `proxy_counter` with +`Principal::Contract(multisig_controller_id)` as admin. It verifies that: + +- direct root `proxy_counter.set_value` without authorization fails; +- a direct signed proxy-admin call from a multisig owner fails because the + target owner is the controller contract, not the human owner; +- a non-owner cannot propose and does not advance nonce state; +- one valid owner proposal leaves the operation pending and does not update the + target; +- duplicate confirmation and wrong-payload confirmation fail without nonce + movement; +- a second distinct owner confirms and executes the target call; +- replaying the used confirmation fails; +- a third owner can be the second signer for a later operation, proving the + threshold is truly 2-of-3 rather than a fixed owner pair. + +The native property test exercises the raw controller state machine across +random owners, ids, targets, TTLs, tombstone windows, and timings. It checks +atomicity for non-owner proposals, non-owner confirmations, duplicate +confirmations, one-of-three pending behavior, expiry pruning, ready-state +tombstoning, tombstone replay rejection, and successful re-use only after the +tombstone has expired. + +## Security Notes + +The controller deliberately has no Ethereum-like `msg.sender` assumption for +Phoenix. Phoenix owners approve by Schnorr-signed `AuthorizedAction`. +Moonlight and contract owners may be accepted through observed runtime context +when the host exposes it. + +The Forge wrapper verifies signatures before consuming nonce state, but only +consumes after the controller accepts the proposal/confirmation/maintenance +operation. This avoids burning a valid signature on duplicate confirmations, +wrong threshold membership, invalid targets, expired proposals, or invalid +maintenance payloads. + +Execution is automatic at threshold. There is no separate `execute` step. This +is simpler and avoids a ready-operation queue, but systems that require a final +execution window, keeper role, or time delay should compose the multisig with a +timelock target. + +A target execution failure emits `multisig/operation_executed` with +`success = false`, removes the proposal, and tombstones the id. Retrying the +same logical call requires a new salt. This avoids accidental replay, but +clients must surface failed execution clearly. + +`propose` can count as a confirmation for an existing operation with the same +operation id. This is acceptable because the signed payload is the exact +operation id, but client UX should prefer the explicit `confirm` method after a +proposal already exists. + +## Residual Risks + +- Host-function correctness is assumed. The contract-owner path depends on + `abi::caller()` exposing the immediate caller contract during nested calls. +- Data-driver/ABI correctness is fuzzed for generated schemas, but independent + clients should be differentially tested against those schemas. +- The controller does not solve key-management policy. Owner selection, + hardware-key usage, emergency procedures, and threshold choice remain + deployment responsibilities. +- No formal verification has been performed for the controller state machine. +- No long-running adversarial local-node campaign has been run for this exact + multisig flow yet; the VM deployment test covers the deterministic contract + invariants. + +## Recommendation + +Use `MultisigController` as the default owner/admin principal for production +references that would otherwise use a single owner. For high-value upgrade or +treasury flows, set the target owner to a timelock controlled by the multisig, +or make the multisig operate only timelock-scheduled actions. diff --git a/docs/dusk-contract-standards.md b/docs/dusk-contract-standards.md index 8730a28f..c5232312 100644 --- a/docs/dusk-contract-standards.md +++ b/docs/dusk-contract-standards.md @@ -217,8 +217,8 @@ The native test suite covers positive and negative paths for: Moonlight/contract approval, Phoenix signed approval, and threshold-gated owner/threshold maintenance; - standalone multisig controller proposal, confirmation, duplicate-confirmation - rejection, expiry cleanup, cancellation, tombstoning, authority updates, and - proxy-as-owner VM execution; + rejection, non-owner rejection, expiry cleanup, cancellation, tombstoning, + authority updates, 2-of-3 property coverage, and proxy-as-owner VM execution; - timelock scheduling, cancellation, execution, and invalid states; - role-gated timelock controller flows, including self-governed delay updates; - reentrancy guard behavior; @@ -234,10 +234,10 @@ The native test suite covers positive and negative paths for: The hardening branch also adds property-based state-machine tests for token, authorization, ownership, role, timelock, proxy, nonce/replay, checkpoint, -royalty, and cap primitives. They compare arbitrary operation sequences -against independent models and assert that rejected operations leave native -state unchanged. See `docs/dusk-contract-standards-hardening.md` for the -current hardening track. +royalty, multisig-controller, and cap primitives. They compare arbitrary +operation sequences against independent models and assert that rejected +operations leave native state unchanged. See +`docs/dusk-contract-standards-hardening.md` for the current hardening track. The ignored VM test deploys all five Wasm examples, checks positive query paths, performs real Moonlight and Phoenix signed calls against the @@ -246,9 +246,10 @@ failures, submits signed DRC20 mint and signed DRC20 approvals, verifies paused DRC20/DRC721 signed mint rejection without nonce movement, submits signed DRC721 owner actions and signed DRC721 approvals, submits a signed proxy admin call, executes a proxy admin call through the standalone multisig controller, -covers replay/wrong-payload failures for those reference flows, and calls -privileged functions without a runtime caller to validate the negative -authorization path at the VM boundary. +covers replay/wrong-payload failures for those reference flows, rejects direct +target calls from individual multisig owners, and calls privileged functions +without a runtime caller to validate the negative authorization path at the VM +boundary. Run the focused suite with: diff --git a/standards/dusk-contract-standards/tests/examples_vm.rs b/standards/dusk-contract-standards/tests/examples_vm.rs index 8a0fae35..224fe768 100644 --- a/standards/dusk-contract-standards/tests/examples_vm.rs +++ b/standards/dusk-contract-standards/tests/examples_vm.rs @@ -344,13 +344,18 @@ fn wasm_examples_deploy_and_answer_queries() { let multisig_owner_a = phoenix_principal(8); let multisig_owner_b = phoenix_principal(9); + let multisig_owner_c = phoenix_principal(10); let multisig = deploy( &mut session, "multisig_controller.wasm", [23u8; 32], &MultisigInit { config: MultisigControllerConfig { - owners: vec![multisig_owner_a, multisig_owner_b], + owners: vec![ + multisig_owner_a, + multisig_owner_b, + multisig_owner_c, + ], threshold: 2, proposal_ttl: 10, tombstone_ttl: 6, @@ -374,6 +379,7 @@ fn wasm_examples_deploy_and_answer_queries() { governed_proxy, multisig_owner_a, multisig_owner_b, + multisig_owner_c, ); } @@ -1864,14 +1870,63 @@ fn exercise_multisig_controller_admin_call( proxy: ContractId, owner_a: Principal, owner_b: Principal, + owner_c: Principal, ) { let mut rng = StdRng::seed_from_u64(5555); let owner_a_sk = SchnorrSecretKey::from(JubJubScalar::from(8u64)); let owner_a_pk = SchnorrPublicKey::from(&owner_a_sk); let owner_b_sk = SchnorrSecretKey::from(JubJubScalar::from(9u64)); let owner_b_pk = SchnorrPublicKey::from(&owner_b_sk); + let owner_c_sk = SchnorrSecretKey::from(JubJubScalar::from(10u64)); + let owner_c_pk = SchnorrPublicKey::from(&owner_c_sk); + let outsider = phoenix_principal(11); + let outsider_sk = SchnorrSecretKey::from(JubJubScalar::from(11u64)); + let outsider_pk = SchnorrPublicKey::from(&outsider_sk); assert_eq!(owner_a, Principal::phoenix_public_key(&owner_a_pk)); assert_eq!(owner_b, Principal::phoenix_public_key(&owner_b_pk)); + assert_eq!(owner_c, Principal::phoenix_public_key(&owner_c_pk)); + assert_eq!(outsider, Principal::phoenix_public_key(&outsider_pk)); + + assert!(session + .call::<_, ()>( + proxy, + "set_value", + &ProxySetValue { + value: 55, + authorization: None, + }, + GAS_LIMIT, + ) + .is_err()); + assert_proxy_value(session, proxy, 0); + + let direct_owner_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_a_sk, + owner_a_pk, + authorized_action( + proxy, + owner_a, + PROXY_ADMIN_DOMAIN, + SET_PROXY_VALUE_ACTION, + 0, + 0, + proxy_value_payload_hash(66), + ), + )); + assert!(session + .call::<_, ()>( + proxy, + "set_value", + &ProxySetValue { + value: 66, + authorization: Some(direct_owner_auth), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce(session, proxy, owner_a, PROXY_ADMIN_DOMAIN, 0); + assert_proxy_value(session, proxy, 0); let target = proxy_set_value_target(proxy, 77, [1u8; 32]); let id: MultisigOperationId = session @@ -1879,6 +1934,44 @@ fn exercise_multisig_controller_admin_call( .expect("compute multisig operation id") .data; + let outsider_propose = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &outsider_sk, + outsider_pk, + authorized_action( + multisig, + outsider, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_PROPOSE_ACTION, + 0, + 0, + id, + ), + )); + assert!(session + .call::<_, Vec>( + multisig, + "propose", + &MultisigPropose { + target: target.clone(), + authorization: Some(outsider_propose), + }, + GAS_LIMIT, + ) + .is_err()); + assert_contract_nonce( + session, + multisig, + outsider, + MULTISIG_CONTROLLER_DOMAIN, + 0, + ); + let pending: Option = session + .call(multisig, "proposal", &id, GAS_LIMIT) + .expect("query rejected multisig operation") + .data; + assert!(pending.is_none()); + let propose_auth = SignedAuthorization::Phoenix(phoenix_auth( &mut rng, &owner_a_sk, @@ -2063,6 +2156,95 @@ fn exercise_multisig_controller_admin_call( 1, ); assert_proxy_value(session, proxy, 77); + + let second_target = proxy_set_value_target(proxy, 88, [2u8; 32]); + let second_id: MultisigOperationId = session + .call(multisig, "operation_id", &second_target, GAS_LIMIT) + .expect("compute second multisig operation id") + .data; + let second_propose_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_a_sk, + owner_a_pk, + authorized_action( + multisig, + owner_a, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_PROPOSE_ACTION, + 1, + 0, + second_id, + ), + )); + let returned: Vec = session + .call( + multisig, + "propose", + &MultisigPropose { + target: second_target, + authorization: Some(second_propose_auth), + }, + GAS_LIMIT, + ) + .expect("propose second multisig operation") + .data; + assert!(returned.is_empty()); + assert_proxy_value(session, proxy, 77); + assert_contract_nonce( + session, + multisig, + owner_a, + MULTISIG_CONTROLLER_DOMAIN, + 2, + ); + + let second_confirm_auth = SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, + &owner_c_sk, + owner_c_pk, + authorized_action( + multisig, + owner_c, + MULTISIG_CONTROLLER_DOMAIN, + MULTISIG_CONFIRM_ACTION, + 0, + 0, + second_id, + ), + )); + let receipt = session + .call::<_, Vec>( + multisig, + "confirm", + &MultisigConfirm { + id: second_id, + authorization: Some(second_confirm_auth), + }, + GAS_LIMIT, + ) + .expect("confirm second multisig operation"); + let execution = receipt + .events + .iter() + .find(|event| event.topic == MULTISIG_OPERATION_EXECUTED_TOPIC) + .map(|event| { + rkyv::from_bytes::(&event.data) + .expect("decode second multisig execution event") + }) + .expect("second multisig execution event"); + assert!( + execution.success, + "second multisig target execution failed: {:?}", + execution.error + ); + assert_proxy_value(session, proxy, 88); + assert_contract_nonce( + session, + multisig, + owner_c, + MULTISIG_CONTROLLER_DOMAIN, + 1, + ); } fn exercise_authorization_counter_signed_calls( diff --git a/standards/dusk-contract-standards/tests/properties.rs b/standards/dusk-contract-standards/tests/properties.rs index ff1fe0b5..56e22ed2 100644 --- a/standards/dusk-contract-standards/tests/properties.rs +++ b/standards/dusk-contract-standards/tests/properties.rs @@ -12,7 +12,9 @@ use dusk_contract_standards::core::{ CallContext, NonceEntry, NonceManager, Principal, ReplayEntry, ReplayGuard, }; use dusk_contract_standards::governance::{ - Timelock, TimelockController, EXECUTOR_ROLE, PROPOSER_ROLE, + MultisigController, MultisigControllerConfig, MultisigControllerStatus, + MultisigOperationId, MultisigPendingOperation, MultisigTarget, Timelock, + TimelockController, EXECUTOR_ROLE, PROPOSER_ROLE, }; use dusk_contract_standards::proxy::UpgradeAdmin; use dusk_contract_standards::token::drc20::{ @@ -33,6 +35,7 @@ use dusk_core::signatures::bls::{ use dusk_core::signatures::schnorr::{ PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, }; +use dusk_core::transfer::data::ContractCall; use dusk_core::JubJubScalar; use proptest::prelude::*; use rand::rngs::StdRng; @@ -74,6 +77,25 @@ fn contract_id(index: u8) -> ContractId { ContractId::from_bytes([index; 32]) } +fn multisig_target( + contract_seed: u8, + arg_seed: u8, + salt_seed: u8, +) -> MultisigTarget { + MultisigTarget { + call: ContractCall::new(contract_id(contract_seed), "set_value") + .with_raw_args(vec![arg_seed, arg_seed.wrapping_add(1)]), + salt: [salt_seed; 32], + } +} + +fn multisig_snapshot( + controller: &MultisigController, + id: MultisigOperationId, +) -> (Option, Option) { + (controller.proposal(id), controller.tombstone_expiry(id)) +} + fn role(index: u8) -> [u8; 32] { let mut role = [0u8; 32]; role[0] = index; @@ -2928,6 +2950,146 @@ proptest! { } } + #[test] + fn multisig_controller_two_of_three_threshold_and_rejections_are_consistent( + proposer in 0usize..3, + second in 0usize..3, + ttl in 2u64..64, + tombstone_ttl in 1u64..64, + now in 0u64..64, + id_seed in any::(), + contract_seed in 1u8..=250, + arg_seed in any::(), + salt_seed in any::(), + ) { + let owners = [principal(1), principal(2), principal(3)]; + let outsider = principal(4); + let id = [id_seed; 32]; + let target = multisig_target(contract_seed, arg_seed, salt_seed); + + let mut controller = MultisigController::new(); + controller.init(MultisigControllerConfig { + owners: owners.to_vec(), + threshold: 2, + proposal_ttl: ttl, + tombstone_ttl, + }); + + let before = multisig_snapshot(&controller, id); + let outsider_propose = catch_unwind(AssertUnwindSafe(|| { + controller.propose(id, target.clone(), outsider, now); + })) + .is_ok(); + prop_assert!(!outsider_propose, "non-owner proposed an operation"); + prop_assert_eq!( + multisig_snapshot(&controller, id), + before, + "failed outsider proposal mutated controller state", + ); + + let proposed = controller.propose(id, target.clone(), owners[proposer], now); + prop_assert_eq!(proposed.status, MultisigControllerStatus::Proposed); + prop_assert_eq!(proposed.confirmations, 1); + prop_assert_eq!(proposed.threshold, 2); + prop_assert!(proposed.ready_operation.is_none()); + let pending = controller.proposal(id).expect("one-of-three stays pending"); + prop_assert_eq!(&pending.target, &target); + prop_assert_eq!(pending.confirmations, vec![owners[proposer]]); + prop_assert_eq!(pending.deadline, now + ttl); + + let before = multisig_snapshot(&controller, id); + let outsider_confirm = catch_unwind(AssertUnwindSafe(|| { + controller.confirm(id, outsider, now); + })) + .is_ok(); + prop_assert!(!outsider_confirm, "non-owner confirmed an operation"); + prop_assert_eq!( + multisig_snapshot(&controller, id), + before, + "failed outsider confirmation mutated controller state", + ); + + let before = multisig_snapshot(&controller, id); + let duplicate_confirm = catch_unwind(AssertUnwindSafe(|| { + controller.confirm(id, owners[proposer], now); + })) + .is_ok(); + prop_assert!(!duplicate_confirm, "duplicate owner confirmation succeeded"); + prop_assert_eq!( + multisig_snapshot(&controller, id), + before, + "failed duplicate confirmation mutated controller state", + ); + + let mut expired = controller.clone(); + let expired_confirm = catch_unwind(AssertUnwindSafe(|| { + expired.confirm(id, owners[(proposer + 1) % owners.len()], now + ttl + 1); + })) + .is_ok(); + prop_assert!(!expired_confirm, "expired proposal was confirmed"); + prop_assert!( + expired.proposal(id).is_none(), + "expired proposal was not pruned", + ); + prop_assert!( + expired.tombstone_expiry(id).is_none(), + "expired proposal created a tombstone", + ); + + if second == proposer { + let before = multisig_snapshot(&controller, id); + let duplicate_second = catch_unwind(AssertUnwindSafe(|| { + controller.confirm(id, owners[second], now); + })) + .is_ok(); + prop_assert!(!duplicate_second, "duplicate generated signer reached quorum"); + prop_assert_eq!( + multisig_snapshot(&controller, id), + before, + "failed generated duplicate mutated controller state", + ); + return Ok(()); + } + + let ready = controller.confirm(id, owners[second], now); + prop_assert_eq!(ready.status, MultisigControllerStatus::Ready); + prop_assert_eq!(ready.confirmations, 2); + prop_assert_eq!(ready.threshold, 2); + let ready_operation = ready.ready_operation.expect("quorum operation"); + prop_assert_eq!(&ready_operation.target, &target); + prop_assert_eq!( + ready_operation.confirmations, + vec![owners[proposer], owners[second]], + ); + prop_assert!(controller.proposal(id).is_none()); + prop_assert_eq!(controller.tombstone_expiry(id), Some(now + tombstone_ttl)); + + let before = multisig_snapshot(&controller, id); + let tombstoned_reproposal = catch_unwind(AssertUnwindSafe(|| { + controller.propose(id, target.clone(), owners[(proposer + 2) % owners.len()], now + tombstone_ttl); + })) + .is_ok(); + prop_assert!( + !tombstoned_reproposal, + "operation id was re-used before tombstone expiry", + ); + prop_assert_eq!( + multisig_snapshot(&controller, id), + before, + "failed tombstoned re-proposal mutated controller state", + ); + + let reproposed = controller.propose( + id, + target, + owners[(proposer + 2) % owners.len()], + now + tombstone_ttl + 1, + ); + prop_assert_eq!(reproposed.status, MultisigControllerStatus::Proposed); + prop_assert_eq!(reproposed.confirmations, 1); + prop_assert!(controller.proposal(id).is_some()); + } + #[test] fn timelock_state_matches_model_and_failed_calls_are_atomic( ops in prop::collection::vec(timelock_op_strategy(), 0..140), From 9cd78def68a4e97dbebb475390c35dd525c79965 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Mon, 27 Apr 2026 00:56:08 +0200 Subject: [PATCH 28/35] standards: exercise multisig in local smoke --- docs/dusk-contract-standards-audit.md | 2 +- ...ct-standards-multisig-controller-review.md | 15 +- docs/dusk-contract-standards.md | 8 +- .../dusk-contract-standards-local-smoke.sh | 99 ++++++++++++ .../examples/encode_local_smoke_args.rs | 149 +++++++++++++++++- 5 files changed, 258 insertions(+), 15 deletions(-) diff --git a/docs/dusk-contract-standards-audit.md b/docs/dusk-contract-standards-audit.md index 3792198b..9020521f 100644 --- a/docs/dusk-contract-standards-audit.md +++ b/docs/dusk-contract-standards-audit.md @@ -60,7 +60,7 @@ The layer does not assume: | MULTISIG-2 | Observed Moonlight/contract owners can count toward quorum; Phoenix owners require signed action approvals. | primitives | | MULTISIG-3 | Multisig owner and threshold maintenance requires current quorum and rejected changes leave state unchanged. | primitives | | MULTISIG-4 | Standalone controller proposal/confirmation requires distinct owners, rejects non-owners and duplicate confirmations without nonce movement, expires stale proposals, tombstones executed ids, and clears stale pending operations after authority-removing changes. | primitives, properties, VM test | -| MULTISIG-5 | A contract-owned proxy/admin path can be controlled by a 2-of-3 standalone multisig controller through observed inter-contract caller context, while direct owner calls to the governed target fail. | VM test | +| MULTISIG-5 | A contract-owned proxy/admin path can be controlled by a 2-of-3 standalone multisig controller through observed inter-contract caller context, while direct owner calls to the governed target fail. | VM test, local-node smoke | | ACCESS-1 | Role grants/revokes are admin-gated, reject zero accounts, and emit typed events when state changes. | primitives, DRC20 reference, data-driver event decoding | | ACCESS-2 | Owner and owner-set initialization reject zero principals atomically. | primitives, properties | | PAUSE-1 | Reference pausable DRC20/DRC721 pause all balance-changing operations. | primitives, VM test, local-node smoke | diff --git a/docs/dusk-contract-standards-multisig-controller-review.md b/docs/dusk-contract-standards-multisig-controller-review.md index db71537f..3059a722 100644 --- a/docs/dusk-contract-standards-multisig-controller-review.md +++ b/docs/dusk-contract-standards-multisig-controller-review.md @@ -4,12 +4,12 @@ Review date: 2026-04-27 ## Scope -Reviewed code: +Reviewed code and validation: - `standards/dusk-contract-standards/src/governance/multisig_controller.rs` - `standards/examples/multisig_controller` - `standards/examples/proxy_counter` -- multisig primitive, property, VM, and data-driver tests under +- multisig primitive, property, VM, local-node smoke, and data-driver tests under `standards/dusk-contract-standards/tests` The review focused on the standalone controller pattern where a target contract @@ -40,9 +40,9 @@ Moonlight/Phoenix/contract owners approve controller operations. No critical or high-severity issues are open from this pass. -The VM test now deploys `multisig_controller` with three Phoenix owners and -threshold `2`, then deploys `proxy_counter` with -`Principal::Contract(multisig_controller_id)` as admin. It verifies that: +The VM test and local-node smoke deploy `multisig_controller` with three +Phoenix owners and threshold `2`, then deploy `proxy_counter` with +`Principal::Contract(multisig_controller_id)` as admin. They verify that: - direct root `proxy_counter.set_value` without authorization fails; - a direct signed proxy-admin call from a multisig owner fails because the @@ -102,9 +102,8 @@ proposal already exists. hardware-key usage, emergency procedures, and threshold choice remain deployment responsibilities. - No formal verification has been performed for the controller state machine. -- No long-running adversarial local-node campaign has been run for this exact - multisig flow yet; the VM deployment test covers the deterministic contract - invariants. +- The deterministic local-node smoke covers this exact multisig flow, but it is + not a long-running adversarial network campaign. ## Recommendation diff --git a/docs/dusk-contract-standards.md b/docs/dusk-contract-standards.md index c5232312..b8cbfd79 100644 --- a/docs/dusk-contract-standards.md +++ b/docs/dusk-contract-standards.md @@ -291,9 +291,11 @@ RUSK_WALLET_BIN=/path/to/rusk-wallet \ The script first runs the native suite, builds Wasm, runs the VM deployment test, serializes deploy-time init arguments using the Dusk ABI serializer, -deploys the five example contracts with `rusk-wallet`, and queries one -exported function from each deployment. `WALLET_DIR` should point at a funded -local wallet profile. +deploys the five example contracts plus a second proxy owned by the multisig +controller, and queries deployed state. It submits positive and negative +signed transactions, including the 2-of-3 multisig-owned proxy flow, and +asserts nonce/value state after rejected calls. `WALLET_DIR` should point at a +funded local wallet profile. For an isolated local smoke wallet, set `WALLET_RESTORE_FILE` to a funded wallet backup. The script restores it into `WALLET_DIR` when the directory does diff --git a/scripts/dusk-contract-standards-local-smoke.sh b/scripts/dusk-contract-standards-local-smoke.sh index 1250fabf..21d47782 100755 --- a/scripts/dusk-contract-standards-local-smoke.sh +++ b/scripts/dusk-contract-standards-local-smoke.sh @@ -452,6 +452,97 @@ run_signed_invariants() { echo "Local signed invariant checks completed" } +run_multisig_invariants() { + local multisig_id="$1" + local proxy_id="$2" + local expires_at="${SIGNED_EXPIRES_AT:-100000}" + local unit_args + local target_args + local op_id + local second_target_args + local second_op_id + + unit_args="$(encode_args unit)" + + assert_eq "multisig-governed proxy initial value" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 0 + + expect_call_rejected "multisig-governed proxy direct set without auth" \ + "$proxy_id" set_value "$(encode_args proxy-set-no-auth 55)" + assert_eq "multisig-governed proxy value after direct no-auth set" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 0 + + expect_call_rejected "multisig-governed proxy direct owner signature" \ + "$proxy_id" set_value "$(encode_args proxy-set "$proxy_id" 0 "$expires_at" 66)" + assert_eq "multisig-governed proxy owner nonce after direct signature" \ + "$(query_u64 "$proxy_id" nonce "$(encode_args nonce proxy-admin phoenix 1)")" 0 + assert_eq "multisig-governed proxy value after direct owner signature" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 0 + + target_args="$(encode_args multisig-target "$proxy_id" 77 1)" + op_id="$(query_hex "$multisig_id" operation_id "$target_args")" + + expect_call_rejected "multisig non-owner propose" \ + "$multisig_id" propose \ + "$(encode_args multisig-propose "$multisig_id" "$proxy_id" 4 0 "$expires_at" 77 1 "$op_id")" + assert_eq "multisig non-owner nonce after rejected propose" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 4)")" 0 + + expect_call_ok "multisig owner one propose" \ + "$multisig_id" propose \ + "$(encode_args multisig-propose "$multisig_id" "$proxy_id" 1 0 "$expires_at" 77 1 "$op_id")" + assert_eq "multisig owner one nonce after propose" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 1)")" 1 + assert_eq "multisig-governed proxy value after one of three" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 0 + + expect_call_rejected "multisig duplicate confirmation" \ + "$multisig_id" confirm \ + "$(encode_args multisig-confirm "$multisig_id" 1 1 "$expires_at" "$op_id")" + assert_eq "multisig owner one nonce after duplicate confirmation" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 1)")" 1 + + expect_call_rejected "multisig wrong payload confirmation" \ + "$multisig_id" confirm \ + "$(encode_args multisig-confirm "$multisig_id" 2 0 "$expires_at" "$op_id" bad-payload)" + assert_eq "multisig owner two nonce after wrong payload" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 2)")" 0 + + expect_call_ok "multisig owner two confirmation executes proxy" \ + "$multisig_id" confirm \ + "$(encode_args multisig-confirm "$multisig_id" 2 0 "$expires_at" "$op_id")" + assert_eq "multisig-governed proxy value after two of three" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 77 + assert_eq "multisig owner two nonce after confirmation" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 2)")" 1 + + expect_call_rejected "multisig confirmation replay" \ + "$multisig_id" confirm \ + "$(encode_args multisig-confirm "$multisig_id" 2 0 "$expires_at" "$op_id")" + assert_eq "multisig owner two nonce after replay" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 2)")" 1 + + second_target_args="$(encode_args multisig-target "$proxy_id" 88 2)" + second_op_id="$(query_hex "$multisig_id" operation_id "$second_target_args")" + expect_call_ok "multisig second operation owner one propose" \ + "$multisig_id" propose \ + "$(encode_args multisig-propose "$multisig_id" "$proxy_id" 1 1 "$expires_at" 88 2 "$second_op_id")" + assert_eq "multisig-governed proxy value after second one of three" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 77 + assert_eq "multisig owner one nonce after second propose" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 1)")" 2 + + expect_call_ok "multisig owner three confirmation executes proxy" \ + "$multisig_id" confirm \ + "$(encode_args multisig-confirm "$multisig_id" 3 0 "$expires_at" "$second_op_id")" + assert_eq "multisig-governed proxy value after owner one plus three" \ + "$(query_u64 "$proxy_id" value "$unit_args")" 88 + assert_eq "multisig owner three nonce after confirmation" \ + "$(query_u64 "$multisig_id" nonce "$(encode_args nonce multisig phoenix 3)")" 1 + + echo "Local multisig invariant checks completed" +} + unit_args="$(encode_args unit)" auth_id="$(deploy_contract \ @@ -489,6 +580,14 @@ proxy_id="$(deploy_contract \ "$(encode_args proxy-init)")" query_contract "proxy counter" "$proxy_id" "value" "$unit_args" +multisig_proxy_id="$(deploy_contract \ + "multisig-owned proxy counter" \ + "${ROOT_DIR}/target/wasm32-unknown-unknown/release/proxy_counter.wasm" \ + "$((DEPLOY_NONCE_BASE + 5))" \ + "$(encode_args proxy-init-contract "$multisig_id")")" +query_contract "multisig-owned proxy counter" "$multisig_proxy_id" "value" "$unit_args" + run_signed_invariants "$auth_id" "$drc20_id" "$drc721_id" "$proxy_id" +run_multisig_invariants "$multisig_id" "$multisig_proxy_id" echo "Local standards smoke completed" diff --git a/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs index aedcd7f3..0e67507b 100644 --- a/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs +++ b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs @@ -3,12 +3,15 @@ use std::error::Error; use std::string::String; use std::vec::Vec; +use bytecheck::CheckBytes; use dusk_contract_standards::auth::{ AuthorizedAction, MoonlightAuthorization, PhoenixSignatureAuthorization, SignedAuthorization, }; use dusk_contract_standards::core::Principal; -use dusk_contract_standards::governance::MultisigControllerConfig; +use dusk_contract_standards::governance::{ + MultisigControllerConfig, MultisigOperationId, MultisigTarget, +}; use dusk_contract_standards::token::drc20::{ Init as Drc20TokenInit, InitBalance as Drc20InitBalance, }; @@ -24,6 +27,7 @@ use dusk_core::signatures::bls::{ use dusk_core::signatures::schnorr::{ PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, }; +use dusk_core::transfer::data::ContractCall; use dusk_core::JubJubScalar; use dusk_vm::host_queries; use rand::rngs::StdRng; @@ -50,6 +54,9 @@ const NFT_SIGNED_APPROVE_ACTION: [u8; 32] = [30u8; 32]; const NFT_SIGNED_APPROVAL_FOR_ALL_ACTION: [u8; 32] = [31u8; 32]; const PROXY_ADMIN_DOMAIN: [u8; 32] = [31u8; 32]; const SET_PROXY_VALUE_ACTION: [u8; 32] = [32u8; 32]; +const MULTISIG_CONTROLLER_DOMAIN: [u8; 32] = [41u8; 32]; +const MULTISIG_PROPOSE_ACTION: [u8; 32] = [42u8; 32]; +const MULTISIG_CONFIRM_ACTION: [u8; 32] = [43u8; 32]; #[derive(Archive, Serialize, Deserialize)] struct Drc20ExampleInit { @@ -122,11 +129,24 @@ struct Drc721AdminCall { } #[derive(Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] struct ProxySetValue { value: u64, authorization: Option, } +#[derive(Archive, Serialize, Deserialize)] +struct MultisigPropose { + target: MultisigTarget, + authorization: Option, +} + +#[derive(Archive, Serialize, Deserialize)] +struct MultisigConfirm { + id: MultisigOperationId, + authorization: Option, +} + struct AdminPhoenixAction { contract: ContractId, admin: Principal, @@ -141,7 +161,7 @@ fn main() -> Result<(), Box> { let args = env::args().collect::>(); let Some(command) = args.get(1).map(String::as_str) else { eprintln!( - "usage: encode_local_smoke_args " + "usage: encode_local_smoke_args " ); std::process::exit(2); }; @@ -182,12 +202,18 @@ fn main() -> Result<(), Box> { })?, "multisig-init" => encode(&MultisigInit { config: MultisigControllerConfig { - owners: vec![admin, phoenix_principal(2)], + owners: vec![admin, phoenix_principal(2), phoenix_principal(3)], threshold: 2, proposal_ttl: 10, tombstone_ttl: 6, }, })?, + "proxy-init-contract" => encode(&ProxyCounterInit { + admin: Principal::contract(contract_arg(&args, 2)?), + implementation: ContractId::from_bytes([9u8; 32]), + upgrade_delay: 0, + rollback_window: 10, + })?, "unit" => encode(&())?, "u64" => encode(&parse_u64_arg(&args, 2, "value")?)?, "nonce" => encode(&NonceQuery { @@ -209,6 +235,13 @@ fn main() -> Result<(), Box> { operator: drc721_operator_principal(), })?, "proxy-set" => encode(&proxy_set_call(&args, admin)?)?, + "proxy-set-no-auth" => encode(&ProxySetValue { + value: parse_u64_arg(&args, 2, "value")?, + authorization: None, + })?, + "multisig-target" => encode(&multisig_target_arg(&args, 2, 3, 4)?)?, + "multisig-propose" => encode(&multisig_propose_call(&args)?)?, + "multisig-confirm" => encode(&multisig_confirm_call(&args)?)?, _ => { eprintln!("unknown command: {command}"); std::process::exit(2); @@ -498,6 +531,94 @@ fn proxy_set_call( }) } +fn multisig_propose_call( + args: &[String], +) -> Result> { + let multisig = contract_arg(args, 2)?; + let owner_seed = parse_u64_arg(args, 4, "owner_seed")?; + let nonce = parse_u64_arg(args, 5, "nonce")?; + let expires_at = parse_u64_arg(args, 6, "expires_at")?; + let id = parse_hex_32(arg(args, 9, "operation_id")?)?; + let variant = args.get(10).map(String::as_str).unwrap_or("valid"); + let owner = phoenix_principal(owner_seed); + let secret = SchnorrSecretKey::from(JubJubScalar::from(owner_seed)); + let public = SchnorrPublicKey::from(&secret); + let action = authorized_action( + multisig, + owner, + MULTISIG_CONTROLLER_DOMAIN, + action_for_variant(MULTISIG_PROPOSE_ACTION, variant), + nonce, + expires_at, + operation_id_for_variant(id, variant), + ); + let mut rng = StdRng::seed_from_u64(44_444 + owner_seed + nonce); + Ok(MultisigPropose { + target: multisig_target_arg(args, 3, 7, 8)?, + authorization: Some(SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, &secret, public, action, + ))), + }) +} + +fn multisig_confirm_call( + args: &[String], +) -> Result> { + let multisig = contract_arg(args, 2)?; + let owner_seed = parse_u64_arg(args, 3, "owner_seed")?; + let nonce = parse_u64_arg(args, 4, "nonce")?; + let expires_at = parse_u64_arg(args, 5, "expires_at")?; + let id = parse_hex_32(arg(args, 6, "operation_id")?)?; + let variant = args.get(7).map(String::as_str).unwrap_or("valid"); + let owner = phoenix_principal(owner_seed); + let secret = SchnorrSecretKey::from(JubJubScalar::from(owner_seed)); + let public = SchnorrPublicKey::from(&secret); + let action = authorized_action( + multisig, + owner, + MULTISIG_CONTROLLER_DOMAIN, + action_for_variant(MULTISIG_CONFIRM_ACTION, variant), + nonce, + expires_at, + operation_id_for_variant(id, variant), + ); + let mut rng = StdRng::seed_from_u64(55_555 + owner_seed + nonce); + Ok(MultisigConfirm { + id, + authorization: Some(SignedAuthorization::Phoenix(phoenix_auth( + &mut rng, &secret, public, action, + ))), + }) +} + +fn multisig_target_arg( + args: &[String], + proxy_index: usize, + value_index: usize, + salt_index: usize, +) -> Result> { + let proxy = contract_arg(args, proxy_index)?; + let value = parse_u64_arg(args, value_index, "value")?; + let salt_seed = parse_u8_arg(args, salt_index, "salt_seed")?; + proxy_set_value_target(proxy, value, [salt_seed; 32]) +} + +fn proxy_set_value_target( + proxy: ContractId, + value: u64, + salt: [u8; 32], +) -> Result> { + Ok(MultisigTarget { + call: ContractCall::new(proxy, "set_value") + .with_args(&ProxySetValue { + value, + authorization: None, + }) + .map_err(|e| format!("serialize proxy set_value target: {e:?}"))?, + salt, + }) +} + fn admin_phoenix_authorization( args: AdminPhoenixAction, variant: &str, @@ -691,6 +812,17 @@ fn payload_bool_for_variant(value: bool, variant: &str) -> bool { } } +fn operation_id_for_variant( + id: MultisigOperationId, + variant: &str, +) -> MultisigOperationId { + if variant == "bad-payload" { + [0xee; 32] + } else { + id + } +} + fn variant_arg(args: &[String]) -> &str { args.get(6).map(String::as_str).unwrap_or("valid") } @@ -715,6 +847,16 @@ fn parse_u64_arg( .map_err(|e| format!("invalid {name}: {e}").into()) } +fn parse_u8_arg( + args: &[String], + index: usize, + name: &str, +) -> Result> { + arg(args, index, name)? + .parse::() + .map_err(|e| format!("invalid {name}: {e}").into()) +} + fn contract_arg( args: &[String], index: usize, @@ -759,6 +901,7 @@ fn domain_arg( "drc721-admin" => Ok(NFT_ADMIN_DOMAIN), "drc721-approval" => Ok(NFT_SIGNED_APPROVE_DOMAIN), "proxy-admin" => Ok(PROXY_ADMIN_DOMAIN), + "multisig" => Ok(MULTISIG_CONTROLLER_DOMAIN), other => Err(format!("unknown domain: {other}").into()), } } From 2184b2797be8027664980c80f6eb50ea0ef92336 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Wed, 27 May 2026 12:24:41 +0200 Subject: [PATCH 29/35] standards: update Forge and Rusk dependencies --- Cargo.lock | 750 +++++++++++++----- Cargo.toml | 14 +- standards/dusk-contract-standards/Cargo.toml | 1 + .../src/access/events.rs | 16 + .../src/governance/multisig_controller.rs | 29 + .../src/proxy/upgrade.rs | 20 + .../src/token/drc20/events.rs | 8 + .../src/token/drc721/events.rs | 28 + .../examples/authorization_counter/src/lib.rs | 6 - .../examples/drc20_roles_pausable/src/lib.rs | 35 +- .../examples/drc721_collection/src/lib.rs | 42 +- .../examples/multisig_controller/src/lib.rs | 11 +- standards/examples/proxy_counter/src/lib.rs | 29 +- 13 files changed, 683 insertions(+), 306 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf5c05dd..bf6033c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,7 +26,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -55,16 +55,16 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", @@ -103,15 +103,24 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object 0.37.3", +] [[package]] name = "arbitrary" @@ -148,7 +157,7 @@ dependencies = [ "blake2", "derivative", "digest 0.10.7", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -292,7 +301,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -323,9 +332,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -365,9 +374,9 @@ checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin_hashes" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +checksum = "4ed83caece3afc59919481b33b472e1432d1abc4641ed9100be142ef5110b406" dependencies = [ "bitcoin-io", "hex-conservative", @@ -375,9 +384,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bitvec" @@ -408,20 +417,21 @@ checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" dependencies = [ "arrayref", "arrayvec", - "constant_time_eq", + "constant_time_eq 0.3.1", ] [[package]] name = "blake3" -version = "1.5.4" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", - "constant_time_eq", + "constant_time_eq 0.4.2", + "cpufeatures 0.3.0", ] [[package]] @@ -490,9 +500,9 @@ checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytecheck" @@ -522,6 +532,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "c-kzg" version = "2.0.0" @@ -546,18 +562,19 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.27" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "charlie" @@ -616,18 +633,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.18" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.18" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstyle", "clap_lex", @@ -635,9 +652,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.6.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "console" @@ -668,11 +685,26 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] @@ -833,9 +865,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -852,9 +884,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crumbles" @@ -874,9 +906,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -931,7 +963,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] @@ -976,13 +1008,13 @@ dependencies = [ [[package]] name = "dlmalloc" -version = "0.2.6" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3264b043b8e977326c1ee9e723da2c1f8d09a99df52cacf00b4dbce5ac54414d" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" dependencies = [ "cfg-if", "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1050,9 +1082,10 @@ dependencies = [ "bytecheck", "dusk-core", "dusk-data-driver", + "dusk-forge", "dusk-vm", "proptest", - "rand 0.8.5", + "rand 0.8.6", "rkyv", "serde", "serde_json", @@ -1063,6 +1096,8 @@ dependencies = [ [[package]] name = "dusk-core" version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7053f16922b6cce10036bda331f5dd373a64ff66972604c2fc3bca594d2158" dependencies = [ "ark-bn254", "ark-groth16", @@ -1083,16 +1118,18 @@ dependencies = [ "phoenix-core", "piecrust-uplink", "poseidon-merkle", - "rand 0.8.5", + "rand 0.8.6", "rkyv", "serde", "serde_with", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] name = "dusk-data-driver" version = "0.3.2-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "191e9b51e34ecb1a22d79de6df7e6e2971121e0069d60d738df9059837f46a1c" dependencies = [ "bytecheck", "dusk-core", @@ -1105,7 +1142,9 @@ dependencies = [ [[package]] name = "dusk-forge" -version = "0.2.2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31edc24ce1a8cfb6ee61d3941145c4b475117c2c0450a6342dc30907c61a5e93" dependencies = [ "dusk-forge-contract", "serde", @@ -1114,11 +1153,13 @@ dependencies = [ [[package]] name = "dusk-forge-contract" -version = "0.1.1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9f361a4eddded990e22c61dece5282257b141bf4d4d8cd2f4fda5a17618791" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] @@ -1173,7 +1214,7 @@ dependencies = [ "msgpacker", "rand_core 0.6.4", "rkyv", - "sha2 0.10.8", + "sha2 0.10.9", "zeroize", ] @@ -1201,10 +1242,11 @@ dependencies = [ [[package]] name = "dusk-vm" version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4e78bea4ffcca10eb95f687d7169e94c06e0246dd4023d9f76b213877b2e123" dependencies = [ "blake2b_simd", "blake3", - "blst", "bytecheck", "c-kzg", "dusk-bytes", @@ -1215,7 +1257,7 @@ dependencies = [ "rkyv", "secp256k1", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "sha3", "tracing", "wasmparser 0.240.0", @@ -1224,6 +1266,8 @@ dependencies = [ [[package]] name = "dusk-wallet-core" version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9539de83eee7581c707e57301e6e9d7488e318dfda2a89c9bd1800e48d486686" dependencies = [ "blake3", "bytecheck", @@ -1232,10 +1276,10 @@ dependencies = [ "dusk-core", "ff", "hkdf", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "rkyv", - "sha2 0.10.8", + "sha2 0.10.9", "zeroize", ] @@ -1256,7 +1300,7 @@ dependencies = [ "indexmap", "libc", "log", - "object", + "object 0.33.0", "once_cell", "paste", "rayon", @@ -1268,7 +1312,7 @@ dependencies = [ "wasmparser 0.202.0", "wasmtime-jit-icache-coherence", "wasmtime-slab", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1288,7 +1332,7 @@ dependencies = [ "dusk-wasmtime-environ", "gimli", "log", - "object", + "object 0.33.0", "target-lexicon", "thiserror", "wasmparser 0.202.0", @@ -1307,7 +1351,7 @@ dependencies = [ "gimli", "indexmap", "log", - "object", + "object 0.33.0", "paste", "serde", "serde_derive", @@ -1340,14 +1384,14 @@ dependencies = [ "wasmtime-asm-macros", "wasmtime-slab", "wasmtime-versioned-export-macros", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "either" -version = "1.13.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "encode_unicode" @@ -1357,9 +1401,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -1368,7 +1412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1379,20 +1423,26 @@ checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "ff" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ "rand_core 0.6.4", "subtle", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fnv" version = "1.0.7" @@ -1417,6 +1467,30 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1429,9 +1503,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -1446,8 +1520,21 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", "wasip2", + "wasip3", ] [[package]] @@ -1490,12 +1577,13 @@ dependencies = [ [[package]] name = "half" -version = "2.4.1" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -1522,7 +1610,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", ] [[package]] @@ -1531,7 +1619,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", ] [[package]] @@ -1556,10 +1644,10 @@ dependencies = [ ] [[package]] -name = "hermit-abi" -version = "0.4.0" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -1611,6 +1699,12 @@ dependencies = [ "dusk-core", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "indexmap" version = "2.11.4" @@ -1625,22 +1719,22 @@ dependencies = [ [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "generic-array", ] [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ - "hermit-abi 0.4.0", + "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1672,16 +1766,18 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1719,11 +1815,11 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -1732,19 +1828,24 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.177" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags", "libc", ] @@ -1756,9 +1857,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "lock_api" @@ -1771,9 +1872,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lru" @@ -1795,9 +1896,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memfd" @@ -1805,7 +1906,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -1858,12 +1959,12 @@ dependencies = [ [[package]] name = "msgpacker-derive" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e926eca5ac0d976018120667ab61ee8a1cff4afdcd202357971b610e7ff57b5a" +checksum = "c334ab77a6d4f29c5c2e41149c0f03af4082923aa364aa0c9876232f33f9853f" dependencies = [ "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] @@ -1920,7 +2021,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.5.2", + "hermit-abi", "libc", ] @@ -1936,11 +2037,20 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oorandom" @@ -2028,11 +2138,11 @@ dependencies = [ "hkdf", "jubjub-elgamal", "jubjub-schnorr", - "rand 0.8.5", + "rand 0.8.6", "rkyv", "serde", "serde_with", - "sha2 0.10.8", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -2053,7 +2163,7 @@ dependencies = [ "indexmap", "memmap2", "piecrust-uplink", - "rand 0.8.5", + "rand 0.8.6", "rkyv", "tempfile", "thiserror", @@ -2075,9 +2185,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "plotters" @@ -2114,7 +2224,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -2142,18 +2252,28 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2193,10 +2313,11 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.23" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" dependencies = [ + "ar_archive_writer", "cc", ] @@ -2228,9 +2349,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.38" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2241,6 +2362,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -2249,9 +2376,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2294,7 +2421,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", ] [[package]] @@ -2323,9 +2450,9 @@ checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" [[package]] name = "rayon" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -2333,9 +2460,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -2356,7 +2483,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "libredox", "thiserror", ] @@ -2376,9 +2503,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2388,9 +2515,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2399,15 +2526,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rend" -version = "0.3.6" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79af64b4b6362ffba04eef3a4e10829718a4896dac19daa741851c86781edf95" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" dependencies = [ "bytecheck", ] @@ -2420,23 +2547,27 @@ checksum = "3df6368f71f205ff9c33c076d170dd56ebf68e8161c733c0caa07a7a5509ed53" [[package]] name = "rkyv" -version = "0.7.39" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cec2b3485b07d96ddfd3134767b8a447b45ea4eb91448d0a35180ec0ffd5ed15" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" dependencies = [ + "bitvec", "bytecheck", + "bytes", "hashbrown 0.12.3", "ptr_meta", "rend", "rkyv_derive", "seahash", + "tinyvec", + "uuid", ] [[package]] name = "rkyv_derive" -version = "0.7.39" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eaedadc88b53e36dd32d940ed21ae4d850d5916f2581526921f553a72ac34c4" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" dependencies = [ "proc-macro2", "quote", @@ -2446,13 +2577,15 @@ dependencies = [ [[package]] name = "rusk-profile" version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18347352b0f276f99c81995904c36302859d1def0a8355e61a489c4fd3f9554b" dependencies = [ "blake3", "console", "dirs", "hex", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "toml", "tracing", "version_check", @@ -2461,12 +2594,14 @@ dependencies = [ [[package]] name = "rusk-prover" version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b6eb985682c51809742bc441bfecb7b821c16f50a2e5cb125c7e31e6d664a9" dependencies = [ "dusk-bytes", "dusk-core", "dusk-plonk", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "rusk-profile", ] @@ -2495,20 +2630,20 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.11.0", - "windows-sys", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] @@ -2572,9 +2707,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -2603,14 +2738,14 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -2621,9 +2756,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -2651,27 +2786,27 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] [[package]] name = "sha3" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest 0.10.7", "keccak", @@ -2689,6 +2824,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "slice-group-by" version = "0.3.1" @@ -2723,7 +2864,7 @@ dependencies = [ "dusk-vm", "dusk-wallet-core", "ff", - "rand 0.8.5", + "rand 0.8.6", "rkyv", "rusk-prover", ] @@ -2747,9 +2888,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2770,15 +2911,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.3", - "windows-sys", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] @@ -2817,7 +2958,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] @@ -2858,6 +2999,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "toml" version = "0.7.8" @@ -2872,9 +3028,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] @@ -2894,9 +3050,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2905,20 +3061,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -2931,7 +3087,7 @@ dependencies = [ "dusk-core", "dusk-vm", "ff", - "rand 0.8.5", + "rand 0.8.6", "ringbuffer", "rkyv", "rusk-profile", @@ -2940,9 +3096,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unarray" @@ -2952,9 +3108,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" @@ -2962,6 +3118,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" version = "0.5.1" @@ -2972,6 +3134,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" @@ -3005,18 +3177,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -3027,9 +3208,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3037,26 +3218,48 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser 0.244.0", +] + [[package]] name = "wasmparser" version = "0.202.0" @@ -3081,6 +3284,18 @@ dependencies = [ "serde", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "wasmtime-asm-macros" version = "20.0.0" @@ -3098,7 +3313,7 @@ checksum = "7a9f93a3289057b26dc75eb84d6e60d7694f7d169c7c09597495de6e016a13ff" dependencies = [ "cfg-if", "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -3128,14 +3343,14 @@ checksum = "8561d9e2920db2a175213d557d71c2ac7695831ab472bbfafb9060cd1034684f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -3163,7 +3378,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -3187,6 +3402,24 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -3265,6 +3498,94 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] [[package]] name = "wyz" @@ -3277,43 +3598,42 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 80534353..c1a95530 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,19 +23,19 @@ resolver = "2" [workspace.dependencies] # Dusk dependencies -dusk-core = { path = "../rusk-private/core" } +dusk-core = "1.6.0" dusk-bytes = "0.1.7" -dusk-data-driver = { path = "../rusk-private/data-drivers/data-driver" } -dusk-forge = { path = "../forge-explicit-emits" } -dusk-vm = { path = "../rusk-private/vm" } -dusk-wallet-core = { path = "../rusk-private/wallet-core" } +dusk-data-driver = "0.3.2-alpha.1" +dusk-forge = "0.3.0" +dusk-vm = "1.6.0" +dusk-wallet-core = "1.6.0" dusk-wasmtime = { version = "21.0.0-alpha", default-features = false, features = [ "cranelift", "runtime", "parallel-compilation", ] } -rusk-profile = { path = "../rusk-private/rusk-profile" } -rusk-prover = { path = "../rusk-private/rusk-prover" } +rusk-profile = "1.6.0" +rusk-prover = "1.6.0" # Other dependencies bytecheck = { version = "0.6.12", default-features = false } diff --git a/standards/dusk-contract-standards/Cargo.toml b/standards/dusk-contract-standards/Cargo.toml index 7394906d..fb00aac9 100644 --- a/standards/dusk-contract-standards/Cargo.toml +++ b/standards/dusk-contract-standards/Cargo.toml @@ -8,6 +8,7 @@ crate-type = ["rlib"] [dependencies] dusk-core = { workspace = true } +dusk-forge = { workspace = true } bytecheck = { workspace = true } rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } serde = { workspace = true, optional = true } diff --git a/standards/dusk-contract-standards/src/access/events.rs b/standards/dusk-contract-standards/src/access/events.rs index 00e089a5..faf8e154 100644 --- a/standards/dusk-contract-standards/src/access/events.rs +++ b/standards/dusk-contract-standards/src/access/events.rs @@ -67,3 +67,19 @@ pub struct RoleRevoked { /// Principal that authorized the revoke. pub sender: Principal, } + +impl dusk_forge::ContractEvent for Paused { + const TOPICS: &'static [&'static str] = &[PAUSED_TOPIC]; +} + +impl dusk_forge::ContractEvent for Unpaused { + const TOPICS: &'static [&'static str] = &[UNPAUSED_TOPIC]; +} + +impl dusk_forge::ContractEvent for RoleGranted { + const TOPICS: &'static [&'static str] = &[ROLE_GRANTED_TOPIC]; +} + +impl dusk_forge::ContractEvent for RoleRevoked { + const TOPICS: &'static [&'static str] = &[ROLE_REVOKED_TOPIC]; +} diff --git a/standards/dusk-contract-standards/src/governance/multisig_controller.rs b/standards/dusk-contract-standards/src/governance/multisig_controller.rs index 0b0b29fb..b98b58d6 100644 --- a/standards/dusk-contract-standards/src/governance/multisig_controller.rs +++ b/standards/dusk-contract-standards/src/governance/multisig_controller.rs @@ -216,6 +216,35 @@ pub struct MultisigTimeLimitsUpdated { pub tombstone_ttl: u64, } +impl dusk_forge::ContractEvent for MultisigOperationProposed { + const TOPICS: &'static [&'static str] = + &[MULTISIG_OPERATION_PROPOSED_TOPIC]; +} + +impl dusk_forge::ContractEvent for MultisigOperationConfirmed { + const TOPICS: &'static [&'static str] = + &[MULTISIG_OPERATION_CONFIRMED_TOPIC]; +} + +impl dusk_forge::ContractEvent for MultisigOperationExecuted { + const TOPICS: &'static [&'static str] = + &[MULTISIG_OPERATION_EXECUTED_TOPIC]; +} + +impl dusk_forge::ContractEvent for MultisigOperationCancelled { + const TOPICS: &'static [&'static str] = + &[MULTISIG_OPERATION_CANCELLED_TOPIC]; +} + +impl dusk_forge::ContractEvent for MultisigAuthorityUpdated { + const TOPICS: &'static [&'static str] = &[MULTISIG_AUTHORITY_UPDATED_TOPIC]; +} + +impl dusk_forge::ContractEvent for MultisigTimeLimitsUpdated { + const TOPICS: &'static [&'static str] = + &[MULTISIG_TIME_LIMITS_UPDATED_TOPIC]; +} + /// Standalone multisig controller primitive. /// /// This state machine tracks operation proposals and confirmations. It does diff --git a/standards/dusk-contract-standards/src/proxy/upgrade.rs b/standards/dusk-contract-standards/src/proxy/upgrade.rs index bc255967..5fab888e 100644 --- a/standards/dusk-contract-standards/src/proxy/upgrade.rs +++ b/standards/dusk-contract-standards/src/proxy/upgrade.rs @@ -98,6 +98,26 @@ pub struct RollbackFinalized { pub implementation: ContractId, } +impl dusk_forge::ContractEvent for UpgradePrepared { + const TOPICS: &'static [&'static str] = &[UPGRADE_PREPARED_TOPIC]; +} + +impl dusk_forge::ContractEvent for UpgradeActivated { + const TOPICS: &'static [&'static str] = &[UPGRADE_ACTIVATED_TOPIC]; +} + +impl dusk_forge::ContractEvent for UpgradeCancelled { + const TOPICS: &'static [&'static str] = &[UPGRADE_CANCELLED_TOPIC]; +} + +impl dusk_forge::ContractEvent for UpgradeRolledBack { + const TOPICS: &'static [&'static str] = &[UPGRADE_ROLLED_BACK_TOPIC]; +} + +impl dusk_forge::ContractEvent for RollbackFinalized { + const TOPICS: &'static [&'static str] = &[ROLLBACK_FINALIZED_TOPIC]; +} + /// Dusk-native upgrade controller. #[derive(Clone, Debug)] pub struct UpgradeAdmin { diff --git a/standards/dusk-contract-standards/src/token/drc20/events.rs b/standards/dusk-contract-standards/src/token/drc20/events.rs index 61390fcd..2c83454e 100644 --- a/standards/dusk-contract-standards/src/token/drc20/events.rs +++ b/standards/dusk-contract-standards/src/token/drc20/events.rs @@ -39,3 +39,11 @@ pub struct Approval { /// Amount. pub amount: u64, } + +impl dusk_forge::ContractEvent for Transfer { + const TOPICS: &'static [&'static str] = &[TRANSFER_TOPIC]; +} + +impl dusk_forge::ContractEvent for Approval { + const TOPICS: &'static [&'static str] = &[APPROVAL_TOPIC]; +} diff --git a/standards/dusk-contract-standards/src/token/drc721/events.rs b/standards/dusk-contract-standards/src/token/drc721/events.rs index 9cbf6eaa..3f952ce5 100644 --- a/standards/dusk-contract-standards/src/token/drc721/events.rs +++ b/standards/dusk-contract-standards/src/token/drc721/events.rs @@ -121,3 +121,31 @@ pub struct TokenRoyaltyCleared { /// Token id. pub token_id: u64, } + +impl dusk_forge::ContractEvent for Transfer { + const TOPICS: &'static [&'static str] = &[TRANSFER_TOPIC]; +} + +impl dusk_forge::ContractEvent for Approval { + const TOPICS: &'static [&'static str] = &[APPROVAL_TOPIC]; +} + +impl dusk_forge::ContractEvent for ApprovalForAll { + const TOPICS: &'static [&'static str] = &[APPROVAL_FOR_ALL_TOPIC]; +} + +impl dusk_forge::ContractEvent for DefaultRoyaltySet { + const TOPICS: &'static [&'static str] = &[DEFAULT_ROYALTY_SET_TOPIC]; +} + +impl dusk_forge::ContractEvent for DefaultRoyaltyCleared { + const TOPICS: &'static [&'static str] = &[DEFAULT_ROYALTY_CLEARED_TOPIC]; +} + +impl dusk_forge::ContractEvent for TokenRoyaltySet { + const TOPICS: &'static [&'static str] = &[TOKEN_ROYALTY_SET_TOPIC]; +} + +impl dusk_forge::ContractEvent for TokenRoyaltyCleared { + const TOPICS: &'static [&'static str] = &[TOKEN_ROYALTY_CLEARED_TOPIC]; +} diff --git a/standards/examples/authorization_counter/src/lib.rs b/standards/examples/authorization_counter/src/lib.rs index 1e851514..9ac1e3dc 100644 --- a/standards/examples/authorization_counter/src/lib.rs +++ b/standards/examples/authorization_counter/src/lib.rs @@ -69,8 +69,6 @@ mod authorization_counter { initialized: false, } } - - #[contract(no_event)] pub fn init(&mut self) { if self.initialized { panic!("AuthorizationCounter: already initialized"); @@ -89,8 +87,6 @@ mod authorization_counter { pub fn nonce(&self, query: NonceQuery) -> u64 { self.authorizations.nonce(query.principal, query.domain) } - - #[contract(no_event)] pub fn set_value_by_moonlight(&mut self, args: SetValueByMoonlight) { self.assert_action( args.authorization.action.contract, @@ -104,8 +100,6 @@ mod authorization_counter { .authorize_moonlight(&args.authorization, now()); self.set_value(principal, args.amount); } - - #[contract(no_event)] pub fn set_value_by_phoenix(&mut self, args: SetValueByPhoenix) { self.assert_action( args.authorization.action.contract, diff --git a/standards/examples/drc20_roles_pausable/src/lib.rs b/standards/examples/drc20_roles_pausable/src/lib.rs index b41bcd7a..2a58341d 100644 --- a/standards/examples/drc20_roles_pausable/src/lib.rs +++ b/standards/examples/drc20_roles_pausable/src/lib.rs @@ -77,7 +77,14 @@ pub struct VotesQuery { pub timepoint: u64, } -#[dusk_forge::contract] +#[dusk_forge::contract(events = [ + Drc20Transfer, + Drc20Approval, + Paused, + Unpaused, + RoleGranted, + RoleRevoked, +])] mod drc20_roles_pausable { use alloc::string::String; use alloc::vec::Vec; @@ -136,8 +143,6 @@ mod drc20_roles_pausable { votes: VotingUnits::new(), } } - - #[contract(emits = [(TRANSFER_TOPIC, Drc20Transfer)])] pub fn init(&mut self, args: Init) { if args.admin.is_zero() { panic!("{}", error::ZERO_PRINCIPAL); @@ -234,8 +239,6 @@ mod drc20_roles_pausable { pub fn has_role(&self, args: RoleQuery) -> bool { self.access.has_role(args.role, args.account) } - - #[contract(emits = [(TRANSFER_TOPIC, Drc20Transfer)])] pub fn transfer(&mut self, args: TransferCall) { self.pausable.assert_not_paused(); let caller = caller(); @@ -256,15 +259,11 @@ mod drc20_roles_pausable { }; Self::emit_transfer(event); } - - #[contract(emits = [(APPROVAL_TOPIC, Drc20Approval)])] pub fn approve(&mut self, args: ApproveCall) { let caller = caller(); let event = self.token.approve(caller, args); Self::emit_approval(event); } - - #[contract(emits = [(APPROVAL_TOPIC, Drc20Approval)])] pub fn approve_by_authorization(&mut self, args: SignedApproveCall) { let principal = self.authorizations.authorize_signed_action( &args.authorization, @@ -288,22 +287,16 @@ mod drc20_roles_pausable { ); Self::emit_approval(event); } - - #[contract(emits = [(APPROVAL_TOPIC, Drc20Approval)])] pub fn increase_allowance(&mut self, args: IncreaseAllowanceCall) { let caller = caller(); let event = self.token.increase_allowance(caller, args); Self::emit_approval(event); } - - #[contract(emits = [(APPROVAL_TOPIC, Drc20Approval)])] pub fn decrease_allowance(&mut self, args: DecreaseAllowanceCall) { let caller = caller(); let event = self.token.decrease_allowance(caller, args); Self::emit_approval(event); } - - #[contract(emits = [(TRANSFER_TOPIC, Drc20Transfer)])] pub fn transfer_from(&mut self, args: TransferFromCall) { self.pausable.assert_not_paused(); let caller = caller(); @@ -324,8 +317,6 @@ mod drc20_roles_pausable { }; Self::emit_transfer(event); } - - #[contract(emits = [(TRANSFER_TOPIC, Drc20Transfer)])] pub fn mint(&mut self, args: MintCall) { self.pausable.assert_not_paused(); if args.to.is_zero() { @@ -349,8 +340,6 @@ mod drc20_roles_pausable { .move_units(None, Some(event.to), event.amount, now()); Self::emit_transfer(event); } - - #[contract(emits = [(TRANSFER_TOPIC, Drc20Transfer)])] pub fn burn(&mut self, amount: u64) { self.pausable.assert_not_paused(); let caller = caller(); @@ -359,8 +348,6 @@ mod drc20_roles_pausable { .move_units(Some(event.from), None, event.amount, now()); Self::emit_transfer(event); } - - #[contract(emits = [(PAUSED_TOPIC, Paused)])] pub fn pause(&mut self, args: AdminCall) { self.pausable.assert_not_paused(); let caller = self.authorize_role_action( @@ -376,8 +363,6 @@ mod drc20_roles_pausable { self.pausable.pause(); Self::emit_paused(caller); } - - #[contract(emits = [(UNPAUSED_TOPIC, Unpaused)])] pub fn unpause(&mut self, args: AdminCall) { self.pausable.assert_paused(); let caller = self.authorize_role_action( @@ -397,8 +382,6 @@ mod drc20_roles_pausable { pub fn paused(&self) -> bool { self.pausable.paused() } - - #[contract(emits = [(ROLE_GRANTED_TOPIC, RoleGranted)])] pub fn grant_role(&mut self, args: RoleCall) { if args.account.is_zero() { panic!("{}", error::ZERO_PRINCIPAL); @@ -420,8 +403,6 @@ mod drc20_roles_pausable { Self::emit_role_granted(args.role, args.account, caller); } } - - #[contract(emits = [(ROLE_REVOKED_TOPIC, RoleRevoked)])] pub fn revoke_role(&mut self, args: RoleCall) { let admin = self.access.get_role_admin(args.role); let caller = self.authorize_role_action( diff --git a/standards/examples/drc721_collection/src/lib.rs b/standards/examples/drc721_collection/src/lib.rs index e4c7bbc5..a0ae76fa 100644 --- a/standards/examples/drc721_collection/src/lib.rs +++ b/standards/examples/drc721_collection/src/lib.rs @@ -74,7 +74,17 @@ pub struct ClearTokenRoyaltyCall { pub authorization: Option, } -#[dusk_forge::contract] +#[dusk_forge::contract(events = [ + Drc721Transfer, + Drc721Approval, + Drc721ApprovalForAll, + Paused, + Unpaused, + DefaultRoyaltySet, + DefaultRoyaltyCleared, + TokenRoyaltySet, + TokenRoyaltyCleared, +])] mod drc721_collection { use alloc::collections::BTreeSet; use alloc::string::String; @@ -136,8 +146,6 @@ mod drc721_collection { royalties: RoyaltyRegistry::new(), } } - - #[contract(emits = [(TRANSFER_TOPIC, Drc721Transfer)])] pub fn init(&mut self, args: Init) { if args.owner.is_zero() { panic!("{}", error::ZERO_PRINCIPAL); @@ -221,20 +229,14 @@ mod drc721_collection { pub fn royalty_info(&self, args: RoyaltyQuery) -> RoyaltyQuote { self.royalties.royalty_info(args.token_id, args.sale_price) } - - #[contract(emits = [(APPROVAL_TOPIC, Drc721Approval)])] pub fn approve(&mut self, args: ApproveCall) { let event = self.token.approve(caller(), args); Self::emit_approval(event); } - - #[contract(emits = [(APPROVAL_FOR_ALL_TOPIC, Drc721ApprovalForAll)])] pub fn set_approval_for_all(&mut self, args: SetApprovalForAllCall) { let event = self.token.set_approval_for_all(caller(), args); Self::emit_approval_for_all(event); } - - #[contract(emits = [(APPROVAL_TOPIC, Drc721Approval)])] pub fn approve_by_authorization(&mut self, args: SignedApproveCall) { let owner = self.token.owner_of(OwnerOf { token_id: args.token_id, @@ -268,8 +270,6 @@ mod drc721_collection { ); Self::emit_approval(event); } - - #[contract(emits = [(APPROVAL_FOR_ALL_TOPIC, Drc721ApprovalForAll)])] pub fn set_approval_for_all_by_authorization( &mut self, args: SignedSetApprovalForAllCall, @@ -303,8 +303,6 @@ mod drc721_collection { ); Self::emit_approval_for_all(event); } - - #[contract(emits = [(TRANSFER_TOPIC, Drc721Transfer)])] pub fn transfer_from(&mut self, args: TransferFromCall) { self.pausable.assert_not_paused(); let caller = caller(); @@ -315,8 +313,6 @@ mod drc721_collection { }; Self::emit_transfer(event); } - - #[contract(emits = [(TRANSFER_TOPIC, Drc721Transfer)])] pub fn mint(&mut self, args: MintCall) { self.pausable.assert_not_paused(); if args.to.is_zero() { @@ -334,15 +330,11 @@ mod drc721_collection { let event = self.token.mint(args.to, args.token_id); Self::emit_transfer(event); } - - #[contract(emits = [(TRANSFER_TOPIC, Drc721Transfer)])] pub fn burn(&mut self, token_id: u64) { self.pausable.assert_not_paused(); let event = self.token.burn(caller(), token_id); Self::emit_transfer(event); } - - #[contract(emits = [(PAUSED_TOPIC, Paused)])] pub fn pause(&mut self, args: AdminCall) { self.pausable.assert_not_paused(); let caller = self.authorize_owner_action( @@ -357,8 +349,6 @@ mod drc721_collection { self.pausable.pause(); Self::emit_paused(caller); } - - #[contract(emits = [(UNPAUSED_TOPIC, Unpaused)])] pub fn unpause(&mut self, args: AdminCall) { self.pausable.assert_paused(); let caller = self.authorize_owner_action( @@ -377,8 +367,6 @@ mod drc721_collection { pub fn paused(&self) -> bool { self.pausable.paused() } - - #[contract(emits = [(DEFAULT_ROYALTY_SET_TOPIC, DefaultRoyaltySet)])] pub fn set_default_royalty(&mut self, args: SetDefaultRoyaltyCall) { validate_royalty(args.info); let caller = self.authorize_owner_action( @@ -393,10 +381,6 @@ mod drc721_collection { self.royalties.set_default_royalty(args.info); Self::emit_default_royalty_set(caller, args.info); } - - #[contract( - emits = [(DEFAULT_ROYALTY_CLEARED_TOPIC, DefaultRoyaltyCleared)] - )] pub fn clear_default_royalty(&mut self, args: AdminCall) { let caller = self.authorize_owner_action( args.authorization.as_ref(), @@ -410,8 +394,6 @@ mod drc721_collection { self.royalties.clear_default_royalty(); Self::emit_default_royalty_cleared(caller); } - - #[contract(emits = [(TOKEN_ROYALTY_SET_TOPIC, TokenRoyaltySet)])] pub fn set_token_royalty(&mut self, args: SetTokenRoyaltyCall) { validate_royalty(args.info); let caller = self.authorize_owner_action( @@ -426,8 +408,6 @@ mod drc721_collection { self.royalties.set_token_royalty(args.token_id, args.info); Self::emit_token_royalty_set(caller, args.token_id, args.info); } - - #[contract(emits = [(TOKEN_ROYALTY_CLEARED_TOPIC, TokenRoyaltyCleared)])] pub fn clear_token_royalty(&mut self, args: ClearTokenRoyaltyCall) { let caller = self.authorize_owner_action( args.authorization.as_ref(), diff --git a/standards/examples/multisig_controller/src/lib.rs b/standards/examples/multisig_controller/src/lib.rs index 70974345..7277a376 100644 --- a/standards/examples/multisig_controller/src/lib.rs +++ b/standards/examples/multisig_controller/src/lib.rs @@ -82,7 +82,14 @@ pub struct NonceQuery { pub domain: NonceDomain, } -#[dusk_forge::contract] +#[dusk_forge::contract(events = [ + MultisigOperationProposed, + MultisigOperationConfirmed, + MultisigOperationExecuted, + MultisigOperationCancelled, + MultisigAuthorityUpdated, + MultisigTimeLimitsUpdated, +])] mod multisig_controller { use alloc::format; use alloc::vec::Vec; @@ -127,8 +134,6 @@ mod multisig_controller { initialized: false, } } - - #[contract(no_event)] pub fn init(&mut self, args: Init) { if self.initialized { panic!("{}", error::ALREADY_INITIALIZED); diff --git a/standards/examples/proxy_counter/src/lib.rs b/standards/examples/proxy_counter/src/lib.rs index 2757f00d..75f9b2bd 100644 --- a/standards/examples/proxy_counter/src/lib.rs +++ b/standards/examples/proxy_counter/src/lib.rs @@ -70,7 +70,18 @@ pub struct ValueChanged { pub sender: Option, } -#[dusk_forge::contract] +impl dusk_forge::ContractEvent for ValueChanged { + const TOPICS: &'static [&'static str] = &[VALUE_CHANGED_TOPIC]; +} + +#[dusk_forge::contract(events = [ + ValueChanged, + UpgradePrepared, + UpgradeActivated, + UpgradeCancelled, + UpgradeRolledBack, + RollbackFinalized, +])] mod proxy_counter { use alloc::vec::Vec; @@ -111,8 +122,6 @@ mod proxy_counter { authorizations: AuthorizationManager::new(), } } - - #[contract(no_event)] pub fn init(&mut self, args: Init) { if self.admin.is_some() { panic!("{}", error::ALREADY_INITIALIZED); @@ -142,8 +151,6 @@ mod proxy_counter { self.store.get_word(COUNTER_KEY)[24..32].try_into().unwrap(), ) } - - #[contract(emits = [(VALUE_CHANGED_TOPIC, ValueChanged)])] pub fn increment(&mut self) { let previous = self.value(); let next = previous.checked_add(1).expect(error::OVERFLOW); @@ -154,8 +161,6 @@ mod proxy_counter { CallContext::current().principal, ); } - - #[contract(emits = [(VALUE_CHANGED_TOPIC, ValueChanged)])] pub fn set_value(&mut self, args: SetValue) { let previous = self.value(); let caller = self.authorize_admin_action( @@ -170,8 +175,6 @@ mod proxy_counter { self.write_value(args.value); Self::emit_value_changed(previous, args.value, Some(caller)); } - - #[contract(emits = [(UPGRADE_PREPARED_TOPIC, UpgradePrepared)])] pub fn prepare_upgrade(&mut self, args: PrepareUpgrade) { let caller = self.authorize_admin_action( args.authorization.as_ref(), @@ -193,8 +196,6 @@ mod proxy_counter { ); Self::emit_upgrade_prepared(event); } - - #[contract(emits = [(UPGRADE_ACTIVATED_TOPIC, UpgradeActivated)])] pub fn activate_upgrade(&mut self, args: AdminCall) -> Vec { let caller = self.authorize_admin_action( args.authorization.as_ref(), @@ -210,8 +211,6 @@ mod proxy_counter { Self::emit_upgrade_activated(event); migrate_data } - - #[contract(emits = [(UPGRADE_CANCELLED_TOPIC, UpgradeCancelled)])] pub fn cancel_pending_upgrade(&mut self, args: AdminCall) { let caller = self.authorize_admin_action( args.authorization.as_ref(), @@ -225,8 +224,6 @@ mod proxy_counter { let event = self.admin_mut().cancel_pending(caller); Self::emit_upgrade_cancelled(event); } - - #[contract(emits = [(UPGRADE_ROLLED_BACK_TOPIC, UpgradeRolledBack)])] pub fn rollback(&mut self, args: AdminCall) { let caller = self.authorize_admin_action( args.authorization.as_ref(), @@ -240,8 +237,6 @@ mod proxy_counter { let event = self.admin_mut().rollback_with_event(caller, now()); Self::emit_upgrade_rolled_back(event); } - - #[contract(emits = [(ROLLBACK_FINALIZED_TOPIC, RollbackFinalized)])] pub fn finalize_rollback_window(&mut self, args: AdminCall) { let caller = self.authorize_admin_action( args.authorization.as_ref(), From 1a035015e7ac376ab3976446dfe8748466cd4f59 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Wed, 27 May 2026 12:38:44 +0200 Subject: [PATCH 30/35] standards: allow DRC721 operators to burn --- .../src/token/drc721/mod.rs | 6 +++++- .../dusk-contract-standards/tests/primitives.rs | 16 +++++++++++++++- .../dusk-contract-standards/tests/properties.rs | 5 ++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/standards/dusk-contract-standards/src/token/drc721/mod.rs b/standards/dusk-contract-standards/src/token/drc721/mod.rs index 0eddf7c9..afbd211c 100644 --- a/standards/dusk-contract-standards/src/token/drc721/mod.rs +++ b/standards/dusk-contract-standards/src/token/drc721/mod.rs @@ -307,7 +307,11 @@ impl Drc721 { self.assert_initialized(); let owner = self.owner_of(OwnerOf { token_id }); let approved = self.token_approvals.get(&token_id).copied(); - if caller != owner && approved != Some(caller) { + let operator = self.is_approved_for_all(IsApprovedForAll { + owner, + operator: caller, + }); + if caller != owner && approved != Some(caller) && !operator { panic!("{}", error::UNAUTHORIZED); } self.owners.remove(&token_id); diff --git a/standards/dusk-contract-standards/tests/primitives.rs b/standards/dusk-contract-standards/tests/primitives.rs index c04b909e..b3550616 100644 --- a/standards/dusk-contract-standards/tests/primitives.rs +++ b/standards/dusk-contract-standards/tests/primitives.rs @@ -1677,9 +1677,20 @@ fn drc721_supports_approval_operator_transfer_and_burn() { operator, })); + token.burn(operator, 1); + assert_eq!(token.total_supply(), 0); + assert_eq!(token.balance_of(BalanceOf721 { account: receiver }), 0); + assert_panics(|| { + token.owner_of(OwnerOf { token_id: 1 }); + }); + assert_eq!( + token.tokens_of(TokensOf { owner: receiver }), + Vec::::new() + ); + token.mint(owner, 2); token.burn(owner, 2); - assert_eq!(token.total_supply(), 1); + assert_eq!(token.total_supply(), 0); assert_panics(|| { token.transfer_from( @@ -1691,6 +1702,9 @@ fn drc721_supports_approval_operator_transfer_and_burn() { }, ); }); + assert_panics(|| { + token.owner_of(OwnerOf { token_id: 1 }); + }); assert_panics(|| { token.owner_of(OwnerOf { token_id: 2 }); }); diff --git a/standards/dusk-contract-standards/tests/properties.rs b/standards/dusk-contract-standards/tests/properties.rs index 56e22ed2..26e93948 100644 --- a/standards/dusk-contract-standards/tests/properties.rs +++ b/standards/dusk-contract-standards/tests/properties.rs @@ -718,7 +718,10 @@ impl Drc721Model { return false; }; let approved = self.approvals.get(&token_id).copied(); - if caller != owner && approved != Some(caller) { + if caller != owner + && approved != Some(caller) + && !self.is_operator(owner, caller) + { return false; } self.owners.remove(&token_id); From 6bbe53e44b66b6cb889dbf3723aa18c4d884c9d5 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Wed, 27 May 2026 12:53:06 +0200 Subject: [PATCH 31/35] standards: fix observed Moonlight caller context --- docs/dusk-contract-standards-audit.md | 7 +- docs/dusk-contract-standards-security.md | 10 +- docs/dusk-contract-standards.md | 4 + .../src/core/context.rs | 121 ++++++++++++++++-- 4 files changed, 122 insertions(+), 20 deletions(-) diff --git a/docs/dusk-contract-standards-audit.md b/docs/dusk-contract-standards-audit.md index 9020521f..f7d7842e 100644 --- a/docs/dusk-contract-standards-audit.md +++ b/docs/dusk-contract-standards-audit.md @@ -31,8 +31,9 @@ Out of scope: The standards layer assumes: -- `CallContext::current()` correctly reports observed Moonlight and contract - callers when the runtime exposes such callers. +- `CallContext::current()` correctly reports observed Moonlight root callers + through `public_sender`, including normal transfer-contract entrypoint + execution, and reports immediate contract callers for nested calls. - Phoenix calls do not expose a stable caller identity. Phoenix authorization must therefore be an explicit Schnorr signature over an `AuthorizedAction`. - Dusk signature primitives verify according to their upstream implementations. @@ -55,7 +56,7 @@ The layer does not assume: | AUTH-1 | Signed actions bind contract, domain, action id, payload hash, nonce, principal, and expiry before nonce/replay consumption. | `tests/primitives.rs`, `tests/properties.rs`, VM test, local-node smoke | | AUTH-2 | Rejected envelope, signature, expiry, role, owner, admin, and replay-key cases do not advance nonce/replay state. | property negative matrices, VM test, local-node smoke | | AUTH-3 | Phoenix authorization proves control of a Schnorr key principal and never relies on observed caller identity. | primitives, properties, signed auth example, local-node smoke | -| AUTH-4 | Moonlight owners can authorize by observed caller or signed BLS action; contract owners only by observed contract caller. | primitives, reference contracts, VM test | +| AUTH-4 | Moonlight owners can authorize by observed root caller or signed BLS action; contract owners only by observed immediate contract caller. Transfer-contract entrypoint calls map to `public_sender`, while nested calls do not. | primitives, reference contracts, VM test, `core::context` unit tests | | MULTISIG-1 | Threshold multisig requires distinct owner quorum and rejects duplicate signers before nonce/replay consumption. | primitives | | MULTISIG-2 | Observed Moonlight/contract owners can count toward quorum; Phoenix owners require signed action approvals. | primitives | | MULTISIG-3 | Multisig owner and threshold maintenance requires current quorum and rejected changes leave state unchanged. | primitives | diff --git a/docs/dusk-contract-standards-security.md b/docs/dusk-contract-standards-security.md index c391b486..638c7bfe 100644 --- a/docs/dusk-contract-standards-security.md +++ b/docs/dusk-contract-standards-security.md @@ -24,9 +24,13 @@ does not prove that a specific Phoenix note exists or was spent. ## Moonlight and Contract Callers Moonlight root calls and inter-contract calls can use `CallContext::current()`. -Signed Moonlight authorization is still useful for relayed calls or workflows -where the submitter is not the owner. Contract principals should be accepted -only from observed inter-contract caller context. +A normal Moonlight transaction reaches the target through the transfer +contract. The standards layer treats that transfer-contract wrapper as the +transaction entrypoint and maps it to `public_sender` only for the root target +call. Nested calls are not rewritten to the public sender; they stay bound to +the immediate caller contract. Signed Moonlight authorization is still useful +for relayed calls or workflows where the submitter is not the owner. Contract +principals should be accepted only from observed inter-contract caller context. ## Payload Binding diff --git a/docs/dusk-contract-standards.md b/docs/dusk-contract-standards.md index b8cbfd79..a618f446 100644 --- a/docs/dusk-contract-standards.md +++ b/docs/dusk-contract-standards.md @@ -33,6 +33,10 @@ instead of forcing everything into an Ethereum-style address. Contracts can use `CallContext::current()` for runtime Moonlight and inter-contract calls, while Phoenix flows should pass an explicit authorization identity plus a nonce or replay key when the application needs that model. +For Moonlight transactions routed through the transfer contract, the standards +layer treats the root transfer-contract call into the target as the transaction +entrypoint and resolves it to the runtime `public_sender`. Nested calls remain +bound to the immediate caller contract. `NonceManager` is intentionally domain-separated by `(principal, domain)`. This lets one contract keep independent monotonic streams for permits, Phoenix diff --git a/standards/dusk-contract-standards/src/core/context.rs b/standards/dusk-contract-standards/src/core/context.rs index 55f7e265..7ac09567 100644 --- a/standards/dusk-contract-standards/src/core/context.rs +++ b/standards/dusk-contract-standards/src/core/context.rs @@ -2,6 +2,13 @@ use crate::core::principal::Principal; +#[cfg(any(all(target_family = "wasm", feature = "contract"), test))] +use dusk_core::abi::ContractId; +#[cfg(any(all(target_family = "wasm", feature = "contract"), test))] +use dusk_core::signatures::bls::PublicKey as BlsPublicKey; +#[cfg(any(all(target_family = "wasm", feature = "contract"), test))] +use dusk_core::transfer::TRANSFER_CONTRACT; + /// Current call context. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct CallContext { @@ -29,25 +36,25 @@ impl CallContext { /// Reads the current Dusk runtime context. /// - /// A root Moonlight call is represented by `public_sender`. An - /// inter-contract call is represented by the immediate caller contract id. - /// Phoenix transactions do not expose a stable owner identity here; Phoenix - /// authorization should be modeled with an explicit Schnorr signature plus - /// nonce/replay protection. + /// A root Moonlight transaction routed through the transfer contract is + /// represented by `public_sender`. An inter-contract call is represented + /// by the immediate caller contract id. Phoenix transactions do not expose + /// a stable owner identity here; Phoenix authorization should be modeled + /// with an explicit Schnorr signature plus nonce/replay protection. #[cfg(all(target_family = "wasm", feature = "contract"))] pub fn current() -> Self { use dusk_core::abi; - if abi::callstack().is_empty() { - return Self::none(); - } - if let Some(caller) = abi::caller() { - return Self::from_principal(Principal::Contract(caller)); - } - let Some(pk) = abi::public_sender() else { - return Self::none(); + let caller = abi::caller(); + let callstack_len = abi::callstack().len(); + let public_sender = match caller { + Some(TRANSFER_CONTRACT) if callstack_len <= 1 => { + abi::public_sender() + } + _ => None, }; - Self::from_principal(Principal::moonlight(&pk)) + + Self::from_runtime_parts(caller, public_sender, callstack_len) } /// Native tests must inject context explicitly. @@ -55,4 +62,90 @@ impl CallContext { pub const fn current() -> Self { Self::none() } + + #[cfg(any(all(target_family = "wasm", feature = "contract"), test))] + fn from_runtime_parts( + caller: Option, + public_sender: Option, + callstack_len: usize, + ) -> Self { + match caller { + Some(TRANSFER_CONTRACT) if callstack_len <= 1 => { + public_sender.as_ref().map_or_else(Self::none, |pk| { + Self::from_principal(Principal::moonlight(pk)) + }) + } + Some(caller) => Self::from_principal(Principal::Contract(caller)), + None => Self::none(), + } + } +} + +#[cfg(test)] +mod tests { + use rand::rngs::StdRng; + use rand::SeedableRng; + + use dusk_core::signatures::bls::{ + PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, + }; + + use super::*; + + fn moonlight_key(seed: u64) -> BlsPublicKey { + let mut rng = StdRng::seed_from_u64(seed); + BlsPublicKey::from(&BlsSecretKey::random(&mut rng)) + } + + fn contract(byte: u8) -> ContractId { + ContractId::from_bytes([byte; 32]) + } + + #[test] + fn transfer_entrypoint_moonlight_uses_public_sender() { + let pk = moonlight_key(1); + let context = CallContext::from_runtime_parts( + Some(TRANSFER_CONTRACT), + Some(pk), + 1, + ); + assert_eq!(context.principal, Some(Principal::moonlight(&pk))); + } + + #[test] + fn transfer_entrypoint_without_public_sender_has_no_actor() { + let context = + CallContext::from_runtime_parts(Some(TRANSFER_CONTRACT), None, 1); + assert_eq!(context.principal, None); + } + + #[test] + fn nested_contract_call_keeps_immediate_contract_caller() { + let pk = moonlight_key(2); + let caller = contract(7); + let context = + CallContext::from_runtime_parts(Some(caller), Some(pk), 2); + assert_eq!(context.principal, Some(Principal::Contract(caller))); + } + + #[test] + fn nested_transfer_call_is_not_rewritten_to_public_sender() { + let pk = moonlight_key(3); + let context = CallContext::from_runtime_parts( + Some(TRANSFER_CONTRACT), + Some(pk), + 2, + ); + assert_eq!( + context.principal, + Some(Principal::Contract(TRANSFER_CONTRACT)) + ); + } + + #[test] + fn no_caller_has_no_observed_actor() { + let pk = moonlight_key(4); + let context = CallContext::from_runtime_parts(None, Some(pk), 0); + assert_eq!(context.principal, None); + } } From 2c98bbe6cfc9eaf69a395120ca0212e32839b9f2 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Wed, 27 May 2026 13:21:37 +0200 Subject: [PATCH 32/35] standards: bind signed authorizations to chain id --- docs/dusk-contract-standards-audit.md | 5 +- docs/dusk-contract-standards-security.md | 6 +- docs/dusk-contract-standards.md | 7 +- .../dusk-contract-standards-local-smoke.sh | 15 +++ .../examples/build_signed_authorizations.rs | 6 + .../examples/encode_local_smoke_args.rs | 8 ++ .../dusk-contract-standards/src/auth/mod.rs | 32 ++++- .../tests/examples_vm.rs | 40 ++++++ .../tests/primitives.rs | 116 +++++++++++++++--- .../tests/properties.rs | 34 ++++- .../examples/authorization_counter/src/lib.rs | 6 + .../examples/drc20_roles_pausable/src/lib.rs | 12 +- .../examples/drc721_collection/src/lib.rs | 18 +-- .../examples/multisig_controller/src/lib.rs | 4 +- standards/examples/proxy_counter/src/lib.rs | 12 +- 15 files changed, 272 insertions(+), 49 deletions(-) diff --git a/docs/dusk-contract-standards-audit.md b/docs/dusk-contract-standards-audit.md index f7d7842e..76bdf55a 100644 --- a/docs/dusk-contract-standards-audit.md +++ b/docs/dusk-contract-standards-audit.md @@ -46,14 +46,15 @@ The layer does not assume: - an Ethereum-like `msg.sender` for Phoenix; - delegatecall-style proxy storage behavior; -- off-chain signatures are safe without contract/domain/action/payload binding; +- off-chain signatures are safe without chain/contract/domain/action/payload + binding; - zero principals are valid owners, recipients, spenders, operators, or admins. ## Invariant Matrix | ID | Invariant | Primary Coverage | | --- | --- | --- | -| AUTH-1 | Signed actions bind contract, domain, action id, payload hash, nonce, principal, and expiry before nonce/replay consumption. | `tests/primitives.rs`, `tests/properties.rs`, VM test, local-node smoke | +| AUTH-1 | Signed actions bind chain id, contract, domain, action id, payload hash, nonce, principal, and expiry before nonce/replay consumption. | `tests/primitives.rs`, `tests/properties.rs`, VM test, local-node smoke | | AUTH-2 | Rejected envelope, signature, expiry, role, owner, admin, and replay-key cases do not advance nonce/replay state. | property negative matrices, VM test, local-node smoke | | AUTH-3 | Phoenix authorization proves control of a Schnorr key principal and never relies on observed caller identity. | primitives, properties, signed auth example, local-node smoke | | AUTH-4 | Moonlight owners can authorize by observed root caller or signed BLS action; contract owners only by observed immediate contract caller. Transfer-contract entrypoint calls map to `public_sender`, while nested calls do not. | primitives, reference contracts, VM test, `core::context` unit tests | diff --git a/docs/dusk-contract-standards-security.md b/docs/dusk-contract-standards-security.md index 638c7bfe..33835028 100644 --- a/docs/dusk-contract-standards-security.md +++ b/docs/dusk-contract-standards-security.md @@ -2,8 +2,10 @@ ## Replay Domains -Every signed action must bind `contract`, `domain`, `action_id`, `nonce`, -`principal`, and `payload_hash`. Use separate nonce domains for unrelated +Every signed action must bind `chain_id`, `contract`, `domain`, `action_id`, +`nonce`, `principal`, and `payload_hash`. Chain binding prevents an unused +authorization for one network from being replayed on another network with the +same contract id and nonce state. Use separate nonce domains for unrelated flows: token approvals, role admin, proxy upgrades, voting, and application actions should not share a nonce stream unless that coupling is deliberate. diff --git a/docs/dusk-contract-standards.md b/docs/dusk-contract-standards.md index a618f446..3c4d2b22 100644 --- a/docs/dusk-contract-standards.md +++ b/docs/dusk-contract-standards.md @@ -50,7 +50,7 @@ optional replay key. This proves control of the Phoenix signing key without pretending that Phoenix exposes an address-like runtime caller. `SignedAuthorization` is the ABI-facing wrapper for explicit authorizations, -`ActionEnvelope` is the expected contract/domain/action/payload binding, and +`ActionEnvelope` is the expected chain/contract/domain/action/payload binding, and `Authorizer` is the reusable per-call adapter. `Ownable`, `OwnerSet`, `AccessControl`, and `UpgradeAdmin` expose action-bound helper methods that accept runtime Moonlight/contract callers when available and otherwise verify @@ -113,6 +113,7 @@ an intentional retry. Clients sign an `AuthorizedAction` for the exact contract action they want to execute. The action includes: +- `chain_id`: the Dusk chain id returned by the target runtime; - `contract`: the target `ContractId`; - `domain`: a 32-byte nonce stream chosen by the contract; - `action_id`: a 32-byte id for the exported operation; @@ -138,8 +139,8 @@ The DRC20 reference exposes `approve_by_authorization`. Its payload hash is: 5. `keccak256` over the concatenated bytes. Admin references follow the same rule. Signed mint, role, royalty, and proxy -calls validate `contract`, `domain`, `action_id`, and `payload_hash` before -consuming the signature nonce. +calls validate `chain_id`, `contract`, `domain`, `action_id`, and +`payload_hash` before consuming the signature nonce. The DRC721 reference exposes `approve_by_authorization` and `set_approval_for_all_by_authorization`. Token approval hashes use the ASCII diff --git a/scripts/dusk-contract-standards-local-smoke.sh b/scripts/dusk-contract-standards-local-smoke.sh index 21d47782..d495dbf2 100755 --- a/scripts/dusk-contract-standards-local-smoke.sh +++ b/scripts/dusk-contract-standards-local-smoke.sh @@ -63,6 +63,21 @@ if ! command -v xxd >/dev/null 2>&1; then exit 2 fi +if [[ -z "${DUSK_CHAIN_ID:-}" ]]; then + chain_info="$(curl -fsS "${RUSK_URL}/on/node/info" 2>/dev/null || true)" + DUSK_CHAIN_ID="$( + printf '%s' "$chain_info" | + sed -n 's/.*"chain_id"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | + head -n1 + )" + if [[ -z "$DUSK_CHAIN_ID" ]]; then + DUSK_CHAIN_ID=250 + echo "Could not read chain id from ${RUSK_URL}; defaulting DUSK_CHAIN_ID=${DUSK_CHAIN_ID}" >&2 + fi +fi +export DUSK_CHAIN_ID +echo "Using DUSK_CHAIN_ID=${DUSK_CHAIN_ID}" + mkdir -p "$WALLET_DIR" if [[ -n "$WALLET_RESTORE_FILE" && ! -f "$WALLET_DIR/wallet.keystore.json" ]]; then diff --git a/standards/dusk-contract-standards/examples/build_signed_authorizations.rs b/standards/dusk-contract-standards/examples/build_signed_authorizations.rs index 48e83e0a..1dd33059 100644 --- a/standards/dusk-contract-standards/examples/build_signed_authorizations.rs +++ b/standards/dusk-contract-standards/examples/build_signed_authorizations.rs @@ -21,6 +21,7 @@ const SIGNED_APPROVE_DOMAIN: NonceDomain = [12u8; 32]; const SIGNED_APPROVE_ACTION: [u8; 32] = [18u8; 32]; const NFT_SIGNED_APPROVE_DOMAIN: NonceDomain = [29u8; 32]; const NFT_SIGNED_APPROVE_ACTION: [u8; 32] = [30u8; 32]; +const CHAIN_ID: u8 = 0xFA; const EXAMPLE_EXPIRES_AT: u64 = 1_000; fn main() { @@ -38,6 +39,7 @@ fn main() { signature: moonlight_sk.sign(&moonlight_action.message_bytes()), }); moonlight.assert_action( + CHAIN_ID, contract, SIGNED_APPROVE_DOMAIN, SIGNED_APPROVE_ACTION, @@ -57,6 +59,7 @@ fn main() { replay_key: None, }); phoenix.assert_action( + CHAIN_ID, contract, SIGNED_APPROVE_DOMAIN, SIGNED_APPROVE_ACTION, @@ -79,6 +82,7 @@ fn main() { replay_key: None, }); nft_approval.assert_action( + CHAIN_ID, contract, NFT_SIGNED_APPROVE_DOMAIN, NFT_SIGNED_APPROVE_ACTION, @@ -94,6 +98,7 @@ fn drc20_signed_approve_action( nonce: u64, ) -> AuthorizedAction { AuthorizedAction { + chain_id: CHAIN_ID, contract, domain: SIGNED_APPROVE_DOMAIN, action_id: SIGNED_APPROVE_ACTION, @@ -124,6 +129,7 @@ fn drc721_signed_approve_action( nonce: u64, ) -> AuthorizedAction { AuthorizedAction { + chain_id: CHAIN_ID, contract, domain: NFT_SIGNED_APPROVE_DOMAIN, action_id: NFT_SIGNED_APPROVE_ACTION, diff --git a/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs index 0e67507b..de8c0116 100644 --- a/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs +++ b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs @@ -669,6 +669,7 @@ fn authorized_action( payload_hash: [u8; 32], ) -> AuthorizedAction { AuthorizedAction { + chain_id: chain_id(), contract, domain, action_id, @@ -679,6 +680,13 @@ fn authorized_action( } } +fn chain_id() -> u8 { + env::var("DUSK_CHAIN_ID") + .unwrap_or_else(|_| "250".to_string()) + .parse() + .expect("DUSK_CHAIN_ID must be a u8") +} + fn phoenix_auth( rng: &mut StdRng, secret_key: &SchnorrSecretKey, diff --git a/standards/dusk-contract-standards/src/auth/mod.rs b/standards/dusk-contract-standards/src/auth/mod.rs index a019998e..a06ed095 100644 --- a/standards/dusk-contract-standards/src/auth/mod.rs +++ b/standards/dusk-contract-standards/src/auth/mod.rs @@ -28,6 +28,8 @@ pub const AUTH_MESSAGE_PREFIX: &[u8] = b"dusk-contract-standards/auth/v1"; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[archive_attr(derive(CheckBytes))] pub struct ActionEnvelope { + /// Dusk chain id this authorization is intended for. + pub chain_id: u8, /// Contract this authorization is intended for. pub contract: ContractId, /// Domain-separated nonce stream. @@ -41,18 +43,37 @@ pub struct ActionEnvelope { impl ActionEnvelope { /// Creates a new expected action envelope. pub const fn new( + chain_id: u8, contract: ContractId, domain: NonceDomain, action_id: [u8; 32], payload_hash: [u8; 32], ) -> Self { Self { + chain_id, contract, domain, action_id, payload_hash, } } + + /// Creates an expected action envelope for the current runtime chain. + #[cfg(all(target_family = "wasm", feature = "contract"))] + pub fn for_current_chain( + contract: ContractId, + domain: NonceDomain, + action_id: [u8; 32], + payload_hash: [u8; 32], + ) -> Self { + Self::new( + dusk_core::abi::chain_id(), + contract, + domain, + action_id, + payload_hash, + ) + } } /// Contract action authorized by a signature. @@ -62,6 +83,8 @@ impl ActionEnvelope { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[archive_attr(derive(CheckBytes))] pub struct AuthorizedAction { + /// Dusk chain id this authorization is intended for. + pub chain_id: u8, /// Contract this authorization is intended for. pub contract: ContractId, /// Domain-separated nonce stream. @@ -91,6 +114,7 @@ impl AuthorizedAction { let principal = self.principal.to_bytes(); let mut out = Vec::with_capacity( AUTH_MESSAGE_PREFIX.len() + + 1 + 32 + 32 + 32 @@ -101,6 +125,7 @@ impl AuthorizedAction { + 32, ); out.extend_from_slice(AUTH_MESSAGE_PREFIX); + out.push(self.chain_id); out.extend_from_slice(&self.contract.to_bytes()); out.extend_from_slice(&self.domain); out.extend_from_slice(&self.action_id); @@ -121,12 +146,14 @@ impl AuthorizedAction { /// action id, and payload hash. pub fn assert_matches( &self, + chain_id: u8, contract: ContractId, domain: NonceDomain, action_id: [u8; 32], payload_hash: [u8; 32], ) { self.assert_envelope(ActionEnvelope::new( + chain_id, contract, domain, action_id, @@ -136,7 +163,8 @@ impl AuthorizedAction { /// Panics unless this action matches the expected call envelope. pub fn assert_envelope(&self, envelope: ActionEnvelope) { - if self.contract != envelope.contract + if self.chain_id != envelope.chain_id + || self.contract != envelope.contract || self.domain != envelope.domain || self.action_id != envelope.action_id || self.payload_hash != envelope.payload_hash @@ -201,12 +229,14 @@ impl SignedAuthorization { /// Panics unless the signed action matches the exact call envelope. pub fn assert_action( &self, + chain_id: u8, contract: ContractId, domain: NonceDomain, action_id: [u8; 32], payload_hash: [u8; 32], ) { self.assert_envelope(ActionEnvelope::new( + chain_id, contract, domain, action_id, diff --git a/standards/dusk-contract-standards/tests/examples_vm.rs b/standards/dusk-contract-standards/tests/examples_vm.rs index 224fe768..b5b9512c 100644 --- a/standards/dusk-contract-standards/tests/examples_vm.rs +++ b/standards/dusk-contract-standards/tests/examples_vm.rs @@ -2348,6 +2348,25 @@ fn exercise_authorization_counter_signed_calls( ) .is_err()); + let mut wrong_chain_action = + counter_action(contract, moonlight, 1, 0, SET_VALUE_ACTION, 12); + wrong_chain_action.chain_id = CHAIN_ID.wrapping_add(1); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_moonlight", + &SetValueByMoonlight { + authorization: moonlight_auth( + &moonlight_sk, + moonlight_pk, + wrong_chain_action, + ), + amount: 12, + }, + GAS_LIMIT, + ) + .is_err()); + let wrong_action = counter_action(contract, moonlight, 1, 0, [9u8; 32], 12); assert!(session .call::<_, ()>( @@ -2524,6 +2543,26 @@ fn exercise_authorization_counter_signed_calls( ) .is_err()); + let mut wrong_chain_action = + counter_action(contract, phoenix, 1, 0, SET_VALUE_ACTION, 22); + wrong_chain_action.chain_id = CHAIN_ID.wrapping_add(1); + assert!(session + .call::<_, ()>( + contract, + "set_value_by_phoenix", + &SetValueByPhoenix { + authorization: phoenix_auth( + &mut rng, + &phoenix_sk, + phoenix_pk, + wrong_chain_action, + ), + amount: 22, + }, + GAS_LIMIT, + ) + .is_err()); + let wrong_action = counter_action(contract, phoenix, 1, 0, [8u8; 32], 22); assert!(session .call::<_, ()>( @@ -2610,6 +2649,7 @@ fn authorized_action( payload_hash: [u8; 32], ) -> AuthorizedAction { AuthorizedAction { + chain_id: CHAIN_ID, contract, domain, action_id, diff --git a/standards/dusk-contract-standards/tests/primitives.rs b/standards/dusk-contract-standards/tests/primitives.rs index b3550616..408e1c4d 100644 --- a/standards/dusk-contract-standards/tests/primitives.rs +++ b/standards/dusk-contract-standards/tests/primitives.rs @@ -46,6 +46,8 @@ use dusk_core::JubJubScalar; use rand::rngs::StdRng; use rand::SeedableRng; +const TEST_CHAIN_ID: u8 = 0xD5; + fn p(byte: u8) -> Principal { Principal::phoenix([byte; 32]) } @@ -88,6 +90,7 @@ fn signed_moonlight_action( nonce: u64, ) -> SignedAuthorization { let action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, contract, domain, action_id, @@ -116,6 +119,7 @@ fn signed_phoenix_action( rng_seed: u64, ) -> SignedAuthorization { let action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, contract, domain, action_id, @@ -251,8 +255,13 @@ fn threshold_multisig_requires_distinct_quorum_before_nonce_consumption() { let domain = [105u8; 32]; let action_id = [106u8; 32]; let payload_hash = [107u8; 32]; - let envelope = - ActionEnvelope::new(contract, domain, action_id, payload_hash); + let envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + payload_hash, + ); let mut multisig = ThresholdMultisig::new(); multisig.init(MultisigConfig { @@ -326,8 +335,13 @@ fn threshold_multisig_requires_distinct_quorum_before_nonce_consumption() { assert_eq!(manager.nonce(owner_a, domain), 0); assert_eq!(manager.nonce(outsider, domain), 0); - let wrong_envelope = - ActionEnvelope::new(contract, domain, action_id, [108u8; 32]); + let wrong_envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + [108u8; 32], + ); assert_panics(|| { multisig.authorize_action( &mut manager, @@ -376,8 +390,13 @@ fn threshold_multisig_counts_observed_moonlight_and_contract_callers() { let domain = [113u8; 32]; let action_id = [114u8; 32]; let payload_hash = [115u8; 32]; - let envelope = - ActionEnvelope::new(contract, domain, action_id, payload_hash); + let envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + payload_hash, + ); let mut multisig = ThresholdMultisig::new(); multisig.init(MultisigConfig { @@ -722,6 +741,7 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { let action_id = [55u8; 32]; let action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, contract, domain, action_id, @@ -745,6 +765,7 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { }); let bad_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, nonce: 1, principal: p(99), ..action @@ -764,6 +785,7 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { let phoenix_pk = SchnorrPublicKey::from(&phoenix_sk); let phoenix = Principal::phoenix_public_key(&phoenix_pk); let phoenix_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, contract, domain: [77u8; 32], action_id, @@ -783,6 +805,7 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { assert!(authorizations.replay_used(phoenix, [99u8; 32])); let expired_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, expires_at: 9, nonce: 1, ..phoenix_action @@ -798,6 +821,7 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { }); let bad_signature_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, nonce: 1, payload_hash: [23u8; 32], ..phoenix_action @@ -814,6 +838,7 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { assert_eq!(authorizations.nonce(phoenix, [77u8; 32]), 1); let replay_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, nonce: 1, payload_hash: [24u8; 32], ..phoenix_action @@ -833,6 +858,7 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { let wrong_pk = SchnorrPublicKey::from(&wrong_sk); let wrong_key = PhoenixSignatureAuthorization { action: AuthorizedAction { + chain_id: TEST_CHAIN_ID, nonce: 1, ..phoenix_action }, @@ -846,6 +872,7 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { let bounded_domain = [88u8; 32]; let bounded_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, contract, domain: bounded_domain, action_id, @@ -864,7 +891,27 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { assert_panics(|| { bounded.authorize_signed_action( &bounded_signed, - ActionEnvelope::new(contract, bounded_domain, action_id, [2u8; 32]), + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + bounded_domain, + action_id, + [2u8; 32], + ), + 9, + ); + }); + assert_eq!(bounded.nonce(moonlight, bounded_domain), 0); + assert_panics(|| { + bounded.authorize_signed_action( + &bounded_signed, + ActionEnvelope::new( + TEST_CHAIN_ID.wrapping_add(1), + contract, + bounded_domain, + action_id, + [1u8; 32], + ), 9, ); }); @@ -872,7 +919,13 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { assert_eq!( bounded.authorize_signed_action( &bounded_signed, - ActionEnvelope::new(contract, bounded_domain, action_id, [1u8; 32]), + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + bounded_domain, + action_id, + [1u8; 32] + ), 10, ), moonlight @@ -880,6 +933,7 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { assert_eq!(bounded.nonce(moonlight, bounded_domain), 1); let expired_bounded_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, nonce: 1, expires_at: 10, payload_hash: [3u8; 32], @@ -894,7 +948,13 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { assert_panics(|| { bounded.authorize_signed_action( &expired_bounded, - ActionEnvelope::new(contract, bounded_domain, action_id, [3u8; 32]), + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + bounded_domain, + action_id, + [3u8; 32], + ), 11, ); }); @@ -932,6 +992,7 @@ fn observed_or_signed_authorization_wraps_owner_role_and_admin_checks() { ); let moonlight_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, contract, domain, action_id, @@ -961,6 +1022,7 @@ fn observed_or_signed_authorization_wraps_owner_role_and_admin_checks() { let phoenix_pk = SchnorrPublicKey::from(&phoenix_sk); let phoenix = Principal::phoenix_public_key(&phoenix_pk); let phoenix_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, contract, domain, action_id, @@ -984,7 +1046,13 @@ fn observed_or_signed_authorization_wraps_owner_role_and_admin_checks() { &mut manager, CallContext::none(), Some(&phoenix_signed), - ActionEnvelope::new(contract, domain, action_id, [49u8; 32]), + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + [49u8; 32] + ), 0, ), phoenix @@ -995,6 +1063,7 @@ fn observed_or_signed_authorization_wraps_owner_role_and_admin_checks() { access.init_admin(moonlight); access.grant_role(moonlight, role, phoenix); let phoenix_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, nonce: 1, payload_hash: [51u8; 32], ..phoenix_action @@ -1012,7 +1081,13 @@ fn observed_or_signed_authorization_wraps_owner_role_and_admin_checks() { &mut manager, CallContext::none(), Some(&phoenix_signed), - ActionEnvelope::new(contract, domain, action_id, [51u8; 32]), + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + [51u8; 32] + ), 0, ), phoenix @@ -1020,6 +1095,7 @@ fn observed_or_signed_authorization_wraps_owner_role_and_admin_checks() { let upgrade = UpgradeAdmin::new(phoenix, c(52), 0, 0); let phoenix_action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, nonce: 2, payload_hash: [53u8; 32], ..phoenix_action @@ -1036,7 +1112,13 @@ fn observed_or_signed_authorization_wraps_owner_role_and_admin_checks() { &mut manager, CallContext::none(), Some(&phoenix_signed), - ActionEnvelope::new(contract, domain, action_id, [53u8; 32]), + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + [53u8; 32] + ), 0, ), phoenix @@ -1064,9 +1146,15 @@ fn failed_owner_role_and_admin_signed_checks_do_not_consume_nonce() { let domain = [76u8; 32]; let action_id = [77u8; 32]; let payload_hash = [78u8; 32]; - let envelope = - ActionEnvelope::new(contract, domain, action_id, payload_hash); + let envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + payload_hash, + ); let action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, contract, domain, action_id, diff --git a/standards/dusk-contract-standards/tests/properties.rs b/standards/dusk-contract-standards/tests/properties.rs index 26e93948..80fa0644 100644 --- a/standards/dusk-contract-standards/tests/properties.rs +++ b/standards/dusk-contract-standards/tests/properties.rs @@ -41,6 +41,8 @@ use proptest::prelude::*; use rand::rngs::StdRng; use rand::SeedableRng; +const TEST_CHAIN_ID: u8 = 0xD5; + fn standards_proptest_config() -> ProptestConfig { ProptestConfig { cases: env_usize("STANDARDS_PROPTEST_CASES") @@ -828,6 +830,7 @@ fn apply_drc721_token(token: &mut Drc721, op: &Drc721Op) -> bool { #[derive(Clone, Copy, Debug)] enum AuthCase { Good, + WrongChain, WrongContract, WrongDomain, WrongAction, @@ -845,6 +848,7 @@ enum AuthCase { fn auth_case_strategy() -> impl Strategy { prop_oneof![ Just(AuthCase::Good), + Just(AuthCase::WrongChain), Just(AuthCase::WrongContract), Just(AuthCase::WrongDomain), Just(AuthCase::WrongAction), @@ -875,6 +879,7 @@ fn signed_action( #[derive(Clone, Copy, Debug)] enum PhoenixAuthCase { Good, + WrongChain, WrongContract, WrongDomain, WrongAction, @@ -893,6 +898,7 @@ enum PhoenixAuthCase { fn phoenix_auth_case_strategy() -> impl Strategy { prop_oneof![ Just(PhoenixAuthCase::Good), + Just(PhoenixAuthCase::WrongChain), Just(PhoenixAuthCase::WrongContract), Just(PhoenixAuthCase::WrongDomain), Just(PhoenixAuthCase::WrongAction), @@ -933,9 +939,15 @@ fn authorization_probe(case: AuthCase) -> (bool, u64) { let domain = [33u8; 32]; let action_id = [34u8; 32]; let payload_hash = [35u8; 32]; - let envelope = - ActionEnvelope::new(contract, domain, action_id, payload_hash); + let envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + payload_hash, + ); let mut action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, contract, domain, action_id, @@ -948,6 +960,10 @@ fn authorization_probe(case: AuthCase) -> (bool, u64) { let expected = match case { AuthCase::Good => signer, AuthCase::WrongExpected => principal(1), + AuthCase::WrongChain => { + action.chain_id = TEST_CHAIN_ID.wrapping_add(1); + signer + } AuthCase::WrongContract => { action.contract = contract_id(36); signer @@ -1055,9 +1071,15 @@ fn phoenix_authorization_probe(case: PhoenixAuthCase) -> (bool, u64, bool) { let action_id = [46u8; 32]; let payload_hash = [47u8; 32]; let replay_key = [48u8; 32]; - let envelope = - ActionEnvelope::new(contract, domain, action_id, payload_hash); + let envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + payload_hash, + ); let mut action = AuthorizedAction { + chain_id: TEST_CHAIN_ID, contract, domain, action_id, @@ -1070,6 +1092,10 @@ fn phoenix_authorization_probe(case: PhoenixAuthCase) -> (bool, u64, bool) { let expected = match case { PhoenixAuthCase::Good | PhoenixAuthCase::ReplayKeyUsed => signer, PhoenixAuthCase::WrongExpected => principal(1), + PhoenixAuthCase::WrongChain => { + action.chain_id = TEST_CHAIN_ID.wrapping_add(1); + signer + } PhoenixAuthCase::WrongContract => { action.contract = contract_id(49); signer diff --git a/standards/examples/authorization_counter/src/lib.rs b/standards/examples/authorization_counter/src/lib.rs index 9ac1e3dc..5d24c691 100644 --- a/standards/examples/authorization_counter/src/lib.rs +++ b/standards/examples/authorization_counter/src/lib.rs @@ -89,6 +89,7 @@ mod authorization_counter { } pub fn set_value_by_moonlight(&mut self, args: SetValueByMoonlight) { self.assert_action( + args.authorization.action.chain_id, args.authorization.action.contract, args.authorization.action.domain, args.authorization.action.action_id, @@ -102,6 +103,7 @@ mod authorization_counter { } pub fn set_value_by_phoenix(&mut self, args: SetValueByPhoenix) { self.assert_action( + args.authorization.action.chain_id, args.authorization.action.contract, args.authorization.action.domain, args.authorization.action.action_id, @@ -116,6 +118,7 @@ mod authorization_counter { fn assert_action( &self, + chain_id: u8, contract: ContractId, domain: NonceDomain, action_id: [u8; 32], @@ -125,6 +128,9 @@ mod authorization_counter { if !self.initialized { panic!("AuthorizationCounter: not initialized"); } + if chain_id != abi::chain_id() { + panic!("AuthorizationCounter: wrong chain"); + } if contract != abi::self_id() { panic!("AuthorizationCounter: wrong contract"); } diff --git a/standards/examples/drc20_roles_pausable/src/lib.rs b/standards/examples/drc20_roles_pausable/src/lib.rs index 2a58341d..d034cff5 100644 --- a/standards/examples/drc20_roles_pausable/src/lib.rs +++ b/standards/examples/drc20_roles_pausable/src/lib.rs @@ -267,7 +267,7 @@ mod drc20_roles_pausable { pub fn approve_by_authorization(&mut self, args: SignedApproveCall) { let principal = self.authorizations.authorize_signed_action( &args.authorization, - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), SIGNED_APPROVE_DOMAIN, SIGNED_APPROVE_ACTION, @@ -328,7 +328,7 @@ mod drc20_roles_pausable { self.authorize_role_action( MINTER_ROLE, args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), TOKEN_ADMIN_DOMAIN, MINT_ACTION, @@ -353,7 +353,7 @@ mod drc20_roles_pausable { let caller = self.authorize_role_action( PAUSER_ROLE, args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), TOKEN_ADMIN_DOMAIN, PAUSE_ACTION, @@ -368,7 +368,7 @@ mod drc20_roles_pausable { let caller = self.authorize_role_action( PAUSER_ROLE, args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), TOKEN_ADMIN_DOMAIN, UNPAUSE_ACTION, @@ -390,7 +390,7 @@ mod drc20_roles_pausable { let caller = self.authorize_role_action( admin, args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), TOKEN_ADMIN_DOMAIN, GRANT_ROLE_ACTION, @@ -408,7 +408,7 @@ mod drc20_roles_pausable { let caller = self.authorize_role_action( admin, args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), TOKEN_ADMIN_DOMAIN, REVOKE_ROLE_ACTION, diff --git a/standards/examples/drc721_collection/src/lib.rs b/standards/examples/drc721_collection/src/lib.rs index a0ae76fa..b9bc4d96 100644 --- a/standards/examples/drc721_collection/src/lib.rs +++ b/standards/examples/drc721_collection/src/lib.rs @@ -246,7 +246,7 @@ mod drc721_collection { } let principal = self.authorizations.authorize_signed_action( &args.authorization, - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), NFT_SIGNED_APPROVE_DOMAIN, NFT_SIGNED_APPROVE_ACTION, @@ -279,7 +279,7 @@ mod drc721_collection { } let principal = self.authorizations.authorize_signed_action( &args.authorization, - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), NFT_SIGNED_APPROVE_DOMAIN, NFT_SIGNED_APPROVAL_FOR_ALL_ACTION, @@ -320,7 +320,7 @@ mod drc721_collection { } self.authorize_owner_action( args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), NFT_ADMIN_DOMAIN, MINT_ACTION, @@ -339,7 +339,7 @@ mod drc721_collection { self.pausable.assert_not_paused(); let caller = self.authorize_owner_action( args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), NFT_ADMIN_DOMAIN, PAUSE_ACTION, @@ -353,7 +353,7 @@ mod drc721_collection { self.pausable.assert_paused(); let caller = self.authorize_owner_action( args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), NFT_ADMIN_DOMAIN, UNPAUSE_ACTION, @@ -371,7 +371,7 @@ mod drc721_collection { validate_royalty(args.info); let caller = self.authorize_owner_action( args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), NFT_ADMIN_DOMAIN, SET_DEFAULT_ROYALTY_ACTION, @@ -384,7 +384,7 @@ mod drc721_collection { pub fn clear_default_royalty(&mut self, args: AdminCall) { let caller = self.authorize_owner_action( args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), NFT_ADMIN_DOMAIN, CLEAR_DEFAULT_ROYALTY_ACTION, @@ -398,7 +398,7 @@ mod drc721_collection { validate_royalty(args.info); let caller = self.authorize_owner_action( args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), NFT_ADMIN_DOMAIN, SET_TOKEN_ROYALTY_ACTION, @@ -411,7 +411,7 @@ mod drc721_collection { pub fn clear_token_royalty(&mut self, args: ClearTokenRoyaltyCall) { let caller = self.authorize_owner_action( args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), NFT_ADMIN_DOMAIN, CLEAR_TOKEN_ROYALTY_ACTION, diff --git a/standards/examples/multisig_controller/src/lib.rs b/standards/examples/multisig_controller/src/lib.rs index 7277a376..e5b18c15 100644 --- a/standards/examples/multisig_controller/src/lib.rs +++ b/standards/examples/multisig_controller/src/lib.rs @@ -370,7 +370,7 @@ mod multisig_controller { let Some(authorization) = authorization else { panic!("{}", error::UNAUTHORIZED); }; - let envelope = ActionEnvelope::new( + let envelope = ActionEnvelope::for_current_chain( abi::self_id(), MULTISIG_CONTROLLER_DOMAIN, action_id, @@ -397,7 +397,7 @@ mod multisig_controller { &self.authorizations, CallContext::current(), approvals, - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), MULTISIG_CONTROLLER_DOMAIN, action_id, diff --git a/standards/examples/proxy_counter/src/lib.rs b/standards/examples/proxy_counter/src/lib.rs index 75f9b2bd..15a8c6ed 100644 --- a/standards/examples/proxy_counter/src/lib.rs +++ b/standards/examples/proxy_counter/src/lib.rs @@ -165,7 +165,7 @@ mod proxy_counter { let previous = self.value(); let caller = self.authorize_admin_action( args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), PROXY_ADMIN_DOMAIN, SET_VALUE_ACTION, @@ -178,7 +178,7 @@ mod proxy_counter { pub fn prepare_upgrade(&mut self, args: PrepareUpgrade) { let caller = self.authorize_admin_action( args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), PROXY_ADMIN_DOMAIN, PREPARE_UPGRADE_ACTION, @@ -199,7 +199,7 @@ mod proxy_counter { pub fn activate_upgrade(&mut self, args: AdminCall) -> Vec { let caller = self.authorize_admin_action( args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), PROXY_ADMIN_DOMAIN, ACTIVATE_UPGRADE_ACTION, @@ -214,7 +214,7 @@ mod proxy_counter { pub fn cancel_pending_upgrade(&mut self, args: AdminCall) { let caller = self.authorize_admin_action( args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), PROXY_ADMIN_DOMAIN, CANCEL_UPGRADE_ACTION, @@ -227,7 +227,7 @@ mod proxy_counter { pub fn rollback(&mut self, args: AdminCall) { let caller = self.authorize_admin_action( args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), PROXY_ADMIN_DOMAIN, ROLLBACK_ACTION, @@ -240,7 +240,7 @@ mod proxy_counter { pub fn finalize_rollback_window(&mut self, args: AdminCall) { let caller = self.authorize_admin_action( args.authorization.as_ref(), - ActionEnvelope::new( + ActionEnvelope::for_current_chain( abi::self_id(), PROXY_ADMIN_DOMAIN, FINALIZE_ROLLBACK_ACTION, From 01a4cb5c702cc03d46475751006e14b1486b7ccc Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Wed, 27 May 2026 13:25:51 +0200 Subject: [PATCH 33/35] standards: add license headers --- .../examples/build_signed_authorizations.rs | 6 ++++++ .../examples/encode_local_smoke_args.rs | 6 ++++++ .../dusk-contract-standards/src/access/access_control.rs | 6 ++++++ standards/dusk-contract-standards/src/access/events.rs | 6 ++++++ standards/dusk-contract-standards/src/access/mod.rs | 6 ++++++ standards/dusk-contract-standards/src/access/ownable.rs | 6 ++++++ standards/dusk-contract-standards/src/access/owner_set.rs | 6 ++++++ standards/dusk-contract-standards/src/access/pausable.rs | 6 ++++++ standards/dusk-contract-standards/src/auth/mod.rs | 6 ++++++ standards/dusk-contract-standards/src/core/context.rs | 6 ++++++ standards/dusk-contract-standards/src/core/error.rs | 6 ++++++ standards/dusk-contract-standards/src/core/mod.rs | 6 ++++++ standards/dusk-contract-standards/src/core/nonce.rs | 6 ++++++ standards/dusk-contract-standards/src/core/principal.rs | 6 ++++++ standards/dusk-contract-standards/src/core/replay.rs | 6 ++++++ .../dusk-contract-standards/src/governance/controller.rs | 6 ++++++ standards/dusk-contract-standards/src/governance/mod.rs | 6 ++++++ .../dusk-contract-standards/src/governance/multisig.rs | 6 ++++++ .../src/governance/multisig_controller.rs | 6 ++++++ .../dusk-contract-standards/src/governance/timelock.rs | 6 ++++++ standards/dusk-contract-standards/src/lib.rs | 6 ++++++ standards/dusk-contract-standards/src/proxy/mod.rs | 6 ++++++ standards/dusk-contract-standards/src/proxy/state_store.rs | 6 ++++++ standards/dusk-contract-standards/src/proxy/upgrade.rs | 6 ++++++ standards/dusk-contract-standards/src/security/mod.rs | 6 ++++++ .../src/security/reentrancy_guard.rs | 6 ++++++ standards/dusk-contract-standards/src/token/drc20/events.rs | 6 ++++++ .../dusk-contract-standards/src/token/drc20/extensions.rs | 6 ++++++ standards/dusk-contract-standards/src/token/drc20/mod.rs | 6 ++++++ standards/dusk-contract-standards/src/token/drc20/types.rs | 6 ++++++ .../dusk-contract-standards/src/token/drc721/events.rs | 6 ++++++ standards/dusk-contract-standards/src/token/drc721/mod.rs | 6 ++++++ .../dusk-contract-standards/src/token/drc721/royalty.rs | 6 ++++++ standards/dusk-contract-standards/src/token/drc721/types.rs | 6 ++++++ standards/dusk-contract-standards/src/token/mod.rs | 6 ++++++ standards/dusk-contract-standards/tests/data_driver_fuzz.rs | 6 ++++++ standards/dusk-contract-standards/tests/examples_vm.rs | 6 ++++++ standards/dusk-contract-standards/tests/primitives.rs | 6 ++++++ standards/dusk-contract-standards/tests/properties.rs | 6 ++++++ standards/examples/authorization_counter/src/lib.rs | 6 ++++++ standards/examples/drc20_roles_pausable/src/lib.rs | 6 ++++++ standards/examples/drc721_collection/src/lib.rs | 6 ++++++ standards/examples/multisig_controller/src/lib.rs | 6 ++++++ standards/examples/proxy_counter/src/lib.rs | 6 ++++++ 44 files changed, 264 insertions(+) diff --git a/standards/dusk-contract-standards/examples/build_signed_authorizations.rs b/standards/dusk-contract-standards/examples/build_signed_authorizations.rs index 1dd33059..05345c8b 100644 --- a/standards/dusk-contract-standards/examples/build_signed_authorizations.rs +++ b/standards/dusk-contract-standards/examples/build_signed_authorizations.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + use std::vec::Vec; use dusk_contract_standards::auth::{ diff --git a/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs index de8c0116..fb78583c 100644 --- a/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs +++ b/standards/dusk-contract-standards/examples/encode_local_smoke_args.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + use std::env; use std::error::Error; use std::string::String; diff --git a/standards/dusk-contract-standards/src/access/access_control.rs b/standards/dusk-contract-standards/src/access/access_control.rs index d9f2b45a..d1efe87a 100644 --- a/standards/dusk-contract-standards/src/access/access_control.rs +++ b/standards/dusk-contract-standards/src/access/access_control.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Role-based access control. use alloc::collections::{BTreeMap, BTreeSet}; diff --git a/standards/dusk-contract-standards/src/access/events.rs b/standards/dusk-contract-standards/src/access/events.rs index faf8e154..670b1c13 100644 --- a/standards/dusk-contract-standards/src/access/events.rs +++ b/standards/dusk-contract-standards/src/access/events.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Access-control and emergency-stop event payloads. use bytecheck::CheckBytes; diff --git a/standards/dusk-contract-standards/src/access/mod.rs b/standards/dusk-contract-standards/src/access/mod.rs index 99e5d047..7d11135c 100644 --- a/standards/dusk-contract-standards/src/access/mod.rs +++ b/standards/dusk-contract-standards/src/access/mod.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Access-control modules. pub mod access_control; diff --git a/standards/dusk-contract-standards/src/access/ownable.rs b/standards/dusk-contract-standards/src/access/ownable.rs index 57e0d855..f9d35c94 100644 --- a/standards/dusk-contract-standards/src/access/ownable.rs +++ b/standards/dusk-contract-standards/src/access/ownable.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Ownership primitives. use crate::auth::{ diff --git a/standards/dusk-contract-standards/src/access/owner_set.rs b/standards/dusk-contract-standards/src/access/owner_set.rs index 378ba6a6..1ff5b0e6 100644 --- a/standards/dusk-contract-standards/src/access/owner_set.rs +++ b/standards/dusk-contract-standards/src/access/owner_set.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Multi-principal ownership. use alloc::collections::BTreeSet; diff --git a/standards/dusk-contract-standards/src/access/pausable.rs b/standards/dusk-contract-standards/src/access/pausable.rs index f1a5805c..5aa2412b 100644 --- a/standards/dusk-contract-standards/src/access/pausable.rs +++ b/standards/dusk-contract-standards/src/access/pausable.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Emergency stop primitive. use crate::core::error; diff --git a/standards/dusk-contract-standards/src/auth/mod.rs b/standards/dusk-contract-standards/src/auth/mod.rs index a06ed095..5f359b99 100644 --- a/standards/dusk-contract-standards/src/auth/mod.rs +++ b/standards/dusk-contract-standards/src/auth/mod.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Replay-protected signed authorization helpers. use alloc::vec::Vec; diff --git a/standards/dusk-contract-standards/src/core/context.rs b/standards/dusk-contract-standards/src/core/context.rs index 7ac09567..7b94b490 100644 --- a/standards/dusk-contract-standards/src/core/context.rs +++ b/standards/dusk-contract-standards/src/core/context.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Runtime call context helpers. use crate::core::principal::Principal; diff --git a/standards/dusk-contract-standards/src/core/error.rs b/standards/dusk-contract-standards/src/core/error.rs index a508866f..5874cf31 100644 --- a/standards/dusk-contract-standards/src/core/error.rs +++ b/standards/dusk-contract-standards/src/core/error.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Stable error strings used by the reusable primitives. pub const ALREADY_INITIALIZED: &str = "DuskStandards: already initialized"; diff --git a/standards/dusk-contract-standards/src/core/mod.rs b/standards/dusk-contract-standards/src/core/mod.rs index a7d8d7b0..14d2602a 100644 --- a/standards/dusk-contract-standards/src/core/mod.rs +++ b/standards/dusk-contract-standards/src/core/mod.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Shared identity, context, replay, and error primitives. pub mod context; diff --git a/standards/dusk-contract-standards/src/core/nonce.rs b/standards/dusk-contract-standards/src/core/nonce.rs index ee095f3a..d791ccd4 100644 --- a/standards/dusk-contract-standards/src/core/nonce.rs +++ b/standards/dusk-contract-standards/src/core/nonce.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Monotonic nonce management for replay-protected authorizations. use alloc::collections::BTreeMap; diff --git a/standards/dusk-contract-standards/src/core/principal.rs b/standards/dusk-contract-standards/src/core/principal.rs index ad8df8d9..15a531e9 100644 --- a/standards/dusk-contract-standards/src/core/principal.rs +++ b/standards/dusk-contract-standards/src/core/principal.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Dusk principal identity. use alloc::vec::Vec; diff --git a/standards/dusk-contract-standards/src/core/replay.rs b/standards/dusk-contract-standards/src/core/replay.rs index 99371d9d..c2edcbb0 100644 --- a/standards/dusk-contract-standards/src/core/replay.rs +++ b/standards/dusk-contract-standards/src/core/replay.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Replay protection for signed authorizations. use alloc::collections::BTreeSet; diff --git a/standards/dusk-contract-standards/src/governance/controller.rs b/standards/dusk-contract-standards/src/governance/controller.rs index 30cd0910..ed99fabf 100644 --- a/standards/dusk-contract-standards/src/governance/controller.rs +++ b/standards/dusk-contract-standards/src/governance/controller.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Role-gated timelock controller. use alloc::vec::Vec; diff --git a/standards/dusk-contract-standards/src/governance/mod.rs b/standards/dusk-contract-standards/src/governance/mod.rs index 3198ea15..9f0cae8b 100644 --- a/standards/dusk-contract-standards/src/governance/mod.rs +++ b/standards/dusk-contract-standards/src/governance/mod.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Governance and scheduling primitives. pub mod controller; diff --git a/standards/dusk-contract-standards/src/governance/multisig.rs b/standards/dusk-contract-standards/src/governance/multisig.rs index 85efe8e9..6ba2f88c 100644 --- a/standards/dusk-contract-standards/src/governance/multisig.rs +++ b/standards/dusk-contract-standards/src/governance/multisig.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Threshold multisig authorization for Dusk principals. use alloc::collections::BTreeSet; diff --git a/standards/dusk-contract-standards/src/governance/multisig_controller.rs b/standards/dusk-contract-standards/src/governance/multisig_controller.rs index b98b58d6..688c9026 100644 --- a/standards/dusk-contract-standards/src/governance/multisig_controller.rs +++ b/standards/dusk-contract-standards/src/governance/multisig_controller.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Standalone multisig controller state machine. use alloc::collections::BTreeMap; diff --git a/standards/dusk-contract-standards/src/governance/timelock.rs b/standards/dusk-contract-standards/src/governance/timelock.rs index a66d204a..3fb6bb5b 100644 --- a/standards/dusk-contract-standards/src/governance/timelock.rs +++ b/standards/dusk-contract-standards/src/governance/timelock.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Timelock operation scheduler. use alloc::collections::BTreeMap; diff --git a/standards/dusk-contract-standards/src/lib.rs b/standards/dusk-contract-standards/src/lib.rs index 43393465..7ca320a9 100644 --- a/standards/dusk-contract-standards/src/lib.rs +++ b/standards/dusk-contract-standards/src/lib.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Dusk-native reusable contract standards and examples. //! //! This crate intentionally does not provide EVM API compatibility. It ports diff --git a/standards/dusk-contract-standards/src/proxy/mod.rs b/standards/dusk-contract-standards/src/proxy/mod.rs index eea204be..2f478a8c 100644 --- a/standards/dusk-contract-standards/src/proxy/mod.rs +++ b/standards/dusk-contract-standards/src/proxy/mod.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Dusk-native proxy and upgrade primitives. pub mod state_store; diff --git a/standards/dusk-contract-standards/src/proxy/state_store.rs b/standards/dusk-contract-standards/src/proxy/state_store.rs index 28d1b472..1485fa89 100644 --- a/standards/dusk-contract-standards/src/proxy/state_store.rs +++ b/standards/dusk-contract-standards/src/proxy/state_store.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Proxy-owned state store. use alloc::collections::BTreeMap; diff --git a/standards/dusk-contract-standards/src/proxy/upgrade.rs b/standards/dusk-contract-standards/src/proxy/upgrade.rs index 5fab888e..69924e4a 100644 --- a/standards/dusk-contract-standards/src/proxy/upgrade.rs +++ b/standards/dusk-contract-standards/src/proxy/upgrade.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Upgrade admin state machine. use alloc::vec::Vec; diff --git a/standards/dusk-contract-standards/src/security/mod.rs b/standards/dusk-contract-standards/src/security/mod.rs index 32d51ef2..d7115562 100644 --- a/standards/dusk-contract-standards/src/security/mod.rs +++ b/standards/dusk-contract-standards/src/security/mod.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Security primitives. pub mod reentrancy_guard; diff --git a/standards/dusk-contract-standards/src/security/reentrancy_guard.rs b/standards/dusk-contract-standards/src/security/reentrancy_guard.rs index c570a409..2a860365 100644 --- a/standards/dusk-contract-standards/src/security/reentrancy_guard.rs +++ b/standards/dusk-contract-standards/src/security/reentrancy_guard.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Reentrancy guard. /// Reentrancy guard module. diff --git a/standards/dusk-contract-standards/src/token/drc20/events.rs b/standards/dusk-contract-standards/src/token/drc20/events.rs index 2c83454e..909852cf 100644 --- a/standards/dusk-contract-standards/src/token/drc20/events.rs +++ b/standards/dusk-contract-standards/src/token/drc20/events.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! DRC20 event payloads. use bytecheck::CheckBytes; diff --git a/standards/dusk-contract-standards/src/token/drc20/extensions.rs b/standards/dusk-contract-standards/src/token/drc20/extensions.rs index f92548a2..078e4707 100644 --- a/standards/dusk-contract-standards/src/token/drc20/extensions.rs +++ b/standards/dusk-contract-standards/src/token/drc20/extensions.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Optional DRC20 policy helpers. use alloc::collections::BTreeMap; diff --git a/standards/dusk-contract-standards/src/token/drc20/mod.rs b/standards/dusk-contract-standards/src/token/drc20/mod.rs index 4342d80b..bd633e28 100644 --- a/standards/dusk-contract-standards/src/token/drc20/mod.rs +++ b/standards/dusk-contract-standards/src/token/drc20/mod.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! DRC20 fungible token primitive. pub mod events; diff --git a/standards/dusk-contract-standards/src/token/drc20/types.rs b/standards/dusk-contract-standards/src/token/drc20/types.rs index 82e1e103..4b54c80c 100644 --- a/standards/dusk-contract-standards/src/token/drc20/types.rs +++ b/standards/dusk-contract-standards/src/token/drc20/types.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! DRC20 rkyv-compatible call types. use alloc::string::String; diff --git a/standards/dusk-contract-standards/src/token/drc721/events.rs b/standards/dusk-contract-standards/src/token/drc721/events.rs index 3f952ce5..001d98d1 100644 --- a/standards/dusk-contract-standards/src/token/drc721/events.rs +++ b/standards/dusk-contract-standards/src/token/drc721/events.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! DRC721 event payloads. use bytecheck::CheckBytes; diff --git a/standards/dusk-contract-standards/src/token/drc721/mod.rs b/standards/dusk-contract-standards/src/token/drc721/mod.rs index afbd211c..c43d8938 100644 --- a/standards/dusk-contract-standards/src/token/drc721/mod.rs +++ b/standards/dusk-contract-standards/src/token/drc721/mod.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! DRC721 non-fungible token primitive. pub mod events; diff --git a/standards/dusk-contract-standards/src/token/drc721/royalty.rs b/standards/dusk-contract-standards/src/token/drc721/royalty.rs index e6bb41fb..d5124feb 100644 --- a/standards/dusk-contract-standards/src/token/drc721/royalty.rs +++ b/standards/dusk-contract-standards/src/token/drc721/royalty.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Optional DRC721 royalty policy helper. use alloc::collections::BTreeMap; diff --git a/standards/dusk-contract-standards/src/token/drc721/types.rs b/standards/dusk-contract-standards/src/token/drc721/types.rs index d031da6f..f312b6b3 100644 --- a/standards/dusk-contract-standards/src/token/drc721/types.rs +++ b/standards/dusk-contract-standards/src/token/drc721/types.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! DRC721 rkyv-compatible call types. use alloc::string::String; diff --git a/standards/dusk-contract-standards/src/token/mod.rs b/standards/dusk-contract-standards/src/token/mod.rs index cefc0666..5c6f5418 100644 --- a/standards/dusk-contract-standards/src/token/mod.rs +++ b/standards/dusk-contract-standards/src/token/mod.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Dusk token primitives. pub mod drc20; diff --git a/standards/dusk-contract-standards/tests/data_driver_fuzz.rs b/standards/dusk-contract-standards/tests/data_driver_fuzz.rs index 3266e024..0e25b7be 100644 --- a/standards/dusk-contract-standards/tests/data_driver_fuzz.rs +++ b/standards/dusk-contract-standards/tests/data_driver_fuzz.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + use std::collections::BTreeMap; use std::fs; use std::path::PathBuf; diff --git a/standards/dusk-contract-standards/tests/examples_vm.rs b/standards/dusk-contract-standards/tests/examples_vm.rs index b5b9512c..86d430e7 100644 --- a/standards/dusk-contract-standards/tests/examples_vm.rs +++ b/standards/dusk-contract-standards/tests/examples_vm.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + use std::fs; use std::path::{Path, PathBuf}; diff --git a/standards/dusk-contract-standards/tests/primitives.rs b/standards/dusk-contract-standards/tests/primitives.rs index 408e1c4d..f54e603d 100644 --- a/standards/dusk-contract-standards/tests/primitives.rs +++ b/standards/dusk-contract-standards/tests/primitives.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + use std::panic::{catch_unwind, AssertUnwindSafe}; use dusk_contract_standards::access::{ diff --git a/standards/dusk-contract-standards/tests/properties.rs b/standards/dusk-contract-standards/tests/properties.rs index 80fa0644..4f15c37f 100644 --- a/standards/dusk-contract-standards/tests/properties.rs +++ b/standards/dusk-contract-standards/tests/properties.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + use std::collections::{BTreeMap, BTreeSet}; use std::panic::{catch_unwind, AssertUnwindSafe}; diff --git a/standards/examples/authorization_counter/src/lib.rs b/standards/examples/authorization_counter/src/lib.rs index 5d24c691..4a19cff8 100644 --- a/standards/examples/authorization_counter/src/lib.rs +++ b/standards/examples/authorization_counter/src/lib.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Example counter controlled by replay-protected Dusk authorizations. #![no_std] diff --git a/standards/examples/drc20_roles_pausable/src/lib.rs b/standards/examples/drc20_roles_pausable/src/lib.rs index d034cff5..e30ef9b8 100644 --- a/standards/examples/drc20_roles_pausable/src/lib.rs +++ b/standards/examples/drc20_roles_pausable/src/lib.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Example DRC20 composed from reusable Dusk contract standards modules. #![no_std] diff --git a/standards/examples/drc721_collection/src/lib.rs b/standards/examples/drc721_collection/src/lib.rs index b9bc4d96..bc4ccbac 100644 --- a/standards/examples/drc721_collection/src/lib.rs +++ b/standards/examples/drc721_collection/src/lib.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Example DRC721 composed from reusable Dusk contract standards modules. #![no_std] diff --git a/standards/examples/multisig_controller/src/lib.rs b/standards/examples/multisig_controller/src/lib.rs index e5b18c15..2007cb8b 100644 --- a/standards/examples/multisig_controller/src/lib.rs +++ b/standards/examples/multisig_controller/src/lib.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Forge reference multisig controller for Dusk standards contracts. #![no_std] diff --git a/standards/examples/proxy_counter/src/lib.rs b/standards/examples/proxy_counter/src/lib.rs index 15a8c6ed..a4553ec6 100644 --- a/standards/examples/proxy_counter/src/lib.rs +++ b/standards/examples/proxy_counter/src/lib.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + //! Dusk-native proxy/state-store example. #![no_std] From 2a1c9df4ead2fee48b0d133d97f7a27bd7c12cc0 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Wed, 27 May 2026 13:44:59 +0200 Subject: [PATCH 34/35] standards: isolate standards workspace from legacy contracts --- .github/workflows/ci.yml | 60 + Cargo.lock | 772 +--- Cargo.toml | 41 +- Makefile | 38 +- .../dusk-contract-standards-audit-grade.sh | 9 +- .../dusk-contract-standards-local-smoke.sh | 10 +- standards/Cargo.lock | 3106 +++++++++++++++++ standards/Cargo.toml | 46 + standards/LICENSE | 373 ++ standards/dusk-contract-standards/Cargo.toml | 21 +- .../examples/authorization_counter/Cargo.toml | 12 +- .../examples/drc20_roles_pausable/Cargo.toml | 12 +- .../examples/drc721_collection/Cargo.toml | 12 +- .../examples/multisig_controller/Cargo.toml | 12 +- standards/examples/proxy_counter/Cargo.toml | 12 +- 15 files changed, 3788 insertions(+), 748 deletions(-) create mode 100644 standards/Cargo.lock create mode 100644 standards/Cargo.toml create mode 100644 standards/LICENSE diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04ec6c45..15589b5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,50 @@ on: - main jobs: + changes: + name: Detect changed contract areas + runs-on: core + outputs: + legacy: ${{ steps.filter.outputs.legacy }} + standards: ${{ steps.filter.outputs.standards }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - id: filter + shell: bash + run: | + set -euo pipefail + + legacy=false + standards=false + + if [[ "${{ github.event_name }}" == "push" ]]; then + legacy=true + standards=true + else + files="$(git diff --name-only \ + "${{ github.event.pull_request.base.sha }}" \ + "${{ github.event.pull_request.head.sha }}")" + + while IFS= read -r file; do + [[ -z "$file" ]] && continue + case "$file" in + genesis/*|tests/*|bin/*|scripts/setup-compiler.sh|scripts/download-rusk.sh|scripts/build-test-contracts.sh) + legacy=true + ;; + esac + case "$file" in + standards/*|docs/dusk-contract-standards*|scripts/dusk-contract-standards-*|Cargo.toml|Cargo.lock|Makefile|README.md|rust-toolchain.toml|.github/workflows/ci.yml|.github/workflows/standards-hardening.yml) + standards=true + ;; + esac + done <<< "$files" + fi + + echo "legacy=$legacy" >> "$GITHUB_OUTPUT" + echo "standards=$standards" >> "$GITHUB_OUTPUT" + dusk_analyzer: name: Dusk Analyzer runs-on: core @@ -13,9 +57,12 @@ jobs: - uses: dsherret/rust-toolchain-file@v1 - run: cargo install --git https://github.com/dusk-network/cargo-dusk-analyzer - run: cargo dusk-analyzer + - run: cargo dusk-analyzer --manifest-path standards/Cargo.toml clippy: name: Clippy check + needs: changes + if: needs.changes.outputs.legacy == 'true' runs-on: core steps: - uses: actions/checkout@v4 @@ -25,9 +72,22 @@ jobs: test: name: Run Tests + needs: changes + if: needs.changes.outputs.legacy == 'true' runs-on: core steps: - uses: actions/checkout@v4 - uses: dsherret/rust-toolchain-file@v1 - run: make keys - run: make test + + standards: + name: Standards CI + needs: changes + if: needs.changes.outputs.standards == 'true' + runs-on: core + steps: + - uses: actions/checkout@v4 + - uses: dsherret/rust-toolchain-file@v1 + - run: rustup target add wasm32-unknown-unknown + - run: make standards-ci diff --git a/Cargo.lock b/Cargo.lock index bf6033c0..7ddf47c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,7 +26,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures 0.2.17", + "cpufeatures", ] [[package]] @@ -127,9 +127,6 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] [[package]] name = "ark-bn254" @@ -156,8 +153,8 @@ dependencies = [ "ark-std", "blake2", "derivative", - "digest 0.10.7", - "sha2 0.10.9", + "digest", + "sha2", ] [[package]] @@ -188,7 +185,7 @@ dependencies = [ "ark-serialize", "ark-std", "derivative", - "digest 0.10.7", + "digest", "itertools 0.10.5", "num-bigint", "num-traits", @@ -267,7 +264,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ "ark-serialize-derive", "ark-std", - "digest 0.10.7", + "digest", "num-bigint", ] @@ -301,7 +298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand 0.8.6", + "rand", ] [[package]] @@ -316,32 +313,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "authorization-counter" -version = "0.1.0" -dependencies = [ - "bytecheck", - "dusk-contract-standards", - "dusk-core", - "dusk-data-driver", - "dusk-forge", - "rkyv", - "serde", - "serde_json", -] - [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bincode" version = "1.3.3" @@ -351,37 +328,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitcoin-io" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" - -[[package]] -name = "bitcoin_hashes" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed83caece3afc59919481b33b472e1432d1abc4641ed9100be142ef5110b406" -dependencies = [ - "bitcoin-io", - "hex-conservative", -] - [[package]] name = "bitflags" version = "2.11.1" @@ -406,41 +352,31 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] name = "blake2b_simd" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" dependencies = [ "arrayref", "arrayvec", - "constant_time_eq 0.3.1", + "constant_time_eq 0.4.2", ] [[package]] name = "blake3" -version = "1.8.5" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", - "constant_time_eq 0.4.2", - "cpufeatures 0.3.0", -] - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", + "constant_time_eq 0.3.1", ] [[package]] @@ -454,31 +390,31 @@ dependencies = [ [[package]] name = "bls12_381-bls" -version = "0.6.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c61c05b7bfa0cff7b7f6bf000589de5541fc03b702adadfbcfbb6d782a2d643" +checksum = "6478fd9a5bbd62d60abf5a589cd68ad6c57d0ea592c2c93b26643fe9e5873883" dependencies = [ - "bs58", "bytecheck", - "dusk-bls12_381", + "dusk-bls12_381 0.13.0", "dusk-bytes", "ff", - "rand_core 0.6.4", + "rand_core", "rkyv", - "serde", - "sha2 0.9.9", "zeroize", ] [[package]] -name = "blst" -version = "0.3.16" +name = "bls12_381-bls" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +checksum = "d4e6b7c24701ff5167f336478ec29dc176455f5396f62b8f0951cc1fdddf249b" dependencies = [ - "cc", - "glob", - "threadpool", + "bytecheck", + "dusk-bls12_381 0.14.2", + "dusk-bytes", + "ff", + "rand_core", + "rkyv", "zeroize", ] @@ -492,12 +428,6 @@ dependencies = [ "rkyv", ] -[[package]] -name = "bs58" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" - [[package]] name = "bumpalo" version = "3.20.3" @@ -538,22 +468,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -[[package]] -name = "c-kzg" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16507ccb84a57d44c40cf9b8e6741eb560bc44122b325dcff2fdbfa4959b2110" -dependencies = [ - "arbitrary", - "blst", - "cc", - "glob", - "hex", - "libc", - "once_cell", - "serde", -] - [[package]] name = "cast" version = "0.3.0" @@ -584,16 +498,6 @@ dependencies = [ "rkyv", ] -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "num-traits", - "serde", -] - [[package]] name = "ciborium" version = "0.2.2" @@ -700,15 +604,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - [[package]] name = "cranelift-bforest" version = "0.107.2" @@ -890,9 +785,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crumbles" -version = "0.4.0" +version = "0.3.2-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3476b7dda7a457e938e4b958aa7b8db66eb3f6bc72894454a86f99bab3dea2" +checksum = "d67910efb2241f4d8f2a184c18db43be0f916b0f85e742376c6b3c9614455b62" dependencies = [ "libc", "rangemap", @@ -911,7 +806,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "typenum", ] @@ -924,15 +819,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - [[package]] name = "derivative" version = "2.2.0" @@ -955,33 +841,13 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "crypto-common", "subtle", ] @@ -1018,31 +884,21 @@ dependencies = [ ] [[package]] -name = "drc20-roles-pausable" -version = "0.1.0" -dependencies = [ - "bytecheck", - "dusk-contract-standards", - "dusk-core", - "dusk-data-driver", - "dusk-forge", - "rkyv", - "serde", - "serde_json", -] - -[[package]] -name = "drc721-collection" -version = "0.1.0" +name = "dusk-bls12_381" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2bd68f9bed48c2e64860e1fd1f5dbb7733eb07d0288fbdcc3763678c742139" dependencies = [ + "blake2b_simd", "bytecheck", - "dusk-contract-standards", - "dusk-core", - "dusk-data-driver", - "dusk-forge", + "dusk-bytes", + "ff", + "group", + "pairing", + "rand_core", "rkyv", - "serde", - "serde_json", + "subtle", + "zeroize", ] [[package]] @@ -1053,15 +909,12 @@ checksum = "633aa835dd9e4db7ad5dc94a3f95d44ff5c6856ec3c83c15eb75366cfb5ee68a" dependencies = [ "blake2b_simd", "bytecheck", - "digest 0.9.0", "dusk-bytes", "ff", "group", - "hex", "pairing", - "rand_core 0.6.4", + "rand_core", "rkyv", - "serde", "subtle", "zeroize", ] @@ -1075,39 +928,19 @@ dependencies = [ "derive-hex", ] -[[package]] -name = "dusk-contract-standards" -version = "0.1.0" -dependencies = [ - "bytecheck", - "dusk-core", - "dusk-data-driver", - "dusk-forge", - "dusk-vm", - "proptest", - "rand 0.8.6", - "rkyv", - "serde", - "serde_json", - "serde_with", - "time", -] - [[package]] name = "dusk-core" -version = "1.6.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7053f16922b6cce10036bda331f5dd373a64ff66972604c2fc3bca594d2158" +checksum = "7972fd42fb3fe67ea5492909f7715f8fe373af98a02943b7e24b5262fbd4678a" dependencies = [ "ark-bn254", "ark-groth16", "ark-relations", "ark-serialize", - "blake2b_simd", - "bls12_381-bls", + "bls12_381-bls 0.5.1", "bytecheck", - "c-kzg", - "dusk-bls12_381", + "dusk-bls12_381 0.14.2", "dusk-bytes", "dusk-jubjub", "dusk-plonk", @@ -1118,48 +951,9 @@ dependencies = [ "phoenix-core", "piecrust-uplink", "poseidon-merkle", - "rand 0.8.6", - "rkyv", - "serde", - "serde_with", - "sha2 0.10.9", -] - -[[package]] -name = "dusk-data-driver" -version = "0.3.2-alpha.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191e9b51e34ecb1a22d79de6df7e6e2971121e0069d60d738df9059837f46a1c" -dependencies = [ - "bytecheck", - "dusk-core", - "dusk-wasmtime", - "parking_lot", + "rand", "rkyv", - "serde", - "serde_json", -] - -[[package]] -name = "dusk-forge" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31edc24ce1a8cfb6ee61d3941145c4b475117c2c0450a6342dc30907c61a5e93" -dependencies = [ - "dusk-forge-contract", - "serde", - "serde_json", -] - -[[package]] -name = "dusk-forge-contract" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9f361a4eddded990e22c61dece5282257b141bf4d4d8cd2f4fda5a17618791" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "sha2", ] [[package]] @@ -1171,14 +965,12 @@ dependencies = [ "bitvec", "blake2b_simd", "bytecheck", - "dusk-bls12_381", + "dusk-bls12_381 0.14.2", "dusk-bytes", "ff", "group", - "hex", - "rand_core 0.6.4", + "rand_core", "rkyv", - "serde", "subtle", "zeroize", ] @@ -1196,14 +988,13 @@ dependencies = [ [[package]] name = "dusk-plonk" -version = "0.22.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949ec126023c47910d93a0fcb35ef20243f75eb7a726fcaa6541d8c44fadb66d" +checksum = "c425aa88fb1fd44c4bb72c0fe6abd37eae9de5b1f42d375d16c95aa1efceb327" dependencies = [ - "blake2b_simd", "bytecheck", "cfg-if", - "dusk-bls12_381", + "dusk-bls12_381 0.14.2", "dusk-bytes", "dusk-jubjub", "ff", @@ -1212,19 +1003,19 @@ dependencies = [ "merlin", "miniz_oxide", "msgpacker", - "rand_core 0.6.4", + "rand_core", "rkyv", - "sha2 0.10.9", + "sha2", "zeroize", ] [[package]] name = "dusk-poseidon" -version = "0.42.0-rc.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb428dc6d7aac399fd2c5929fd6e0b2c7b83392d3eb8c9a7b29a916d0087063d" +checksum = "f66925505adc9db3671ebed59f5f2f196306efbe6ede8de9761e7830af5ddb84" dependencies = [ - "dusk-bls12_381", + "dusk-bls12_381 0.14.2", "dusk-jubjub", "dusk-plonk", "dusk-safe", @@ -1241,23 +1032,19 @@ dependencies = [ [[package]] name = "dusk-vm" -version = "1.6.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4e78bea4ffcca10eb95f687d7169e94c06e0246dd4023d9f76b213877b2e123" +checksum = "9970b6794ad562dadd2949943655defde02fe21090514c0f88729c709c86a31e" dependencies = [ "blake2b_simd", "blake3", - "bytecheck", - "c-kzg", "dusk-bytes", "dusk-core", "dusk-poseidon", "lru", "piecrust", "rkyv", - "secp256k1", "serde", - "sha2 0.10.9", "sha3", "tracing", "wasmparser 0.240.0", @@ -1265,9 +1052,9 @@ dependencies = [ [[package]] name = "dusk-wallet-core" -version = "1.6.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9539de83eee7581c707e57301e6e9d7488e318dfda2a89c9bd1800e48d486686" +checksum = "f68eafa8ccc68368f904889b86bbcf2298cad90c856ce13c0db96649503e2114" dependencies = [ "blake3", "bytecheck", @@ -1276,10 +1063,10 @@ dependencies = [ "dusk-core", "ff", "hkdf", - "rand 0.8.6", - "rand_chacha 0.3.1", + "rand", + "rand_chacha", "rkyv", - "sha2 0.10.9", + "sha2", "zeroize", ] @@ -1433,7 +1220,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -1443,24 +1230,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "funty" version = "2.0.0" @@ -1512,18 +1287,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - [[package]] name = "getrandom" version = "0.4.2" @@ -1532,7 +1295,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi 6.0.0", + "r-efi", "wasip2", "wasip3", ] @@ -1558,12 +1321,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "group" version = "0.13.0" @@ -1571,7 +1328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -1628,7 +1385,9 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash 0.1.5", + "allocator-api2", + "equivalent", + "foldhash", "serde", ] @@ -1637,11 +1396,6 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] [[package]] name = "heck" @@ -1660,18 +1414,6 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] - -[[package]] -name = "hex-conservative" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" -dependencies = [ - "arrayvec", -] [[package]] name = "hkdf" @@ -1688,7 +1430,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -1784,32 +1526,29 @@ dependencies = [ [[package]] name = "jubjub-elgamal" -version = "0.5.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22c81bad2a54e37f6eadcdba35f35761b7dba5665af26ea16f29f0d9372fb64f" +checksum = "0c2b02b5859a6ebd729e4b7f9970963d3f3be4f0aebc700d0b2d392b9fd8201c" dependencies = [ - "dusk-bytes", "dusk-jubjub", "dusk-plonk", ] [[package]] name = "jubjub-schnorr" -version = "0.7.0-rc.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a7208d3397238bd5fce848642b923f7aebed6d58cd472ad72027ba68167167" +checksum = "008885a6d1b73ef18a827ad0faf7cf48d1c5a28d52a9e863b90a421a92799443" dependencies = [ - "bs58", "bytecheck", - "dusk-bls12_381", + "dusk-bls12_381 0.14.2", "dusk-bytes", "dusk-jubjub", "dusk-plonk", "dusk-poseidon", "ff", - "rand_core 0.6.4", + "rand_core", "rkyv", - "serde", "zeroize", ] @@ -1819,7 +1558,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures 0.2.17", + "cpufeatures", ] [[package]] @@ -1861,15 +1600,6 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.30" @@ -1878,11 +1608,11 @@ checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lru" -version = "0.16.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.16.1", + "hashbrown 0.15.5", ] [[package]] @@ -1935,7 +1665,7 @@ checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" dependencies = [ "byteorder", "keccak", - "rand_core 0.6.4", + "rand_core", "zeroize", ] @@ -1950,9 +1680,9 @@ dependencies = [ [[package]] name = "msgpacker" -version = "0.4.8" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134a72ec3a6d034e9c767344ce8aebde228eef4e1343fff3ea209993f12920b3" +checksum = "79f77b4108a99ed7e834b46df33537a644ea94863a98a9140017eda1d08249f6" dependencies = [ "msgpacker-derive", ] @@ -1967,20 +1697,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "multisig-controller" -version = "0.1.0" -dependencies = [ - "bytecheck", - "dusk-contract-standards", - "dusk-core", - "dusk-data-driver", - "dusk-forge", - "rkyv", - "serde", - "serde_json", -] - [[package]] name = "num-bigint" version = "0.4.6" @@ -1991,12 +1707,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-integer" version = "0.1.46" @@ -2015,16 +1725,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" version = "0.33.0" @@ -2073,29 +1773,6 @@ dependencies = [ "group", ] -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - [[package]] name = "paste" version = "1.0.15" @@ -2104,11 +1781,11 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "phoenix-circuits" -version = "0.8.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5583c3db8adc437f7854319a6a815666b95475797ba82d8cb5f9f5a9f18fe55" +checksum = "d42a8482526bf7bcb3a6d34ad171a26f495319a520b9eb8a68837d837e3b0cf9" dependencies = [ - "dusk-bls12_381", + "dusk-bls12_381 0.14.2", "dusk-bytes", "dusk-jubjub", "dusk-plonk", @@ -2121,41 +1798,38 @@ dependencies = [ [[package]] name = "phoenix-core" -version = "0.35.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1930c6dd9d1054cf0cd725325323a5381fe3854708a2cebe38ccab8699579e2e" +checksum = "5b45ed942d9d166f7a45d16630529d68d68cfde7f37191d4646dfbe4b350b9cc" dependencies = [ "aes-gcm", - "base64", - "bs58", + "bls12_381-bls 0.4.1", "bytecheck", - "dusk-bls12_381", + "dusk-bls12_381 0.14.2", "dusk-bytes", "dusk-jubjub", "dusk-poseidon", "ff", - "hex", "hkdf", "jubjub-elgamal", "jubjub-schnorr", - "rand 0.8.6", + "rand", "rkyv", - "serde", - "serde_with", - "sha2 0.10.9", + "sha2", "subtle", "zeroize", ] [[package]] name = "piecrust" -version = "0.30.0" +version = "0.29.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "768a69c4aac88337ec2c1b9ac1624d4c52bd50c6ecbdaed70b5854006d56fe01" +checksum = "ab8d02351245508a0d2497a27720d7ef1a15c8733275340b110a9c7789b1b22b" dependencies = [ "blake3", "bytecheck", "const-decoder", + "constant_time_eq 0.3.1", "crumbles", "dusk-merkle", "dusk-wasmtime", @@ -2163,7 +1837,7 @@ dependencies = [ "indexmap", "memmap2", "piecrust-uplink", - "rand 0.8.6", + "rand", "rkyv", "tempfile", "thiserror", @@ -2172,15 +1846,14 @@ dependencies = [ [[package]] name = "piecrust-uplink" -version = "0.20.0" +version = "0.19.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df8d2d3f5deb052f61c65d01d892c4babe604cd884cdd2fcef7f6da4392b476" +checksum = "97ff1da9d532178dc05a9b07f9457a1138a5f0b4253ea062a138a20cf3907ed7" dependencies = [ "bytecheck", "dlmalloc", "hex", "rkyv", - "serde", ] [[package]] @@ -2224,19 +1897,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "opaque-debug", "universal-hash", ] [[package]] name = "poseidon-merkle" -version = "0.9.0-rc.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95e801b94d059823f3dc9d2af14e2a7438863e4b23736e324ebb368991545d52" +checksum = "6316347b2cf759ff0303ad280008ac89a36ed9f970e4cdfd1585d8ba5ed60134" dependencies = [ "bytecheck", - "dusk-bls12_381", + "dusk-bls12_381 0.14.2", "dusk-bytes", "dusk-merkle", "dusk-plonk", @@ -2244,12 +1917,6 @@ dependencies = [ "rkyv", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2278,39 +1945,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proptest" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" -dependencies = [ - "bit-set", - "bit-vec", - "bitflags", - "num-traits", - "rand 0.9.4", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "proxy-counter" -version = "0.1.0" -dependencies = [ - "bytecheck", - "dusk-contract-standards", - "dusk-core", - "dusk-data-driver", - "dusk-forge", - "rkyv", - "serde", - "serde_json", -] - [[package]] name = "psm" version = "0.1.31" @@ -2341,12 +1975,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quote" version = "1.0.45" @@ -2356,12 +1984,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "r-efi" version = "6.0.0" @@ -2381,18 +2003,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", + "rand_chacha", + "rand_core", ] [[package]] @@ -2402,17 +2014,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", + "rand_core", ] [[package]] @@ -2424,24 +2026,6 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.5", -] - [[package]] name = "rangemap" version = "1.7.1" @@ -2468,15 +2052,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - [[package]] name = "redox_users" version = "0.4.6" @@ -2576,16 +2151,16 @@ dependencies = [ [[package]] name = "rusk-profile" -version = "1.6.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18347352b0f276f99c81995904c36302859d1def0a8355e61a489c4fd3f9554b" +checksum = "4f37ff8ed7398a5442f16e26f8acee801ef5fe42d8982b9cb586cb46f432db9d" dependencies = [ "blake3", "console", "dirs", "hex", "serde", - "sha2 0.10.9", + "sha2", "toml", "tracing", "version_check", @@ -2593,15 +2168,15 @@ dependencies = [ [[package]] name = "rusk-prover" -version = "1.6.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b6eb985682c51809742bc441bfecb7b821c16f50a2e5cb125c7e31e6d664a9" +checksum = "06ec33ea54518b45d9d26ec791ddc6430f96dcf75083e32170c3d87f7082f692" dependencies = [ "dusk-bytes", "dusk-core", "dusk-plonk", "once_cell", - "rand 0.8.6", + "rand", "rusk-profile", ] @@ -2652,18 +2227,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", -] - [[package]] name = "same-file" version = "1.0.6" @@ -2673,38 +2236,12 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "seahash" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" -[[package]] -name = "secp256k1" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" -dependencies = [ - "bitcoin_hashes", - "rand 0.9.4", - "secp256k1-sys", -] - -[[package]] -name = "secp256k1-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" -dependencies = [ - "cc", -] - [[package]] name = "semver" version = "1.0.28" @@ -2763,34 +2300,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "3.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" -dependencies = [ - "base64", - "chrono", - "hex", - "serde", - "serde_derive", - "serde_json", - "time", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "sha2" version = "0.10.9" @@ -2798,8 +2307,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", + "cpufeatures", + "digest", ] [[package]] @@ -2808,7 +2317,7 @@ version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ - "digest 0.10.7", + "digest", "keccak", ] @@ -2864,7 +2373,7 @@ dependencies = [ "dusk-vm", "dusk-wallet-core", "ff", - "rand 0.8.6", + "rand", "rkyv", "rusk-prover", ] @@ -2961,34 +2470,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] -name = "time" -version = "0.3.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - [[package]] name = "tinytemplate" version = "1.2.1" @@ -3087,7 +2568,7 @@ dependencies = [ "dusk-core", "dusk-vm", "ff", - "rand 0.8.6", + "rand", "ringbuffer", "rkyv", "rusk-profile", @@ -3100,12 +2581,6 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -3150,15 +2625,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index c1a95530..616438b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,5 @@ [workspace] members = [ - # Dusk-native reusable contract primitives - "standards/dusk-contract-standards", - "standards/examples/authorization_counter", - "standards/examples/drc20_roles_pausable", - "standards/examples/drc721_collection", - "standards/examples/multisig_controller", - "standards/examples/proxy_counter", - # Test contracts "tests/alice", "tests/bob", @@ -22,20 +14,15 @@ members = [ resolver = "2" [workspace.dependencies] -# Dusk dependencies -dusk-core = "1.6.0" +# Dusk dependencies used by the legacy genesis/test contracts. The standards +# crates pin their Aegis/Forge dependencies in their own manifests so they do +# not force genesis contracts onto a compiler-incompatible dependency graph. +dusk-core = "=1.4.0" dusk-bytes = "0.1.7" -dusk-data-driver = "0.3.2-alpha.1" -dusk-forge = "0.3.0" -dusk-vm = "1.6.0" -dusk-wallet-core = "1.6.0" -dusk-wasmtime = { version = "21.0.0-alpha", default-features = false, features = [ - "cranelift", - "runtime", - "parallel-compilation", -] } -rusk-profile = "1.6.0" -rusk-prover = "1.6.0" +dusk-vm = "=1.4.3" +dusk-wallet-core = "=1.4.0" +rusk-profile = "=1.4.0" +rusk-prover = "=1.3.0" # Other dependencies bytecheck = { version = "0.6.12", default-features = false } @@ -45,18 +32,6 @@ rand = { version = "0.8.5", default-features = false } parking_lot = "0.12.3" ringbuffer = "0.15" rkyv = { version = "0.7.39", default-features = false } -serde = { version = "1", default-features = false, features = [ - "derive", - "alloc", -] } -serde_json = { version = "1", default-features = false } -serde_with = { version = "=3.9.0", default-features = false, features = [ - "hex", -] } - -# Keep the data-driver dependency graph compatible with the repository's -# current nightly toolchain. -time = { version = "=0.3.36", default-features = false } [profile.dev.build-override] opt-level = 3 diff --git a/Makefile b/Makefile index d46dc276..2e74a462 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ STANDARDS_EXAMPLES := standards/examples/authorization_counter standards/examples/drc20_roles_pausable standards/examples/drc721_collection standards/examples/multisig_controller standards/examples/proxy_counter -SUBDIRS := standards/dusk-contract-standards $(STANDARDS_EXAMPLES) tests/alice tests/bob tests/charlie genesis/transfer genesis/stake tests/host_fn +LEGACY_SUBDIRS := tests/alice tests/bob tests/charlie genesis/transfer genesis/stake tests/host_fn STANDARDS_PROPTEST_CASES ?= 8192 STANDARDS_PROPTEST_MAX_SHRINK_ITERS ?= 16384 STANDARDS_DATA_DRIVER_FUZZ_CASES ?= 2048 STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS ?= 4096 -all: setup-compiler $(SUBDIRS) ## Build all the contracts +all: setup-compiler $(LEGACY_SUBDIRS) ## Build the legacy genesis/test contracts help: ## Display this help screen @grep -h \ @@ -13,10 +13,28 @@ help: ## Display this help screen awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' test: wasm ## Run all the tests in the subfolder - $(MAKE) $(SUBDIRS) MAKECMDGOALS=test + $(MAKE) $(LEGACY_SUBDIRS) MAKECMDGOALS=test wasm: setup-compiler ## Generate the WASM for all the contracts - $(MAKE) $(SUBDIRS) MAKECMDGOALS=wasm + $(MAKE) $(LEGACY_SUBDIRS) MAKECMDGOALS=wasm + +standards-fmt: ## Check Dusk standards formatting without the Dusk compiler bundle + cargo fmt --manifest-path standards/Cargo.toml --all --check + +standards-check: ## Check the Dusk standards crate without the Dusk compiler bundle + $(MAKE) -C standards/dusk-contract-standards check + +standards-test: ## Test the Dusk standards crate without the Dusk compiler bundle + $(MAKE) -C standards/dusk-contract-standards test + +standards-wasm: ## Build standards reference contracts without the Dusk compiler bundle + $(MAKE) $(STANDARDS_EXAMPLES) MAKECMDGOALS=wasm + +standards-clippy: ## Run standards clippy without the Dusk compiler bundle + $(MAKE) -C standards/dusk-contract-standards clippy + $(MAKE) $(STANDARDS_EXAMPLES) MAKECMDGOALS=clippy + +standards-ci: standards-fmt standards-check standards-clippy standards-test standards-wasm ## Run regular standards CI checks standards-data-drivers: ## Generate Forge data-driver WASM for standards reference contracts $(MAKE) $(STANDARDS_EXAMPLES) MAKECMDGOALS=wasm-dd @@ -24,17 +42,17 @@ standards-data-drivers: ## Generate Forge data-driver WASM for standards referen standards-properties: ## Run the longer standards property hardening suite STANDARDS_PROPTEST_CASES=$(STANDARDS_PROPTEST_CASES) \ STANDARDS_PROPTEST_MAX_SHRINK_ITERS=$(STANDARDS_PROPTEST_MAX_SHRINK_ITERS) \ - cargo test -p dusk-contract-standards --test properties + cargo test --manifest-path standards/Cargo.toml -p dusk-contract-standards --test properties standards-data-driver-fuzz: standards-data-drivers ## Fuzz Forge data-driver JSON/rkyv input codecs STANDARDS_DATA_DRIVER_FUZZ_CASES=$(STANDARDS_DATA_DRIVER_FUZZ_CASES) \ STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS=$(STANDARDS_DATA_DRIVER_FUZZ_SHRINK_ITERS) \ - cargo test -p dusk-contract-standards --test data_driver_fuzz -- --ignored + cargo test --manifest-path standards/Cargo.toml -p dusk-contract-standards --test data_driver_fuzz -- --ignored standards-hardening: standards-properties standards-data-driver-fuzz ## Run long standards hardening checks clippy: setup-compiler ## Run clippy - $(MAKE) $(SUBDIRS) MAKECMDGOALS=clippy + $(MAKE) $(LEGACY_SUBDIRS) MAKECMDGOALS=clippy keys: ## Create the keys for the circuits ./scripts/download-rusk.sh @@ -44,9 +62,9 @@ COMPILER_VERSION=v0.3.0-rc.1 setup-compiler: ## Setup the Dusk Contract Compiler @./scripts/setup-compiler.sh $(COMPILER_VERSION) -doc: $(SUBDIRS) ## Run doc gen +doc: $(LEGACY_SUBDIRS) ## Run doc gen -$(SUBDIRS): +$(LEGACY_SUBDIRS) $(STANDARDS_EXAMPLES): $(MAKE) -C $@ $(MAKECMDGOALS) -.PHONY: all test help standards-data-drivers standards-properties standards-data-driver-fuzz standards-hardening $(SUBDIRS) +.PHONY: all test help standards-fmt standards-check standards-test standards-wasm standards-clippy standards-ci standards-data-drivers standards-properties standards-data-driver-fuzz standards-hardening $(LEGACY_SUBDIRS) $(STANDARDS_EXAMPLES) diff --git a/scripts/dusk-contract-standards-audit-grade.sh b/scripts/dusk-contract-standards-audit-grade.sh index be16faf6..7816fea8 100755 --- a/scripts/dusk-contract-standards-audit-grade.sh +++ b/scripts/dusk-contract-standards-audit-grade.sh @@ -14,15 +14,16 @@ cd "$ROOT_DIR" echo "Checking formatting" cargo fmt --check +cargo fmt --manifest-path standards/Cargo.toml --all --check echo "Running standards clippy" -cargo clippy -p dusk-contract-standards --all-targets -- -D warnings +cargo clippy --manifest-path standards/Cargo.toml -p dusk-contract-standards --all-targets -- -D warnings echo "Running native standards tests" -cargo test -p dusk-contract-standards +cargo test --manifest-path standards/Cargo.toml -p dusk-contract-standards echo "Building reference contract Wasm" -cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ +cargo build --manifest-path standards/Cargo.toml --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ -p authorization-counter \ -p drc20-roles-pausable \ -p drc721-collection \ @@ -31,7 +32,7 @@ cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,multisig-controller/contract,proxy-counter/contract echo "Running VM reference deployment tests" -cargo test -p dusk-contract-standards --test examples_vm -- --ignored +cargo test --manifest-path standards/Cargo.toml -p dusk-contract-standards --test examples_vm -- --ignored echo "Running long property and data-driver hardening" STANDARDS_PROPTEST_CASES="$STANDARDS_PROPTEST_CASES" \ diff --git a/scripts/dusk-contract-standards-local-smoke.sh b/scripts/dusk-contract-standards-local-smoke.sh index d495dbf2..e5488aef 100755 --- a/scripts/dusk-contract-standards-local-smoke.sh +++ b/scripts/dusk-contract-standards-local-smoke.sh @@ -12,10 +12,10 @@ DEPLOY_NONCE_BASE="${DEPLOY_NONCE_BASE:-100}" cd "$ROOT_DIR" echo "Running standards unit tests" -cargo test -p dusk-contract-standards +cargo test --manifest-path standards/Cargo.toml -p dusk-contract-standards echo "Building example contracts" -cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ +cargo build --manifest-path standards/Cargo.toml --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ -p authorization-counter \ -p drc20-roles-pausable \ -p drc721-collection \ @@ -25,7 +25,7 @@ cargo build --release -Z build-std=core,alloc --target wasm32-unknown-unknown \ echo "Building Forge data-drivers" CARGO_TARGET_DIR="${ROOT_DIR}/target/data-driver" \ -cargo build --release --target wasm32-unknown-unknown \ +cargo build --manifest-path standards/Cargo.toml --release --target wasm32-unknown-unknown \ -p authorization-counter \ -p drc20-roles-pausable \ -p drc721-collection \ @@ -34,7 +34,7 @@ cargo build --release --target wasm32-unknown-unknown \ --features authorization-counter/data-driver-js,drc20-roles-pausable/data-driver-js,drc721-collection/data-driver-js,multisig-controller/data-driver-js,proxy-counter/data-driver-js echo "Running VM example deployment tests" -cargo test -p dusk-contract-standards --test examples_vm -- --ignored +cargo test --manifest-path standards/Cargo.toml -p dusk-contract-standards --test examples_vm -- --ignored if [[ -z "$RUSK_WALLET_BIN" ]]; then cat >&2 <<'MSG' @@ -96,7 +96,7 @@ if [[ -n "$WALLET_RESTORE_FILE" && ! -f "$WALLET_DIR/wallet.keystore.json" ]]; t fi encode_args() { - cargo run -q -p dusk-contract-standards --example encode_local_smoke_args -- "$@" + cargo run -q --manifest-path standards/Cargo.toml -p dusk-contract-standards --example encode_local_smoke_args -- "$@" } deploy_contract() { diff --git a/standards/Cargo.lock b/standards/Cargo.lock new file mode 100644 index 00000000..084d2f65 --- /dev/null +++ b/standards/Cargo.lock @@ -0,0 +1,3106 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0453232ace82dee0dd0b4c87a59bd90f7b53b314f3e0f61fe2ee7c8a16482289" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object 0.37.3", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "ark-bn254" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-crypto-primitives" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3a13b34da09176a8baba701233fdffbaa7c1b1192ce031a3da4e55ce1f1a56" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-snark", + "ark-std", + "blake2", + "derivative", + "digest 0.10.7", + "sha2 0.10.9", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools 0.10.5", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-groth16" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ceafa83848c3e390f1cbf124bc3193b3e639b3f02009e0e290809a501b95fc" +dependencies = [ + "ark-crypto-primitives", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-relations" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00796b6efc05a3f48225e59cb6a2cda78881e7c390872d5786aaf112f31fb4f0" +dependencies = [ + "ark-ff", + "ark-std", + "tracing", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-snark" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d3cc6833a335bb8a600241889ead68ee89a3cf8448081fb7694c0fe503da63" +dependencies = [ + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "authorization-counter" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed83caece3afc59919481b33b472e1432d1abc4641ed9100be142ef5110b406" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq 0.3.1", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq 0.4.2", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bls12_381-bls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c61c05b7bfa0cff7b7f6bf000589de5541fc03b702adadfbcfbb6d782a2d643" +dependencies = [ + "bs58", + "bytecheck", + "dusk-bls12_381", + "dusk-bytes", + "ff", + "rand_core 0.6.4", + "rkyv", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "c-kzg" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16507ccb84a57d44c40cf9b8e6741eb560bc44122b325dcff2fdbfa4959b2110" +dependencies = [ + "arbitrary", + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "const-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5241cd7938b1b415942e943ea96f615953d500b50347b505b0b507080bad5a6f" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-bforest" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebf72ceaf38f7d41194d0cf6748214d8ef7389167fe09aad80f87646dbfa325b" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-codegen" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee7fde5cd9173f00ce02c491ee9e306d64740f4b1a697946e0474f389999e13" +dependencies = [ + "bumpalo", + "cranelift-bforest", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.14.5", + "log", + "regalloc2", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49bec6a517e78d4067500dc16acb558e772491a2bcb37301127448adfb8413c" +dependencies = [ + "cranelift-codegen-shared", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ead4ea497b2dc2ac31fcabd6d5d0d5dc25b3964814122e343724bdf65a53c843" + +[[package]] +name = "cranelift-control" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81e8028c8d711ea7592648e70221f2e54acb8665f7ecd49545f021ec14c3341" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32acd0632ba65c2566e75f64af9ef094bb8d90e58a9fbd33d920977a9d85c054" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a395a704934aa944ba8939cac9001174b9ae5236f48bc091f89e33bb968336f6" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b325ce81c4ee7082dc894537eb342c37898e14230fe7c02ea945691db3e2dd01" + +[[package]] +name = "cranelift-native" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea11f5ac85996fa093075d66397922d4f56085d5d84ec13043d0cd4f159c6818" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-wasm" +version = "0.107.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f175d4e299a8edabfbd64fa93c7650836cc8ad7f4879f9bd2632575a1f12d0" +dependencies = [ + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "itertools 0.12.1", + "log", + "smallvec", + "wasmparser 0.202.0", + "wasmtime-types", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crumbles" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3476b7dda7a457e938e4b958aa7b8db66eb3f6bc72894454a86f99bab3dea2" +dependencies = [ + "libc", + "rangemap", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-hex" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6618553c32cd1c1f4fbdb9418cc035f3168422f24406ebb08576f6db5ed6ec" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "dlmalloc" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "drc20-roles-pausable" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "drc721-collection" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "dusk-bls12_381" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633aa835dd9e4db7ad5dc94a3f95d44ff5c6856ec3c83c15eb75366cfb5ee68a" +dependencies = [ + "blake2b_simd", + "bytecheck", + "digest 0.9.0", + "dusk-bytes", + "ff", + "group", + "hex", + "pairing", + "rand_core 0.6.4", + "rkyv", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "dusk-bytes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d209b92f0741edf1d99369bd4c1a1ef2fd0a85e885220f9e3fb0df3c61337f" +dependencies = [ + "derive-hex", +] + +[[package]] +name = "dusk-contract-standards" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "dusk-vm", + "proptest", + "rand 0.8.6", + "rkyv", + "serde", + "serde_json", + "serde_with", + "time", +] + +[[package]] +name = "dusk-core" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7053f16922b6cce10036bda331f5dd373a64ff66972604c2fc3bca594d2158" +dependencies = [ + "ark-bn254", + "ark-groth16", + "ark-relations", + "ark-serialize", + "blake2b_simd", + "bls12_381-bls", + "bytecheck", + "c-kzg", + "dusk-bls12_381", + "dusk-bytes", + "dusk-jubjub", + "dusk-plonk", + "dusk-poseidon", + "ff", + "jubjub-schnorr", + "phoenix-circuits", + "phoenix-core", + "piecrust-uplink", + "poseidon-merkle", + "rand 0.8.6", + "rkyv", + "serde", + "serde_with", + "sha2 0.10.9", +] + +[[package]] +name = "dusk-data-driver" +version = "0.3.2-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "191e9b51e34ecb1a22d79de6df7e6e2971121e0069d60d738df9059837f46a1c" +dependencies = [ + "bytecheck", + "dusk-core", + "dusk-wasmtime", + "parking_lot", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "dusk-forge" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31edc24ce1a8cfb6ee61d3941145c4b475117c2c0450a6342dc30907c61a5e93" +dependencies = [ + "dusk-forge-contract", + "serde", + "serde_json", +] + +[[package]] +name = "dusk-forge-contract" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9f361a4eddded990e22c61dece5282257b141bf4d4d8cd2f4fda5a17618791" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dusk-jubjub" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49c388e710446aabbe7c7717109a239cea254594253ceb94e9134976c5b59509" +dependencies = [ + "bitvec", + "blake2b_simd", + "bytecheck", + "dusk-bls12_381", + "dusk-bytes", + "ff", + "group", + "hex", + "rand_core 0.6.4", + "rkyv", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "dusk-merkle" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6af2fdeaa2b2e290827e6a421575ac03836213d2ed00952edd9de153581a097" +dependencies = [ + "bytecheck", + "dusk-bytes", + "rkyv", +] + +[[package]] +name = "dusk-plonk" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "949ec126023c47910d93a0fcb35ef20243f75eb7a726fcaa6541d8c44fadb66d" +dependencies = [ + "blake2b_simd", + "bytecheck", + "cfg-if", + "dusk-bls12_381", + "dusk-bytes", + "dusk-jubjub", + "ff", + "hashbrown 0.9.1", + "itertools 0.9.0", + "merlin", + "miniz_oxide", + "msgpacker", + "rand_core 0.6.4", + "rkyv", + "sha2 0.10.9", + "zeroize", +] + +[[package]] +name = "dusk-poseidon" +version = "0.42.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb428dc6d7aac399fd2c5929fd6e0b2c7b83392d3eb8c9a7b29a916d0087063d" +dependencies = [ + "dusk-bls12_381", + "dusk-jubjub", + "dusk-plonk", + "dusk-safe", +] + +[[package]] +name = "dusk-safe" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3954d110d0d0f20555048d7171c2e6dfb54fd9a4220d30cc73dc66a88af42d" +dependencies = [ + "zeroize", +] + +[[package]] +name = "dusk-vm" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4e78bea4ffcca10eb95f687d7169e94c06e0246dd4023d9f76b213877b2e123" +dependencies = [ + "blake2b_simd", + "blake3", + "bytecheck", + "c-kzg", + "dusk-bytes", + "dusk-core", + "dusk-poseidon", + "lru", + "piecrust", + "rkyv", + "secp256k1", + "serde", + "sha2 0.10.9", + "sha3", + "tracing", + "wasmparser 0.240.0", +] + +[[package]] +name = "dusk-wasmtime" +version = "21.0.0-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74e44c86778e534ece079bd7cfb8668a9a7591c4703585c6f593258699449f10" +dependencies = [ + "anyhow", + "bincode", + "bumpalo", + "cfg-if", + "dusk-wasmtime-cranelift", + "dusk-wasmtime-environ", + "dusk-wasmtime-runtime", + "gimli", + "indexmap", + "libc", + "log", + "object 0.33.0", + "once_cell", + "paste", + "rayon", + "rustix 0.38.44", + "serde", + "serde_derive", + "serde_json", + "target-lexicon", + "wasmparser 0.202.0", + "wasmtime-jit-icache-coherence", + "wasmtime-slab", + "windows-sys 0.52.0", +] + +[[package]] +name = "dusk-wasmtime-cranelift" +version = "21.0.0-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc15917a4c6e33e358f0bfaf2bac0700487f055dab627088fa70e60cef37804e" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "cranelift-wasm", + "dusk-wasmtime-environ", + "gimli", + "log", + "object 0.33.0", + "target-lexicon", + "thiserror", + "wasmparser 0.202.0", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "dusk-wasmtime-environ" +version = "21.0.0-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391fc0ac7b153f80ffddb9e19a02fc3d642d58989448fbbeb44c5d5992e17fc6" +dependencies = [ + "anyhow", + "bincode", + "cranelift-entity", + "gimli", + "indexmap", + "log", + "object 0.33.0", + "paste", + "serde", + "serde_derive", + "target-lexicon", + "thiserror", + "wasmparser 0.202.0", + "wasmtime-types", +] + +[[package]] +name = "dusk-wasmtime-runtime" +version = "21.0.0-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d033283093a4faa4b8ddc32085ab3825faa1ca8dc6785324c46fdc7760ad627d" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "dusk-wasmtime-environ", + "indexmap", + "libc", + "log", + "mach2", + "memfd", + "memoffset", + "paste", + "psm", + "rustix 0.38.44", + "sptr", + "wasmtime-asm-macros", + "wasmtime-slab", + "wasmtime-versioned-export-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash 0.4.8", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.12", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jubjub-elgamal" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22c81bad2a54e37f6eadcdba35f35761b7dba5665af26ea16f29f0d9372fb64f" +dependencies = [ + "dusk-bytes", + "dusk-jubjub", + "dusk-plonk", +] + +[[package]] +name = "jubjub-schnorr" +version = "0.7.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a7208d3397238bd5fce848642b923f7aebed6d58cd472ad72027ba68167167" +dependencies = [ + "bs58", + "bytecheck", + "dusk-bls12_381", + "dusk-bytes", + "dusk-jubjub", + "dusk-plonk", + "dusk-poseidon", + "ff", + "rand_core 0.6.4", + "rkyv", + "serde", + "zeroize", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + +[[package]] +name = "memmap2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "msgpacker" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134a72ec3a6d034e9c767344ce8aebde228eef4e1343fff3ea209993f12920b3" +dependencies = [ + "msgpacker-derive", +] + +[[package]] +name = "msgpacker-derive" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c334ab77a6d4f29c5c2e41149c0f03af4082923aa364aa0c9876232f33f9853f" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "multisig-controller" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8dd6c0cdf9429bce006e1362bfce61fa1bfd8c898a643ed8d2b471934701d3d" +dependencies = [ + "crc32fast", + "hashbrown 0.14.5", + "indexmap", + "memchr", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "phoenix-circuits" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5583c3db8adc437f7854319a6a815666b95475797ba82d8cb5f9f5a9f18fe55" +dependencies = [ + "dusk-bls12_381", + "dusk-bytes", + "dusk-jubjub", + "dusk-plonk", + "dusk-poseidon", + "jubjub-elgamal", + "jubjub-schnorr", + "phoenix-core", + "poseidon-merkle", +] + +[[package]] +name = "phoenix-core" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930c6dd9d1054cf0cd725325323a5381fe3854708a2cebe38ccab8699579e2e" +dependencies = [ + "aes-gcm", + "base64", + "bs58", + "bytecheck", + "dusk-bls12_381", + "dusk-bytes", + "dusk-jubjub", + "dusk-poseidon", + "ff", + "hex", + "hkdf", + "jubjub-elgamal", + "jubjub-schnorr", + "rand 0.8.6", + "rkyv", + "serde", + "serde_with", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "piecrust" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "768a69c4aac88337ec2c1b9ac1624d4c52bd50c6ecbdaed70b5854006d56fe01" +dependencies = [ + "blake3", + "bytecheck", + "const-decoder", + "crumbles", + "dusk-merkle", + "dusk-wasmtime", + "hex", + "indexmap", + "memmap2", + "piecrust-uplink", + "rand 0.8.6", + "rkyv", + "tempfile", + "thiserror", + "tracing", +] + +[[package]] +name = "piecrust-uplink" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df8d2d3f5deb052f61c65d01d892c4babe604cd884cdd2fcef7f6da4392b476" +dependencies = [ + "bytecheck", + "dlmalloc", + "hex", + "rkyv", + "serde", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "poseidon-merkle" +version = "0.9.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95e801b94d059823f3dc9d2af14e2a7438863e4b23736e324ebb368991545d52" +dependencies = [ + "bytecheck", + "dusk-bls12_381", + "dusk-bytes", + "dusk-merkle", + "dusk-plonk", + "dusk-poseidon", + "rkyv", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proxy-counter" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-contract-standards", + "dusk-core", + "dusk-data-driver", + "dusk-forge", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "psm" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regalloc2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad156d539c879b7a24a363a2016d77961786e71f48f2e2fc8302a92abd2429a6" +dependencies = [ + "hashbrown 0.13.2", + "log", + "rustc-hash", + "slice-group-by", + "smallvec", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "secp256k1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.4", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +dependencies = [ + "base64", + "chrono", + "hex", + "serde", + "serde_derive", + "serde_json", + "time", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slice-group-by" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasmparser" +version = "0.202.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6998515d3cf3f8b980ef7c11b29a9b1017d4cf86b99ae93b546992df9931413" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.240.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b722dcf61e0ea47440b53ff83ccb5df8efec57a69d150e4f24882e4eba7e24a4" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "20.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625ee94c72004f3ea0228989c9506596e469517d7d0ed66f7300d1067bdf1ca9" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "20.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9f93a3289057b26dc75eb84d6e60d7694f7d169c7c09597495de6e016a13ff" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "wasmtime-slab" +version = "20.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b3655075824a374c536a2b2cc9283bb765fcdf3d58b58587862c48571ad81ef" + +[[package]] +name = "wasmtime-types" +version = "20.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d5381ff174faded38c7b2085fbe430dff59489c87a91403354d710075750fb" +dependencies = [ + "cranelift-entity", + "serde", + "serde_derive", + "thiserror", + "wasmparser 0.202.0", +] + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "20.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8561d9e2920db2a175213d557d71c2ac7695831ab472bbfafb9060cd1034684f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/standards/Cargo.toml b/standards/Cargo.toml new file mode 100644 index 00000000..ff77249f --- /dev/null +++ b/standards/Cargo.toml @@ -0,0 +1,46 @@ +[workspace] +members = [ + "dusk-contract-standards", + "examples/authorization_counter", + "examples/drc20_roles_pausable", + "examples/drc721_collection", + "examples/multisig_controller", + "examples/proxy_counter", +] + +resolver = "2" + +[workspace.dependencies] +dusk-core = "=1.6.0" +dusk-data-driver = "=0.3.2-alpha.1" +dusk-forge = "=0.3.0" +dusk-vm = "=1.6.0" + +bytecheck = { version = "0.6.12", default-features = false } +rand = { version = "0.8.5", default-features = false } +rkyv = { version = "0.7.39", default-features = false } +serde = { version = "1", default-features = false, features = [ + "derive", + "alloc", +] } +serde_json = { version = "1", default-features = false } +serde_with = { version = "=3.9.0", default-features = false, features = [ + "hex", +] } +time = { version = "=0.3.36", default-features = false } + +[profile.dev.build-override] +opt-level = 3 +debug = false +debug-assertions = false +overflow-checks = false +incremental = false +codegen-units = 16 + +[profile.release.build-override] +opt-level = 3 +debug = false +debug-assertions = false +overflow-checks = false +incremental = false +codegen-units = 16 diff --git a/standards/LICENSE b/standards/LICENSE new file mode 100644 index 00000000..a612ad98 --- /dev/null +++ b/standards/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/standards/dusk-contract-standards/Cargo.toml b/standards/dusk-contract-standards/Cargo.toml index fb00aac9..cf0171d1 100644 --- a/standards/dusk-contract-standards/Cargo.toml +++ b/standards/dusk-contract-standards/Cargo.toml @@ -7,21 +7,26 @@ edition = "2021" crate-type = ["rlib"] [dependencies] -dusk-core = { workspace = true } -dusk-forge = { workspace = true } +dusk-core = "=1.6.0" +dusk-forge = "=0.3.0" bytecheck = { workspace = true } rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } -serde = { workspace = true, optional = true } -serde_with = { workspace = true, optional = true } -time = { workspace = true, optional = true } +serde = { version = "1", default-features = false, features = [ + "derive", + "alloc", +], optional = true } +serde_with = { version = "=3.9.0", default-features = false, features = [ + "hex", +], optional = true } +time = { version = "=0.3.36", default-features = false, optional = true } [features] contract = [] serde = ["dep:serde", "dep:serde_with", "dep:time", "dusk-core/serde"] [dev-dependencies] -dusk-data-driver = { workspace = true, features = ["reader"] } -dusk-vm = { workspace = true } +dusk-data-driver = { version = "=0.3.2-alpha.1", features = ["reader"] } +dusk-vm = "=1.6.0" proptest = "1.6.0" rand = { workspace = true, features = ["std_rng"] } -serde_json = { workspace = true, features = ["std"] } +serde_json = { version = "1", default-features = false, features = ["std"] } diff --git a/standards/examples/authorization_counter/Cargo.toml b/standards/examples/authorization_counter/Cargo.toml index 674f3669..628f068d 100644 --- a/standards/examples/authorization_counter/Cargo.toml +++ b/standards/examples/authorization_counter/Cargo.toml @@ -7,16 +7,14 @@ edition = "2021" crate-type = ["cdylib"] [target.'cfg(target_family = "wasm")'.dependencies] -dusk-core = { workspace = true } -dusk-data-driver = { workspace = true, optional = true } -dusk-forge = { workspace = true } +dusk-core = "=1.6.0" +dusk-data-driver = { version = "=0.3.2-alpha.1", optional = true } +dusk-forge = "=0.3.0" dusk-contract-standards = { path = "../../dusk-contract-standards" } bytecheck = { workspace = true } rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } -serde = { workspace = true, optional = true } -serde_json = { workspace = true, default-features = false, features = [ - "alloc", -], optional = true } +serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } +serde_json = { version = "1", default-features = false, features = ["alloc"], optional = true } [features] contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] diff --git a/standards/examples/drc20_roles_pausable/Cargo.toml b/standards/examples/drc20_roles_pausable/Cargo.toml index 0f33de7c..bb9a6488 100644 --- a/standards/examples/drc20_roles_pausable/Cargo.toml +++ b/standards/examples/drc20_roles_pausable/Cargo.toml @@ -7,16 +7,14 @@ edition = "2021" crate-type = ["cdylib"] [target.'cfg(target_family = "wasm")'.dependencies] -dusk-core = { workspace = true } -dusk-data-driver = { workspace = true, optional = true } -dusk-forge = { workspace = true } +dusk-core = "=1.6.0" +dusk-data-driver = { version = "=0.3.2-alpha.1", optional = true } +dusk-forge = "=0.3.0" dusk-contract-standards = { path = "../../dusk-contract-standards" } bytecheck = { workspace = true } rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } -serde = { workspace = true, optional = true } -serde_json = { workspace = true, default-features = false, features = [ - "alloc", -], optional = true } +serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } +serde_json = { version = "1", default-features = false, features = ["alloc"], optional = true } [features] contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] diff --git a/standards/examples/drc721_collection/Cargo.toml b/standards/examples/drc721_collection/Cargo.toml index bd0c0e00..3cf58430 100644 --- a/standards/examples/drc721_collection/Cargo.toml +++ b/standards/examples/drc721_collection/Cargo.toml @@ -7,16 +7,14 @@ edition = "2021" crate-type = ["cdylib"] [target.'cfg(target_family = "wasm")'.dependencies] -dusk-core = { workspace = true } -dusk-data-driver = { workspace = true, optional = true } -dusk-forge = { workspace = true } +dusk-core = "=1.6.0" +dusk-data-driver = { version = "=0.3.2-alpha.1", optional = true } +dusk-forge = "=0.3.0" dusk-contract-standards = { path = "../../dusk-contract-standards" } bytecheck = { workspace = true } rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } -serde = { workspace = true, optional = true } -serde_json = { workspace = true, default-features = false, features = [ - "alloc", -], optional = true } +serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } +serde_json = { version = "1", default-features = false, features = ["alloc"], optional = true } [features] contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] diff --git a/standards/examples/multisig_controller/Cargo.toml b/standards/examples/multisig_controller/Cargo.toml index 199b14f2..3ee38a20 100644 --- a/standards/examples/multisig_controller/Cargo.toml +++ b/standards/examples/multisig_controller/Cargo.toml @@ -7,16 +7,14 @@ edition = "2021" crate-type = ["cdylib"] [target.'cfg(target_family = "wasm")'.dependencies] -dusk-core = { workspace = true } -dusk-data-driver = { workspace = true, optional = true } -dusk-forge = { workspace = true } +dusk-core = "=1.6.0" +dusk-data-driver = { version = "=0.3.2-alpha.1", optional = true } +dusk-forge = "=0.3.0" dusk-contract-standards = { path = "../../dusk-contract-standards" } bytecheck = { workspace = true } rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } -serde = { workspace = true, optional = true } -serde_json = { workspace = true, default-features = false, features = [ - "alloc", -], optional = true } +serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } +serde_json = { version = "1", default-features = false, features = ["alloc"], optional = true } [features] contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] diff --git a/standards/examples/proxy_counter/Cargo.toml b/standards/examples/proxy_counter/Cargo.toml index 8402c9f5..806a8c94 100644 --- a/standards/examples/proxy_counter/Cargo.toml +++ b/standards/examples/proxy_counter/Cargo.toml @@ -7,16 +7,14 @@ edition = "2021" crate-type = ["cdylib"] [target.'cfg(target_family = "wasm")'.dependencies] -dusk-core = { workspace = true } -dusk-data-driver = { workspace = true, optional = true } -dusk-forge = { workspace = true } +dusk-core = "=1.6.0" +dusk-data-driver = { version = "=0.3.2-alpha.1", optional = true } +dusk-forge = "=0.3.0" dusk-contract-standards = { path = "../../dusk-contract-standards" } bytecheck = { workspace = true } rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } -serde = { workspace = true, optional = true } -serde_json = { workspace = true, default-features = false, features = [ - "alloc", -], optional = true } +serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } +serde_json = { version = "1", default-features = false, features = ["alloc"], optional = true } [features] contract = ["dusk-core/abi-dlmalloc", "dusk-contract-standards/contract"] From ba37da00363dd9fae5f32fa00f474cd42a464950 Mon Sep 17 00:00:00 2001 From: Hein Dauven Date: Wed, 27 May 2026 15:45:34 +0200 Subject: [PATCH 35/35] standards: harden signed auth integration paths --- Makefile | 10 +- .../dusk-contract-standards-audit-grade.sh | 3 +- .../dusk-contract-standards-local-smoke.sh | 3 +- standards/Cargo.lock | 10 ++ standards/Cargo.toml | 1 + .../src/access/access_control.rs | 2 +- .../src/access/ownable.rs | 2 +- .../src/access/owner_set.rs | 2 +- .../dusk-contract-standards/src/auth/mod.rs | 59 ++++++++- .../src/proxy/upgrade.rs | 2 +- .../tests/examples_vm.rs | 123 ++++++++++++++++++ .../tests/primitives.rs | 64 +++++++-- .../examples/authorization_counter/src/lib.rs | 63 +++------ .../examples/moonlight_call_router/Cargo.toml | 17 +++ .../examples/moonlight_call_router/Makefile | 23 ++++ .../examples/moonlight_call_router/src/lib.rs | 57 ++++++++ 16 files changed, 373 insertions(+), 68 deletions(-) create mode 100644 standards/examples/moonlight_call_router/Cargo.toml create mode 100644 standards/examples/moonlight_call_router/Makefile create mode 100644 standards/examples/moonlight_call_router/src/lib.rs diff --git a/Makefile b/Makefile index 2e74a462..3bbad23c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ STANDARDS_EXAMPLES := standards/examples/authorization_counter standards/examples/drc20_roles_pausable standards/examples/drc721_collection standards/examples/multisig_controller standards/examples/proxy_counter +STANDARDS_TEST_HELPERS := standards/examples/moonlight_call_router +STANDARDS_WASM_CONTRACTS := $(STANDARDS_EXAMPLES) $(STANDARDS_TEST_HELPERS) LEGACY_SUBDIRS := tests/alice tests/bob tests/charlie genesis/transfer genesis/stake tests/host_fn STANDARDS_PROPTEST_CASES ?= 8192 STANDARDS_PROPTEST_MAX_SHRINK_ITERS ?= 16384 @@ -28,11 +30,11 @@ standards-test: ## Test the Dusk standards crate without the Dusk compiler bundl $(MAKE) -C standards/dusk-contract-standards test standards-wasm: ## Build standards reference contracts without the Dusk compiler bundle - $(MAKE) $(STANDARDS_EXAMPLES) MAKECMDGOALS=wasm + $(MAKE) $(STANDARDS_WASM_CONTRACTS) MAKECMDGOALS=wasm standards-clippy: ## Run standards clippy without the Dusk compiler bundle $(MAKE) -C standards/dusk-contract-standards clippy - $(MAKE) $(STANDARDS_EXAMPLES) MAKECMDGOALS=clippy + $(MAKE) $(STANDARDS_WASM_CONTRACTS) MAKECMDGOALS=clippy standards-ci: standards-fmt standards-check standards-clippy standards-test standards-wasm ## Run regular standards CI checks @@ -64,7 +66,7 @@ setup-compiler: ## Setup the Dusk Contract Compiler doc: $(LEGACY_SUBDIRS) ## Run doc gen -$(LEGACY_SUBDIRS) $(STANDARDS_EXAMPLES): +$(LEGACY_SUBDIRS) $(STANDARDS_WASM_CONTRACTS): $(MAKE) -C $@ $(MAKECMDGOALS) -.PHONY: all test help standards-fmt standards-check standards-test standards-wasm standards-clippy standards-ci standards-data-drivers standards-properties standards-data-driver-fuzz standards-hardening $(LEGACY_SUBDIRS) $(STANDARDS_EXAMPLES) +.PHONY: all test help standards-fmt standards-check standards-test standards-wasm standards-clippy standards-ci standards-data-drivers standards-properties standards-data-driver-fuzz standards-hardening $(LEGACY_SUBDIRS) $(STANDARDS_WASM_CONTRACTS) diff --git a/scripts/dusk-contract-standards-audit-grade.sh b/scripts/dusk-contract-standards-audit-grade.sh index 7816fea8..d422caa9 100755 --- a/scripts/dusk-contract-standards-audit-grade.sh +++ b/scripts/dusk-contract-standards-audit-grade.sh @@ -27,9 +27,10 @@ cargo build --manifest-path standards/Cargo.toml --release -Z build-std=core,all -p authorization-counter \ -p drc20-roles-pausable \ -p drc721-collection \ + -p moonlight-call-router \ -p multisig-controller \ -p proxy-counter \ - --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,multisig-controller/contract,proxy-counter/contract + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,moonlight-call-router/contract,multisig-controller/contract,proxy-counter/contract echo "Running VM reference deployment tests" cargo test --manifest-path standards/Cargo.toml -p dusk-contract-standards --test examples_vm -- --ignored diff --git a/scripts/dusk-contract-standards-local-smoke.sh b/scripts/dusk-contract-standards-local-smoke.sh index e5488aef..59362e19 100755 --- a/scripts/dusk-contract-standards-local-smoke.sh +++ b/scripts/dusk-contract-standards-local-smoke.sh @@ -19,9 +19,10 @@ cargo build --manifest-path standards/Cargo.toml --release -Z build-std=core,all -p authorization-counter \ -p drc20-roles-pausable \ -p drc721-collection \ + -p moonlight-call-router \ -p multisig-controller \ -p proxy-counter \ - --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,multisig-controller/contract,proxy-counter/contract + --features authorization-counter/contract,drc20-roles-pausable/contract,drc721-collection/contract,moonlight-call-router/contract,multisig-controller/contract,proxy-counter/contract echo "Building Forge data-drivers" CARGO_TARGET_DIR="${ROOT_DIR}/target/data-driver" \ diff --git a/standards/Cargo.lock b/standards/Cargo.lock index 084d2f65..32a0da6f 100644 --- a/standards/Cargo.lock +++ b/standards/Cargo.lock @@ -1693,6 +1693,16 @@ dependencies = [ "adler", ] +[[package]] +name = "moonlight-call-router" +version = "0.1.0" +dependencies = [ + "bytecheck", + "dusk-core", + "dusk-forge", + "rkyv", +] + [[package]] name = "msgpacker" version = "0.4.8" diff --git a/standards/Cargo.toml b/standards/Cargo.toml index ff77249f..3101a21a 100644 --- a/standards/Cargo.toml +++ b/standards/Cargo.toml @@ -4,6 +4,7 @@ members = [ "examples/authorization_counter", "examples/drc20_roles_pausable", "examples/drc721_collection", + "examples/moonlight_call_router", "examples/multisig_controller", "examples/proxy_counter", ] diff --git a/standards/dusk-contract-standards/src/access/access_control.rs b/standards/dusk-contract-standards/src/access/access_control.rs index d1efe87a..0e37a4ce 100644 --- a/standards/dusk-contract-standards/src/access/access_control.rs +++ b/standards/dusk-contract-standards/src/access/access_control.rs @@ -107,7 +107,7 @@ impl AccessControl { let Some(authorization) = authorization else { panic!("{}", error::UNAUTHORIZED); }; - authorizer.require_signed_if(authorization, |principal| { + authorizer.require_unbound_signed_if(authorization, |principal| { self.has_role(role, principal) }) } diff --git a/standards/dusk-contract-standards/src/access/ownable.rs b/standards/dusk-contract-standards/src/access/ownable.rs index f9d35c94..0c67de6d 100644 --- a/standards/dusk-contract-standards/src/access/ownable.rs +++ b/standards/dusk-contract-standards/src/access/ownable.rs @@ -72,7 +72,7 @@ impl Ownable { let owner = self .owner .unwrap_or_else(|| panic!("{}", error::NOT_INITIALIZED)); - authorizer.require_principal(owner, authorization) + authorizer.require_principal_unbound(owner, authorization) } /// Authorizes the owner through runtime context or an action-bound signed diff --git a/standards/dusk-contract-standards/src/access/owner_set.rs b/standards/dusk-contract-standards/src/access/owner_set.rs index 1ff5b0e6..f93e5f13 100644 --- a/standards/dusk-contract-standards/src/access/owner_set.rs +++ b/standards/dusk-contract-standards/src/access/owner_set.rs @@ -85,7 +85,7 @@ impl OwnerSet { let Some(authorization) = authorization else { panic!("{}", error::UNAUTHORIZED); }; - authorizer.require_signed_if(authorization, |principal| { + authorizer.require_unbound_signed_if(authorization, |principal| { self.is_owner(principal) }) } diff --git a/standards/dusk-contract-standards/src/auth/mod.rs b/standards/dusk-contract-standards/src/auth/mod.rs index 5f359b99..17393e29 100644 --- a/standards/dusk-contract-standards/src/auth/mod.rs +++ b/standards/dusk-contract-standards/src/auth/mod.rs @@ -287,7 +287,11 @@ impl<'a> Authorizer<'a> { } /// Verifies an exact expected principal. - pub fn require_principal( + /// + /// Signed fallbacks are not checked against a call envelope. Prefer + /// [`Self::require_principal_action`] in public contract methods unless + /// the caller has already bound the action elsewhere. + pub fn require_principal_unbound( &mut self, expected: Principal, authorization: Option<&SignedAuthorization>, @@ -322,17 +326,17 @@ impl<'a> Authorizer<'a> { /// This is a low-level primitive. Public contract methods should usually /// prefer `require_signed_action` so the intended call envelope is checked /// before nonce state is consumed. - pub fn require_signed( + pub fn require_unbound_signed( &mut self, authorization: &SignedAuthorization, ) -> Principal { self.authorizations - .authorize_signed(authorization, self.now) + .authorize_unbound_signed(authorization, self.now) } /// Verifies a signed authorization, applies an authorization predicate, /// and only then consumes nonce/replay state. - pub fn require_signed_if( + pub fn require_unbound_signed_if( &mut self, authorization: &SignedAuthorization, is_authorized: impl FnOnce(Principal) -> bool, @@ -413,6 +417,10 @@ impl AuthorizationManager { /// context. Moonlight can also be authorized by a BLS signed action. /// Phoenix requires a Schnorr signed action because Phoenix does not expose /// a stable runtime caller identity to contracts. + /// + /// Signed fallbacks are not checked against a call envelope. Prefer + /// [`Self::authorize_principal_action`] in public contract methods unless + /// the caller has already bound the action elsewhere. pub fn authorize_principal( &mut self, expected: Principal, @@ -475,7 +483,7 @@ impl AuthorizationManager { /// This is a low-level primitive. Public contract methods should usually /// prefer `authorize_signed_action` so the intended call envelope is /// checked before nonce state is consumed. - pub fn authorize_signed( + pub fn authorize_unbound_signed( &mut self, authorization: &SignedAuthorization, now: u64, @@ -531,11 +539,28 @@ impl AuthorizationManager { } /// Verifies and consumes a Moonlight BLS authorization. - pub fn authorize_moonlight( + /// + /// The signed action is not checked against a call envelope. Prefer + /// [`Self::authorize_moonlight_action`] in public contract methods. + pub fn authorize_unbound_moonlight( + &mut self, + auth: &MoonlightAuthorization, + now: u64, + ) -> Principal { + let principal = self.verify_moonlight(auth, now); + self.consume_action(principal, auth.action.domain, auth.action.nonce); + principal + } + + /// Verifies and consumes a Moonlight BLS authorization for an exact call + /// envelope. + pub fn authorize_moonlight_action( &mut self, auth: &MoonlightAuthorization, + envelope: ActionEnvelope, now: u64, ) -> Principal { + auth.action.assert_envelope(envelope); let principal = self.verify_moonlight(auth, now); self.consume_action(principal, auth.action.domain, auth.action.nonce); principal @@ -561,7 +586,10 @@ impl AuthorizationManager { } /// Verifies and consumes a Phoenix authorization. - pub fn authorize_phoenix( + /// + /// The signed action is not checked against a call envelope. Prefer + /// [`Self::authorize_phoenix_action`] in public contract methods. + pub fn authorize_unbound_phoenix( &mut self, auth: &PhoenixSignatureAuthorization, now: u64, @@ -574,6 +602,23 @@ impl AuthorizationManager { principal } + /// Verifies and consumes a Phoenix Schnorr authorization for an exact call + /// envelope. + pub fn authorize_phoenix_action( + &mut self, + auth: &PhoenixSignatureAuthorization, + envelope: ActionEnvelope, + now: u64, + ) -> Principal { + auth.action.assert_envelope(envelope); + let principal = self.verify_phoenix(auth, now); + self.consume_action(principal, auth.action.domain, auth.action.nonce); + if let Some(key) = auth.replay_key { + self.replays.consume(principal, key); + } + principal + } + /// Verifies a Phoenix Schnorr authorization without consuming nonce/replay /// state. fn verify_phoenix( diff --git a/standards/dusk-contract-standards/src/proxy/upgrade.rs b/standards/dusk-contract-standards/src/proxy/upgrade.rs index 69924e4a..084405cb 100644 --- a/standards/dusk-contract-standards/src/proxy/upgrade.rs +++ b/standards/dusk-contract-standards/src/proxy/upgrade.rs @@ -185,7 +185,7 @@ impl UpgradeAdmin { authorizer: &mut Authorizer<'_>, authorization: Option<&SignedAuthorization>, ) -> Principal { - authorizer.require_principal(self.admin, authorization) + authorizer.require_principal_unbound(self.admin, authorization) } /// Authorizes the upgrade admin through runtime context or an action-bound diff --git a/standards/dusk-contract-standards/tests/examples_vm.rs b/standards/dusk-contract-standards/tests/examples_vm.rs index 86d430e7..edf421f5 100644 --- a/standards/dusk-contract-standards/tests/examples_vm.rs +++ b/standards/dusk-contract-standards/tests/examples_vm.rs @@ -37,6 +37,7 @@ use dusk_core::signatures::schnorr::{ PublicKey as SchnorrPublicKey, SecretKey as SchnorrSecretKey, }; use dusk_core::transfer::data::ContractCall; +use dusk_core::transfer::TRANSFER_CONTRACT; use dusk_core::JubJubScalar; use dusk_vm::host_queries; use dusk_vm::{ContractData, Session, VM}; @@ -114,6 +115,14 @@ struct Drc20AdminCall { authorization: Option, } +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[archive_attr(derive(CheckBytes))] +struct ForwardCall { + contract: ContractId, + function: String, + args: Vec, +} + #[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[archive_attr(derive(CheckBytes))] struct Drc721ExampleInit { @@ -191,6 +200,12 @@ fn wasm_examples_deploy_and_answer_queries() { let mut session = vm.genesis_session(CHAIN_ID); let admin = phoenix_principal(7); + let moonlight_router = deploy( + &mut session, + "moonlight_call_router.wasm", + TRANSFER_CONTRACT.to_bytes(), + &(), + ); let auth_counter = deploy(&mut session, "authorization_counter.wasm", [19u8; 32], &()); let auth_value: u64 = session @@ -256,6 +271,35 @@ fn wasm_examples_deploy_and_answer_queries() { ) .is_err()); exercise_drc20_signed_calls(&mut session, drc20, admin); + let moonlight_owner_sk = moonlight_secret(110); + let moonlight_owner_pk = BlsPublicKey::from(&moonlight_owner_sk); + let moonlight_owner = Principal::moonlight(&moonlight_owner_pk); + let moonlight_drc20 = deploy( + &mut session, + "drc20_roles_pausable.wasm", + [25u8; 32], + &Drc20ExampleInit { + admin, + token: Init20 { + name: "VM Moonlight Token".into(), + symbol: "VML".into(), + decimals: 9, + initial_balances: vec![InitBalance { + account: moonlight_owner, + amount: 100, + }], + }, + cap: 1_000, + }, + ); + exercise_drc20_moonlight_observed_caller( + &mut session, + moonlight_router, + moonlight_drc20, + moonlight_owner_pk, + moonlight_owner, + admin, + ); let drc721 = deploy( &mut session, @@ -953,6 +997,72 @@ fn exercise_drc20_signed_calls( assert_contract_nonce(session, contract, admin, TOKEN_ADMIN_DOMAIN, 3); } +fn exercise_drc20_moonlight_observed_caller( + session: &mut Session, + router: ContractId, + contract: ContractId, + moonlight_pk: BlsPublicKey, + moonlight: Principal, + admin: Principal, +) { + assert_drc20_balance(session, contract, moonlight, 100); + assert_drc20_balance(session, contract, admin, 0); + + let transfer = TransferCall20 { + to: admin, + amount: 25, + }; + assert!(session + .call::<_, ()>(contract, "transfer", &transfer, GAS_LIMIT) + .is_err()); + assert_drc20_balance(session, contract, moonlight, 100); + assert_drc20_balance(session, contract, admin, 0); + + session + .set_meta(Metadata::PUBLIC_SENDER, Some(moonlight_pk)) + .expect("set public sender"); + session + .call::<_, Vec>( + router, + "forward", + &ForwardCall { + contract, + function: "transfer".to_string(), + args: Session::serialize_data(&transfer) + .expect("serialize routed transfer"), + }, + GAS_LIMIT, + ) + .expect("route transfer with observed Moonlight caller"); + let _ = session.remove_meta(Metadata::PUBLIC_SENDER); + assert_drc20_balance(session, contract, moonlight, 75); + assert_drc20_balance(session, contract, admin, 25); + + let outsider_pk = BlsPublicKey::from(&moonlight_secret(111)); + session + .set_meta(Metadata::PUBLIC_SENDER, Some(outsider_pk)) + .expect("set outsider public sender"); + assert!(session + .call::<_, Vec>( + router, + "forward", + &ForwardCall { + contract, + function: "transfer".to_string(), + args: Session::serialize_data(&TransferCall20 { + to: admin, + amount: 1, + }) + .expect("serialize routed transfer"), + }, + GAS_LIMIT, + ) + .is_err()); + let _ = session.remove_meta(Metadata::PUBLIC_SENDER); + assert_drc20_balance(session, contract, moonlight, 75); + assert_drc20_balance(session, contract, admin, 25); +} + fn exercise_drc721_signed_calls( session: &mut Session, contract: ContractId, @@ -2758,6 +2868,19 @@ fn assert_contract_nonce( assert_eq!(actual_nonce, nonce); } +fn assert_drc20_balance( + session: &mut Session, + contract: ContractId, + account: Principal, + expected: u64, +) { + let actual: u64 = session + .call(contract, "balance_of", &BalanceOf20 { account }, GAS_LIMIT) + .expect("query drc20 balance") + .data; + assert_eq!(actual, expected); +} + fn assert_proxy_value( session: &mut Session, contract: ContractId, diff --git a/standards/dusk-contract-standards/tests/primitives.rs b/standards/dusk-contract-standards/tests/primitives.rs index f54e603d..94ca4a70 100644 --- a/standards/dusk-contract-standards/tests/primitives.rs +++ b/standards/dusk-contract-standards/tests/primitives.rs @@ -764,10 +764,20 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { }; let mut authorizations = AuthorizationManager::new(); - assert_eq!(authorizations.authorize_moonlight(&auth, 99), moonlight); + let envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + domain, + action_id, + [66u8; 32], + ); + assert_eq!( + authorizations.authorize_moonlight_action(&auth, envelope, 99), + moonlight + ); assert_eq!(authorizations.nonce(moonlight, domain), 1); assert_panics(|| { - authorizations.authorize_moonlight(&auth, 99); + authorizations.authorize_moonlight_action(&auth, envelope, 99); }); let bad_action = AuthorizedAction { @@ -783,7 +793,7 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { signature: bad_signature, }; assert_panics(|| { - authorizations.authorize_moonlight(&bad_auth, 99); + authorizations.authorize_moonlight_action(&bad_auth, envelope, 99); }); let mut rng = StdRng::seed_from_u64(1234); @@ -806,7 +816,21 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { signature: phoenix_sk.sign(&mut rng, phoenix_action.message_hash()), replay_key: Some([99u8; 32]), }; - assert_eq!(authorizations.authorize_phoenix(&phoenix_auth, 1), phoenix); + let phoenix_envelope = ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + [77u8; 32], + action_id, + [22u8; 32], + ); + assert_eq!( + authorizations.authorize_phoenix_action( + &phoenix_auth, + phoenix_envelope, + 1, + ), + phoenix + ); assert_eq!(authorizations.nonce(phoenix, [77u8; 32]), 1); assert!(authorizations.replay_used(phoenix, [99u8; 32])); @@ -823,7 +847,7 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { replay_key: None, }; assert_panics(|| { - authorizations.authorize_phoenix(&expired, 10); + authorizations.authorize_phoenix_action(&expired, phoenix_envelope, 10); }); let bad_signature_action = AuthorizedAction { @@ -839,7 +863,17 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { replay_key: None, }; assert_panics(|| { - authorizations.authorize_phoenix(&bad_signature, 1); + authorizations.authorize_phoenix_action( + &bad_signature, + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + [77u8; 32], + action_id, + [23u8; 32], + ), + 1, + ); }); assert_eq!(authorizations.nonce(phoenix, [77u8; 32]), 1); @@ -856,7 +890,17 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { replay_key: Some([99u8; 32]), }; assert_panics(|| { - authorizations.authorize_phoenix(&replay, 1); + authorizations.authorize_phoenix_action( + &replay, + ActionEnvelope::new( + TEST_CHAIN_ID, + contract, + [77u8; 32], + action_id, + [24u8; 32], + ), + 1, + ); }); assert_eq!(authorizations.nonce(phoenix, [77u8; 32]), 1); @@ -873,7 +917,11 @@ fn authorization_manager_verifies_moonlight_and_phoenix_paths() { replay_key: None, }; assert_panics(|| { - authorizations.authorize_phoenix(&wrong_key, 1); + authorizations.authorize_phoenix_action( + &wrong_key, + phoenix_envelope, + 1, + ); }); let bounded_domain = [88u8; 32]; diff --git a/standards/examples/authorization_counter/src/lib.rs b/standards/examples/authorization_counter/src/lib.rs index 4a19cff8..c20eda20 100644 --- a/standards/examples/authorization_counter/src/lib.rs +++ b/standards/examples/authorization_counter/src/lib.rs @@ -52,9 +52,9 @@ mod authorization_counter { use authorization_counter_types::{ NonceQuery, SetValueByMoonlight, SetValueByPhoenix, }; - use dusk_contract_standards::auth::AuthorizationManager; + use dusk_contract_standards::auth::{ActionEnvelope, AuthorizationManager}; use dusk_contract_standards::core::{NonceDomain, Principal}; - use dusk_core::abi::{self, ContractId}; + use dusk_core::abi; const SET_VALUE_DOMAIN: NonceDomain = [3u8; 32]; const SET_VALUE_ACTION: [u8; 32] = [4u8; 32]; @@ -94,58 +94,35 @@ mod authorization_counter { self.authorizations.nonce(query.principal, query.domain) } pub fn set_value_by_moonlight(&mut self, args: SetValueByMoonlight) { - self.assert_action( - args.authorization.action.chain_id, - args.authorization.action.contract, - args.authorization.action.domain, - args.authorization.action.action_id, - args.authorization.action.payload_hash, - args.amount, + let envelope = self.action_envelope(args.amount); + let principal = self.authorizations.authorize_moonlight_action( + &args.authorization, + envelope, + now(), ); - let principal = self - .authorizations - .authorize_moonlight(&args.authorization, now()); self.set_value(principal, args.amount); } pub fn set_value_by_phoenix(&mut self, args: SetValueByPhoenix) { - self.assert_action( - args.authorization.action.chain_id, - args.authorization.action.contract, - args.authorization.action.domain, - args.authorization.action.action_id, - args.authorization.action.payload_hash, - args.amount, + let envelope = self.action_envelope(args.amount); + let principal = self.authorizations.authorize_phoenix_action( + &args.authorization, + envelope, + now(), ); - let principal = self - .authorizations - .authorize_phoenix(&args.authorization, now()); self.set_value(principal, args.amount); } - fn assert_action( - &self, - chain_id: u8, - contract: ContractId, - domain: NonceDomain, - action_id: [u8; 32], - payload_hash: [u8; 32], - amount: u64, - ) { + fn action_envelope(&self, amount: u64) -> ActionEnvelope { if !self.initialized { panic!("AuthorizationCounter: not initialized"); } - if chain_id != abi::chain_id() { - panic!("AuthorizationCounter: wrong chain"); - } - if contract != abi::self_id() { - panic!("AuthorizationCounter: wrong contract"); - } - if domain != SET_VALUE_DOMAIN || action_id != SET_VALUE_ACTION { - panic!("AuthorizationCounter: wrong action"); - } - if payload_hash != amount_hash(amount) { - panic!("AuthorizationCounter: wrong payload"); - } + ActionEnvelope::new( + abi::chain_id(), + abi::self_id(), + SET_VALUE_DOMAIN, + SET_VALUE_ACTION, + amount_hash(amount), + ) } fn set_value(&mut self, principal: Principal, amount: u64) { diff --git a/standards/examples/moonlight_call_router/Cargo.toml b/standards/examples/moonlight_call_router/Cargo.toml new file mode 100644 index 00000000..f4b6cffa --- /dev/null +++ b/standards/examples/moonlight_call_router/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "moonlight-call-router" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[target.'cfg(target_family = "wasm")'.dependencies] +dusk-core = "=1.6.0" +dusk-forge = "=0.3.0" +bytecheck = { workspace = true } +rkyv = { workspace = true, features = ["size_32", "alloc", "validation"] } + +[features] +contract = ["dusk-core/abi-dlmalloc"] +data-driver = [] diff --git a/standards/examples/moonlight_call_router/Makefile b/standards/examples/moonlight_call_router/Makefile new file mode 100644 index 00000000..816da2cd --- /dev/null +++ b/standards/examples/moonlight_call_router/Makefile @@ -0,0 +1,23 @@ +TARGET_DIR ?= ../../../target + +all: wasm clippy + +wasm: + @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ + CARGO_TARGET_DIR=$(TARGET_DIR) \ + cargo build \ + --release \ + --color=always \ + -Z build-std=core,alloc \ + --target wasm32-unknown-unknown \ + --features contract \ + -p moonlight-call-router + +test: + +clippy: + @cargo clippy -p moonlight-call-router -Z build-std=core,alloc --release --target wasm32-unknown-unknown --features contract -- -D warnings + +doc: + +.PHONY: all wasm test clippy doc diff --git a/standards/examples/moonlight_call_router/src/lib.rs b/standards/examples/moonlight_call_router/src/lib.rs new file mode 100644 index 00000000..358f49fe --- /dev/null +++ b/standards/examples/moonlight_call_router/src/lib.rs @@ -0,0 +1,57 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Test-only router that models the transfer-contract Moonlight call path. + +#![no_std] +#![cfg(target_family = "wasm")] + +extern crate alloc; +extern crate self as moonlight_call_router_types; + +use alloc::string::String; +use alloc::vec::Vec; + +use bytecheck::CheckBytes; +use dusk_core::abi::ContractId; +use rkyv::{Archive, Deserialize, Serialize}; + +#[derive(Archive, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[archive_attr(derive(CheckBytes))] +pub struct ForwardCall { + pub contract: ContractId, + pub function: String, + pub args: Vec, +} + +#[dusk_forge::contract] +mod moonlight_call_router { + use alloc::vec::Vec; + + use dusk_core::abi; + use moonlight_call_router_types::ForwardCall; + + pub struct MoonlightCallRouter; + + impl MoonlightCallRouter { + pub const fn new() -> Self { + Self + } + + pub fn init(&mut self) {} + + pub fn forward(&mut self, call: ForwardCall) -> Vec { + abi::call_raw(call.contract, &call.function, &call.args) + .unwrap_or_else(|err| panic!("MoonlightCallRouter: {err:?}")) + } + } + + impl Default for MoonlightCallRouter { + fn default() -> Self { + Self::new() + } + } +}