diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9dc0c05..586d8a23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,8 +72,9 @@ jobs: cache-all-crates: "true" # Yes, it's a string - name: Install Cargo Plugins run: cargo install cargo-make cargo-near --locked + # builds all contracts in the workspace with poa factory global contracts disabled - name: Build - run: cargo make build + run: cargo make test-build - name: Upload Artifacts uses: actions/upload-artifact@v4 with: diff --git a/Cargo.lock b/Cargo.lock index d65ce6aa..c7c84a12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1019,11 +1019,16 @@ dependencies = [ name = "defuse-poa-token" version = "0.1.0" dependencies = [ + "arbitrary_with", "defuse-admin-utils", + "defuse-borsh-utils", "defuse-near-utils", + "defuse-test-utils", + "impl-tools", "near-contract-standards", "near-plugins", "near-sdk", + "rstest", ] [[package]] @@ -2231,6 +2236,7 @@ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -2379,20 +2385,21 @@ dependencies = [ [[package]] name = "near-chain-configs" -version = "0.30.1" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35aca612e6aee487a185766f51f5218e305257270136f0b911f2ecd4184c8986" +checksum = "398bbc9829c66d1b52a213d3094f0ec5ab61d6fd4b3c05b98aabd1b6e70bcf44" dependencies = [ "anyhow", "bytesize", "chrono", - "derive_more 1.0.0", - "near-config-utils 0.30.1", - "near-crypto 0.30.1", - "near-parameters 0.30.1", - "near-primitives 0.30.1", - "near-time 0.30.1", + "derive_more 2.0.1", + "near-config-utils 0.31.1", + "near-crypto 0.31.1", + "near-parameters", + "near-primitives", + "near-time", "num-rational", + "parking_lot", "serde", "serde_json", "sha2", @@ -2478,6 +2485,7 @@ dependencies = [ "near-schema-checker-lib 0.31.1", "near-stdx 0.31.1", "primitive-types", + "rand 0.8.5", "secp256k1", "serde", "serde_json", @@ -2485,22 +2493,13 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "near-fmt" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c0e4d846b9c27b30e5f24e788fb8cc55c046f72e2048e2539dbcb04d9a71c4" -dependencies = [ - "near-primitives-core 0.30.1", -] - [[package]] name = "near-fmt" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96e9fa3af54e4b13f4f0657e3ae640e63f7f8953a5fbedf836bc47b43f973dde" dependencies = [ - "near-primitives-core 0.31.1", + "near-primitives-core", ] [[package]] @@ -2516,17 +2515,17 @@ dependencies = [ [[package]] name = "near-jsonrpc-client" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b8a74b8311731d9ed1a995549fc7ec1d103d5c8c7eaab35e240f894343b0b4" +checksum = "0d03f5dd8adf26ecf27f2a898bee6b7df80ea769ae6d7b4d6b9b49bf11d7838b" dependencies = [ "borsh", "lazy_static", "log", "near-chain-configs", - "near-crypto 0.30.1", + "near-crypto 0.31.1", "near-jsonrpc-primitives", - "near-primitives 0.30.1", + "near-primitives", "reqwest", "serde", "serde_json", @@ -2535,40 +2534,22 @@ dependencies = [ [[package]] name = "near-jsonrpc-primitives" -version = "0.30.1" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63ac3e779b1ad979957f05e43c92a79fbe7e1315647ab4d530e2a9a66bc62f5e" +checksum = "3feacba264550d045c5d5e26f1bdb4857a5122739590ddf6c4ed22d56d518301" dependencies = [ "arbitrary", "near-chain-configs", - "near-crypto 0.30.1", - "near-primitives 0.30.1", - "near-schema-checker-lib 0.30.1", + "near-crypto 0.31.1", + "near-primitives", + "near-schema-checker-lib 0.31.1", + "near-time", "serde", "serde_json", "thiserror 2.0.12", "time", ] -[[package]] -name = "near-parameters" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dbb139bec6b7088d6afab0a3662725e5ee82d0ad725b67c1d45447c3d45fe55" -dependencies = [ - "borsh", - "enum-map", - "near-account-id", - "near-primitives-core 0.30.1", - "near-schema-checker-lib 0.30.1", - "num-rational", - "serde", - "serde_repr", - "serde_yaml", - "strum 0.24.1", - "thiserror 2.0.12", -] - [[package]] name = "near-parameters" version = "0.31.1" @@ -2578,7 +2559,7 @@ dependencies = [ "borsh", "enum-map", "near-account-id", - "near-primitives-core 0.31.1", + "near-primitives-core", "near-schema-checker-lib 0.31.1", "num-rational", "serde", @@ -2611,48 +2592,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "near-primitives" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f06d70f10eb505600ec6627f5fd64a1d1700af71098282e339c993e6319151d" -dependencies = [ - "arbitrary", - "base64 0.21.7", - "bitvec", - "borsh", - "bytes", - "bytesize", - "cfg-if 1.0.0", - "chrono", - "derive_more 1.0.0", - "easy-ext", - "enum-map", - "hex", - "itertools 0.12.1", - "near-crypto 0.30.1", - "near-fmt 0.30.1", - "near-parameters 0.30.1", - "near-primitives-core 0.30.1", - "near-schema-checker-lib 0.30.1", - "near-stdx 0.30.1", - "near-time 0.30.1", - "num-rational", - "ordered-float", - "primitive-types", - "rand 0.8.5", - "rand_chacha 0.3.1", - "serde", - "serde_json", - "serde_with", - "sha3", - "smart-default", - "strum 0.24.1", - "thiserror 2.0.12", - "tracing", - "zstd 0.13.3", -] - [[package]] name = "near-primitives" version = "0.31.1" @@ -2672,15 +2611,17 @@ dependencies = [ "hex", "itertools 0.12.1", "near-crypto 0.31.1", - "near-fmt 0.31.1", - "near-parameters 0.31.1", - "near-primitives-core 0.31.1", + "near-fmt", + "near-parameters", + "near-primitives-core", "near-schema-checker-lib 0.31.1", "near-stdx 0.31.1", - "near-time 0.31.1", + "near-time", "num-rational", "ordered-float", "primitive-types", + "rand 0.8.5", + "rand_chacha 0.3.1", "serde", "serde_json", "serde_with", @@ -2692,27 +2633,6 @@ dependencies = [ "zstd 0.13.3", ] -[[package]] -name = "near-primitives-core" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "408abb7e360ae1353d5a3cde62b4a2f0abde96c9ff4045f23cabda15c22b6ec9" -dependencies = [ - "arbitrary", - "base64 0.21.7", - "borsh", - "bs58 0.4.0", - "derive_more 1.0.0", - "enum-map", - "near-account-id", - "near-schema-checker-lib 0.30.1", - "num-rational", - "serde", - "serde_repr", - "sha2", - "thiserror 2.0.12", -] - [[package]] name = "near-primitives-core" version = "0.31.1" @@ -2803,9 +2723,9 @@ dependencies = [ "near-account-id", "near-crypto 0.31.1", "near-gas", - "near-parameters 0.31.1", - "near-primitives 0.31.1", - "near-primitives-core 0.31.1", + "near-parameters", + "near-primitives", + "near-primitives-core", "near-sdk-macros", "near-sys", "near-token", @@ -2852,16 +2772,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e64e114297f37c94aa20df6a6f92822a1b41da76b4961298caf08ba80b7779b2" -[[package]] -name = "near-time" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806ae1785ed79e99e9183646e5fe18ecee504385350a45c600ee189d904808a9" -dependencies = [ - "serde", - "time", -] - [[package]] name = "near-time" version = "0.31.1" @@ -2896,8 +2806,8 @@ dependencies = [ "enum-map", "lru", "near-crypto 0.31.1", - "near-parameters 0.31.1", - "near-primitives-core 0.31.1", + "near-parameters", + "near-primitives-core", "near-schema-checker-lib 0.31.1", "near-stdx 0.31.1", "num-rational", @@ -2918,9 +2828,9 @@ dependencies = [ [[package]] name = "near-workspaces" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "263c833751e15cd242beff1f72ae199d8f78a2b453852e88bbff47d43cb81ca9" +checksum = "1f02d7882561f4be4797b3483603f2ca48c9b4fde8c0487961cb70dad7c93267" dependencies = [ "async-trait", "base64 0.22.1", @@ -2931,11 +2841,11 @@ dependencies = [ "libc", "near-abi-client", "near-account-id", - "near-crypto 0.30.1", + "near-crypto 0.31.1", "near-gas", "near-jsonrpc-client", "near-jsonrpc-primitives", - "near-primitives 0.30.1", + "near-primitives", "near-sandbox-utils", "near-token", "rand 0.8.5", diff --git a/Cargo.toml b/Cargo.toml index e0947390..8a90088a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ near-contract-standards = "5.15" near-crypto = "0.30" near-plugins = { git = "https://github.com/Near-One/near-plugins", tag = "v0.5.0" } near-sdk = "5.18" -near-workspaces = "0.20" +near-workspaces = "0.21" cargo-near-build = "0.9.0" p256 = { version = "0.13", default-features = false, features = ["ecdsa"] } diff --git a/Makefile.toml b/Makefile.toml index 14ec58ae..5912953b 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -5,8 +5,6 @@ skip_core_tasks = true [env] TARGET_DIR = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/res" POA_TOKEN_WASM = "${TARGET_DIR}/defuse_poa_token.wasm" -POA_TOKEN_WITH_NO_REGISTRATION_DIR = "${TARGET_DIR}/poa-token-no-registration" -POA_TOKEN_WASM_NO_REGISTRATION_WASM = "${POA_TOKEN_WITH_NO_REGISTRATION_DIR}/defuse_poa_token.wasm" MT_RECEIVER_STUB_DIR = "${TARGET_DIR}/multi-token-receiver-stub" MT_RECEIVER_STUB_WASM = "${MT_RECEIVER_STUB_DIR}/multi_token_receiver_stub.wasm" @@ -22,10 +20,8 @@ alias = "build" dependencies = [ "add-cache-dir-tag", "build-defuse", - "build-poa-factory", - "build-poa-token-no-registration", - "contract-stats", - "build-multi-token-receiver-stub", + "build-poa-factory-global", + "contract-stats" ] [tasks.build-defuse] @@ -45,7 +41,7 @@ args = [ "--no-embed-abi", ] -[tasks.build-poa-factory] +[tasks.build-poa-factory-global] dependencies = ["add-cache-dir-tag", "build-poa-token"] command = "cargo" args = [ @@ -56,14 +52,14 @@ args = [ "--manifest-path", "./poa-factory/Cargo.toml", "--features", - "contract", + "contract,global_contracts", "--out-dir", "${TARGET_DIR}", "--no-embed-abi", ] -[tasks.build-poa-token] -dependencies = ["add-cache-dir-tag"] +[tasks.build-poa-factory] +dependencies = ["add-cache-dir-tag", "build-poa-token"] command = "cargo" args = [ "near", @@ -71,7 +67,7 @@ args = [ "non-reproducible-wasm", "--locked", "--manifest-path", - "./poa-token/Cargo.toml", + "./poa-factory/Cargo.toml", "--features", "contract", "--out-dir", @@ -79,7 +75,7 @@ args = [ "--no-embed-abi", ] -[tasks.build-poa-token-no-registration] +[tasks.build-poa-token] dependencies = ["add-cache-dir-tag"] command = "cargo" args = [ @@ -90,9 +86,9 @@ args = [ "--manifest-path", "./poa-token/Cargo.toml", "--features", - "contract,no-registration", + "contract", "--out-dir", - "${POA_TOKEN_WITH_NO_REGISTRATION_DIR}", + "${TARGET_DIR}", "--no-embed-abi", ] @@ -105,7 +101,6 @@ dependencies = [ "add-cache-dir-tag", "build-defuse-reproducible", "build-poa-factory-reproducible", - "build-poa-token-no-registration-reproducible", "defuse-reproducible-checksum", "contract-stats", ] @@ -135,6 +130,7 @@ args = [ "--out-dir", "${TARGET_DIR}", ] + [tasks.build-poa-token-reproducible] dependencies = ["add-cache-dir-tag"] command = "cargo" @@ -148,21 +144,6 @@ args = [ "${TARGET_DIR}", ] -[tasks.build-poa-token-no-registration-reproducible] -dependencies = ["add-cache-dir-tag"] -command = "cargo" -args = [ - "near", - "build", - "reproducible-wasm", - "--manifest-path", - "./poa-token/Cargo.toml", - "--out-dir", - "${POA_TOKEN_WITH_NO_REGISTRATION_DIR}", - "--variant", - "no_registration", -] - [tasks.defuse-reproducible-checksum] dependencies = ["build-defuse-reproducible"] script = [ @@ -177,13 +158,20 @@ script = [ [tasks.test] alias = "tests" +[tasks.test-build] +dependencies = [ + "build-defuse", + "build-poa-factory", + "build-multi-token-receiver-stub" +] + [tasks.nextest] -dependencies = ["build"] +dependencies = ["test-build"] command = "cargo" args = ["nextest", "run", "--all", "${@}"] [tasks.tests] -dependencies = ["build"] +dependencies = ["test-build"] run_task = "run-tests" [tasks.run-tests] diff --git a/poa-factory/Cargo.toml b/poa-factory/Cargo.toml index dc731c42..1e028217 100644 --- a/poa-factory/Cargo.toml +++ b/poa-factory/Cargo.toml @@ -21,7 +21,7 @@ container_build_command = [ "build", "non-reproducible-wasm", "--locked", - "--features=contract", + "--features=contract,global_contracts", "--no-embed-abi" ] @@ -37,6 +37,7 @@ near-sdk.workspace = true [features] contract = [] +global_contracts = ["near-sdk/global-contracts"] [build-dependencies] cargo-near-build = { workspace = true, features = ["build_external"] } diff --git a/poa-factory/src/contract.rs b/poa-factory/src/contract.rs index a06eba5c..f730f9eb 100644 --- a/poa-factory/src/contract.rs +++ b/poa-factory/src/contract.rs @@ -1,7 +1,7 @@ use core::iter; use std::collections::{HashMap, HashSet}; -use defuse_admin_utils::full_access_keys::FullAccessKeys; +use defuse_admin_utils::full_access_keys::{FullAccessKeys, ext_full_access_keys}; use defuse_near_utils::{CURRENT_ACCOUNT_ID, UnwrapOrPanicError, gas_left}; use defuse_poa_token::ext_poa_fungible_token; use near_contract_standards::fungible_token::{core::ext_ft_core, metadata::FungibleTokenMetadata}; @@ -19,16 +19,23 @@ use near_sdk::{ store::IterableSet, }; -use crate::PoaFactory; +use crate::{PoaFactory, TokenFullAccessKeys}; -const POA_TOKEN_WASM: &[u8] = include_bytes!(std::env!("POA_TOKEN_WASM")); +/// Initial balance to deploy each token contract with size ~ 500B +#[cfg(feature = "global_contracts")] +const POA_TOKEN_INIT_BALANCE: NearToken = NearToken::from_millinear(5); +#[cfg(not(feature = "global_contracts"))] const POA_TOKEN_INIT_BALANCE: NearToken = NearToken::from_near(3); + const POA_TOKEN_NEW_GAS: Gas = Gas::from_tgas(10); const POA_TOKEN_FT_DEPOSIT_GAS: Gas = Gas::from_tgas(10); /// Copied from `near_contract_standards::fungible_token::core_impl::GAS_FOR_FT_TRANSFER_CALL` const POA_TOKEN_FT_TRANSFER_CALL_MIN_GAS: Gas = Gas::from_tgas(30); +#[cfg(not(feature = "global_contracts"))] +const POA_TOKEN_WASM: &[u8] = include_bytes!(std::env!("POA_TOKEN_WASM")); + #[derive(AccessControlRole, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[near(serializers = [json])] pub enum Role { @@ -94,7 +101,12 @@ impl PoaFactory for Contract { #[pause] #[access_control_any(roles(Role::DAO, Role::TokenDeployer))] #[payable] - fn deploy_token(&mut self, token: String, metadata: Option) -> Promise { + fn deploy_token( + &mut self, + token: String, + metadata: Option, + no_registration: Option, + ) -> Promise { if let Some(metadata) = metadata.as_ref() { metadata.assert_valid(); } @@ -111,19 +123,30 @@ impl PoaFactory for Contract { "not enough deposit attached to deploy PoA token" ); - Promise::new(Self::token_id(token)) + let mut promise = Promise::new(Self::token_id(token)) .create_account() - .transfer(POA_TOKEN_INIT_BALANCE) - .deploy_contract(POA_TOKEN_WASM.to_vec()) - .function_call( - "new".to_string(), - serde_json::to_vec(&json!({ - "metadata": metadata, - })) - .unwrap_or_panic_display(), - NearToken::from_yoctonear(0), - POA_TOKEN_NEW_GAS, - ) + .transfer(POA_TOKEN_INIT_BALANCE); + + // TODO: Remove it as soon as near-workspaces-rs supports deploying global contracts + #[cfg(not(feature = "global_contracts"))] + { + promise = promise.deploy_contract(POA_TOKEN_WASM.to_vec()); + } + #[cfg(feature = "global_contracts")] + { + promise = promise.use_global_contract_by_account_id(CURRENT_ACCOUNT_ID.clone()); + } + + promise.function_call( + "new".to_string(), + serde_json::to_vec(&json!({ + "metadata": metadata, + "no_registration": no_registration, + })) + .unwrap_or_panic_display(), + NearToken::from_yoctonear(0), + POA_TOKEN_NEW_GAS, + ) } #[pause] @@ -220,6 +243,27 @@ impl FullAccessKeys for Contract { } } +#[near] +impl TokenFullAccessKeys for Contract { + #[access_control_any(roles(Role::DAO))] + #[payable] + fn add_token_full_access_key(&mut self, token: String, public_key: PublicKey) -> Promise { + assert_one_yocto(); + ext_full_access_keys::ext(Self::token_id(token)) + .with_attached_deposit(NearToken::from_yoctonear(1)) + .add_full_access_key(public_key) + } + + #[access_control_any(roles(Role::DAO))] + #[payable] + fn delete_token_full_access_key(&mut self, token: String, public_key: PublicKey) -> Promise { + assert_one_yocto(); + ext_full_access_keys::ext(Self::token_id(token)) + .with_attached_deposit(NearToken::from_yoctonear(1)) + .delete_key(public_key) + } +} + #[derive(BorshSerialize, BorshStorageKey)] #[borsh(crate = "::near_sdk::borsh")] enum Prefix { diff --git a/poa-factory/src/lib.rs b/poa-factory/src/lib.rs index 0fe59eb3..ec3f39be 100644 --- a/poa-factory/src/lib.rs +++ b/poa-factory/src/lib.rs @@ -6,13 +6,28 @@ use std::collections::HashMap; use defuse_admin_utils::full_access_keys::FullAccessKeys; use near_contract_standards::fungible_token::metadata::FungibleTokenMetadata; use near_plugins::AccessControllable; -use near_sdk::{AccountId, Promise, ext_contract, json_types::U128}; +use near_sdk::{AccountId, Promise, PublicKey, ext_contract, json_types::U128}; + +pub trait TokenFullAccessKeys { + /// Adds a full access key to the given token contract. + /// NOTE: MUST attach 1 yⓃ for security purposes. + fn add_token_full_access_key(&mut self, token: String, public_key: PublicKey) -> Promise; + + /// Deletes a full access key from the given token contract. + /// NOTE: MUST attach 1 yⓃ for security purposes. + fn delete_token_full_access_key(&mut self, token: String, public_key: PublicKey) -> Promise; +} #[ext_contract(ext_poa_factory)] -pub trait PoaFactory: AccessControllable + FullAccessKeys { +pub trait PoaFactory: AccessControllable + FullAccessKeys + TokenFullAccessKeys { /// Deploys new token to `token.`. /// Requires to attach enough Ⓝ to cover storage costs. - fn deploy_token(&mut self, token: String, metadata: Option) -> Promise; + fn deploy_token( + &mut self, + token: String, + metadata: Option, + no_registration: Option, + ) -> Promise; /// Sets metadata on `token.`. /// NOTE: MUST attach 1 yⓃ for security purposes. diff --git a/poa-token/Cargo.toml b/poa-token/Cargo.toml index 5f3c22bf..87000d3a 100644 --- a/poa-token/Cargo.toml +++ b/poa-token/Cargo.toml @@ -26,27 +26,21 @@ container_build_command = [ ] -[package.metadata.near.reproducible_build.variant.no_registration] -image = "sourcescan/cargo-near:0.16.2-rust-1.86.0" -image_digest = "sha256:74c24d4d912f893198b8b13e01d43e0f78f3b00b3df45bf555a707eb4918a54e" -container_build_command = [ - "cargo", - "near", - "build", - "non-reproducible-wasm", - "--locked", - "--features=contract,no-registration", - "--no-embed-abi" -] - [dependencies] defuse-admin-utils.workspace = true defuse-near-utils.workspace = true +defuse-borsh-utils.workspace = true near-contract-standards.workspace = true near-plugins.workspace = true near-sdk = { workspace = true, features = ["unstable"] } +impl-tools.workspace = true + +[dev-dependencies] +rstest.workspace = true +defuse-test-utils.workspace = true +arbitrary_with.workspace = true + [features] contract = [] -no-registration = [] diff --git a/poa-token/src/contract.rs b/poa-token/src/contract/mod.rs similarity index 76% rename from poa-token/src/contract.rs rename to poa-token/src/contract/mod.rs index c1f88c37..6e99ffe6 100644 --- a/poa-token/src/contract.rs +++ b/poa-token/src/contract/mod.rs @@ -1,5 +1,7 @@ use defuse_admin_utils::full_access_keys::FullAccessKeys; +use defuse_borsh_utils::adapters::As; use defuse_near_utils::{CURRENT_ACCOUNT_ID, PREDECESSOR_ACCOUNT_ID}; +use impl_tools::autoimpl; use near_contract_standards::{ fungible_token::{ FungibleToken, FungibleTokenCore, FungibleTokenResolver, @@ -14,7 +16,12 @@ use near_sdk::{ assert_one_yocto, borsh::BorshSerialize, env, json_types::U128, near, require, store::Lazy, }; -use crate::{PoaFungibleToken, WITHDRAW_MEMO_PREFIX}; +use crate::{ + NoRegistration, PoaFungibleToken, WITHDRAW_MEMO_PREFIX, + contract::state::{DEFAULT_NO_REGISTRATION, MaybeVersionedContractState, State, StateV0}, +}; + +mod state; #[near( contract_state, @@ -25,16 +32,25 @@ use crate::{PoaFungibleToken, WITHDRAW_MEMO_PREFIX}; ) )] #[derive(Ownable, PanicOnDefault)] +#[autoimpl(Deref using self.state)] +#[autoimpl(DerefMut using self.state)] pub struct Contract { - token: FungibleToken, - metadata: Lazy, + #[borsh( + deserialize_with = "As::::deserialize", + serialize_with = "As::::serialize" + )] + state: State, } #[near] impl Contract { #[init] #[allow(dead_code)] - pub fn new(owner_id: Option, metadata: Option) -> Self { + pub fn new( + owner_id: Option, + metadata: Option, + no_registration: Option, + ) -> Self { let metadata = metadata.unwrap_or_else(|| FungibleTokenMetadata { spec: FT_METADATA_SPEC.to_string(), name: Default::default(), @@ -47,8 +63,11 @@ impl Contract { metadata.assert_valid(); let contract = Self { - token: FungibleToken::new(Prefix::FungibleToken), - metadata: Lazy::new(Prefix::Metadata, metadata), + state: State { + token: FungibleToken::new(Prefix::FungibleToken), + metadata: Lazy::new(Prefix::Metadata, metadata), + no_registration: no_registration.unwrap_or(DEFAULT_NO_REGISTRATION), + }, }; let owner = owner_id.unwrap_or_else(|| PREDECESSOR_ACCOUNT_ID.clone()); @@ -57,6 +76,7 @@ impl Contract { contract.owner_storage_key(), owner.as_bytes() )); + OwnershipTransferred { previous_owner: None, new_owner: Some(owner), @@ -64,6 +84,20 @@ impl Contract { .emit(); contract } + + #[payable] + #[private] + #[init(ignore_state)] + pub fn migrate(no_registration: bool) -> Self { + assert_one_yocto(); + + let old_state: StateV0 = env::state_read().expect("failed to load state"); + let mut state: State = old_state.into(); + + state.no_registration = no_registration; + + Self { state } + } } #[near] @@ -81,6 +115,7 @@ impl PoaFungibleToken for Contract { fn ft_deposit(&mut self, owner_id: AccountId, amount: U128, memo: Option) { self.token.storage_deposit(Some(owner_id.clone()), None); self.token.internal_deposit(&owner_id, amount.into()); + FtMint { owner_id: &owner_id, amount, @@ -145,12 +180,19 @@ impl FungibleTokenResolver for Contract { #[near] impl StorageManagement for Contract { #[payable] - #[cfg_attr(feature = "no-registration", only(self, owner))] fn storage_deposit( &mut self, account_id: Option, registration_only: Option, ) -> StorageBalance { + if self.no_registration { + require!( + *PREDECESSOR_ACCOUNT_ID == self.owner_get().unwrap_or_else(|| unreachable!()) + || *PREDECESSOR_ACCOUNT_ID == *CURRENT_ACCOUNT_ID, + "Method is private" + ); + } + self.token.storage_deposit(account_id, registration_only) } @@ -211,6 +253,20 @@ impl FullAccessKeys for Contract { } } +#[near] +impl NoRegistration for Contract { + fn no_registration(&self) -> bool { + self.no_registration + } + + #[only(self, owner)] + #[payable] + fn set_no_registration(&mut self, no_registration: bool) { + assert_one_yocto(); + self.no_registration = no_registration; + } +} + #[derive(BorshSerialize, BorshStorageKey)] #[borsh(crate = "::near_sdk::borsh")] enum Prefix { diff --git a/poa-token/src/contract/state.rs b/poa-token/src/contract/state.rs new file mode 100644 index 00000000..db943e23 --- /dev/null +++ b/poa-token/src/contract/state.rs @@ -0,0 +1,243 @@ +use defuse_borsh_utils::adapters::{BorshDeserializeAs, BorshSerializeAs}; +use defuse_near_utils::PanicOnClone; +use near_contract_standards::fungible_token::{FungibleToken, metadata::FungibleTokenMetadata}; +use near_sdk::{ + borsh::{BorshDeserialize, BorshSerialize}, + near, + store::Lazy, +}; +use std::{ + borrow::Cow, + io::{self, Read}, +}; + +pub const DEFAULT_NO_REGISTRATION: bool = true; + +#[near(serializers=[borsh])] +enum VersionedState<'a> { + V0(Cow<'a, PanicOnClone>), + Latest(Cow<'a, PanicOnClone>), +} + +impl From> for State { + fn from(versioned: VersionedState<'_>) -> Self { + // Borsh always deserializes into `Cow::Owned`, so it's + // safe to call `Cow::>::into_owned()` here. + match versioned { + VersionedState::V0(Cow::Owned(contract)) => contract.into_inner().into(), + VersionedState::Latest(Cow::Owned(contract)) => contract.into_inner(), + _ => unreachable!("Borsh always deserializes into `Cow::Owned`"), + } + } +} + +// Used for current contract serialization +impl<'a> From<&'a State> for VersionedState<'a> { + fn from(value: &'a State) -> Self { + // always serialize as latest version + Self::Latest(Cow::Borrowed(PanicOnClone::from_ref(value))) + } +} + +// Used for legacy contract deserialization +impl From for VersionedState<'_> { + fn from(value: StateV0) -> Self { + Self::V0(Cow::Owned(value.into())) + } +} + +#[near(serializers=[borsh])] +pub struct State { + pub token: FungibleToken, + pub metadata: Lazy, + pub no_registration: bool, +} + +#[near(serializers=[borsh])] +pub struct StateV0 { + pub token: FungibleToken, + pub metadata: Lazy, +} + +impl From for State { + fn from(StateV0 { token, metadata }: StateV0) -> Self { + Self { + token, + metadata, + no_registration: DEFAULT_NO_REGISTRATION, + } + } +} + +pub struct MaybeVersionedContractState; + +impl MaybeVersionedContractState { + /// This is a magic number that is used to differentiate between + /// borsh-serialized representations of legacy and versioned [`Contract`]s: + /// * versioned [`Contract`]s always start with this prefix + /// * legacy [`Contract`] starts with other bytes + /// + /// This is safe to assume that legacy [`Contract`] doesn't start with + /// this prefix, since the first 4 bytes in legacy [`Contract`] were used + /// to denote the byte array of prefix in [`LookupMap`] for + /// `token.accounts`, which is equal to 'Prefix::FungibleToken' + const VERSIONED_MAGIC_PREFIX: u32 = u32::MAX; +} + +impl BorshDeserializeAs for MaybeVersionedContractState { + fn deserialize_as(reader: &mut R) -> io::Result + where + R: io::Read, + { + // There will always be 4 bytes for u32: + // * either `VERSIONED_MAGIC_PREFIX`, + // * or Vec for `Contract.token.accounts.key_prefix` + let mut buf = [0u8; size_of::()]; + reader.read_exact(&mut buf)?; + let prefix = u32::deserialize_reader(&mut buf.as_slice())?; + + if prefix == Self::VERSIONED_MAGIC_PREFIX { + VersionedState::deserialize_reader(reader) + } else { + // legacy state + StateV0::deserialize_reader( + // prepend already consumed part of the reader + &mut buf.chain(reader), + ) + .map(Into::into) + } + .map(Into::into) + } +} + +impl BorshSerializeAs for MaybeVersionedContractState +where + for<'a> VersionedState<'a>: From<&'a T>, +{ + fn serialize_as(source: &T, writer: &mut W) -> io::Result<()> + where + W: io::Write, + { + ( + // always serialize as versioned and prepend magic prefix + Self::VERSIONED_MAGIC_PREFIX, + VersionedState::from(source), + ) + .serialize(writer) + } +} + +#[cfg(test)] +mod tests { + use crate::contract::{Contract, Prefix}; + + use super::*; + + use arbitrary_with::{Arbitrary, As, arbitrary}; + use defuse_near_utils::arbitrary::ArbitraryAccountId; + use defuse_test_utils::random::make_arbitrary; + use near_contract_standards::fungible_token::{Balance, FungibleTokenCore}; + use near_sdk::{AccountId, StorageUsage, borsh, json_types::U128}; + use rstest::rstest; + + fn deserialize_and_check_legacy_state(serialized_legacy: &[u8], data: &TokenData) { + let mut versioned: Contract = borsh::from_slice(serialized_legacy).unwrap(); + + data.assert_metadata(&versioned.state.metadata); + data.assert_token(&versioned.state.token); + + let new_owner_id: AccountId = "new-owner.testnet".parse().unwrap(); + let amount = U128::from(1_000_000_000_000); + versioned.token.internal_register_account(&new_owner_id); + versioned + .token + .internal_deposit(&new_owner_id, amount.into()); + + let serialized_versioned = borsh::to_vec(&versioned).unwrap(); + drop(versioned); + + let versioned: Contract = borsh::from_slice(&serialized_versioned).unwrap(); + + data.assert_metadata(&versioned.state.metadata); + data.assert_token(&versioned.state.token); + assert!(versioned.ft_balance_of(new_owner_id) == amount); + } + + #[derive(Arbitrary)] + struct AccountData { + #[arbitrary(with = As::::arbitrary)] + pub account_id: AccountId, + pub balance: Balance, + } + + /// Data for legacy token state creation + #[derive(Arbitrary)] + struct TokenData { + pub accounts: Vec, + pub account_storage_usage: StorageUsage, + + pub spec: String, + pub name: String, + pub symbol: String, + pub icon: Option, + pub reference: Option, + pub decimals: u8, + } + + impl TokenData { + pub fn create_legacy_state(&self) -> StateV0 { + let mut token = FungibleToken::new(Prefix::FungibleToken); + token.account_storage_usage = self.account_storage_usage; + + for account_data in &self.accounts { + token.internal_register_account(&account_data.account_id); + token.internal_deposit(&account_data.account_id, account_data.balance); + } + + let metadata = FungibleTokenMetadata { + spec: self.spec.clone(), + name: self.name.clone(), + symbol: self.symbol.clone(), + icon: self.icon.clone(), + reference: self.reference.clone(), + reference_hash: None, + decimals: self.decimals, + }; + + StateV0 { + token, + metadata: Lazy::new(Prefix::Metadata, metadata), + } + } + + pub fn assert_metadata(&self, metadata: &FungibleTokenMetadata) { + assert_eq!(metadata.spec, self.spec); + assert_eq!(metadata.name, self.name); + assert_eq!(metadata.symbol, self.symbol); + assert_eq!(metadata.icon, self.icon); + assert_eq!(metadata.reference, self.reference); + assert_eq!(metadata.decimals, self.decimals); + } + + pub fn assert_token(&self, token: &FungibleToken) { + assert_eq!(token.account_storage_usage, self.account_storage_usage); + + for account_data in &self.accounts { + let balance = token.internal_unwrap_balance_of(&account_data.account_id); + assert_eq!(balance, account_data.balance); + } + } + } + + #[rstest] + fn legacy_token_upgrade(#[from(make_arbitrary)] data: TokenData) { + let legacy_acc = data.create_legacy_state(); + let serialized_legacy = + borsh::to_vec(&legacy_acc).expect("unable to serialize legacy Account"); + + // we need to drop it, so all collections from near-sdk flush to storage + drop(legacy_acc); + + deserialize_and_check_legacy_state(&serialized_legacy, &data); + } +} diff --git a/poa-token/src/lib.rs b/poa-token/src/lib.rs index bbc05215..7eb44cbf 100644 --- a/poa-token/src/lib.rs +++ b/poa-token/src/lib.rs @@ -12,6 +12,15 @@ use near_contract_standards::{ use near_plugins::Ownable; use near_sdk::{AccountId, ext_contract, json_types::U128}; +pub trait NoRegistration { + /// Returns whether the token allows accounts initialize storage deposits by itself. + fn no_registration(&self) -> bool; + + /// Enables/disables registration. + /// NOTE: MUST attach 1 yⓃ for security purposes. + fn set_no_registration(&mut self, no_registration: bool); +} + /// Fungible token that allows minting only by its owner. /// To withdraw, users can call `ft_transfer` on the deployed token, /// pass token itself as `receiver_id` and provide destination address @@ -24,6 +33,7 @@ pub trait PoaFungibleToken: + StorageManagement + Ownable + FullAccessKeys + + NoRegistration { /// Sets metadata. /// NOTE: MUST attach 1 yⓃ for security purposes. diff --git a/tests/src/tests/defuse/env/builder.rs b/tests/src/tests/defuse/env/builder.rs index f067b17b..2e9f6342 100644 --- a/tests/src/tests/defuse/env/builder.rs +++ b/tests/src/tests/defuse/env/builder.rs @@ -30,7 +30,7 @@ pub struct EnvBuilder { self_as_super_admin: bool, deployer_as_super_admin: bool, disable_ft_storage_deposit: bool, - disable_registration: bool, + no_registration: bool, // Create only unique users (no reusing from persistent state) create_unique_users: bool, @@ -77,8 +77,8 @@ impl EnvBuilder { self } - pub const fn no_registration(mut self, no_reg_value: bool) -> Self { - self.disable_registration = no_reg_value; + pub const fn no_registration(mut self, no_registration: bool) -> Self { + self.no_registration = no_registration; self } @@ -134,7 +134,7 @@ impl EnvBuilder { poa_factory: poa_factory.clone(), sandbox, disable_ft_storage_deposit: self.disable_ft_storage_deposit, - disable_registration: self.disable_registration, + disable_registration: self.no_registration, seed: Seed::from_entropy(), next_user_index: AtomicUsize::new(0), }; diff --git a/tests/src/tests/defuse/env/mod.rs b/tests/src/tests/defuse/env/mod.rs index 46259316..f4682294 100644 --- a/tests/src/tests/defuse/env/mod.rs +++ b/tests/src/tests/defuse/env/mod.rs @@ -25,7 +25,6 @@ use futures::future::try_join_all; use multi_token_receiver_stub::MTReceiverMode; use near_sdk::{AccountId, NearToken, env::sha256}; use near_workspaces::{Account, Contract}; - use std::{ ops::Deref, sync::{ @@ -105,7 +104,7 @@ impl Env { pub async fn create_named_token(&self, name: &str) -> AccountId { let root = self.sandbox.root_account(); - root.poa_factory_deploy_token(self.poa_factory.id(), name, None) + root.poa_factory_deploy_token(self.poa_factory.id(), name, None, self.disable_registration) .await .unwrap() } diff --git a/tests/src/tests/defuse/env/storage.rs b/tests/src/tests/defuse/env/storage.rs index 9e0124d7..38950ab8 100644 --- a/tests/src/tests/defuse/env/storage.rs +++ b/tests/src/tests/defuse/env/storage.rs @@ -103,7 +103,7 @@ impl Env { .subaccount_name(&token_id.clone().into_contract_id()); let token = root - .poa_factory_deploy_token(self.poa_factory.id(), &token_name, None) + .poa_factory_deploy_token(self.poa_factory.id(), &token_name, None, true) .await?; self.ft_storage_deposit_for_accounts(&token, vec![root.id(), self.defuse.id()]) diff --git a/tests/src/tests/defuse/intents/ft_withdraw.rs b/tests/src/tests/defuse/intents/ft_withdraw.rs index a013b41d..fded4149 100644 --- a/tests/src/tests/defuse/intents/ft_withdraw.rs +++ b/tests/src/tests/defuse/intents/ft_withdraw.rs @@ -108,10 +108,12 @@ async fn ft_withdraw_intent() { .unwrap() .into_result() .unwrap(); + // wrap NEAR user.near_deposit(env.wnear.id(), STORAGE_DEPOSIT) .await .unwrap(); + // deposit wNEAR user.defuse_ft_deposit( env.defuse.id(), @@ -170,6 +172,7 @@ async fn ft_withdraw_intent() { env.defuse_execute_intents(env.defuse.id(), [valid_payload]) .await .unwrap(); + let new_defuse_balance = env .defuse .as_account() @@ -177,6 +180,7 @@ async fn ft_withdraw_intent() { .await .unwrap() .balance; + assert!( new_defuse_balance >= old_defuse_balance, "contract balance must not decrease" diff --git a/tests/src/tests/defuse/storage/mod.rs b/tests/src/tests/defuse/storage/mod.rs index 03ece60e..48e55847 100644 --- a/tests/src/tests/defuse/storage/mod.rs +++ b/tests/src/tests/defuse/storage/mod.rs @@ -67,7 +67,6 @@ async fn storage_deposit_success( { let storage_balance_ft1_user1 = env.storage_balance_of(&ft, user.id()).await.unwrap(); - let storage_balance_ft1_user2 = env.storage_balance_of(&ft, other_user.id()).await.unwrap(); assert_eq!( @@ -160,7 +159,6 @@ async fn storage_deposit_fails_user_has_no_balance_in_intents() { { let storage_balance_ft1_user1 = env.storage_balance_of(&ft, user.id()).await.unwrap(); - let storage_balance_ft1_user2 = env.storage_balance_of(&ft, other_user.id()).await.unwrap(); assert_eq!( diff --git a/tests/src/tests/poa/factory.rs b/tests/src/tests/poa/factory.rs index d277c14e..672b207f 100644 --- a/tests/src/tests/poa/factory.rs +++ b/tests/src/tests/poa/factory.rs @@ -32,12 +32,14 @@ pub trait PoAFactoryExt { factory: &AccountId, token: &str, metadata: impl Into>, + no_registration: bool, ) -> anyhow::Result; async fn poa_deploy_token( &self, token: &str, metadata: impl Into>, + no_registration: bool, ) -> anyhow::Result; async fn poa_factory_ft_deposit( @@ -102,11 +104,13 @@ impl PoAFactoryExt for near_workspaces::Account { factory: &AccountId, token: &str, metadata: impl Into>, + no_registration: bool, ) -> anyhow::Result { self.call(factory, "deploy_token") .args_json(json!({ "token": token, "metadata": metadata.into(), + "no_registration": no_registration, })) .deposit(NearToken::from_near(4)) .max_gas() @@ -121,8 +125,9 @@ impl PoAFactoryExt for near_workspaces::Account { &self, token: &str, metadata: impl Into>, + no_registration: bool, ) -> anyhow::Result { - self.poa_factory_deploy_token(self.id(), token, metadata) + self.poa_factory_deploy_token(self.id(), token, metadata, no_registration) .await } @@ -192,9 +197,10 @@ impl PoAFactoryExt for near_workspaces::Contract { factory: &AccountId, token: &str, metadata: impl Into>, + no_registration: bool, ) -> anyhow::Result { self.as_account() - .poa_factory_deploy_token(factory, token, metadata) + .poa_factory_deploy_token(factory, token, metadata, no_registration) .await } @@ -202,8 +208,11 @@ impl PoAFactoryExt for near_workspaces::Contract { &self, token: &str, metadata: impl Into>, + no_registration: bool, ) -> anyhow::Result { - self.as_account().poa_deploy_token(token, metadata).await + self.as_account() + .poa_deploy_token(token, metadata, no_registration) + .await } async fn poa_factory_ft_deposit( @@ -275,20 +284,20 @@ mod tests { .await .unwrap(); - user.poa_factory_deploy_token(poa_factory.id(), "ft1", None) + user.poa_factory_deploy_token(poa_factory.id(), "ft1", None, false) .await .unwrap_err(); - root.poa_factory_deploy_token(poa_factory.id(), "ft1.abc", None) + root.poa_factory_deploy_token(poa_factory.id(), "ft1.abc", None, false) .await .unwrap_err(); let ft1 = root - .poa_factory_deploy_token(poa_factory.id(), "ft1", None) + .poa_factory_deploy_token(poa_factory.id(), "ft1", None, false) .await .unwrap(); - root.poa_factory_deploy_token(poa_factory.id(), "ft1", None) + root.poa_factory_deploy_token(poa_factory.id(), "ft1", None, false) .await .unwrap_err(); @@ -315,4 +324,75 @@ mod tests { 1000 ); } + + #[tokio::test] + #[rstest] + async fn deploy_mint_no_registration() { + let sandbox = Sandbox::new().await.unwrap(); + let root = sandbox.root_account(); + let user = sandbox + .create_account("user1") + .await + .expect("Failed to create user"); + + let poa_factory = root + .deploy_poa_factory( + "poa-factory", + [root.id().clone()], + [ + (Role::TokenDeployer, [root.id().clone()]), + (Role::TokenDepositer, [root.id().clone()]), + ], + [ + (Role::TokenDeployer, [root.id().clone()]), + (Role::TokenDepositer, [root.id().clone()]), + ], + ) + .await + .unwrap(); + + user.poa_factory_deploy_token(poa_factory.id(), "ft1", None, true) + .await + .unwrap_err(); + + root.poa_factory_deploy_token(poa_factory.id(), "ft1.abc", None, true) + .await + .unwrap_err(); + + let ft1 = root + .poa_factory_deploy_token(poa_factory.id(), "ft1", None, true) + .await + .unwrap(); + + root.poa_factory_deploy_token(poa_factory.id(), "ft1", None, true) + .await + .unwrap_err(); + + assert_eq!( + sandbox.ft_token_balance_of(&ft1, user.id()).await.unwrap(), + 0 + ); + + assert!( + root.ft_storage_deposit(&ft1, None) + .await + .err() + .unwrap() + .to_string() + .contains("Method is private") + ); + + user.poa_factory_ft_deposit(poa_factory.id(), "ft1", user.id(), 1000, None, None) + .await + .unwrap_err(); + + root.poa_factory_ft_deposit(poa_factory.id(), "ft1", user.id(), 1000, None, None) + .await + .unwrap(); + + assert_eq!( + sandbox.ft_token_balance_of(&ft1, user.id()).await.unwrap(), + 1000 + ); + } }