diff --git a/contract/src/pause.rs b/contract/src/pause.rs index bb0e336..f5b0180 100644 --- a/contract/src/pause.rs +++ b/contract/src/pause.rs @@ -24,8 +24,7 @@ use crate::{api::PauseApi, event::Event, Contract, ContractExt, Role}; #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[serde(rename_all = "snake_case")] pub enum Feature { - /// Token movement: `ft_transfer`, `ft_transfer_call`, `burn`, - /// `storage_unregister`. + /// Token movement: `ft_transfer`, `ft_transfer_call`, `burn`. Token, /// Reward minting: `defer_batch`. Minting, diff --git a/contract/src/storage.rs b/contract/src/storage.rs index 4620474..bb13dff 100644 --- a/contract/src/storage.rs +++ b/contract/src/storage.rs @@ -1,10 +1,7 @@ -use near_contract_standards::{ - fungible_token::events::FtBurn, - storage_management::{StorageBalance, StorageBalanceBounds, StorageManagement}, -}; +use near_contract_standards::storage_management::{StorageBalance, StorageBalanceBounds, StorageManagement}; use near_sdk::{env, near, AccountId, NearToken}; -use crate::{Contract, ContractExt, Feature}; +use crate::{Contract, ContractExt}; #[near] impl StorageManagement for Contract { @@ -19,25 +16,12 @@ impl StorageManagement for Contract { } #[payable] + #[allow(unused_variables)] fn storage_unregister(&mut self, force: Option) -> bool { - self.assert_feature_enabled(Feature::Token); - self.assert_not_in_denylist(vec![&env::predecessor_account_id()]); - - self.token - .internal_storage_unregister(force) - .inspect(|(account_id, balance)| { - if *balance == 0 { - return; - } - - FtBurn { - owner_id: account_id, - amount: (*balance).into(), - memo: None, - } - .emit(); - }) - .is_some() + // Unregistering is intentionally disabled: the Sweat Foundation + // subsidizes the storage of accounts created with Sweat Wallet, so we + // don't allow users to unregister and reclaim their staked funds. + env::panic_str("storage_unregister is disabled"); } fn storage_balance_bounds(&self) -> StorageBalanceBounds { diff --git a/integration-tests/tests/pause.rs b/integration-tests/tests/pause.rs index ebd8d68..e699c0b 100644 --- a/integration-tests/tests/pause.rs +++ b/integration-tests/tests/pause.rs @@ -311,18 +311,6 @@ async fn test_pause_token() -> anyhow::Result<()> { assert!(result.has_panic("Feature 'token' is paused")); info!("burn: {result:?}"); - info!("call storage_unregister(force=true) [signer=alice] — pause check fires before denylist check"); - let result = context - .alice - .call(context.sweat.id(), "storage_unregister") - .args_json(json!({ "force": true })) - .deposit(NearToken::from_yoctonear(1)) - .transact() - .await? - .into_result(); - assert!(result.has_panic("Feature 'token' is paused")); - info!("storage_unregister: {result:?}"); - info!("call defer_batch([(alice, 10_000)]) [signer=oracle] — minting feature not paused, should succeed"); context .oracle() diff --git a/integration-tests/tests/storage.rs b/integration-tests/tests/storage.rs new file mode 100644 index 0000000..d1e3c1b --- /dev/null +++ b/integration-tests/tests/storage.rs @@ -0,0 +1,58 @@ +use near_workspaces::types::NearToken; +use serde_json::{json, Value}; +use tracing::info; + +mod common; +use common::{panic::PanicFinder, prepare::Context}; + +/// Unregistering is intentionally disabled: the Sweat Foundation subsidizes the +/// storage of accounts created with Sweat Wallet, so `storage_unregister` must +/// never let a user reclaim their staked funds. It panics for every `force` +/// value and leaves the account registered. +#[tokio::test] +#[tracing::instrument] +async fn test_storage_unregister_is_disabled() -> anyhow::Result<()> { + let context = Context::builder().build().await?; + + info!("view storage_balance_of(alice) — registered during setup"); + let balance_before: Option = context + .sweat + .view("storage_balance_of") + .args_json(json!({ "account_id": context.alice.id() })) + .await? + .json()?; + assert!(balance_before.is_some(), "alice must be registered before the call"); + info!("storage_balance_of: {balance_before:?}"); + + for force in [None, Some(false), Some(true)] { + info!("call storage_unregister(force={force:?}) [signer=alice]"); + let result = context + .alice + .call(context.sweat.id(), "storage_unregister") + .args_json(json!({ "force": force })) + .deposit(NearToken::from_yoctonear(1)) + .transact() + .await? + .into_result(); + assert!( + result.has_panic("storage_unregister is disabled"), + "storage_unregister must panic (force={force:?})" + ); + info!("storage_unregister: {result:?}"); + + info!("view storage_balance_of(alice) — must stay registered"); + let balance_after: Option = context + .sweat + .view("storage_balance_of") + .args_json(json!({ "account_id": context.alice.id() })) + .await? + .json()?; + assert_eq!( + balance_after, balance_before, + "alice's storage balance must be untouched (force={force:?})" + ); + info!("storage_balance_of: {balance_after:?}"); + } + + Ok(()) +} diff --git a/res/contract.wasm b/res/contract.wasm index ab98c26..fbc992e 100644 Binary files a/res/contract.wasm and b/res/contract.wasm differ diff --git a/res/contract_abi.json b/res/contract_abi.json index bfa6b78..742c42b 100644 --- a/res/contract_abi.json +++ b/res/contract_abi.json @@ -7,7 +7,7 @@ "compiler": "rustc 1.86.0", "builder": "cargo-near cargo-near-build 0.11.0" }, - "wasm_hash": "GmVHEmAEZx6FjHiKJJbDVPRF3cNdCfYAYDoJJzBGk2Vt" + "wasm_hash": "5eVVDjnVTvVQP7oKbkDDyAA2LQvFodCy45xZqKhtsDCK" }, "body": { "functions": [ @@ -1437,7 +1437,7 @@ "description": "A contract feature that can be independently paused.\n\nSerialized in snake_case so call args read e.g. `{\"features\": [\"token\", \"minting\"]}`.", "oneOf": [ { - "description": "Token movement: `ft_transfer`, `ft_transfer_call`, `burn`, `storage_unregister`.", + "description": "Token movement: `ft_transfer`, `ft_transfer_call`, `burn`.", "type": "string", "enum": [ "token" diff --git a/res/contract_abi.zst b/res/contract_abi.zst index d8e5a07..82a4afc 100644 Binary files a/res/contract_abi.zst and b/res/contract_abi.zst differ