diff --git a/Cargo.toml b/Cargo.toml index 0d458b960..47f7e56c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,8 +68,10 @@ timelock_controller = ["openbrush_contracts/timelock_controller"] proxy = ["openbrush_contracts/proxy"] diamond = ["openbrush_contracts/diamond"] upgradeable = ["openbrush_contracts/upgradeable"] -governance = ["openbrush_contracts/governance"] -utils = ["openbrush_contracts/utils"] +governance = ["openbrush_contracts/governance", "openbrush_contracts/checkpoints"] +crypto = ["openbrush_contracts/crypto"] +nonces = ["openbrush_contracts/nonces"] +checkpoints = ["openbrush_contracts/checkpoints"] test-all = [ "psp22", @@ -85,7 +87,9 @@ test-all = [ "proxy", "diamond", "governance", - "utils", + "crypto", + "nonces", + "checkpoints" ] [profile.release] diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 9d122d68f..ff456408e 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -20,7 +20,7 @@ scale = { package = "parity-scale-codec", version = "3", default-features = fals scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } -openbrush = { version = "~4.0.0-beta.1", package = "openbrush_lang", path = "../lang", default-features = false } +openbrush = { version = "~4.0.0-beta.1", package = "openbrush_lang", path = "../lang", default-features = false, features = ["crypto", "checkpoints"] } pallet-assets-chain-extension = { git = "https://github.com/Brushfam/pallet-assets-chain-extension", branch = "polkadot-v0.9.37", default-features = false, features = ["ink-lang"] } @@ -40,7 +40,7 @@ std = [ "openbrush/std", "pallet-assets-chain-extension/ink-std", ] -psp22 = [] +psp22 = ["nonces", "crypto"] psp22_pallet = [] psp34 = [] psp37 = [] @@ -60,10 +60,14 @@ diamond = [ ] governance = [ "timelock_controller", - "utils" + "crypto", + "nonces", + "checkpoints" ] -utils = [] upgradeable = ["ownable"] +crypto = ["openbrush/crypto"] +nonces = [] +checkpoints = ["openbrush/checkpoints"] test-all = [ "psp22", "psp34", @@ -75,7 +79,6 @@ test-all = [ "pausable", "timelock_controller", "proxy", - "utils", "diamond", "governance", -] \ No newline at end of file +] diff --git a/contracts/src/governance/extensions/governor_quorum/data.rs b/contracts/src/governance/extensions/governor_quorum/data.rs index 793f72bff..32e983b0b 100644 --- a/contracts/src/governance/extensions/governor_quorum/data.rs +++ b/contracts/src/governance/extensions/governor_quorum/data.rs @@ -20,7 +20,7 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -pub use crate::utils::checkpoint::Checkpoints; +pub use openbrush::utils::checkpoints::Checkpoints; #[derive(Debug, Default)] #[openbrush::storage_item] diff --git a/contracts/src/governance/extensions/governor_quorum/impls.rs b/contracts/src/governance/extensions/governor_quorum/impls.rs index 49dc2ba27..80532884b 100644 --- a/contracts/src/governance/extensions/governor_quorum/impls.rs +++ b/contracts/src/governance/extensions/governor_quorum/impls.rs @@ -59,7 +59,10 @@ pub trait QuorumImpl: /// Returns the current quorum numerator fn quorum_numerator(&self) -> u128 { - let history = self.data::().quorum_numerator_history.get_or_default(); + let history = self + .data::() + .quorum_numerator_history + .get_or_default(); let (exist, _, last_value) = history.latest_checkpoint(); diff --git a/contracts/src/governance/governor/impls.rs b/contracts/src/governance/governor/impls.rs index 672b0c088..6d5ddc005 100644 --- a/contracts/src/governance/governor/impls.rs +++ b/contracts/src/governance/governor/impls.rs @@ -19,32 +19,29 @@ // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +use crate::governance::{ + extensions::{ + governor_settings::{ + GovernorSettingsImpl, + GovernorSettingsInternal, + }, + governor_votes::GovernorVotesInternal, + }, + governor::{ + Data, + GovernorEvents, + GovernorInternal, + GovernorStorageGetters, + TimestampProvider, + }, +}; pub use crate::{ governance::governor, traits::{ - errors::governance::GovernanceError, + errors::GovernanceError, governance::*, - types::SignatureType, - }, -}; -use crate::{ - governance::{ - extensions::{ - governor_settings::{ - GovernorSettingsImpl, - GovernorSettingsInternal, - }, - governor_votes::GovernorVotesInternal, - }, - governor::{ - Data, - GovernorEvents, - GovernorInternal, - GovernorStorageGetters, - TimestampProvider, - }, + types::Signature, }, - utils::crypto, }; use ink::{ env::{ @@ -326,22 +323,15 @@ pub trait GovernorImpl: proposal_id: ProposalId, support: VoteType, reason: String, - signature: SignatureType, + signature: Signature, ) -> Result { - let message = crypto::hash_message( - (proposal_id.clone(), support.clone(), reason.clone(), Vec::::new()) - .encode() - .as_slice(), - )?; - - let voter = Self::env().caller(); - let valid = crypto::verify_signature(&message, &voter.clone(), &signature)?; + let message = (proposal_id.clone(), support.clone(), reason.clone(), Vec::::new()).encode(); - if !valid { - return Err(GovernanceError::InvalidSignature(voter.clone())) + if !signature.verify(&message, &Self::env().caller()) { + return Err(GovernanceError::InvalidSignature(Self::env().caller())) } - self._cast_vote(proposal_id, voter, support, reason) + self._cast_vote(proposal_id, Self::env().caller(), support, reason) } /// Casts a vote with signature and parameters for a proposal from a message sender. Returns the number of votes already casted for the proposal by the sender @@ -350,23 +340,16 @@ pub trait GovernorImpl: proposal_id: ProposalId, support: VoteType, reason: String, - signature: SignatureType, + signature: Signature, params: Vec, ) -> Result { - let message = crypto::hash_message( - (proposal_id.clone(), support.clone(), reason.clone(), params.clone()) - .encode() - .as_slice(), - )?; + let message = (proposal_id.clone(), support.clone(), reason.clone(), params.clone()).encode(); - let voter = Self::env().caller(); - let valid = crypto::verify_signature(&message, &voter.clone(), &signature)?; - - if !valid { - return Err(GovernanceError::InvalidSignature(voter.clone())) + if !signature.verify(&message, &Self::env().caller()) { + return Err(GovernanceError::InvalidSignature(Self::env().caller())) } - self._cast_vote_with_params(proposal_id, voter, support, reason, params) + self._cast_vote_with_params(proposal_id, Self::env().caller(), support, reason, params) } /// Relays a transaction or function call to an arbitrary target. In cases where the governance executor diff --git a/contracts/src/governance/governor/internal.rs b/contracts/src/governance/governor/internal.rs index 65698f084..74d663ce4 100644 --- a/contracts/src/governance/governor/internal.rs +++ b/contracts/src/governance/governor/internal.rs @@ -33,7 +33,7 @@ use crate::{ }, }, traits::{ - errors::governance::GovernanceError, + errors::GovernanceError, governance::{ CancelationStatus, ExecutionStatus, @@ -46,7 +46,6 @@ use crate::{ ALL_PROPOSAL_STATES, }, }, - utils::crypto, }; use ink::{ env::{ @@ -65,12 +64,15 @@ use ink::{ vec::Vec, }, }; -use openbrush::traits::{ - AccountId, - Balance, - DefaultEnv, - Storage, - String, +use openbrush::{ + traits::{ + AccountId, + Balance, + DefaultEnv, + Storage, + String, + }, + utils::crypto, }; use scale::Encode; @@ -85,7 +87,7 @@ pub trait GovernorInternal: ) -> Result { let message = (transactions, description_hash).encode(); - crypto::hash_message(message.as_slice()).map_err(|err| err.into()) + Ok(crypto::hash_blake2b256(message.as_slice())) } /// Current state of a proposal, following Compound's convention @@ -326,7 +328,7 @@ pub trait GovernorInternal: /// Return the hash of the description. fn _hash_description(&self, description: String) -> Result { - Ok(crypto::hash_message(description.as_bytes())?) + Ok(crypto::hash_blake2b256(description.as_bytes())) } } diff --git a/contracts/src/governance/utils/votes/data.rs b/contracts/src/governance/utils/votes/data.rs index 5c98b6880..6278cb1a6 100644 --- a/contracts/src/governance/utils/votes/data.rs +++ b/contracts/src/governance/utils/votes/data.rs @@ -20,10 +20,10 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -use crate::utils::checkpoint::Checkpoints; use openbrush::{ storage::Mapping, traits::AccountId, + utils::checkpoints::Checkpoints, }; #[derive(Default, Debug)] diff --git a/contracts/src/governance/utils/votes/impls.rs b/contracts/src/governance/utils/votes/impls.rs index f63f70a46..34a88db99 100644 --- a/contracts/src/governance/utils/votes/impls.rs +++ b/contracts/src/governance/utils/votes/impls.rs @@ -21,15 +21,8 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use crate::{ - governance::utils::votes::{ - Data, - VotesEvents, - VotesInternal, - }, - utils::{ - crypto, - nonces::NoncesImpl, - }, + governance::utils::votes::*, + nonces, }; pub use crate::{ governance::{ @@ -39,9 +32,8 @@ pub use crate::{ traits::{ errors::GovernanceError, governance::utils::votes::*, - types::SignatureType, + types::Signature, }, - utils::checkpoint::*, }; use openbrush::traits::{ AccountId, @@ -52,7 +44,7 @@ use openbrush::traits::{ use scale::Encode; /// Common interface for `PSP22Votes`, and other `Votes`-enabled contracts. -pub trait VotesImpl: Storage + VotesInternal + NoncesImpl + VotesEvents + TimestampProvider { +pub trait VotesImpl: Storage + VotesInternal + nonces::NoncesImpl + VotesEvents + TimestampProvider { /// The amount of votes owned by `account`. fn get_votes(&self, account: AccountId) -> Balance { self.data::() @@ -110,16 +102,17 @@ pub trait VotesImpl: Storage + VotesInternal + NoncesImpl + VotesEvents + &mut self, signer: AccountId, delegatee: AccountId, - nonce: u128, + nonce: u64, expiry: Timestamp, - signature: SignatureType, + signature: Signature, ) -> Result<(), GovernanceError> { if TimestampProvider::block_timestamp(self) > expiry { return Err(GovernanceError::ExpiredSignature(expiry)) } - let message_hash = crypto::hash_message(Encode::encode(&(&delegatee, &nonce, &expiry)).as_slice())?; - let verify_result = crypto::verify_signature(&message_hash, &signer, &signature)?; - if !verify_result { + + let message = (&delegatee, &nonce, &expiry).encode(); + + if !signature.verify(&message, &signer) { return Err(GovernanceError::InvalidSignature(signer)) } else { self._use_checked_nonce(&signer, nonce)?; diff --git a/contracts/src/governance/utils/votes/internal.rs b/contracts/src/governance/utils/votes/internal.rs index 9ae8af834..843208fa1 100644 --- a/contracts/src/governance/utils/votes/internal.rs +++ b/contracts/src/governance/utils/votes/internal.rs @@ -28,20 +28,20 @@ use crate::{ VotesEvents, }, }, - traits::errors::{ - CheckpointsError, - GovernanceError, + traits::errors::GovernanceError, +}; +use openbrush::{ + traits::{ + AccountId, + Balance, + Storage, }, - utils::checkpoint::{ + utils::checkpoints::{ Checkpoint, Checkpoints, + CheckpointsError, }, }; -use openbrush::traits::{ - AccountId, - Balance, - Storage, -}; pub trait VotesInternal: Storage + VotesEvents + TimestampProvider { /// Returns the total number of votes. diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 86d915227..62a5b4ee8 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -61,5 +61,5 @@ pub use upgradeability::diamond; pub use upgradeability::proxy; #[cfg(feature = "upgradeable")] pub use upgradeability::upgradeable; -#[cfg(feature = "utils")] -pub use utils::*; +#[cfg(feature = "nonces")] +pub use utils::nonces; diff --git a/contracts/src/token/psp22/extensions/permit.rs b/contracts/src/token/psp22/extensions/permit.rs new file mode 100644 index 000000000..5be379028 --- /dev/null +++ b/contracts/src/token/psp22/extensions/permit.rs @@ -0,0 +1,143 @@ +// Copyright (c) 2012-2022 Supercolony +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the"Software"), +// to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +pub use crate::{ + nonces::*, + psp22, + psp22::extensions::permit, + traits::psp22::{ + extensions::permit::*, + *, + }, +}; +use openbrush::{ + traits::{ + AccountId, + Balance, + Storage, + }, + utils::crypto::hash_blake2b256, +}; + +pub use openbrush::utils::crypto::Signature; + +pub use psp22::{ + Internal as _, + InternalImpl as _, + PSP22Impl, +}; +use scale::Encode; + +#[derive(Default, Debug)] +#[openbrush::storage_item] +pub struct Data { + #[lazy] + pub cached_domain_separator: [u8; 32], +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, scale::Encode, scale::Decode)] +pub struct PermitMessage { + pub domain_separator: [u8; 32], + pub owner: AccountId, + pub spender: AccountId, + pub amount: Balance, + pub deadline: u64, + pub nonce: u64, +} + +pub trait PSP22PermitImpl: Internal { + fn permit( + &mut self, + owner: AccountId, + spender: AccountId, + amount: Balance, + deadline: u64, + signature: Signature, + ) -> Result<(), PSP22Error> { + self._permit(owner, spender, amount, deadline, signature) + } + + fn domain_separator(&mut self) -> [u8; 32] { + self._domain_separator() + } +} + +pub trait Internal { + fn _permit( + &mut self, + owner: AccountId, + spender: AccountId, + amount: Balance, + deadline: u64, + signature: Signature, + ) -> Result<(), PSP22Error>; + + fn _domain_separator(&mut self) -> [u8; 32]; +} + +pub trait InternalImpl: Storage + psp22::Internal + NoncesImpl { + fn _permit( + &mut self, + owner: AccountId, + spender: AccountId, + amount: Balance, + deadline: u64, + signature: Signature, + ) -> Result<(), PSP22Error> { + let block_time = Self::env().block_timestamp(); + if deadline < block_time { + return Err(PSP22Error::PermitExpired) + } + + let nonce = self._use_nonce(&owner)?; + let domain_separator = self._domain_separator(); + + let message = &scale::Encode::encode(&PermitMessage { + domain_separator, + owner, + spender, + amount, + deadline, + nonce, + }); + + if signature.verify(message, &owner) { + self._approve_from_to(owner, spender, amount)?; + Ok(()) + } else { + Err(PSP22Error::PermitInvalidSignature) + } + } + + fn _domain_separator(&mut self) -> [u8; 32] { + let cached = self.data::().cached_domain_separator.get_or_default(); + + if self.data::().cached_domain_separator.get().is_none() { + let account_hash = hash_blake2b256(&Self::env().account_id().encode()); + + self.data::().cached_domain_separator.set(&account_hash); + + account_hash + } else { + cached + } + } +} diff --git a/contracts/src/token/psp22/extensions/votes.rs b/contracts/src/token/psp22/extensions/votes.rs index c1cd6d27d..1c82b83d3 100644 --- a/contracts/src/token/psp22/extensions/votes.rs +++ b/contracts/src/token/psp22/extensions/votes.rs @@ -26,10 +26,10 @@ use crate::{ psp22, psp22::PSP22Error, traits::errors::GovernanceError, - utils::checkpoint::Checkpoint, }; use ink::prelude::vec; use openbrush::traits::AccountId; +pub use openbrush::utils::checkpoints::Checkpoint; /// Extension of ERC20 to support Compound-like voting and delegation. /// diff --git a/contracts/src/token/psp22/mod.rs b/contracts/src/token/psp22/mod.rs index 35f12f8b9..2a4722726 100644 --- a/contracts/src/token/psp22/mod.rs +++ b/contracts/src/token/psp22/mod.rs @@ -29,6 +29,7 @@ pub mod extensions { pub mod flashmint; pub mod metadata; pub mod mintable; + pub mod permit; #[cfg(feature = "governance")] pub mod votes; pub mod wrapper; diff --git a/contracts/src/traits/errors/flashloan.rs b/contracts/src/traits/errors/flashloan.rs index 4ea923ca9..01688ae40 100644 --- a/contracts/src/traits/errors/flashloan.rs +++ b/contracts/src/traits/errors/flashloan.rs @@ -110,6 +110,11 @@ impl From for FlashLenderError { PSP22Error::ZeroRecipientAddress => FlashLenderError::Custom(String::from("PSP22: Zero Recipient Address")), PSP22Error::ZeroSenderAddress => FlashLenderError::Custom(String::from("PSP22: Zero Sender Address")), PSP22Error::SafeTransferCheckFailed(message) => FlashLenderError::Custom(message), + PSP22Error::PermitInvalidSignature => { + FlashLenderError::Custom(String::from("PSP22: Permit Invalid Signature")) + } + PSP22Error::PermitExpired => FlashLenderError::Custom(String::from("PSP22: Permit Expired")), + PSP22Error::NoncesError(_) => FlashLenderError::Custom(String::from("PSP22: Nonces Error")), } } } diff --git a/contracts/src/traits/errors/governance.rs b/contracts/src/traits/errors/governance.rs index 360b10b95..41814a053 100644 --- a/contracts/src/traits/errors/governance.rs +++ b/contracts/src/traits/errors/governance.rs @@ -21,20 +21,22 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use crate::traits::{ - errors::{ - CheckpointsError, - CryptoError, - NoncesError, - }, + errors::NoncesError, governance::{ ProposalId, ProposalState, Transaction, }, }; -use openbrush::traits::{ - AccountId, - Timestamp, +use openbrush::{ + traits::{ + AccountId, + Timestamp, + }, + utils::{ + checkpoints::CheckpointsError, + crypto::CryptoError, + }, }; /// The Governor error type. Contract will throw one of this errors. diff --git a/contracts/src/traits/errors/mod.rs b/contracts/src/traits/errors/mod.rs index 94d882278..df817bcdc 100644 --- a/contracts/src/traits/errors/mod.rs +++ b/contracts/src/traits/errors/mod.rs @@ -21,11 +21,9 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mod access_control; -mod checkpoints; -mod crypto; mod diamond; mod flashloan; -pub(crate) mod governance; +mod governance; mod nonces; mod ownable; mod pausable; @@ -38,8 +36,6 @@ mod timelock_controller; mod upgradeable; pub use access_control::AccessControlError; -pub use checkpoints::CheckpointsError; -pub use crypto::CryptoError; pub use diamond::DiamondError; pub use flashloan::{ FlashBorrowerError, diff --git a/contracts/src/traits/errors/nonces.rs b/contracts/src/traits/errors/nonces.rs index 3255b660e..d97afbbcb 100644 --- a/contracts/src/traits/errors/nonces.rs +++ b/contracts/src/traits/errors/nonces.rs @@ -25,7 +25,7 @@ use openbrush::traits::AccountId; #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub enum NoncesError { - /// - InvalidAccountNonce(AccountId, u128), + /// Invalid nonce for account + InvalidAccountNonce(AccountId, u64), NonceOverflow, } diff --git a/contracts/src/traits/errors/psp22.rs b/contracts/src/traits/errors/psp22.rs index e203c2603..2d4cd6421 100644 --- a/contracts/src/traits/errors/psp22.rs +++ b/contracts/src/traits/errors/psp22.rs @@ -21,6 +21,7 @@ use super::{ AccessControlError, + NoncesError, OwnableError, PausableError, ReentrancyGuardError, @@ -43,6 +44,12 @@ pub enum PSP22Error { ZeroSenderAddress, /// Returned if safe transfer check fails SafeTransferCheckFailed(String), + /// Returned if permit signature is invalid + PermitInvalidSignature, + /// Returned if permit deadline is expired + PermitExpired, + /// Returned if permit nonce is invalid + NoncesError(NoncesError), } impl From for PSP22Error { @@ -113,16 +120,7 @@ pub enum PSP22TokenTimelockError { impl From for PSP22TokenTimelockError { fn from(error: PSP22Error) -> Self { - match error { - PSP22Error::Custom(message) => PSP22TokenTimelockError::PSP22Error(PSP22Error::Custom(message)), - PSP22Error::InsufficientBalance => PSP22TokenTimelockError::PSP22Error(PSP22Error::InsufficientBalance), - PSP22Error::InsufficientAllowance => PSP22TokenTimelockError::PSP22Error(PSP22Error::InsufficientAllowance), - PSP22Error::ZeroRecipientAddress => PSP22TokenTimelockError::PSP22Error(PSP22Error::ZeroRecipientAddress), - PSP22Error::ZeroSenderAddress => PSP22TokenTimelockError::PSP22Error(PSP22Error::ZeroSenderAddress), - PSP22Error::SafeTransferCheckFailed(message) => { - PSP22TokenTimelockError::PSP22Error(PSP22Error::SafeTransferCheckFailed(message)) - } - } + PSP22TokenTimelockError::PSP22Error(error) } } @@ -149,3 +147,9 @@ impl From for PSP22TokenTimelockError { PSP22TokenTimelockError::PSP22Error(guard.into()) } } + +impl From for PSP22Error { + fn from(error: NoncesError) -> Self { + PSP22Error::NoncesError(error) + } +} diff --git a/contracts/src/traits/governance/governor/mod.rs b/contracts/src/traits/governance/governor/mod.rs index a6c04bfb2..85958abce 100644 --- a/contracts/src/traits/governance/governor/mod.rs +++ b/contracts/src/traits/governance/governor/mod.rs @@ -29,7 +29,7 @@ pub use crate::traits::{ Transaction, VoteType, }, - types::SignatureType, + types::Signature, }; use ink::prelude::vec::Vec; use openbrush::traits::{ @@ -137,7 +137,7 @@ pub trait Governor { proposal_id: ProposalId, support: VoteType, reason: String, - signature: SignatureType, + signature: Signature, ) -> Result; /// Casts a vote with signature and parameters for a proposal from a message sender. Returns the number of votes already casted for the proposal by the sender @@ -147,7 +147,7 @@ pub trait Governor { proposal_id: ProposalId, support: VoteType, reason: String, - signature: SignatureType, + signature: Signature, params: Vec, ) -> Result; diff --git a/contracts/src/traits/governance/mod.rs b/contracts/src/traits/governance/mod.rs index d07bdd0a1..4f639a536 100644 --- a/contracts/src/traits/governance/mod.rs +++ b/contracts/src/traits/governance/mod.rs @@ -43,6 +43,8 @@ pub mod utils { pub mod votes; } +pub use openbrush::utils::crypto::Signature; + pub type ProposalId = [u8; 32]; pub type HashType = [u8; 32]; pub type Selector = [u8; 4]; diff --git a/contracts/src/traits/governance/utils/votes.rs b/contracts/src/traits/governance/utils/votes.rs index 78e2c5de9..ed3ba6ebf 100644 --- a/contracts/src/traits/governance/utils/votes.rs +++ b/contracts/src/traits/governance/utils/votes.rs @@ -22,7 +22,7 @@ pub use crate::traits::{ errors::GovernanceError, - types::SignatureType, + types::Signature, }; use openbrush::traits::{ AccountId, @@ -59,9 +59,9 @@ pub trait Votes { &mut self, signer: AccountId, delegatee: AccountId, - nonce: u128, + nonce: u64, expiry: Timestamp, - signature: SignatureType, + signature: Signature, ) -> Result<(), GovernanceError>; } diff --git a/contracts/src/traits/mod.rs b/contracts/src/traits/mod.rs index d6716b00b..81884a985 100644 --- a/contracts/src/traits/mod.rs +++ b/contracts/src/traits/mod.rs @@ -25,6 +25,7 @@ pub mod diamond; pub mod errors; pub mod flashloan; pub mod governance; +pub mod nonces; pub mod ownable; pub mod pausable; pub mod payment_splitter; @@ -33,6 +34,5 @@ pub mod psp22; pub mod psp34; pub mod psp37; pub mod upgradeable; -pub mod utils; pub mod types; diff --git a/contracts/src/traits/errors/crypto.rs b/contracts/src/traits/nonces/mod.rs similarity index 78% rename from contracts/src/traits/errors/crypto.rs rename to contracts/src/traits/nonces/mod.rs index e75370064..53c180e9d 100644 --- a/contracts/src/traits/errors/crypto.rs +++ b/contracts/src/traits/nonces/mod.rs @@ -20,12 +20,13 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -use openbrush::traits::String; +pub use crate::traits::errors::NoncesError; +use openbrush::traits::AccountId; -#[derive(scale::Decode, scale::Encode, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] -pub enum CryptoError { - EcdsaRecoverFailed, - EcdsaToEthAddressFailed, - Other(String), +/// Provides tracking nonces for addresses. Nonces will only increment. +#[openbrush::trait_definition] +pub trait Nonces { + /// Returns the nonce of `account`. + #[ink(message)] + fn nonces(&self, account: AccountId) -> u64; } diff --git a/lang/src/utils.rs b/contracts/src/traits/psp22/extensions/permit.rs similarity index 56% rename from lang/src/utils.rs rename to contracts/src/traits/psp22/extensions/permit.rs index 32974687a..d3c603d3f 100644 --- a/lang/src/utils.rs +++ b/contracts/src/traits/psp22/extensions/permit.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2022 Supercolony +// Copyright (c) 2012-2023 727-ventures // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the"Software"), @@ -19,18 +19,33 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -pub use const_format; -pub use xxhash_rust; +/// Extension of [`PSP22`] that allows create `amount` tokens +/// and assigns them to `account`, increasing the total supply +pub use crate::traits::errors::PSP22Error; +use openbrush::traits::{ + AccountId, + Balance, +}; +pub use openbrush::utils::crypto::Signature; -use xxhash_rust::const_xxh32::xxh32; +#[openbrush::wrapper] +pub type PSP22PermitRef = dyn PSP22Permit; -/// The value 0 is a valid seed. -const XXH32_SEED: u32 = 0; +#[openbrush::trait_definition] +pub trait PSP22Permit { + /// Permit allows `spender` to spend `value` tokens on behalf of `owner` with a signature + /// + /// See [`PSP22::_approve`]. + #[ink(message)] + fn permit( + &mut self, + owner: AccountId, + spender: AccountId, + value: Balance, + deadline: u64, + signature: Signature, + ) -> Result<(), PSP22Error>; -pub struct ConstHasher; - -impl ConstHasher { - pub const fn hash(str: &str) -> u32 { - xxh32(str.as_bytes(), XXH32_SEED) - } + #[ink(message)] + fn domain_separator(&mut self) -> [u8; 32]; } diff --git a/contracts/src/traits/psp22/extensions/votes.rs b/contracts/src/traits/psp22/extensions/votes.rs index 4115cd111..acaedfd25 100644 --- a/contracts/src/traits/psp22/extensions/votes.rs +++ b/contracts/src/traits/psp22/extensions/votes.rs @@ -20,14 +20,12 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -use crate::{ - traits::{ - errors::GovernanceError, - governance::utils::votes::*, - }, - utils::checkpoint::Checkpoint, +use crate::traits::{ + errors::GovernanceError, + governance::utils::votes::*, }; use openbrush::traits::AccountId; +pub use openbrush::utils::checkpoints::Checkpoint; /// Extension of ERC20 to support Compound-like voting and delegation. /// diff --git a/contracts/src/traits/psp22/mod.rs b/contracts/src/traits/psp22/mod.rs index 04a9b0f58..628ad3a72 100644 --- a/contracts/src/traits/psp22/mod.rs +++ b/contracts/src/traits/psp22/mod.rs @@ -29,6 +29,7 @@ pub mod extensions { pub mod capped; pub mod metadata; pub mod mintable; + pub mod permit; #[cfg(feature = "governance")] pub mod votes; pub mod wrapper; diff --git a/contracts/src/traits/types.rs b/contracts/src/traits/types.rs index 1ded80e2e..f33e58206 100644 --- a/contracts/src/traits/types.rs +++ b/contracts/src/traits/types.rs @@ -42,4 +42,4 @@ impl Default for Id { } } -pub type SignatureType = [u8; 65]; +pub use openbrush::utils::crypto::Signature; diff --git a/contracts/src/traits/utils/mod.rs b/contracts/src/traits/utils/mod.rs deleted file mode 100644 index 6da806eaa..000000000 --- a/contracts/src/traits/utils/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod nonces; diff --git a/contracts/src/traits/utils/nonces.rs b/contracts/src/traits/utils/nonces.rs deleted file mode 100644 index 1d234fd75..000000000 --- a/contracts/src/traits/utils/nonces.rs +++ /dev/null @@ -1,9 +0,0 @@ -use openbrush::traits::AccountId; - -/// Provides tracking nonces for addresses. Nonces will only increment. -#[openbrush::trait_definition] -pub trait Nonces { - /// Returns the nonce of `account`. - #[ink(message)] - fn nonces(&self, account: AccountId) -> u128; -} diff --git a/contracts/src/utils/crypto/mod.rs b/contracts/src/utils/crypto/mod.rs deleted file mode 100644 index 010bf37c0..000000000 --- a/contracts/src/utils/crypto/mod.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2023 Brushfam -// Copyright (c) 2012-2022 Supercolony -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the"Software"), -// to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -use crate::traits::{ - errors::CryptoError, - types::SignatureType, -}; -use ink::primitives::AccountId; - -/// Verifies the signature of a message hash with the account -pub fn verify_signature( - message_hash: &[u8; 32], - account: &AccountId, - signature: &SignatureType, -) -> Result { - let mut output = [0u8; 33]; - - ink::env::ecdsa_recover(&signature, message_hash, &mut output).map_err(|_| CryptoError::EcdsaRecoverFailed)?; - - let recovered_account = pub_key_to_ss58(&output)?; - - if recovered_account != account.clone() { - return Ok(false) - } - - Ok(true) -} - -/// Hashes a message -pub fn hash_message(message: &[u8]) -> Result<[u8; 32], CryptoError> { - let mut output = [0u8; 32]; - - ink::env::hash_bytes::(message, &mut output); - - Ok(output) -} - -/// Converts a public key to SS58 -pub fn pub_key_to_ss58(pub_key: &[u8; 33]) -> Result { - hash_message(pub_key).map(|hash| AccountId::from(hash)) -} - -/// Converts a public key to an Ethereum address -pub fn pub_key_to_eth_address(pub_key: &[u8; 33]) -> Result<[u8; 20], CryptoError> { - let mut output = [0u8; 20]; - - ink::env::ecdsa_to_eth_address(pub_key, &mut output).map_err(|_| CryptoError::EcdsaToEthAddressFailed)?; - - Ok(output) -} diff --git a/contracts/src/utils/mod.rs b/contracts/src/utils/mod.rs index 71eb1ff64..2fe214417 100644 --- a/contracts/src/utils/mod.rs +++ b/contracts/src/utils/mod.rs @@ -20,9 +20,5 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -#[cfg(feature = "utils")] -pub mod checkpoint; -#[cfg(feature = "utils")] -pub mod crypto; -#[cfg(feature = "utils")] +#[cfg(feature = "nonces")] pub mod nonces; diff --git a/contracts/src/utils/nonces/mod.rs b/contracts/src/utils/nonces/mod.rs index b28400857..0524e3301 100644 --- a/contracts/src/utils/nonces/mod.rs +++ b/contracts/src/utils/nonces/mod.rs @@ -20,52 +20,6 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -pub use crate::{ - traits::{ - errors::NoncesError, - utils::nonces::*, - }, - utils::nonces, -}; -use openbrush::{ - storage::Mapping, - traits::{ - AccountId, - Storage, - }, -}; +pub mod nonces; -#[derive(Default, Debug)] -#[openbrush::storage_item] -pub struct Data { - pub nonces: Mapping, -} - -/// Provides tracking nonces for addresses. Nonces will only increment. -pub trait NoncesImpl: Storage { - /// Returns the nonce of `account`. - fn nonces(&self, account: &AccountId) -> u128 { - self.data().nonces.get(account).unwrap_or_default() - } - - /// Returns the next nonce of `account`, and increments the nonce. - fn _use_nonce(&mut self, account: &AccountId) -> Result { - let nonce = self.nonces(account); - self.data() - .nonces - .insert(account, &(nonce.checked_add(1).ok_or(NoncesError::NonceOverflow)?)); - Ok(nonce) - } - - /// Returns the next nonce of `account`, and increments the nonce if `nonce` matches the current nonce. - fn _use_checked_nonce(&mut self, account: &AccountId, nonce: u128) -> Result { - let current_nonce = self.nonces(&account); - if nonce != current_nonce { - return Err(NoncesError::InvalidAccountNonce(account.clone(), current_nonce)) - } - self.data() - .nonces - .insert(account, &(nonce.checked_add(1).ok_or(NoncesError::NonceOverflow)?)); - Ok(nonce) - } -} +pub use nonces::*; diff --git a/contracts/src/utils/nonces/nonces.rs b/contracts/src/utils/nonces/nonces.rs new file mode 100644 index 000000000..67a03a350 --- /dev/null +++ b/contracts/src/utils/nonces/nonces.rs @@ -0,0 +1,68 @@ +// Copyright (c) 2023 Brushfam +// Copyright (c) 2012-2022 Supercolony +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the"Software"), +// to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +pub use crate::{ + nonces, + traits::nonces::*, +}; +use openbrush::{ + storage::Mapping, + traits::{ + AccountId, + Storage, + }, +}; + +#[derive(Default, Debug)] +#[openbrush::storage_item] +pub struct Data { + pub nonces: Mapping, +} + +/// Provides tracking nonces for addresses. Nonces will only increment. +pub trait NoncesImpl: Storage { + /// Returns the nonce of `account`. + fn nonces(&self, account: &AccountId) -> u64 { + self.data().nonces.get(account).unwrap_or_default() + } + + /// Returns the next nonce of `account`, and increments the nonce. + fn _use_nonce(&mut self, account: &AccountId) -> Result { + let nonce = self.nonces(account); + self.data() + .nonces + .insert(account, &(nonce.checked_add(1).ok_or(NoncesError::NonceOverflow)?)); + Ok(nonce) + } + + /// Returns the next nonce of `account`, and increments the nonce if `nonce` matches the current nonce. + fn _use_checked_nonce(&mut self, account: &AccountId, nonce: u64) -> Result { + let current_nonce = self.nonces(&account); + if nonce != current_nonce { + return Err(NoncesError::InvalidAccountNonce(account.clone(), current_nonce)) + } + self.data() + .nonces + .insert(account, &(nonce.checked_add(1).ok_or(NoncesError::NonceOverflow)?)); + Ok(nonce) + } +} diff --git a/example_project_structure/contracts/lending/lib.rs b/example_project_structure/contracts/lending/lib.rs index c5f6c1505..6040fd8ab 100644 --- a/example_project_structure/contracts/lending/lib.rs +++ b/example_project_structure/contracts/lending/lib.rs @@ -40,13 +40,11 @@ pub mod my_lending { use ink::ToAccountId; use lending_project::impls::lending::*; use loan_contract::loan::LoanContractRef; - use openbrush::{ - traits::{ - DefaultEnv, - Storage, - String, - }, - utils::xxhash_rust::const_xxh32::xxh32, + use openbrush::traits::{ + xxh32, + DefaultEnv, + Storage, + String, }; use scale::Encode; use shares_contract::shares::SharesContractRef; diff --git a/examples/governance/governor/Cargo.toml b/examples/governance/governor/Cargo.toml index 192b5b136..732345226 100755 --- a/examples/governance/governor/Cargo.toml +++ b/examples/governance/governor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "my_governor" -version= "4.0.0-beta" +version= "4.0.0-beta.1" authors = ["Brushfam "] edition = "2021" @@ -11,7 +11,7 @@ scale = { package = "parity-scale-codec", version = "3", default-features = fals scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } # These dependencies -openbrush = { path = "../../..", default-features = false, features = ["governance", "utils"] } +openbrush = { path = "../../..", default-features = false, features = ["governance"] } [dev-dependencies] ink_e2e = "4.2.1" diff --git a/examples/psp22_extensions/permit/.gitignore b/examples/psp22_extensions/permit/.gitignore new file mode 100644 index 000000000..8de8f877e --- /dev/null +++ b/examples/psp22_extensions/permit/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/examples/psp22_extensions/permit/Cargo.toml b/examples/psp22_extensions/permit/Cargo.toml new file mode 100644 index 000000000..b750335bd --- /dev/null +++ b/examples/psp22_extensions/permit/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "my_psp22_permit" +version = "4.0.0-beta.1" +authors = ["Brushfam "] +edition = "2021" + +[dependencies] +ink = { version = "4.3.0", default-features = false} +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } + +# These dependencies +openbrush = { path = "../../..", default-features = false, features = ["psp22"] } +ecdsa = { version = "0.16.7", default-features = false, optional = true } + +[dev-dependencies] +ink_e2e = "4.3.0" +ink_env = "4.3.0" +test_helpers = { path = "../../test_helpers", default-features = false } +secp256k1 = { version = "0.27.0", default-features = false } +subxt-signer = "0.31.0" + + +[lib] +name = "my_psp22_permit" +path = "lib.rs" + + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", + # These dependencies + "openbrush/std", +] +ink-as-dependency = [] +e2e-tests = [ + "ecdsa" +] + +[profile.dev] +codegen-units = 16 diff --git a/examples/psp22_extensions/permit/README.md b/examples/psp22_extensions/permit/README.md new file mode 100644 index 000000000..99babb8dc --- /dev/null +++ b/examples/psp22_extensions/permit/README.md @@ -0,0 +1,8 @@ +## PSP22 contract (ERC20 analogue) with 'Permit' extension + +> If you are trying to run this on run, and getting errors, try to use +> +> `` +> AR=/opt/homebrew/opt/llvm/bin/llvm-ar CC=/opt/homebrew/opt/llvm/bin/clang cargo contract build +`` +> , and check [this](https://substrate.stackexchange.com/questions/1098/how-to-use-sp-core-in-libraries-that-target-wasm-for-the-web) \ No newline at end of file diff --git a/examples/psp22_extensions/permit/lib.rs b/examples/psp22_extensions/permit/lib.rs new file mode 100644 index 000000000..bd036af70 --- /dev/null +++ b/examples/psp22_extensions/permit/lib.rs @@ -0,0 +1,31 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +pub use crate::my_psp22_permit::*; + +#[openbrush::implementation(PSP22, PSP22Permit, Nonces)] +#[openbrush::contract] +pub mod my_psp22_permit { + use openbrush::traits::Storage; + + #[ink(storage)] + #[derive(Default, Storage)] + pub struct Contract { + #[storage_field] + psp22: psp22::Data, + #[storage_field] + nonces: nonces::Data, + #[storage_field] + psp22_permit: psp22::extensions::permit::Data, + } + + impl Contract { + #[ink(constructor)] + pub fn new(total_supply: Balance) -> Self { + let mut instance = Self::default(); + + psp22::Internal::_mint_to(&mut instance, Self::env().caller(), total_supply).expect("Should mint"); + + instance + } + } +} diff --git a/examples/psp22_extensions/permit/tests/e2e.rs b/examples/psp22_extensions/permit/tests/e2e.rs new file mode 100644 index 000000000..03b2673a6 --- /dev/null +++ b/examples/psp22_extensions/permit/tests/e2e.rs @@ -0,0 +1,395 @@ +#![cfg(feature = "e2e-tests")] + +extern crate my_psp22_permit; + +use ink::env::hash::{ + Blake2x256, + HashOutput, +}; +#[rustfmt::skip] +use ink_e2e::build_message; +use openbrush::contracts::{ + nonces::nonces_external::Nonces, + psp22::{ + extensions::permit::psp22permit_external::PSP22Permit, + psp22_external::PSP22, + }, +}; +#[rustfmt::skip] +use crate::my_psp22_permit::*; +// use openbrush::contracts::psp22::extensions::permit::PERMIT_TYPE_HASH; +use openbrush::{ + contracts::psp22::extensions::permit::PermitMessage, + traits::{ + AccountId, + Balance, + }, + utils::crypto::{ + hash_blake2b256, + Signature, + }, +}; +use scale::Encode; +use secp256k1::{ + ecdsa::RecoverableSignature, + Message, + PublicKey, + SecretKey, + SECP256K1, +}; +use test_helpers::{ + address_of, + balance_of, + method_call, + method_call_dry_run, +}; + +type E2EResult = Result>; + +#[ink_e2e::test] +async fn assigns_initial_balance(mut client: ink_e2e::Client) -> E2EResult<()> { + let constructor = ContractRef::new(1000); + let address = client + .instantiate("my_psp22_permit", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + assert!(matches!(balance_of!(client, address, Alice), 1000)); + + Ok(()) +} + +#[ink_e2e::test] +async fn nonce_should_be_equal_zero(mut client: ink_e2e::Client) -> E2EResult<()> { + let constructor = ContractRef::new(1000); + let address = client + .instantiate("my_psp22_permit", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let nonce = method_call_dry_run!(client, address, nonces(address_of!(Alice))); + + assert!(matches!(nonce, 0)); + + Ok(()) +} + +#[ink_e2e::test] +async fn check_domain_separator(mut client: ink_e2e::Client) -> E2EResult<()> { + let constructor = ContractRef::new(1000); + let address = &client + .instantiate("my_psp22_permit", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let mut output = ::Type::default(); + ink::env::hash_bytes::(&address.encode(), &mut output); + let domain_separator: [u8; 32] = method_call_dry_run!(client, address, domain_separator()); + let real_domain_separator: [u8; 32] = output; + assert_eq!(domain_separator, real_domain_separator); + + Ok(()) +} + +#[ink_e2e::test] +async fn permit_accepts_owner_signature(mut client: ink_e2e::Client) -> E2EResult<()> { + let constructor = ContractRef::new(1000); + let address = client + .instantiate("my_psp22_permit", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let nonce: u64 = method_call_dry_run!(client, address, nonces(address_of!(Alice))); + let deadline: u64 = 30_000_000_000_000; + let amount: Balance = 1000; + + let domain_separator: [u8; 32] = method_call_dry_run!(client, address, domain_separator()); + + let seckey = [ + 59, 148, 11, 85, 134, 130, 61, 253, 2, 174, 59, 70, 27, 180, 51, 107, 94, 203, 174, 253, 102, 39, 170, 146, 46, + 252, 4, 143, 236, 12, 136, 28, + ]; + let pubkey = PublicKey::from_secret_key( + &SECP256K1, + &SecretKey::from_slice(&seckey).expect("seckey creation failed"), + ); + + let owner = AccountId::from(hash_blake2b256(&pubkey.serialize().to_vec())); + let spender = address_of!(Bob); + + let permit_message = PermitMessage { + domain_separator, + owner, + spender, + amount, + deadline, + nonce, + }; + + let message = &scale::Encode::encode(&permit_message); + + let msg_hash = hash_blake2b256(message); + + let msg = Message::from_slice(&msg_hash).expect("message creation failed"); + let seckey = SecretKey::from_slice(&seckey).expect("secret key creation failed"); + let recoverable_signature: RecoverableSignature = SECP256K1.sign_ecdsa_recoverable(&msg, &seckey); + + let recovery_id = recoverable_signature.serialize_compact().0.to_i32() as u8; + let mut signature = recoverable_signature.serialize_compact().1.to_vec(); + signature.push(recovery_id); + let signature_with_recovery_id: [u8; 65] = signature + .try_into() + .expect("unable to create signature with recovery id"); + + let permit_signature = method_call_dry_run!( + client, + address, + permit( + owner, + spender, + amount, + deadline, + Signature::ECDSA(signature_with_recovery_id) + ) + ); + + println!("permit_signature: {:?}", permit_signature); + assert!(matches!(permit_signature, Ok(_))); + + Ok(()) +} + +#[ink_e2e::test] +async fn permit_rejects_reused_signature(mut client: ink_e2e::Client) -> E2EResult<()> { + let constructor = ContractRef::new(1000); + let address = client + .instantiate("my_psp22_permit", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let nonce: u64 = method_call_dry_run!(client, address, nonces(address_of!(Alice))); + let deadline: u64 = 30_000_000_000_000; + let amount: Balance = 1000; + + let domain_separator: [u8; 32] = method_call_dry_run!(client, address, domain_separator()); + + let seckey = [ + 59, 148, 11, 85, 134, 130, 61, 253, 2, 174, 59, 70, 27, 180, 51, 107, 94, 203, 174, 253, 102, 39, 170, 146, 46, + 252, 4, 143, 236, 12, 136, 28, + ]; + let pubkey = PublicKey::from_secret_key( + &SECP256K1, + &SecretKey::from_slice(&seckey).expect("seckey creation failed"), + ); + + let owner = AccountId::from(hash_blake2b256(&pubkey.serialize().to_vec())); + let spender = address_of!(Bob); + + let permit_message = PermitMessage { + domain_separator, + owner, + spender, + amount, + deadline, + nonce, + }; + + let message = &scale::Encode::encode(&permit_message); + + let msg_hash = hash_blake2b256(message); + + let msg = Message::from_slice(&msg_hash).expect("message creation failed"); + let seckey = SecretKey::from_slice(&seckey).expect("secret key creation failed"); + let recoverable_signature: RecoverableSignature = SECP256K1.sign_ecdsa_recoverable(&msg, &seckey); + + let recovery_id = recoverable_signature.serialize_compact().0.to_i32() as u8; + let mut signature = recoverable_signature.serialize_compact().1.to_vec(); + signature.push(recovery_id); + let signature_with_recovery_id: [u8; 65] = signature + .try_into() + .expect("unable to create signature with recovery id"); + + let first_permit_result = method_call_dry_run!( + client, + address, + permit( + owner, + spender, + amount, + deadline, + Signature::ECDSA(signature_with_recovery_id) + ) + ); + let _call_permit = method_call!( + client, + address, + permit( + owner, + spender, + amount, + deadline, + Signature::ECDSA(signature_with_recovery_id) + ) + ); + + assert!(matches!(first_permit_result, Ok(_))); + + let second_permit_result = method_call_dry_run!( + client, + address, + permit( + owner, + spender, + amount, + deadline, + Signature::ECDSA(signature_with_recovery_id) + ) + ); + + println!("second_permit_result: {:?}", second_permit_result); + + assert!(matches!(second_permit_result, Err(_))); + + Ok(()) +} + +#[ink_e2e::test] +async fn permit_rejects_other_signature(mut client: ink_e2e::Client) -> E2EResult<()> { + let constructor = ContractRef::new(1000); + let address = client + .instantiate("my_psp22_permit", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let nonce: u64 = method_call_dry_run!(client, address, nonces(address_of!(Alice))); + let deadline: u64 = 30_000_000_000_000; + let amount: Balance = 1000; + + let domain_separator: [u8; 32] = method_call_dry_run!(client, address, domain_separator()); + + let seckey = [ + 59, 148, 11, 85, 134, 130, 61, 253, 2, 174, 59, 70, 27, 180, 51, 107, 94, 203, 174, 253, 102, 39, 170, 146, 46, + 252, 4, 143, 236, 12, 136, 28, + ]; + let _pubkey = PublicKey::from_secret_key( + &SECP256K1, + &SecretKey::from_slice(&seckey).expect("seckey creation failed"), + ); + + let owner = address_of!(Alice); + let spender = address_of!(Bob); + + let permit_message = PermitMessage { + domain_separator, + owner, + spender, + amount, + deadline, + nonce, + }; + + let message = &scale::Encode::encode(&permit_message); + + let msg_hash = hash_blake2b256(message); + + let msg = Message::from_slice(&msg_hash).expect("message creation failed"); + let seckey = SecretKey::from_slice(&seckey).expect("secret key creation failed"); + let recoverable_signature: RecoverableSignature = SECP256K1.sign_ecdsa_recoverable(&msg, &seckey); + + let recovery_id = recoverable_signature.serialize_compact().0.to_i32() as u8; + let mut signature = recoverable_signature.serialize_compact().1.to_vec(); + signature.push(recovery_id); + let signature_with_recovery_id: [u8; 65] = signature + .try_into() + .expect("unable to create signature with recovery id"); + + let first_permit_result = method_call_dry_run!( + client, + address, + permit( + owner, + spender, + amount, + deadline, + Signature::ECDSA(signature_with_recovery_id) + ) + ); + + assert!(matches!(first_permit_result, Err(_))); + + Ok(()) +} + +#[ink_e2e::test] +async fn permit_rejects_expired_permit(mut client: ink_e2e::Client) -> E2EResult<()> { + let constructor = ContractRef::new(1000); + let address = client + .instantiate("my_psp22_permit", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let nonce: u64 = method_call_dry_run!(client, address, nonces(address_of!(Alice))); + let deadline: u64 = 1; + let amount: Balance = 1000; + + let domain_separator: [u8; 32] = method_call_dry_run!(client, address, domain_separator()); + + let seckey = [ + 59, 148, 11, 85, 134, 130, 61, 253, 2, 174, 59, 70, 27, 180, 51, 107, 94, 203, 174, 253, 102, 39, 170, 146, 46, + 252, 4, 143, 236, 12, 136, 28, + ]; + let pubkey = PublicKey::from_secret_key( + &SECP256K1, + &SecretKey::from_slice(&seckey).expect("seckey creation failed"), + ); + + let owner = AccountId::from(hash_blake2b256(&pubkey.serialize().to_vec())); + let spender = address_of!(Bob); + + let permit_message = PermitMessage { + domain_separator, + owner, + spender, + amount, + deadline, + nonce, + }; + + let message = &scale::Encode::encode(&permit_message); + + let msg_hash = hash_blake2b256(message); + + let msg = Message::from_slice(&msg_hash).expect("message creation failed"); + let seckey = SecretKey::from_slice(&seckey).expect("secret key creation failed"); + let recoverable_signature: RecoverableSignature = SECP256K1.sign_ecdsa_recoverable(&msg, &seckey); + + let recovery_id = recoverable_signature.serialize_compact().0.to_i32() as u8; + let mut signature = recoverable_signature.serialize_compact().1.to_vec(); + signature.push(recovery_id); + let signature_with_recovery_id: [u8; 65] = signature + .try_into() + .expect("unable to create signature with recovery id"); + + let permit_signature = method_call_dry_run!( + client, + address, + permit( + owner, + spender, + amount, + deadline, + Signature::ECDSA(signature_with_recovery_id) + ) + ); + + assert!(matches!(permit_signature, Err(_))); + + Ok(()) +} diff --git a/examples/psp22_extensions/votes/Cargo.toml b/examples/psp22_extensions/votes/Cargo.toml index 4789b3c3a..3e4d2649c 100755 --- a/examples/psp22_extensions/votes/Cargo.toml +++ b/examples/psp22_extensions/votes/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "my_psp22_votes" -version= "4.0.0-beta" +version= "4.0.0-beta.1" authors = ["Brushfam "] edition = "2021" diff --git a/examples/test_helpers/lib.rs b/examples/test_helpers/lib.rs index b0f30068f..59a4b2fcf 100644 --- a/examples/test_helpers/lib.rs +++ b/examples/test_helpers/lib.rs @@ -173,6 +173,22 @@ macro_rules! method_call { .expect("method_call failed") .return_value() }}; + ($client:ident, $address:ident, $method:ident($($args:expr),*)) => {{ + let _msg = build_message::($address.clone()).call(|contract| contract.$method($($args),*)); + $client + .call(&ink_e2e::alice(), _msg, 0, None) + .await + .expect("method_call failed") + .return_value() + }}; + ($client:ident, $address:ident, $signer:ident, $method:ident($($args:expr),*)) => {{ + let _msg = build_message::($address.clone()).call(|contract| contract.$method($($args),*)); + $client + .call(&ink_e2e::$signer(), _msg, 0, None) + .await + .expect("method_call failed") + .return_value() + }}; } #[macro_export] @@ -184,6 +200,13 @@ macro_rules! method_call_dry_run { .await .return_value() }}; + ($client:ident, $address:ident, $method:ident($($args:expr),*)) => {{ + let _msg = build_message::($address.clone()).call(|contract| contract.$method($($args),*)); + $client + .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) + .await + .return_value() + }}; ($client:ident, $address:ident, $signer:ident, $method:ident) => {{ let _msg = build_message::($address.clone()).call(|contract| contract.$method()); $client @@ -191,4 +214,11 @@ macro_rules! method_call_dry_run { .await .return_value() }}; + ($client:ident, $address:ident, $signer:ident, $method:ident($($args:expr),*)) => {{ + let _msg = build_message::($address.clone()).call(|contract| contract.$method($($args),*)); + $client + .call_dry_run(&ink_e2e::$signer(), &_msg, 0, None) + .await + .return_value() + }}; } diff --git a/examples/utils/nonces/Cargo.toml b/examples/utils/nonces/Cargo.toml index 776402a38..127d1b3f1 100755 --- a/examples/utils/nonces/Cargo.toml +++ b/examples/utils/nonces/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nonces" -version= "4.0.0-beta" +version= "4.0.0-beta.1" authors = ["Brushfam "] edition = "2021" @@ -11,7 +11,7 @@ scale = { package = "parity-scale-codec", version = "3", default-features = fals scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } # These dependencies -openbrush = { path = "../../..", default-features = false, features = ["utils"] } +openbrush = { path = "../../..", default-features = false, features = ["nonces"] } [dev-dependencies] ink_e2e = "4.2.1" diff --git a/examples/utils/nonces/lib.rs b/examples/utils/nonces/lib.rs index ebb0398e9..dffea725b 100755 --- a/examples/utils/nonces/lib.rs +++ b/examples/utils/nonces/lib.rs @@ -2,7 +2,7 @@ #[openbrush::implementation(Nonces)] #[openbrush::contract] -pub mod my_nonces { +pub mod nonces { use openbrush::traits::Storage; #[ink(storage)] @@ -19,12 +19,12 @@ pub mod my_nonces { } #[ink(message)] - pub fn use_nonce(&mut self, account: AccountId) -> Result { + pub fn use_nonce(&mut self, account: AccountId) -> Result { NoncesImpl::_use_nonce(self, &account) } #[ink(message)] - pub fn use_checked_nonce(&mut self, account: AccountId, nonce: u128) -> Result { + pub fn use_checked_nonce(&mut self, account: AccountId, nonce: u64) -> Result { NoncesImpl::_use_checked_nonce(self, &account, nonce) } } diff --git a/lang/Cargo.toml b/lang/Cargo.toml index a144542e4..0c54cbd33 100644 --- a/lang/Cargo.toml +++ b/lang/Cargo.toml @@ -37,4 +37,6 @@ std = [ "ink/std", "scale/std", "scale-info/std", -] \ No newline at end of file +] +checkpoints = [] +crypto = [] \ No newline at end of file diff --git a/lang/codegen/Cargo.toml b/lang/codegen/Cargo.toml index 64497b492..b9c64d566 100644 --- a/lang/codegen/Cargo.toml +++ b/lang/codegen/Cargo.toml @@ -22,10 +22,10 @@ fs2 = "0.4.3" serde = { version = "1.0", features = ["derive"] } cargo_metadata = "0.13.1" unwrap = "1.2.1" -blake2 = "0.9" +blake2 = "0.10.6" heck = "0.3.1" -ink_ir = { version = "4.1.0-beta", default-features = false } -ink_primitives = { version = "4.1.0-beta", default-features = false } +ink_ir = { version = "4.2.1", default-features = false } +ink_primitives = { version = "4.2.1", default-features = false } synstructure = "0.12" [lib] diff --git a/lang/codegen/src/implementation.rs b/lang/codegen/src/implementation.rs index 6abfd9851..6371bba60 100644 --- a/lang/codegen/src/implementation.rs +++ b/lang/codegen/src/implementation.rs @@ -81,6 +81,7 @@ pub fn generate(attrs: TokenStream, ink_module: TokenStream) -> TokenStream { "PSP22" => impl_psp22(&mut impl_args), "PSP22Mintable" => impl_psp22_mintable(&mut impl_args), "PSP22Burnable" => impl_psp22_burnable(&mut impl_args), + "PSP22Permit" => impl_psp22_permit(&mut impl_args), "PSP22Metadata" => impl_psp22_metadata(&mut impl_args), "PSP22Capped" => impl_psp22_capped(&mut impl_args), "PSP22Wrapper" => impl_psp22_wrapper(&mut impl_args), @@ -149,6 +150,7 @@ fn cleanup_imports(imports: &mut HashMap<&str, syn::ItemUse>) { "PSP22Capped", "PSP22Metadata", "PSP22Wrapper", + "PSP22Permit", "Flashmint", ]; check_and_remove_import("PSP22", psp22_impls, imports); diff --git a/lang/codegen/src/implementations.rs b/lang/codegen/src/implementations.rs index c446cbae4..05aa3b9d5 100644 --- a/lang/codegen/src/implementations.rs +++ b/lang/codegen/src/implementations.rs @@ -44,6 +44,14 @@ impl<'a> ImplArgs<'a> { .expect("Should parse"); self.imports.insert("vec", vec_import); } + + fn signature_import(&mut self) { + let sig_import = syn::parse2::(quote!( + use openbrush::utils::crypto::Signature; + )) + .expect("Should parse"); + self.imports.insert("Signature", sig_import); + } } pub(crate) fn impl_psp22(impl_args: &mut ImplArgs) { @@ -255,6 +263,78 @@ pub(crate) fn impl_psp22_burnable(impl_args: &mut ImplArgs) { impl_args.items.push(syn::Item::Impl(burnable)); } +pub(crate) fn impl_psp22_permit(impl_args: &mut ImplArgs) { + let storage_struct_name = impl_args.contract_name(); + let permit_internal_impl = syn::parse2::(quote!( + impl permit::InternalImpl for #storage_struct_name {} + )) + .expect("Should parse"); + + let permit_internal = syn::parse2::(quote!( + impl permit::Internal for #storage_struct_name { + fn _permit( + &mut self, + owner: AccountId, + spender: AccountId, + amount: Balance, + deadline: u64, + signature: Signature, + ) -> Result<(), PSP22Error> { + permit::InternalImpl::_permit(self, owner, spender, amount, deadline, signature) + } + fn _domain_separator(&mut self) -> [u8; 32] { + permit::InternalImpl::_domain_separator(self) + } + } + )) + .expect("Should parse"); + + let permit_impl = syn::parse2::(quote!( + impl permit::PSP22PermitImpl for #storage_struct_name {} + )) + .expect("Should parse"); + + let permit = syn::parse2::(quote!( + impl permit::PSP22Permit for #storage_struct_name { + #[ink(message)] + fn permit( + &mut self, + owner: AccountId, + spender: AccountId, + value: Balance, + deadline: u64, + signature: Signature, + ) -> Result<(), PSP22Error> { + permit::PSP22PermitImpl::permit(self, owner, spender, value, deadline, signature) + } + + #[ink(message)] + fn domain_separator(&mut self) -> [u8; 32] { + permit::PSP22PermitImpl::domain_separator(self) + } + } + )) + .expect("Should parse"); + + let import = syn::parse2::(quote!( + use openbrush::contracts::psp22::extensions::permit::*; + )) + .expect("Should parse"); + + impl_args.imports.insert("PSP22Permit", import); + impl_args.signature_import(); + impl_args.vec_import(); + + // TODO + // override_functions("PSP22Permit", &mut burnable, impl_args.map); + + impl_args.items.push(syn::Item::Impl(permit_internal_impl)); + impl_args.items.push(syn::Item::Impl(permit_internal)); + + impl_args.items.push(syn::Item::Impl(permit_impl)); + impl_args.items.push(syn::Item::Impl(permit)); +} + pub(crate) fn impl_psp22_metadata(impl_args: &mut ImplArgs) { let storage_struct_name = impl_args.contract_name(); let metadata_impl = syn::parse2::(quote!( @@ -631,9 +711,9 @@ pub(crate) fn impl_psp22_votes(impl_args: &mut ImplArgs) { &mut self, signer: AccountId, delegatee: AccountId, - nonce: u128, + nonce: u64, expiry: Timestamp, - signature: SignatureType, + signature: Signature, ) -> Result<(), GovernanceError> { VotesImpl::delegate_by_signature(self, signer, delegatee, nonce, expiry, signature) } @@ -3023,7 +3103,7 @@ pub(crate) fn impl_governor(impl_args: &mut ImplArgs) { proposal_id: ProposalId, support: VoteType, reason: String, - signature: SignatureType, + signature: Signature, ) -> Result { GovernorImpl::cast_vote_with_signature(self, proposal_id, support, reason, signature) } @@ -3034,7 +3114,7 @@ pub(crate) fn impl_governor(impl_args: &mut ImplArgs) { proposal_id: ProposalId, support: VoteType, reason: String, - signature: SignatureType, + signature: Signature, params: Vec, ) -> Result { GovernorImpl::cast_vote_with_signature_and_params(self, proposal_id, support, reason, signature, params) @@ -3072,7 +3152,7 @@ pub(crate) fn impl_nonces(impl_args: &mut ImplArgs) { let nonces = syn::parse2::(quote!( impl Nonces for #storage_struct_name { #[ink(message)] - fn nonces(&self, account: AccountId) -> u128 { + fn nonces(&self, account: AccountId) -> u64 { NoncesImpl::nonces(self, &account) } } @@ -3080,7 +3160,7 @@ pub(crate) fn impl_nonces(impl_args: &mut ImplArgs) { .expect("Should parse"); let import = syn::parse2::(quote!( - use openbrush::contracts::utils::nonces::*; + use openbrush::contracts::nonces::*; )) .expect("Should parse"); impl_args.imports.insert("Nonces", import); diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 64ce6c0dd..eb710792f 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -19,7 +19,7 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -#![cfg_attr(not(feature = "std"), no_std, no_main)] +#![cfg_attr(not(feature = "std"), no_std)] mod macros; pub mod storage; diff --git a/lang/src/macros.rs b/lang/src/macros.rs index 5d3a86bb2..ccdef93ce 100644 --- a/lang/src/macros.rs +++ b/lang/src/macros.rs @@ -25,14 +25,14 @@ #[macro_export] macro_rules! storage_unique_key { ($struct:ident) => {{ - $crate::utils::ConstHasher::hash($crate::utils::const_format::concatcp!( + $crate::traits::ConstHasher::hash($crate::traits::const_format::concatcp!( ::core::module_path!(), "::", ::core::stringify!($struct) )) }}; ($struct:literal, $field:literal) => {{ - $crate::utils::ConstHasher::hash($crate::utils::const_format::concatcp!( + $crate::traits::ConstHasher::hash($crate::traits::const_format::concatcp!( ::core::module_path!(), "::", $struct, @@ -44,7 +44,7 @@ macro_rules! storage_unique_key { #[test] fn correct_storage_key() { - use crate::utils::ConstHasher; + use crate::traits::ConstHasher; use ink::storage::traits::StorageKey; mod contracts { diff --git a/lang/src/traits.rs b/lang/src/traits.rs index acc118539..ab38b9aa2 100644 --- a/lang/src/traits.rs +++ b/lang/src/traits.rs @@ -23,12 +23,14 @@ use ::ink::env::{ DefaultEnvironment, Environment, }; +pub use const_format; use core::mem::ManuallyDrop; use ink::storage::traits::{ Storable, StorageKey, }; pub use openbrush_lang_macro::Storage; +pub use xxhash_rust::const_xxh32::xxh32; /// Aliases for types of the default environment pub type AccountId = ::AccountId; @@ -123,3 +125,14 @@ pub trait Flush: Storable + Sized + StorageKey { } impl Flush for T {} + +/// The value 0 is a valid seed. +const XXH32_SEED: u32 = 0; + +pub struct ConstHasher; + +impl ConstHasher { + pub const fn hash(str: &str) -> u32 { + xxh32(str.as_bytes(), XXH32_SEED) + } +} diff --git a/contracts/src/utils/checkpoint/mod.rs b/lang/src/utils/checkpoints.rs similarity index 97% rename from contracts/src/utils/checkpoint/mod.rs rename to lang/src/utils/checkpoints.rs index 6951e1883..389197fe5 100644 --- a/contracts/src/utils/checkpoint/mod.rs +++ b/lang/src/utils/checkpoints.rs @@ -20,7 +20,6 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -pub use crate::traits::errors::CheckpointsError; use ink::prelude::vec::Vec; /// Struct, for checkpointing values as they change at different points in @@ -41,6 +40,13 @@ pub struct Checkpoint { pub value: u128, } +#[derive(scale::Decode, scale::Encode, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] +pub enum CheckpointsError { + UnorderedInsertion, +} + +/// Counting sqrt using Newton's method. fn sqrt(x: u64) -> u64 { let mut z = (x + 1) / 2; let mut y = x; diff --git a/lang/src/utils/crypto.rs b/lang/src/utils/crypto.rs new file mode 100644 index 000000000..9cd8c2b62 --- /dev/null +++ b/lang/src/utils/crypto.rs @@ -0,0 +1,98 @@ +// Copyright (c) 2012-2022 Supercolony +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the"Software"), +// to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +use crate::traits::String; +use ink::env::hash; + +use crate::traits::AccountId; + +/// Hashing function for bytes +pub fn hash_blake2b256(input: &[u8]) -> [u8; 32] { + let mut output = ::Type::default(); + ink::env::hash_bytes::(input, &mut output); + output +} + +/// Converts a compressed public key to SS58 format +pub fn pub_key_to_ss58(pub_key: &[u8; 33]) -> AccountId { + AccountId::from(hash_blake2b256(pub_key)) +} + +/// Converts a public key to an Ethereum address +pub fn pub_key_to_eth_address(pub_key: &[u8; 33]) -> Result<[u8; 20], CryptoError> { + let mut output = [0u8; 20]; + + ink::env::ecdsa_to_eth_address(pub_key, &mut output).map_err(|_| CryptoError::EcdsaToEthAddressFailed)?; + + Ok(output) +} + +/// Enum to represent different signature types +/// +/// # Support of signatures +/// +/// - `ECDSA` - ECDSA signature with 65 bytes +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Signature { + ECDSA([u8; 65]), +} + +impl Signature { + /// Verifies different type of signatures + /// + /// # Arguments + /// + /// - `message` - The message to verify + /// - `pub_key` - The public key to verify the message with + /// + /// # Returns + /// + /// - `true` if the signature is valid + /// - `false` if the signature is invalid + /// + /// # Supported signatures + /// + /// - `ECDSA` + #[allow(unreachable_patterns)] + pub fn verify(&self, message: &[u8], address: &AccountId) -> bool { + match self { + // Verifies ECDSA signature + Signature::ECDSA(sig) => { + let mut output: [u8; 33] = [0; 33]; + let message_hash = hash_blake2b256(message); + + let result = ink::env::ecdsa_recover(sig, &message_hash, &mut output); + + return result.is_ok() && pub_key_to_ss58(&output) == address.clone() + } + _ => false, + } + } +} + +#[derive(scale::Decode, scale::Encode, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))] +pub enum CryptoError { + EcdsaRecoverFailed, + EcdsaToEthAddressFailed, + Other(String), +} diff --git a/contracts/src/traits/errors/checkpoints.rs b/lang/src/utils/mod.rs similarity index 86% rename from contracts/src/traits/errors/checkpoints.rs rename to lang/src/utils/mod.rs index a1d904b2e..46af1b0a6 100644 --- a/contracts/src/traits/errors/checkpoints.rs +++ b/lang/src/utils/mod.rs @@ -20,8 +20,7 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum CheckpointsError { - UnorderedInsertion, -} +#[cfg(feature = "checkpoints")] +pub mod checkpoints; +#[cfg(feature = "crypto")] +pub mod crypto; diff --git a/package.json b/package.json index 33e086f69..f2cd64897 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openbrush-contracts", - "version": "4.0.0-beta", + "version": "4.0.0-beta.1", "private": true, "dependencies": { "@727-ventures/typechain-compiler": "^1.1.4",