diff --git a/Cargo.lock b/Cargo.lock index 9f689b91e..f9610c848 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,6 +153,15 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bip32" version = "0.4.0" @@ -3663,6 +3672,22 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +[[package]] +name = "verifier-test" +version = "0.1.0" +dependencies = [ + "bincode", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "cw-verifier-middleware", + "cw2 0.16.0", + "dao-testing", + "thiserror", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 491cb3d3b..14c4ecbe2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ bip32 = "0.4.0" hex = "0.4.3" ripemd = "0.1.3" bech32 = "0.9.1" +bincode = "1.3" # One commit ahead of version 0.3.0. Allows initialization with an # optional owner. @@ -100,6 +101,7 @@ cw-paginate = { path = "./packages/cw-paginate", version = "*" } dao-interface = { path = "./packages/dao-interface", version = "*" } dao-voting = { path = "./packages/dao-voting", version = "*" } + # v1 dependencies. used for state migrations. cw-core-v1 = { package = "cw-core", version = "0.1.0", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" } cw-proposal-single-v1 = { package = "cw-proposal-single", version = "0.1.0", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" } diff --git a/contracts/external/cw-verifier-middleware/Cargo.toml b/contracts/external/cw-verifier-middleware/Cargo.toml index 4c6f3ac09..14e0185dd 100644 --- a/contracts/external/cw-verifier-middleware/Cargo.toml +++ b/contracts/external/cw-verifier-middleware/Cargo.toml @@ -19,6 +19,7 @@ ripemd = { workspace = true } bech32 = { workspace = true } serde_json = { workspace = true} + [dev-dependencies] secp256k1 = { workspace = true, features = ["rand-std", "bitcoin-hashes-std"] } \ No newline at end of file diff --git a/contracts/external/cw-verifier-middleware/src/error.rs b/contracts/external/cw-verifier-middleware/src/error.rs index 1711d3988..9e4eb34e2 100644 --- a/contracts/external/cw-verifier-middleware/src/error.rs +++ b/contracts/external/cw-verifier-middleware/src/error.rs @@ -1,4 +1,5 @@ use bech32::Error as Bech32Error; + use cosmwasm_std::OverflowError; use cosmwasm_std::{StdError, VerificationError}; use hex::FromHexError; @@ -6,7 +7,7 @@ use secp256k1::Error as Secp256k1Error; use serde_json::Error as SerdeError; use thiserror::Error; -#[derive(Error, Debug, PartialEq)] +#[derive(Error, Debug)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), @@ -27,7 +28,7 @@ pub enum ContractError { OverflowError(#[from] OverflowError), #[error("{0}")] - SerdeError(String), + SerdeError(#[from] SerdeError), #[error("Invalid nonce")] InvalidNonce, @@ -42,8 +43,8 @@ pub enum ContractError { InvalidPublicKeyLength { length: usize }, } -impl From for ContractError { - fn from(error: SerdeError) -> Self { - ContractError::SerdeError(error.to_string()) - } -} +// impl From for ContractError { +// fn from(error: SerdeError) -> Self { +// ContractError::SerdeError(error.to_string()) +// } +// } diff --git a/contracts/external/cw-verifier-middleware/src/lib.rs b/contracts/external/cw-verifier-middleware/src/lib.rs index dbed528fc..56cb0db19 100644 --- a/contracts/external/cw-verifier-middleware/src/lib.rs +++ b/contracts/external/cw-verifier-middleware/src/lib.rs @@ -1,10 +1,10 @@ #![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] -pub mod verify; pub mod error; pub mod msg; pub mod state; - +pub mod utils; +pub mod verify; #[cfg(test)] -mod testing; \ No newline at end of file +mod testing; diff --git a/contracts/external/cw-verifier-middleware/src/testing/mod.rs b/contracts/external/cw-verifier-middleware/src/testing/mod.rs index 4ddd54de5..9f1e9f269 100644 --- a/contracts/external/cw-verifier-middleware/src/testing/mod.rs +++ b/contracts/external/cw-verifier-middleware/src/testing/mod.rs @@ -1,2 +1,2 @@ +mod adversarial_tests; mod tests; -mod adversarial_tests; \ No newline at end of file diff --git a/contracts/external/cw-verifier-middleware/src/testing/tests.rs b/contracts/external/cw-verifier-middleware/src/testing/tests.rs index dfd01368f..77e1866a9 100644 --- a/contracts/external/cw-verifier-middleware/src/testing/tests.rs +++ b/contracts/external/cw-verifier-middleware/src/testing/tests.rs @@ -1,3 +1,4 @@ +use cosmwasm_schema::cw_serde; use cosmwasm_std::{ testing::{mock_dependencies, mock_env, mock_info}, to_binary, Addr, BlockInfo, DepsMut, HexBinary, Uint128, VerificationError, @@ -10,6 +11,7 @@ use crate::{ error::ContractError, msg::{Payload, WrappedMessage}, state::NONCES, + utils::get_wrapped_msg, verify::{get_sign_doc, pk_to_addr, verify}, }; @@ -19,11 +21,16 @@ pub const COMPRESSED_PK: &str = pub const UNCOMPRESSED_PK: &str = "04f620cd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28fb722c0dacb5f005f583630dae8bbe7f5eaba70f129fc279d7ff421ae8c9eb79"; pub const JUNO_PREFIX: &str = "juno"; +#[cw_serde] +pub enum TestExecuteMsg { + Test, +} + #[test] fn test_pk_to_addr_uncompressed() { let deps = mock_dependencies(); let generated_address = - pk_to_addr(deps.as_ref(), UNCOMPRESSED_PK.to_string(), JUNO_PREFIX).unwrap(); + pk_to_addr(&deps.api, UNCOMPRESSED_PK.to_string(), JUNO_PREFIX).unwrap(); assert_eq!(generated_address, Addr::unchecked(JUNO_ADDRESS)); } @@ -31,8 +38,7 @@ fn test_pk_to_addr_uncompressed() { #[test] fn test_pk_to_addr_compressed() { let deps = mock_dependencies(); - let generated_address = - pk_to_addr(deps.as_ref(), COMPRESSED_PK.to_string(), JUNO_PREFIX).unwrap(); + let generated_address = pk_to_addr(&deps.api, COMPRESSED_PK.to_string(), JUNO_PREFIX).unwrap(); assert_eq!(generated_address, Addr::unchecked(JUNO_ADDRESS)); } @@ -40,7 +46,7 @@ fn test_pk_to_addr_compressed() { fn test_pk_to_addr_invalid_hex_length() { let invalid_length_pk = "".to_string(); let deps = mock_dependencies(); - let err: ContractError = pk_to_addr(deps.as_ref(), invalid_length_pk, JUNO_PREFIX).unwrap_err(); + let err: ContractError = pk_to_addr(&deps.api, invalid_length_pk, JUNO_PREFIX).unwrap_err(); assert!(matches!(err, ContractError::InvalidPublicKeyLength { .. })); } @@ -50,7 +56,7 @@ fn test_pk_to_addr_not_hex_pk() { let non_hex_pk = "03zzzzcd2e33d3f6af5a43d5b3ca3b9b7f653aa980ae56714cc5eb7637fd1eeb28".to_string(); let deps = mock_dependencies(); - let err: ContractError = pk_to_addr(deps.as_ref(), non_hex_pk, JUNO_PREFIX).unwrap_err(); + let err: ContractError = pk_to_addr(&deps.api, non_hex_pk, JUNO_PREFIX).unwrap_err(); assert!(matches!(err, ContractError::FromHexError { .. })); } @@ -59,18 +65,59 @@ fn test_pk_to_addr_not_hex_pk() { fn test_pk_to_addr_bech32_invalid_human_readable_part() { let deps = mock_dependencies(); let err: ContractError = - pk_to_addr(deps.as_ref(), UNCOMPRESSED_PK.to_string(), "jUnO").unwrap_err(); + pk_to_addr(&deps.api, UNCOMPRESSED_PK.to_string(), "jUnO").unwrap_err(); assert!(matches!(err, ContractError::Bech32Error { .. })); } #[test] fn test_verify_success() { - fn test_verify_success() { - // This test generates a payload in which the signature is base64 encoded, and the public key is hex encoded. - // The test then calls verify to validate that the signature is correctly verified. + // This test generates a payload in which the signature is base64 encoded, and the public key is hex encoded. + // The test then calls verify to validate that the signature is correctly verified. + + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary(&TestExecuteMsg::Test {}).unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: "juno".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), + }; + + let mut deps = mock_dependencies(); + let wrapped_msg = get_wrapped_msg(&deps.api, payload.clone()); + + // Verify + let env = mock_env(); + let info = mock_info("creator", &[]); + verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ) + .unwrap(); + + // Verify nonce was incremented correctly + let nonce = NONCES + .load( + &deps.storage, + ( + &wrapped_msg.public_key.to_hex(), + &Addr::unchecked(payload.contract_address), + &payload.contract_version, + ), + ) + .unwrap(); + assert_eq!(nonce, Uint128::from(1u128)) +} - let payload = Payload { +// The type that verify deserializes to does not match the serialized message type. +#[test] +fn test_verify_wrong_message_type() { + let payload = Payload { nonce: Uint128::from(0u128), msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), expiration: None, @@ -80,27 +127,21 @@ fn test_verify_success() { chain_id: "juno-1".to_string(), }; - let mut deps = mock_dependencies(); - let wrapped_msg = get_wrapped_msg(deps.as_mut(), payload.clone()); - - // Verify - let env = mock_env(); - let mut info = mock_info("creator", &[]); - verify(deps.as_mut(), env, &mut info, wrapped_msg.clone()).unwrap(); - - // Verify nonce was incremented correctly - let nonce = NONCES - .load( - &deps.storage, - ( - &wrapped_msg.public_key.to_hex(), - &Addr::unchecked(payload.contract_address), - &payload.contract_version, - ), - ) - .unwrap(); - assert_eq!(nonce, Uint128::from(1u128)) - } + let mut deps = mock_dependencies(); + let wrapped_msg = get_wrapped_msg(&deps.api, payload.clone()); + + // Verify + let env = mock_env(); + let info = mock_info("creator", &[]); + let res = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ); + + assert!(matches!(res, Err(ContractError::Std(_)))); } #[test] @@ -120,15 +161,21 @@ fn test_verify_invalid_pk() { // Generate wrapped message let mut deps = mock_dependencies(); - let mut wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); + let mut wrapped_msg = get_wrapped_msg(&deps.api, payload); // Set public key to invalid wrapped_msg.public_key = Vec::from("incorrect_public_key").into(); // Verify with incorrect public key let env = mock_env(); - let mut info = mock_info("creator", &[]); - let result = verify(deps.as_mut(), env, &mut info, wrapped_msg); + let info = mock_info("creator", &[]); + let result = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ); // Ensure that there was a pub key parsing error assert!(matches!( @@ -141,7 +188,7 @@ fn test_verify_invalid_pk() { fn test_verify_wrong_pk() { let payload = Payload { nonce: Uint128::from(0u128), - msg: to_binary("test").unwrap(), + msg: to_binary(&TestExecuteMsg::Test {}).unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: "juno".to_string(), @@ -172,8 +219,14 @@ fn test_verify_wrong_pk() { // Verify with incorrect public key let mut deps = mock_dependencies(); let env = mock_env(); - let mut info = mock_info("creator", &[]); - let result = verify(deps.as_mut(), env, &mut info, wrapped_msg); + let info = mock_info("creator", &[]); + let result = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ); // Ensure that there was a signature verification error assert!(matches!(result, Err(ContractError::SignatureInvalid))); @@ -183,20 +236,27 @@ fn test_verify_wrong_pk() { fn test_verify_incorrect_nonce() { let mut deps = mock_dependencies(); let env = mock_env(); - let mut info = mock_info("creator", &[]); + let info = mock_info("creator", &[]); // get a default wrapped message and verify it let payload = Payload { nonce: Uint128::from(0u128), - msg: to_binary("eyJpbnN0YW50aWF0ZV9jb250cmFjdF93aXRoX3NlbGZfYWRtaW4iOnsiY29kZV9pZCI6MTY4OCwiaW5zdGFudGlhdGVfbXNnIjp7fX19ICA=").unwrap(), + msg: to_binary(&TestExecuteMsg::Test {}).unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: JUNO_PREFIX.to_string(), contract_version: "version-1".to_string(), chain_id: "juno-1".to_string(), }; - let wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); - verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap(); + let wrapped_msg = get_wrapped_msg(&deps.api, payload); + verify::( + &deps.api, + &mut deps.storage, + &env, + info.clone(), + wrapped_msg.clone(), + ) + .unwrap(); // skip a nonce iteration let invalid_nonce_payload = Payload { @@ -208,8 +268,15 @@ fn test_verify_incorrect_nonce() { contract_version: "version-1".to_string(), chain_id: "juno-1".to_string(), }; - let wrapped_msg = get_wrapped_msg(deps.as_mut(), invalid_nonce_payload); - let err = verify(deps.as_mut(), env, &mut info, wrapped_msg).unwrap_err(); + let wrapped_msg = get_wrapped_msg(&deps.api, invalid_nonce_payload); + let err = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ) + .unwrap_err(); // verify the invalid nonce error assert!(matches!(err, ContractError::InvalidNonce)); @@ -219,7 +286,7 @@ fn test_verify_incorrect_nonce() { fn test_verify_expired_message() { let mut deps = mock_dependencies(); let env = mock_env(); - let mut info = mock_info("creator", &[]); + let info = mock_info("creator", &[]); // get an expired message let payload = Payload { @@ -231,10 +298,16 @@ fn test_verify_expired_message() { contract_version: "version-1".to_string(), chain_id: "juno-1".to_string(), }; - let wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); - - let err: ContractError = - verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap_err(); + let wrapped_msg = get_wrapped_msg(&deps.api, payload); + + let err: ContractError = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ) + .unwrap_err(); assert!(matches!(err, ContractError::MessageExpired)); } @@ -243,7 +316,7 @@ fn test_verify_expired_message() { fn test_verify_wrong_payload() { let mut deps = mock_dependencies(); let env = mock_env(); - let mut info = mock_info("creator", &[]); + let info = mock_info("creator", &[]); // Generate a keypair let secp = Secp256k1::new(); @@ -283,8 +356,14 @@ fn test_verify_wrong_payload() { public_key: hex_encoded.clone(), }; - let err: ContractError = - verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap_err(); + let err: ContractError = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ) + .unwrap_err(); assert!(matches!(err, ContractError::SignatureInvalid { .. })); } @@ -293,7 +372,7 @@ fn test_verify_wrong_payload() { fn test_verify_malformed_signature() { let mut deps = mock_dependencies(); let env = mock_env(); - let mut info = mock_info("creator", &[]); + let info = mock_info("creator", &[]); let payload = Payload { nonce: Uint128::from(0u128), @@ -305,12 +384,18 @@ fn test_verify_malformed_signature() { chain_id: "juno-1".to_string(), }; - let mut wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); + let mut wrapped_msg = get_wrapped_msg(&deps.api, payload); let malformed_sig = Vec::from("malformed signature"); wrapped_msg.signature = malformed_sig.into(); - let err: ContractError = - verify(deps.as_mut(), env.clone(), &mut info, wrapped_msg).unwrap_err(); + let err: ContractError = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ) + .unwrap_err(); assert!(matches!(err, ContractError::VerificationError { .. })); } @@ -319,7 +404,7 @@ fn test_verify_malformed_signature() { fn test_verify_correct_address() { let payload = Payload { nonce: Uint128::from(0u128), - msg: to_binary("test").unwrap(), + msg: to_binary(&TestExecuteMsg::Test {}).unwrap(), expiration: None, contract_address: Addr::unchecked("contract_address").to_string(), bech32_prefix: "juno".to_string(), @@ -328,16 +413,23 @@ fn test_verify_correct_address() { }; let mut deps = mock_dependencies(); - let wrapped_msg = get_wrapped_msg(deps.as_mut(), payload); + let wrapped_msg = get_wrapped_msg(&deps.api, payload); let mut env = mock_env(); env.block.height = 1; - let mut info = mock_info("creator", &[]); - verify(deps.as_mut(), env, &mut info, wrapped_msg.clone()).unwrap(); + let info = mock_info("creator", &[]); + let (_, verified_info) = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ) + .unwrap(); - let addr = pk_to_addr(deps.as_ref(), wrapped_msg.public_key.to_hex(), JUNO_PREFIX).unwrap(); + let addr = pk_to_addr(&deps.api, wrapped_msg.public_key.to_hex(), JUNO_PREFIX).unwrap(); - assert_eq!(info.sender, addr); + assert_eq!(verified_info.sender, addr); } // Generate a validly signed message but without creating a sign doc first. @@ -374,47 +466,22 @@ fn test_verify_no_sign_doc() { // Verify should fail let env = mock_env(); - let mut info = mock_info("creator", &[]); - let res = verify(deps.as_mut(), env, &mut info, wrapped_msg.clone()); + let info = mock_info("creator", &[]); + let res = verify::( + &deps.api, + &mut deps.storage, + &env, + info, + wrapped_msg.clone(), + ); assert!(matches!(res, Err(ContractError::SignatureInvalid { .. }))); } -// signs a given payload and returns the wrapped message -fn get_wrapped_msg(deps: DepsMut, payload: Payload) -> WrappedMessage { - // Generate a keypair - let secp = Secp256k1::new(); - let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); - - // Generate signdoc - let signer_addr = pk_to_addr( - deps.as_ref(), - public_key.to_hex(), // to_hex ensures that the public key has the expected number of bytes - &payload.bech32_prefix, - ) - .unwrap(); - - let payload_ser = serde_json::to_string(&payload).unwrap(); - - let sign_doc = get_sign_doc(signer_addr.as_str(), &payload_ser, &"juno-1").unwrap(); - - // Hash and sign the payload - let msg_hash = Sha256::digest(&to_binary(&sign_doc).unwrap()); - let msg = Message::from_slice(&msg_hash).unwrap(); - let sig = secp.sign_ecdsa(&msg, &secret_key); - - // Wrap the message - let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); - WrappedMessage { - payload, - signature: sig.serialize_compact().into(), - public_key: hex_encoded.clone(), - } -} - /* Moar tests to write: wrong version load a keypair corresponding to pre-known address and validate that address in info was set correctly test integrating with another contract wrong contract address +deserialize message into wrong type */ diff --git a/contracts/external/cw-verifier-middleware/src/utils.rs b/contracts/external/cw-verifier-middleware/src/utils.rs new file mode 100644 index 000000000..828f21973 --- /dev/null +++ b/contracts/external/cw-verifier-middleware/src/utils.rs @@ -0,0 +1,40 @@ +use cosmwasm_std::{to_binary, Api, HexBinary}; +use secp256k1::{hashes::hex::ToHex, rand::rngs::OsRng, Message, Secp256k1}; +use sha2::{Digest, Sha256}; + +use crate::{ + msg::{Payload, WrappedMessage}, + verify::{get_sign_doc, pk_to_addr}, +}; + +// signs a given payload and returns the wrapped message +pub fn get_wrapped_msg(api: &dyn Api, payload: Payload) -> WrappedMessage { + // Generate a keypair + let secp = Secp256k1::new(); + let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); + + // Generate signdoc + let signer_addr = pk_to_addr( + api, + public_key.to_hex(), // to_hex ensures that the public key has the expected number of bytes + &payload.bech32_prefix, + ) + .unwrap(); + + let payload_ser = serde_json::to_string(&payload).unwrap(); + + let sign_doc = get_sign_doc(signer_addr.as_str(), &payload_ser, &"juno-1").unwrap(); + + // Hash and sign the payload + let msg_hash = Sha256::digest(&to_binary(&sign_doc).unwrap()); + let msg = Message::from_slice(&msg_hash).unwrap(); + let sig = secp.sign_ecdsa(&msg, &secret_key); + + // Wrap the message + let hex_encoded = HexBinary::from(public_key.serialize_uncompressed()); + WrappedMessage { + payload, + signature: sig.serialize_compact().into(), + public_key: hex_encoded.clone(), + } +} diff --git a/contracts/external/cw-verifier-middleware/src/verify.rs b/contracts/external/cw-verifier-middleware/src/verify.rs index 600ebbc29..16d5c3c2b 100644 --- a/contracts/external/cw-verifier-middleware/src/verify.rs +++ b/contracts/external/cw-verifier-middleware/src/verify.rs @@ -4,28 +4,31 @@ use crate::{ state::{CONTRACT_ADDRESS, NONCES}, }; use bech32::{ToBase32, Variant}; -use cosmwasm_schema::schemars::_serde_json::json; +use cosmwasm_schema::{schemars::_serde_json::json, serde::de::DeserializeOwned}; use cosmwasm_std::{ - to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, StdError, StdResult, Uint128, + from_slice, to_binary, Addr, Api, DepsMut, Env, MessageInfo, StdError, Storage, Uint128, }; use ripemd::Ripemd160; -use secp256k1::hashes::serde_macros; use sha2::{Digest, Sha256}; const UNCOMPRESSED_HEX_PK_LEN: usize = 130; const COMPRESSED_HEX_PK_LEN: usize = 66; -pub fn verify( - deps: DepsMut, - env: Env, - info: &mut MessageInfo, +pub fn verify( + api: &dyn Api, + storage: &mut dyn Storage, + env: &Env, + info: MessageInfo, wrapped_msg: WrappedMessage, -) -> Result { +) -> Result<(T, MessageInfo), ContractError> +where + T: DeserializeOwned, +{ let payload = wrapped_msg.payload; let signer_addr = pk_to_addr( - deps.as_ref(), + api, wrapped_msg.public_key.to_hex(), // to_hex ensures that the public key has the expected number of bytes &payload.bech32_prefix, )?; @@ -42,7 +45,7 @@ pub fn verify( let msg_hash = Sha256::digest(&msg_ser); // Verify the signature - let sig_valid = deps.api.secp256k1_verify( + let sig_valid = api.secp256k1_verify( msg_hash.as_slice(), &wrapped_msg.signature, wrapped_msg.public_key.as_slice(), @@ -52,7 +55,14 @@ pub fn verify( return Err(ContractError::SignatureInvalid {}); } - let validated_contract_addr = deps.api.addr_validate(&payload.contract_address)?; + // Validate that the message has not expired + if let Some(expiration) = payload.expiration { + if expiration.is_expired(&env.block) { + return Err(ContractError::MessageExpired {}); + } + } + + let validated_contract_addr = api.addr_validate(&payload.contract_address)?; let pk = wrapped_msg.public_key.to_hex(); let nonce_key = ( pk.as_str(), @@ -62,7 +72,7 @@ pub fn verify( // Validate that the message has the correct nonce let nonce = NONCES - .may_load(deps.storage, nonce_key)? + .may_load(storage, nonce_key)? .unwrap_or(Uint128::from(0u128)); if payload.nonce != nonce { @@ -70,33 +80,33 @@ pub fn verify( } // Increment nonce - NONCES.update(deps.storage, nonce_key, |nonce: Option| { + NONCES.update(storage, nonce_key, |nonce: Option| { nonce .unwrap_or(Uint128::from(0u128)) .checked_add(Uint128::from(1u128)) .map_err(|e| StdError::from(e)) })?; - // Validate that the message has not expired - if let Some(expiration) = payload.expiration { - if expiration.is_expired(&env.block) { - return Err(ContractError::MessageExpired {}); - } - } - // Set the message sender to the address corresponding to the provided public key. - info.sender = signer_addr; + // Construct a new MessageInfo with the signer as the sender + let verified_info = MessageInfo { + sender: signer_addr, + funds: info.funds, + }; + + // Deserialize message into expected type + let verified_msg = from_slice::(&payload.msg.to_vec())?; - // Return info with updater sender and msg to be deserialized by caller - return Ok(payload.msg); + // Return info with sender and deserialized msg + return Ok((verified_msg, verified_info)); } -pub fn initialize_contract_addr(deps: DepsMut, env: Env) -> Result<(), ContractError> { +pub fn initialize_contract_addr(deps: DepsMut, env: &Env) -> Result<(), ContractError> { CONTRACT_ADDRESS.save(deps.storage, &env.contract.address.to_string())?; Ok(()) } // Takes an compressed or uncompressed hex-encoded EC public key and a bech32 prefix and derives the bech32 address. -pub(crate) fn pk_to_addr(deps: Deps, hex_pk: String, prefix: &str) -> Result { +pub fn pk_to_addr(api: &dyn Api, hex_pk: String, prefix: &str) -> Result { // Decode PK from hex let raw_pk = hex::decode(&hex_pk)?; @@ -124,16 +134,12 @@ pub(crate) fn pk_to_addr(deps: Deps, hex_pk: String, prefix: &str) -> Result Result { +pub fn get_sign_doc(signer: &str, message: &str, chain_id: &str) -> Result { let doc = json!({ "account_number": "0", "chain_id": chain_id, diff --git a/packages/dao-testing/Cargo.toml b/packages/dao-testing/Cargo.toml index 51659227d..2af01331b 100644 --- a/packages/dao-testing/Cargo.toml +++ b/packages/dao-testing/Cargo.toml @@ -42,4 +42,4 @@ dao-voting-cw721-staked = { workspace = true } dao-voting-native-staked = { workspace = true } cw721-base = { workspace = true } cw-core-v1 = { workspace = true, features = ["library"] } -dao-proposal-condorcet = { workspace = true } +dao-proposal-condorcet = { workspace = true } \ No newline at end of file diff --git a/packages/dao-testing/src/helpers.rs b/packages/dao-testing/src/helpers.rs index 8984588a4..d773270e1 100644 --- a/packages/dao-testing/src/helpers.rs +++ b/packages/dao-testing/src/helpers.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{to_binary, Addr, Binary, Empty, Uint128}; +use cosmwasm_std::{to_binary, Addr, Api, Binary, DepsMut, Empty, HexBinary, Uint128}; use cw20::Cw20Coin; use cw_multi_test::{App, Contract, ContractWrapper, Executor}; use cw_utils::Duration; diff --git a/test-contracts/verifier-test/Cargo.toml b/test-contracts/verifier-test/Cargo.toml new file mode 100644 index 000000000..bf9ae3285 --- /dev/null +++ b/test-contracts/verifier-test/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "verifier-test" +version = "0.1.0" +authors = ["bluenote"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-storage = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +thiserror = { workspace = true } +cw-verifier-middleware = {workspace = true} +bincode = "1.3" + +[dev-dependencies] +cw-multi-test = { workspace = true } +dao-testing = { workspace = true} diff --git a/test-contracts/verifier-test/src/contract.rs b/test-contracts/verifier-test/src/contract.rs new file mode 100644 index 000000000..6ed7fb14c --- /dev/null +++ b/test-contracts/verifier-test/src/contract.rs @@ -0,0 +1,51 @@ +use crate::{ + error::ContractError, + msg::{ExecuteMsg, InnerExecuteMsg, InstantiateMsg, QueryMsg}, +}; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cw2::set_contract_version; +use cw_verifier_middleware::verify::verify; + +const CONTRACT_NAME: &str = "crates.io:cw-verifier-test"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(Response::new().add_attribute("method", "instantiate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let (verified_msg, verified_info) = + verify::(deps.api, deps.storage, &env, info, msg.wrapped_msg)?; + match verified_msg { + InnerExecuteMsg::Execute => execute_execute(deps, env, verified_info)?, + }; + Ok(Response::default()) +} + +pub fn execute_execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, +) -> Result { + Ok(Response::default().add_attribute("action", "execute_execute")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult { + Ok(Binary::default()) +} diff --git a/test-contracts/verifier-test/src/error.rs b/test-contracts/verifier-test/src/error.rs new file mode 100644 index 000000000..74726833f --- /dev/null +++ b/test-contracts/verifier-test/src/error.rs @@ -0,0 +1,15 @@ +use cosmwasm_std::StdError; +use cw_verifier_middleware::error::ContractError as VerifyError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + VerifyError(#[from] VerifyError), + + #[error("Unauthorized")] + Unauthorized {}, +} diff --git a/test-contracts/verifier-test/src/lib.rs b/test-contracts/verifier-test/src/lib.rs new file mode 100644 index 000000000..2ed82bd3f --- /dev/null +++ b/test-contracts/verifier-test/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +pub mod error; +pub mod msg; +#[cfg(test)] +mod tests; diff --git a/test-contracts/verifier-test/src/msg.rs b/test-contracts/verifier-test/src/msg.rs new file mode 100644 index 000000000..ae55a9f7f --- /dev/null +++ b/test-contracts/verifier-test/src/msg.rs @@ -0,0 +1,21 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::CosmosMsg; +use cw_verifier_middleware::msg::WrappedMessage; + +#[cw_serde] +pub struct InstantiateMsg {} + +#[cw_serde] +pub enum InnerExecuteMsg { + Execute, +} + +#[cw_serde] + +pub struct ExecuteMsg { + pub wrapped_msg: WrappedMessage, +} + +#[cw_serde] + +pub struct QueryMsg {} diff --git a/test-contracts/verifier-test/src/tests.rs b/test-contracts/verifier-test/src/tests.rs new file mode 100644 index 000000000..6d044696a --- /dev/null +++ b/test-contracts/verifier-test/src/tests.rs @@ -0,0 +1,68 @@ +use cosmwasm_std::testing::{MockApi, MockStorage}; +use cosmwasm_std::{to_binary, Addr, Api, Empty, Storage, Uint128}; +use cw_multi_test::{AppBuilder, Executor, Router}; +use cw_multi_test::{Contract, ContractWrapper}; +use cw_verifier_middleware::msg::Payload; +use cw_verifier_middleware::utils::get_wrapped_msg; + +use crate::msg::{ExecuteMsg, InnerExecuteMsg}; + +fn test_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ); + Box::new(contract) +} + +fn no_init( + _: &mut Router, + _: &dyn Api, + _: &mut dyn Storage, +) { +} + +#[test] +fn test_verify() { + let api = MockApi::default(); + let storage = MockStorage::new(); + + let mut app = AppBuilder::new() + .with_api(api) + .with_storage(storage) + .build(no_init); + + let code_id = app.store_code(test_contract()); + let contract = app + .instantiate_contract( + code_id, + Addr::unchecked("admin"), + &crate::msg::InstantiateMsg {}, + &[], + "test contract", + None, + ) + .unwrap(); + + let payload = Payload { + nonce: Uint128::from(0u128), + msg: to_binary(&InnerExecuteMsg::Execute {}).unwrap(), + expiration: None, + contract_address: Addr::unchecked("contract_address").to_string(), + bech32_prefix: "juno".to_string(), + contract_version: "version-1".to_string(), + chain_id: "juno-1".to_string(), + }; + + let wrapped_msg = get_wrapped_msg(&api, payload); + app.execute_contract( + Addr::unchecked("ADMIN"), + contract, + &ExecuteMsg { + wrapped_msg: wrapped_msg, + }, + &[], + ) + .unwrap(); +}