From b58504d77218461ccc1757454e95097106e5d6d7 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 12 Dec 2024 13:54:31 +0100 Subject: [PATCH] storage account v1 (#119) --- Cargo.lock | 22 + Cargo.toml | 1 + .../storage_account/.cargo/config.toml | 3 + contracts/accounts/storage_account/Cargo.toml | 36 ++ contracts/accounts/storage_account/README.md | 4 + .../schema/valence-storage-account.json | 395 ++++++++++++++ .../storage_account/src/bin/schema.rs | 12 + .../accounts/storage_account/src/contract.rs | 144 ++++++ contracts/accounts/storage_account/src/lib.rs | 6 + contracts/accounts/storage_account/src/msg.rs | 24 + .../accounts/storage_account/src/state.rs | 8 + .../accounts/storage_account/src/tests.rs | 483 ++++++++++++++++++ docs/src/components/accounts.md | 18 +- 13 files changed, 1150 insertions(+), 6 deletions(-) create mode 100644 contracts/accounts/storage_account/.cargo/config.toml create mode 100644 contracts/accounts/storage_account/Cargo.toml create mode 100644 contracts/accounts/storage_account/README.md create mode 100644 contracts/accounts/storage_account/schema/valence-storage-account.json create mode 100644 contracts/accounts/storage_account/src/bin/schema.rs create mode 100644 contracts/accounts/storage_account/src/contract.rs create mode 100644 contracts/accounts/storage_account/src/lib.rs create mode 100644 contracts/accounts/storage_account/src/msg.rs create mode 100644 contracts/accounts/storage_account/src/state.rs create mode 100644 contracts/accounts/storage_account/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index b037fca8..b3c2663d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5546,6 +5546,28 @@ dependencies = [ "valence-test-dynamic-ratio", ] +[[package]] +name = "valence-storage-account" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema 2.1.4", + "cosmwasm-std 2.1.4", + "cw-denom", + "cw-multi-test", + "cw-ownable", + "cw-storage-plus 2.0.0", + "cw2 2.0.0", + "cw20 2.0.0", + "cw20-base", + "getset", + "itertools 0.13.0", + "schemars", + "serde", + "sha2 0.10.8", + "thiserror 1.0.69", + "valence-account-utils", +] + [[package]] name = "valence-template-library" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7b29ad6f..4947427a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ osmosis-std-derive = "0.26.0" # our contracts valence-authorization = { path = "contracts/authorization", features = ["library"] } valence-base-account = { path = "contracts/accounts/base_account", features = ["library"] } +valence-storage-account = { path = "contracts/accounts/storage_account", features = ["library"] } valence-processor = { path = "contracts/processor", features = ["library"] } valence-splitter-library = { path = "contracts/libraries/splitter", features = ["library"] } valence-test-dynamic-ratio = { path = "contracts/testing/test-dynamic-ratio", features = ["library"] } diff --git a/contracts/accounts/storage_account/.cargo/config.toml b/contracts/accounts/storage_account/.cargo/config.toml new file mode 100644 index 00000000..5f6aa466 --- /dev/null +++ b/contracts/accounts/storage_account/.cargo/config.toml @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" diff --git a/contracts/accounts/storage_account/Cargo.toml b/contracts/accounts/storage_account/Cargo.toml new file mode 100644 index 00000000..30daca00 --- /dev/null +++ b/contracts/accounts/storage_account/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "valence-storage-account" +version = "0.1.0" +authors = ["Timewave Labs"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true, features = ["stargate"] } +cw-ownable = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +valence-account-utils = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +cw-denom = { workspace = true } +cw-ownable = { workspace = true } +cw20 = { workspace = true } +cw20-base = { workspace = true } +getset = { workspace = true } +itertools = { workspace = true } +sha2 = { workspace = true } +valence-account-utils = { workspace = true, features = ["testing"] } diff --git a/contracts/accounts/storage_account/README.md b/contracts/accounts/storage_account/README.md new file mode 100644 index 00000000..0e8d21d2 --- /dev/null +++ b/contracts/accounts/storage_account/README.md @@ -0,0 +1,4 @@ +# Valence Storage Account + +The **Valence Storage Account** is a type of Valence account that can store +arbitrary data blobs. diff --git a/contracts/accounts/storage_account/schema/valence-storage-account.json b/contracts/accounts/storage_account/schema/valence-storage-account.json new file mode 100644 index 00000000..ff876fed --- /dev/null +++ b/contracts/accounts/storage_account/schema/valence-storage-account.json @@ -0,0 +1,395 @@ +{ + "contract_name": "valence-storage-account", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "admin", + "approved_libraries" + ], + "properties": { + "admin": { + "type": "string" + }, + "approved_libraries": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "approve_library" + ], + "properties": { + "approve_library": { + "type": "object", + "required": [ + "library" + ], + "properties": { + "library": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_library" + ], + "properties": { + "remove_library": { + "type": "object", + "required": [ + "library" + ], + "properties": { + "library": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "post_blob" + ], + "properties": { + "post_blob": { + "type": "object", + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string" + }, + "value": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "list_approved_libraries" + ], + "properties": { + "list_approved_libraries": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "blob" + ], + "properties": { + "blob": { + "type": "object", + "required": [ + "key" + ], + "properties": { + "key": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's ownership information", + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "blob": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Binary", + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "list_approved_libraries": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + }, + "ownership": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ownership_for_String", + "description": "The contract's ownership info", + "type": "object", + "properties": { + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "type": [ + "string", + "null" + ] + }, + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + } + } +} diff --git a/contracts/accounts/storage_account/src/bin/schema.rs b/contracts/accounts/storage_account/src/bin/schema.rs new file mode 100644 index 00000000..086df9e2 --- /dev/null +++ b/contracts/accounts/storage_account/src/bin/schema.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::write_api; + +use valence_account_utils::msg::InstantiateMsg; +use valence_storage_account::msg::{ExecuteMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/accounts/storage_account/src/contract.rs b/contracts/accounts/storage_account/src/contract.rs new file mode 100644 index 00000000..4f2f7a9d --- /dev/null +++ b/contracts/accounts/storage_account/src/contract.rs @@ -0,0 +1,144 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdError, + StdResult, +}; +use cw2::set_contract_version; +use valence_account_utils::{error::ContractError, msg::InstantiateMsg}; + +use crate::{ + msg::{ExecuteMsg, QueryMsg}, + state::{APPROVED_LIBRARIES, BLOB_STORE}, +}; + +// version info for migration info +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +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, +) -> StdResult { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + cw_ownable::initialize_owner(deps.storage, deps.api, Some(&msg.admin))?; + + msg.approved_libraries.iter().try_for_each(|library| { + APPROVED_LIBRARIES.save(deps.storage, deps.api.addr_validate(library)?, &Empty {}) + })?; + + Ok(Response::new() + .add_attribute("method", "instantiate") + .add_attribute("admin", msg.admin)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::ApproveLibrary { library } => execute::approve_library(deps, info, library), + ExecuteMsg::RemoveLibrary { library } => execute::remove_library(deps, info, library), + ExecuteMsg::UpdateOwnership(action) => execute::update_ownership(deps, env, info, action), + ExecuteMsg::PostBlob { key, value } => execute::try_post_blob(deps, info, key, value), + } +} + +mod execute { + use cosmwasm_std::{ensure, Binary, DepsMut, Empty, Env, MessageInfo, Response}; + use valence_account_utils::error::{ContractError, UnauthorizedReason}; + + use crate::state::{APPROVED_LIBRARIES, BLOB_STORE}; + + pub fn try_post_blob( + deps: DepsMut, + info: MessageInfo, + key: String, + value: Binary, + ) -> Result { + // If not admin, check if it's an approved library + ensure!( + cw_ownable::is_owner(deps.storage, &info.sender)? + || APPROVED_LIBRARIES.has(deps.storage, info.sender.clone()), + ContractError::Unauthorized(UnauthorizedReason::NotAdminOrApprovedLibrary) + ); + + // store the value + BLOB_STORE.save(deps.storage, key.clone(), &value)?; + + Ok(Response::new() + .add_attribute("method", "post_blob") + .add_attribute("key", key) + .add_attribute("value", format!("{:?}", value))) + } + + pub fn approve_library( + deps: DepsMut, + info: MessageInfo, + library: String, + ) -> Result { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + let library_addr = deps.api.addr_validate(&library)?; + APPROVED_LIBRARIES.save(deps.storage, library_addr.clone(), &Empty {})?; + + Ok(Response::new() + .add_attribute("method", "approve_library") + .add_attribute("library", library_addr)) + } + + pub fn remove_library( + deps: DepsMut, + info: MessageInfo, + library: String, + ) -> Result { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + let library_addr = deps.api.addr_validate(&library)?; + APPROVED_LIBRARIES.remove(deps.storage, library_addr.clone()); + + Ok(Response::new() + .add_attribute("method", "remove_library") + .add_attribute("library", library_addr)) + } + + pub fn update_ownership( + deps: DepsMut, + env: Env, + info: MessageInfo, + action: cw_ownable::Action, + ) -> Result { + let result = cw_ownable::update_ownership(deps, &env.block, &info.sender, action.clone())?; + Ok(Response::default() + .add_attribute("method", "update_ownership") + .add_attribute("action", format!("{:?}", action)) + .add_attribute("result", format!("{:?}", result))) + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Ownership {} => to_json_binary(&cw_ownable::get_ownership(deps.storage)?), + QueryMsg::ListApprovedLibraries {} => { + let libraries = APPROVED_LIBRARIES + .keys(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + to_json_binary(&libraries) + } + QueryMsg::Blob { key } => { + let blob = BLOB_STORE.may_load(deps.storage, key)?; + + match blob { + Some(val) => to_json_binary(&val), + None => Err(StdError::generic_err("blob not found")), + } + } + } +} diff --git a/contracts/accounts/storage_account/src/lib.rs b/contracts/accounts/storage_account/src/lib.rs new file mode 100644 index 00000000..b88f5887 --- /dev/null +++ b/contracts/accounts/storage_account/src/lib.rs @@ -0,0 +1,6 @@ +pub mod contract; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod tests; diff --git a/contracts/accounts/storage_account/src/msg.rs b/contracts/accounts/storage_account/src/msg.rs new file mode 100644 index 00000000..7046f1d2 --- /dev/null +++ b/contracts/accounts/storage_account/src/msg.rs @@ -0,0 +1,24 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Binary; +use cw_ownable::{cw_ownable_execute, cw_ownable_query}; + +#[cw_ownable_execute] +#[cw_serde] +pub enum ExecuteMsg { + // Add library to approved list (only admin) + ApproveLibrary { library: String }, + // Remove library from approved list (only admin) + RemoveLibrary { library: String }, + // store a payload in storage + PostBlob { key: String, value: Binary }, +} + +#[cw_ownable_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Vec)] + ListApprovedLibraries {}, // Get list of approved libraries + #[returns(Binary)] + Blob { key: String }, // Get blob from storage +} diff --git a/contracts/accounts/storage_account/src/state.rs b/contracts/accounts/storage_account/src/state.rs new file mode 100644 index 00000000..e655da3c --- /dev/null +++ b/contracts/accounts/storage_account/src/state.rs @@ -0,0 +1,8 @@ +use cosmwasm_std::{Addr, Binary, Empty}; +use cw_storage_plus::Map; + +/// Approved libraries that can execute actions on behalf of the account +pub const APPROVED_LIBRARIES: Map = Map::new("libraries"); + +/// string key indexed blob (`cosmwasm_std::Binary`) storage +pub const BLOB_STORE: Map = Map::new("blob_store"); diff --git a/contracts/accounts/storage_account/src/tests.rs b/contracts/accounts/storage_account/src/tests.rs new file mode 100644 index 00000000..d2943384 --- /dev/null +++ b/contracts/accounts/storage_account/src/tests.rs @@ -0,0 +1,483 @@ +use cosmwasm_std::{coin, from_json, to_json_binary, Addr, Binary, Coin}; +use cw_multi_test::{error::AnyResult, App, AppResponse, ContractWrapper, Executor}; +use cw_ownable::{Ownership, OwnershipError}; +use getset::{Getters, Setters}; +use itertools::sorted; +use std::string::ToString; +use valence_account_utils::{ + error::ContractError, + msg::InstantiateMsg, + testing::{AccountTestSuite, AccountTestSuiteBase}, +}; + +use crate::msg::{ExecuteMsg, QueryMsg}; + +const NTRN: &str = "untrn"; +const ONE_MILLION: u128 = 1_000_000_000_000_u128; +const BLOB_KEY: &str = "test_blob"; + +#[derive(Getters, Setters)] +struct StorageAccountTestSuite { + #[getset(get)] + inner: AccountTestSuiteBase, + #[getset(get)] + input_balances: Option>, +} + +impl Default for StorageAccountTestSuite { + fn default() -> Self { + Self::new(None) + } +} + +#[allow(dead_code)] +impl StorageAccountTestSuite { + pub fn new(input_balances: Option>) -> Self { + // storage account contract + let account_code = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ); + + let inner = AccountTestSuiteBase::new(Box::new(account_code)); + + Self { + inner, + input_balances, + } + } + + pub fn account_init(&mut self, approved_libraries: Vec) -> Addr { + let init_msg = InstantiateMsg { + admin: self.owner().to_string(), + approved_libraries, + }; + let acc_addr = self.contract_init(self.account_code_id(), "base_account", &init_msg, &[]); + + if let Some(balances) = self.input_balances.as_ref().cloned() { + let amounts = balances + .iter() + .map(|(amount, denom)| coin(*amount, denom.to_string())) + .collect::>(); + self.init_balance(&acc_addr, amounts); + } + + acc_addr + } + + fn approve_library(&mut self, addr: Addr, library: Addr) -> AnyResult { + self.contract_execute( + addr, + &ExecuteMsg::ApproveLibrary { + library: library.to_string(), + }, + ) + } + + fn approve_library_non_owner(&mut self, addr: Addr, library: Addr) -> AnyResult { + let non_owner = self.api().addr_make("non_owner"); + self.app_mut().execute_contract( + non_owner, + addr, + &ExecuteMsg::ApproveLibrary { + library: library.to_string(), + }, + &[], + ) + } + + fn remove_library(&mut self, addr: Addr, library: Addr) -> AnyResult { + self.contract_execute( + addr, + &ExecuteMsg::RemoveLibrary { + library: library.to_string(), + }, + ) + } + + fn remove_library_non_owner(&mut self, addr: Addr, library: Addr) -> AnyResult { + let non_owner = self.api().addr_make("non_owner"); + self.app_mut().execute_contract( + non_owner, + addr, + &ExecuteMsg::RemoveLibrary { + library: library.to_string(), + }, + &[], + ) + } + + fn transfer_ownership(&mut self, addr: Addr, new_owner: Addr) -> AnyResult { + self.contract_execute( + addr, + &ExecuteMsg::UpdateOwnership(cw_ownable::Action::TransferOwnership { + new_owner: new_owner.to_string(), + expiry: None, + }), + ) + } + + fn transfer_ownership_non_owner( + &mut self, + addr: Addr, + new_owner: Addr, + ) -> AnyResult { + self.app_mut().execute_contract( + new_owner.clone(), + addr, + &ExecuteMsg::UpdateOwnership(cw_ownable::Action::TransferOwnership { + new_owner: new_owner.to_string(), + expiry: None, + }), + &[], + ) + } + + fn accept_ownership(&mut self, addr: Addr, new_owner: Addr) -> AnyResult { + self.app_mut().execute_contract( + new_owner, + addr, + &ExecuteMsg::UpdateOwnership(cw_ownable::Action::AcceptOwnership {}), + &[], + ) + } + + fn renounce_ownership(&mut self, addr: Addr) -> AnyResult { + self.contract_execute( + addr, + &ExecuteMsg::UpdateOwnership(cw_ownable::Action::RenounceOwnership {}), + ) + } + + fn renounce_ownership_non_owner(&mut self, addr: Addr, sender: Addr) -> AnyResult { + self.app_mut().execute_contract( + sender, + addr, + &ExecuteMsg::UpdateOwnership(cw_ownable::Action::RenounceOwnership {}), + &[], + ) + } + + fn post_blob(&mut self, addr: Addr, key: &str, blob: Binary) -> AnyResult { + self.contract_execute( + addr, + &ExecuteMsg::PostBlob { + key: key.to_string(), + value: blob, + }, + ) + } + + fn query_approved_libraries(&mut self, addr: &Addr) -> Vec { + self.query_wasm(addr, &QueryMsg::ListApprovedLibraries {}) + } + + fn query_owership(&mut self, addr: &Addr) -> Ownership { + self.query_wasm(addr, &QueryMsg::Ownership {}) + } + + fn query_blob(&mut self, acc: Addr, key: &str) -> Option { + self.query_wasm( + &acc, + &QueryMsg::Blob { + key: key.to_string(), + }, + ) + } +} + +impl AccountTestSuite for StorageAccountTestSuite { + fn app(&self) -> &App { + self.inner.app() + } + + fn app_mut(&mut self) -> &mut App { + self.inner.app_mut() + } + + fn owner(&self) -> &Addr { + self.inner.owner() + } + + fn account_code_id(&self) -> u64 { + self.inner.account_code_id() + } + + fn cw20_code_id(&self) -> u64 { + self.inner.cw20_code_id() + } +} + +#[test] +fn instantiate_with_no_approved_libraries() { + let mut suite = StorageAccountTestSuite::default(); + + // Instantiate storage account contract + let acc = suite.account_init(vec![]); + + // Verify owner + let owner_res: Ownership = suite.query_owership(&acc); + assert_eq!(owner_res.owner, Some(suite.owner().clone())); + + // Verify approved libraries + let approved_libraries: Vec = suite.query_approved_libraries(&acc); + assert_eq!(approved_libraries, Vec::::new()); +} + +#[test] +fn instantiate_with_approved_libraries() { + let mut suite = StorageAccountTestSuite::default(); + + let lib1 = suite.api().addr_make("library_1"); + let lib2 = suite.api().addr_make("library_2"); + + // Instantiate storage account contract with approved libraries + let acc = suite.account_init(vec![lib1.to_string(), lib2.to_string()]); + + // Verify owner + let owner_res: Ownership = suite.query_owership(&acc); + assert_eq!(owner_res.owner, Some(suite.owner().clone())); + + // Verify approved libraries + let approved_libraries: Vec = suite.query_approved_libraries(&acc); + assert_eq!( + approved_libraries, + sorted(vec![lib1, lib2]).collect::>() + ); +} + +#[test] +fn approve_library_by_owner() { + let mut suite = StorageAccountTestSuite::default(); + + let lib1 = suite.api().addr_make("library_1"); + let lib2 = suite.api().addr_make("library_2"); + let lib3 = suite.api().addr_make("library_3"); + + // Instantiate storage account contract with approved libraries + let acc = suite.account_init(vec![lib1.to_string(), lib2.to_string()]); + + // Owner approves new library on account + suite.approve_library(acc.clone(), lib3.clone()).unwrap(); + + // Verify approved libraries + let approved_libraries = sorted(suite.query_approved_libraries(&acc)).collect::>(); + assert_eq!( + approved_libraries, + sorted(vec![lib1, lib2, lib3]).collect::>() + ); +} + +#[test] +fn approve_library_by_non_owner() { + let mut suite = StorageAccountTestSuite::default(); + + let lib1 = suite.api().addr_make("library_1"); + let lib2 = suite.api().addr_make("library_2"); + let lib3 = suite.api().addr_make("library_3"); + + // Instantiate storage account contract with approved libraries + let acc = suite.account_init(vec![lib1.to_string(), lib2.to_string()]); + + // Owner approves new library on account + let res = suite.approve_library_non_owner(acc.clone(), lib3.clone()); + assert!(res.is_err()); + + assert_eq!( + res.unwrap_err().downcast::().unwrap(), + ContractError::OwnershipError(cw_ownable::OwnershipError::NotOwner) + ); +} + +#[test] +fn remove_library_by_owner() { + let mut suite = StorageAccountTestSuite::default(); + + let lib1 = suite.api().addr_make("library_1"); + let lib2 = suite.api().addr_make("library_2"); + let lib3 = suite.api().addr_make("library_3"); + + // Instantiate storage account contract with approved libraries + let acc = suite.account_init(vec![lib1.to_string(), lib2.to_string(), lib3.to_string()]); + + // Owner approves new library on account + suite.remove_library(acc.clone(), lib2.clone()).unwrap(); + + // Verify approved libraries + let approved_libraries = sorted(suite.query_approved_libraries(&acc)).collect::>(); + assert_eq!( + approved_libraries, + sorted(vec![lib1, lib3]).collect::>() + ); +} + +#[test] +fn remove_library_by_non_owner() { + let mut suite = StorageAccountTestSuite::default(); + + let lib1 = suite.api().addr_make("library_1"); + let lib2 = suite.api().addr_make("library_2"); + let lib3 = suite.api().addr_make("library_3"); + + // Instantiate storage account contract with approved libraries + let acc = suite.account_init(vec![lib1.to_string(), lib2.to_string(), lib3.to_string()]); + + // Owner approves new library on account + let res = suite.remove_library_non_owner(acc.clone(), lib3.clone()); + assert!(res.is_err()); + + assert_eq!( + res.unwrap_err().downcast::().unwrap(), + ContractError::OwnershipError(cw_ownable::OwnershipError::NotOwner) + ); +} + +#[test] +fn transfer_account_ownership_by_owner() { + let mut suite = StorageAccountTestSuite::default(); + + // Instantiate storage account contract + let acc = suite.account_init(vec![]); + + // Owner transfer ownership to new owner + let new_owner = suite.api().addr_make("new_owner"); + suite + .transfer_ownership(acc.clone(), new_owner.clone()) + .unwrap(); + + // Verify new owner is pending + let owership: Ownership = suite.query_owership(&acc); + assert_eq!( + owership, + Ownership { + owner: Some(suite.owner().clone()), + pending_owner: Some(new_owner.clone()), + pending_expiry: None, + } + ); + + // New owner accepts ownership + suite + .accept_ownership(acc.clone(), new_owner.clone()) + .unwrap(); + + // Verify ownership has been transferred + let owership: Ownership = suite.query_owership(&acc); + assert_eq!( + owership, + Ownership { + owner: Some(new_owner), + pending_owner: None, + pending_expiry: None, + } + ); +} + +#[test] +fn transfer_account_ownership_by_non_owner() { + let mut suite = StorageAccountTestSuite::default(); + + // Instantiate storage account contract + let acc = suite.account_init(vec![]); + + // New owner tries to transfer ownership to itself + let new_owner = suite.api().addr_make("new_owner"); + let res = suite.transfer_ownership_non_owner(acc.clone(), new_owner.clone()); + assert!(res.is_err()); + + assert_eq!( + res.unwrap_err().downcast::().unwrap(), + ContractError::OwnershipError(OwnershipError::NotOwner) + ); +} + +#[test] +fn renounce_account_ownership() { + let mut suite = StorageAccountTestSuite::default(); + + // Instantiate storage account contract + let acc = suite.account_init(vec![]); + + // Owner renounces ownership + suite.renounce_ownership(acc.clone()).unwrap(); + + // Verify owership has been renounced + let owership: Ownership = suite.query_owership(&acc); + assert_eq!( + owership, + Ownership { + owner: None, + pending_owner: None, + pending_expiry: None, + } + ); +} + +#[test] +fn renounce_account_ownership_by_non_owner() { + let mut suite = StorageAccountTestSuite::default(); + + // Instantiate storage account contract + let acc = suite.account_init(vec![]); + + // Owner renounces ownership + let non_owner = suite.api().addr_make("non_owner"); + let res = suite.renounce_ownership_non_owner(acc.clone(), non_owner); + assert!(res.is_err()); + + assert_eq!( + res.unwrap_err().downcast::().unwrap(), + ContractError::OwnershipError(OwnershipError::NotOwner) + ); +} + +#[test] +fn post_data_blob_admin() { + let mut suite = StorageAccountTestSuite::new(Some(vec![(ONE_MILLION, NTRN.to_string())])); + + // Instantiate storage account contract + let acc = suite.account_init(vec![]); + + // generate a blob to be stored + let blob = to_json_binary(&coin(ONE_MILLION, NTRN)).unwrap(); + + suite.post_blob(acc.clone(), BLOB_KEY, blob).unwrap(); + + // get the posted blob and try to reconstruct it + let blob_query_result = suite.query_blob(acc, BLOB_KEY).unwrap(); + + let coin: Coin = from_json(blob_query_result).unwrap(); + + // assert that the underlying data is the same + assert_eq!(coin.denom, NTRN); + assert_eq!(coin.amount.u128(), ONE_MILLION); +} + +#[test] +#[should_panic(expected = "Not the admin or an approved library")] +fn post_data_blob_unauthorized() { + let mut suite = StorageAccountTestSuite::new(Some(vec![(ONE_MILLION, NTRN.to_string())])); + + // Instantiate storage account contract + let acc = suite.account_init(vec![]); + + // generate a blob to be stored + let blob = to_json_binary(&coin(ONE_MILLION, NTRN)).unwrap(); + + suite + .inner + .app_mut() + .execute_contract( + Addr::unchecked("not the real"), + acc.clone(), + &ExecuteMsg::PostBlob { + key: BLOB_KEY.to_string(), + value: blob, + }, + &[], + ) + .unwrap(); + + // TODO: unit test for approved libraries +} diff --git a/docs/src/components/accounts.md b/docs/src/components/accounts.md index dcf9cc31..212e5f1a 100644 --- a/docs/src/components/accounts.md +++ b/docs/src/components/accounts.md @@ -1,16 +1,18 @@ # Accounts +## Valence Base Account + **Valence Programs** usually perform operations on tokens, accross multiple domains, and to ensure that the funds remain safe throughout a program's execution, they rely on a primitive called **Valence Accounts**. -A **Valence Account** is an escrow contract that can hold balances for various supported token-types (e.g., in Cosmos `ics-20` or `cw-20`), and that ensures that only a restricted set of operations can be performed on the held tokens. -A **Valence Account** is created (instantiated) on a specific **domain**, and bound to a specific **Valence Program**. **Valence Programs** will typically use multiple accounts, during the program's lifecycle, for different purposes. **Valence Accounts** are generic in nature, how they are used to form a program is entirely up to the program's creator. +A **Valence Base Account** is an escrow contract that can hold balances for various supported token-types (e.g., in Cosmos `ics-20` or `cw-20`), and that ensures that only a restricted set of operations can be performed on the held tokens. +A **Valence Base Account** is created (instantiated) on a specific **domain**, and bound to a specific **Valence Program**. **Valence Programs** will typically use multiple accounts, during the program's lifecycle, for different purposes. **Valence Base Accounts** are generic in nature, how they are used to form a program is entirely up to the program's creator. Using a simple _token swap program_ as an example: the program receives an amount of **Token A** in an **input account**, and will **swap** these **Token A** for **Token B** using a **DEX** on the **same domain** (e.g. Neutron). After the swap operation, the received amount of **Token B** will be temporarily held in a **transfer account**, before being transfered to a final **output account** on another domain (e.g. Osmosis). For this, the program will create the following accounts: -- A **Valence Account** is created on the **Neutron domain** to act as the **Input account**. -- A **Valence Account** is created on the **Neutron domain** to act as the **Transfer account**. -- A **Valence Account** is created on the **Osmosis domain** to act as the **Output account**. +- A **Valence Base Account** is created on the **Neutron domain** to act as the **Input account**. +- A **Valence Base Account** is created on the **Neutron domain** to act as the **Transfer account**. +- A **Valence Base Account** is created on the **Osmosis domain** to act as the **Output account**. ```mermaid --- @@ -34,4 +36,8 @@ graph LR ``` Note: this is a simplified representation. -**Valence Accounts** do not perform any operation by themselves on the held funds, the operations are performed by **[Valence Libraries](./libraries_and_functions.md)**. +**Valence Base Accounts** do not perform any operation by themselves on the held funds, the operations are performed by **[Valence Libraries](./libraries_and_functions.md)**. + +## Valence Storage Account + +TODO