diff --git a/.gitignore b/.gitignore index af0ad46..fabb587 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /target Cargo.lock near-plugins/tests/contracts/*/target +examples/target # Ignore IDE data .vscode/ diff --git a/README.md b/README.md index 1d8a0dc..cf6ff60 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Documentation of all methods provided by `Pausable` is available in the [definit ### [Upgradable](/near-plugins/src/upgradable.rs) -Allows a contract to be upgraded by owner without having a Full Access Key. +Allows a contract to be upgraded by owner with delay and without having a Full Access Key. Contract example using _Upgradable_ plugin. Note that it requires the contract to be Ownable. @@ -77,6 +77,7 @@ impl Counter { fn new() -> Self { let mut contract = Self {}; contract.owner_set(Some(near_sdk::env::predecessor_account_id())); + contract.up_init_staging_duration(std::time::Duration::from_secs(60).as_nanos().try_into().unwrap()); // 1 minute contract } } @@ -85,6 +86,9 @@ impl Counter { To upgrade the contract first call `up_stage_code` passing the binary as first argument serialized as borsh. Then call `up_deploy_code`. This functions must be called from the owner. +To update the staging delay first call `up_stage_update_staging_duration` passing the new delay duration. Then call `up_apply_update_staging_duration`. +This functions must be called from the owner. + Documentation of all methods provided by the derived implementation of `Upgradable` is available in the [definition of the trait](/near-plugins/src/upgradable.rs). More examples and guidelines for interacting with an `Upgradable` contract can be found [here](/examples/upgradable-examples/README.md). ### [AccessControllable](/near-plugins/src/access_controllable.rs) diff --git a/examples/upgradable-examples/README.md b/examples/upgradable-examples/README.md index 381e81f..2bd9fd8 100644 --- a/examples/upgradable-examples/README.md +++ b/examples/upgradable-examples/README.md @@ -66,26 +66,26 @@ To upgrade the contract first call up_stage_code passing the binary as first arg ## The contract methods description ### up_storage_key -`up_storage_key` is a _view_ method that returns a key of the storage slot for stage code. -By default, `b"__CODE__"` is used. For changing, the attribute `upgradable` can be used. +`up_storage_prefix` is a _view_ method that returns the storage prefix for slots related to upgradable. +By default, `b"__up__"` is used. For changing, the attribute `upgradable` can be used. ```shell -$ near view up_storage_key -View call: .up_storage_key() +$ near view up_storage_prefix +View call: .up_storage_prefix() [ - 95, 95, 80, 65, 85, - 83, 69, 68, 95, 95 + 95, 95, 117, + 112, 95, 95 ] $ python3 ->>> print(' '.join(str(b) for b in bytes("__CODE__", 'utf8'))) -95 95 67 79 68 69 95 95 +>>> print(' '.join(str(b) for b in bytes("__up__", 'utf8'))) +95 95 117 112 95 95 ``` -Example of changing paused storage key: +Example of changing the storage prefix: ```rust #[near_bindgen] #[derive(Ownable, Upgradable, Default, BorshSerialize, BorshDeserialize)] -#[upgradable(code_storage_key="OTHER_CODE_STORAGE_KEY")] +#[upgradable(storage_prefix="OTHER_CODE_STORAGE_PREFIX")] struct Counter { counter: u64, } @@ -105,7 +105,7 @@ But it doesn't work in that way because we can't provide in Bash so long args... For running `up_satge_code` take a look on `up_stage_code/src/main.rs` script. ```shell $ cd up_stage_code -$ cargo run -- "" +$ cargo run -- -p '' $ cd .. ``` Where `` is `$HOME/.near-credentials/testnet/.json` diff --git a/examples/upgradable-examples/up_stage_code/src/main.rs b/examples/upgradable-examples/up_stage_code/src/main.rs index d96455f..9686c14 100644 --- a/examples/upgradable-examples/up_stage_code/src/main.rs +++ b/examples/upgradable-examples/up_stage_code/src/main.rs @@ -33,9 +33,12 @@ async fn main() { let contract: Account = match &*args.network { "testnet" => get_contract!(testnet, args.path_to_key), - "mainnet" => get_contract!(mainnet, args.path_to_key), + "mainnet" => get_contract!(mainnet, args.path_to_key), "betanet" => get_contract!(betanet, args.path_to_key), - network => panic!("Unknown network {}. Possible networks: testnet, mainnet, betanet", network) + network => panic!( + "Unknown network {}. Possible networks: testnet, mainnet, betanet", + network + ), }; let wasm = std::fs::read(&args.wasm).unwrap(); diff --git a/examples/upgradable-examples/upgradable_base/src/lib.rs b/examples/upgradable-examples/upgradable_base/src/lib.rs index 0b8a8d7..8d74504 100644 --- a/examples/upgradable-examples/upgradable_base/src/lib.rs +++ b/examples/upgradable-examples/upgradable_base/src/lib.rs @@ -15,6 +15,7 @@ impl Counter { pub fn new() -> Self { let mut contract = Self { counter: 0 }; contract.owner_set(Some(near_sdk::env::predecessor_account_id())); + contract.up_init_staging_duration(std::time::Duration::from_secs(60).as_nanos().try_into().unwrap()); // 1 minute contract } diff --git a/near-plugins-derive/src/upgradable.rs b/near-plugins-derive/src/upgradable.rs index b80838e..aa72f76 100644 --- a/near-plugins-derive/src/upgradable.rs +++ b/near-plugins-derive/src/upgradable.rs @@ -7,9 +7,11 @@ use syn::{parse_macro_input, DeriveInput}; #[derive(FromDeriveInput, Default)] #[darling(default, attributes(upgradable), forward_attrs(allow, doc, cfg))] struct Opts { - code_storage_key: Option, + storage_prefix: Option, } +const DEFAULT_STORAGE_PREFIX: &str = "__up__"; + /// Generates the token stream for the `Upgradable` macro. pub fn derive_upgradable(input: TokenStream) -> TokenStream { let cratename = cratename(); @@ -18,29 +20,94 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream { let opts = Opts::from_derive_input(&input).expect("Wrong options"); let DeriveInput { ident, .. } = input; - let code_storage_key = opts - .code_storage_key - .unwrap_or_else(|| "__CODE__".to_string()); + let storage_prefix = opts + .storage_prefix + .unwrap_or_else(|| DEFAULT_STORAGE_PREFIX.to_string()); let output = quote! { + /// Used to make storage prefixes unique. Not to be used directly, + /// instead it should be prepended to the storage prefix specified by + /// the user. + #[derive(::near_sdk::borsh::BorshSerialize)] + enum __UpgradableStorageKey { + Code, + StagingTimestamp, + StagingDuration, + NewStagingDuration, + NewStagingDurationTimestamp, + } + + impl #ident { + fn up_get_timestamp(&self, key: __UpgradableStorageKey) -> Option<::near_sdk::Timestamp> { + near_sdk::env::storage_read(self.up_storage_key(key).as_ref()).map(|timestamp_bytes| { + ::near_sdk::Timestamp::try_from_slice(×tamp_bytes).unwrap_or_else(|_| + near_sdk::env::panic_str("Upgradable: Invalid u64 timestamp format") + ) + }) + } + + fn up_get_duration(&self, key: __UpgradableStorageKey) -> Option<::near_sdk::Duration> { + near_sdk::env::storage_read(self.up_storage_key(key).as_ref()).map(|duration_bytes| { + ::near_sdk::Duration::try_from_slice(&duration_bytes).unwrap_or_else(|_| + near_sdk::env::panic_str("Upgradable: Invalid u64 Duration format") + ) + }) + } + + fn up_set_timestamp(&self, key: __UpgradableStorageKey, value: ::near_sdk::Timestamp) { + self.up_storage_write(key, &value.try_to_vec().unwrap()); + } + + fn up_set_duration(&self, key: __UpgradableStorageKey, value: ::near_sdk::Duration) { + self.up_storage_write(key, &value.try_to_vec().unwrap()); + } + + fn up_storage_key(&self, key: __UpgradableStorageKey) -> Vec { + let key_vec = key + .try_to_vec() + .unwrap_or_else(|_| ::near_sdk::env::panic_str("Storage key should be serializable")); + [(#storage_prefix).as_bytes(), key_vec.as_slice()].concat() + } + + fn up_storage_write(&self, key: __UpgradableStorageKey, value: &[u8]) { + near_sdk::env::storage_write(self.up_storage_key(key).as_ref(), &value); + } + + fn up_set_staging_duration_unchecked(&self, staging_duration: near_sdk::Duration) { + self.up_storage_write(__UpgradableStorageKey::StagingDuration, &staging_duration.try_to_vec().unwrap()); + } + } + #[near_bindgen] impl Upgradable for #ident { - fn up_storage_key(&self) -> Vec{ - (#code_storage_key).as_bytes().to_vec() + fn up_storage_prefix(&self) -> &'static [u8] { + (#storage_prefix).as_bytes() + } + + fn up_get_delay_status(&self) -> #cratename::UpgradableDurationStatus { + near_plugins::UpgradableDurationStatus { + staging_duration: self.up_get_duration(__UpgradableStorageKey::StagingDuration), + staging_timestamp: self.up_get_timestamp(__UpgradableStorageKey::StagingTimestamp), + new_staging_duration: self.up_get_duration(__UpgradableStorageKey::NewStagingDuration), + new_staging_duration_timestamp: self.up_get_timestamp(__UpgradableStorageKey::NewStagingDurationTimestamp), + } } #[#cratename::only(owner)] fn up_stage_code(&mut self, #[serializer(borsh)] code: Vec) { if code.is_empty() { - near_sdk::env::storage_remove(self.up_storage_key().as_ref()); + near_sdk::env::storage_remove(self.up_storage_key(__UpgradableStorageKey::Code).as_ref()); + near_sdk::env::storage_remove(self.up_storage_key(__UpgradableStorageKey::StagingTimestamp).as_ref()); } else { - near_sdk::env::storage_write(self.up_storage_key().as_ref(), code.as_ref()); + let timestamp = near_sdk::env::block_timestamp() + self.up_get_duration(__UpgradableStorageKey::StagingDuration).unwrap_or(0); + self.up_storage_write(__UpgradableStorageKey::Code, &code); + self.up_set_timestamp(__UpgradableStorageKey::StagingTimestamp, timestamp); } } #[result_serializer(borsh)] fn up_staged_code(&self) -> Option> { - near_sdk::env::storage_read(self.up_storage_key().as_ref()) + near_sdk::env::storage_read(self.up_storage_key(__UpgradableStorageKey::Code).as_ref()) } fn up_staged_code_hash(&self) -> Option<::near_sdk::CryptoHash> { @@ -50,9 +117,59 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream { #[#cratename::only(owner)] fn up_deploy_code(&mut self) -> near_sdk::Promise { + let staging_timestamp = self.up_get_timestamp(__UpgradableStorageKey::StagingTimestamp) + .unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: staging timestamp isn't set")); + + if near_sdk::env::block_timestamp() < staging_timestamp { + near_sdk::env::panic_str( + format!( + "Upgradable: Deploy code too early: staging ends on {}", + staging_timestamp + ) + .as_str(), + ); + } + near_sdk::Promise::new(near_sdk::env::current_account_id()) .deploy_contract(self.up_staged_code().unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: No staged code"))) } + + #[#cratename::only(owner)] + fn up_init_staging_duration(&mut self, staging_duration: near_sdk::Duration) { + near_sdk::require!(self.up_get_duration(__UpgradableStorageKey::StagingDuration).is_none(), "Upgradable: staging duration was already initialized"); + self.up_set_staging_duration_unchecked(staging_duration); + } + + #[#cratename::only(owner)] + fn up_stage_update_staging_duration(&mut self, staging_duration: near_sdk::Duration) { + let current_staging_duration = self.up_get_duration(__UpgradableStorageKey::StagingDuration) + .unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: staging duration isn't initialized")); + + self.up_set_duration(__UpgradableStorageKey::NewStagingDuration, staging_duration); + let staging_duration_timestamp = near_sdk::env::block_timestamp() + current_staging_duration; + self.up_set_timestamp(__UpgradableStorageKey::NewStagingDurationTimestamp, staging_duration_timestamp); + } + + #[#cratename::only(owner)] + fn up_apply_update_staging_duration(&mut self) { + let staging_timestamp = self.up_get_timestamp(__UpgradableStorageKey::NewStagingDurationTimestamp) + .unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: No staged update")); + + if near_sdk::env::block_timestamp() < staging_timestamp { + near_sdk::env::panic_str( + format!( + "Upgradable: Update duration too early: staging ends on {}", + staging_timestamp + ) + .as_str(), + ); + } + + let new_duration = self.up_get_duration(__UpgradableStorageKey::NewStagingDuration) + .unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: No staged duration update")); + + self.up_set_duration(__UpgradableStorageKey::StagingDuration, new_duration); + } } }; diff --git a/near-plugins/src/lib.rs b/near-plugins/src/lib.rs index cf12c5c..516250d 100644 --- a/near-plugins/src/lib.rs +++ b/near-plugins/src/lib.rs @@ -18,6 +18,7 @@ pub use near_plugins_derive::{ pub use ownable::Ownable; pub use pausable::Pausable; pub use upgradable::Upgradable; +pub use upgradable::UpgradableDurationStatus; // Re-exporting these dependencies avoids requiring contracts to depend on them. // For example, without re-exporting `bitflags` a contract using the access diff --git a/near-plugins/src/upgradable.rs b/near-plugins/src/upgradable.rs index 618f60d..1d0fafe 100644 --- a/near-plugins/src/upgradable.rs +++ b/near-plugins/src/upgradable.rs @@ -49,9 +49,11 @@ use serde::Serialize; /// Trait describing the functionality of the _Upgradable_ plugin. pub trait Upgradable { - /// Key of storage slot to save the staged code. - /// By default b"__CODE__" is used. - fn up_storage_key(&self) -> Vec; + /// Returns the storage prefix for slots related to upgradable. + fn up_storage_prefix(&self) -> &'static [u8]; + + /// Returns all staging durations and timestamps. + fn up_get_delay_status(&self) -> UpgradableDurationStatus; /// Allows authorized account to stage some code to be potentially deployed later. /// If a previous code was staged but not deployed, it is discarded. @@ -65,6 +67,23 @@ pub trait Upgradable { /// Allows authorized account to deploy staged code. If no code is staged the method fails. fn up_deploy_code(&mut self) -> Promise; + + /// Initialize the duration of the delay for deploying the staged code. + fn up_init_staging_duration(&mut self, staging_duration: near_sdk::Duration); + + /// Allows authorized account to stage update of the staging duration. + fn up_stage_update_staging_duration(&mut self, staging_duration: near_sdk::Duration); + + /// Allows authorized account to apply the staging duration update. + fn up_apply_update_staging_duration(&mut self); +} + +#[derive(Serialize)] +pub struct UpgradableDurationStatus { + pub staging_duration: Option, + pub staging_timestamp: Option, + pub new_staging_duration: Option, + pub new_staging_duration_timestamp: Option, } /// Event emitted when the code is staged @@ -114,6 +133,7 @@ mod tests { use crate as near_plugins; use crate::test_utils::get_context; use crate::{Ownable, Upgradable}; + use borsh::{BorshDeserialize, BorshSerialize}; use near_sdk::env::sha256; use near_sdk::{near_bindgen, testing_env, VMContext}; use std::convert::TryInto; @@ -168,4 +188,162 @@ mod tests { counter.up_deploy_code(); } + + #[test] + fn test_stage_code_with_delay() { + let (mut counter, mut ctx) = setup_basic(); + + ctx.predecessor_account_id = "eli.test".to_string().try_into().unwrap(); + testing_env!(ctx.clone()); + + assert_eq!(counter.up_staged_code(), None); + + let staging_duration: u64 = std::time::Duration::from_secs(60) + .as_nanos() + .try_into() + .unwrap(); + counter.up_init_staging_duration(staging_duration); + + let staging_timestamp = ctx.block_timestamp + staging_duration; + counter.up_stage_code(vec![1]); + assert_eq!( + counter.up_get_delay_status().staging_timestamp.unwrap(), + staging_timestamp + ); + + assert_eq!(counter.up_staged_code(), Some(vec![1])); + + ctx.block_timestamp = staging_duration; + testing_env!(ctx); + + assert_eq!( + counter.up_staged_code_hash(), + Some(sha256(vec![1].as_slice()).try_into().unwrap()) + ); + + counter.up_deploy_code(); + } + + #[test] + #[should_panic(expected = "Upgradable: Deploy code too early: staging ends on")] + fn test_panic_stage_code_with_delay() { + let (mut counter, mut ctx) = setup_basic(); + + ctx.predecessor_account_id = "eli.test".to_string().try_into().unwrap(); + testing_env!(ctx.clone()); + + assert_eq!(counter.up_staged_code(), None); + + let staging_duration: u64 = std::time::Duration::from_secs(60) + .as_nanos() + .try_into() + .unwrap(); + counter.up_init_staging_duration(staging_duration); + + let staging_timestamp = ctx.block_timestamp + staging_duration; + counter.up_stage_code(vec![1]); + assert_eq!( + counter.up_get_delay_status().staging_timestamp.unwrap(), + staging_timestamp + ); + + assert_eq!(counter.up_staged_code(), Some(vec![1])); + + assert_eq!( + counter.up_staged_code_hash(), + Some(sha256(vec![1].as_slice()).try_into().unwrap()) + ); + + ctx.block_timestamp = staging_timestamp - 1; + testing_env!(ctx); + + counter.up_deploy_code(); + } + + #[test] + fn test_update_delay_duration() { + let (mut counter, mut ctx) = setup_basic(); + + ctx.predecessor_account_id = "eli.test".to_string().try_into().unwrap(); + testing_env!(ctx.clone()); + + assert_eq!(counter.up_staged_code(), None); + + let staging_duration: u64 = std::time::Duration::from_secs(60) + .as_nanos() + .try_into() + .unwrap(); + let staging_timestamp = ctx.block_timestamp + staging_duration; + + counter.up_init_staging_duration(staging_duration); + assert_eq!( + counter.up_get_delay_status().staging_duration.unwrap(), + staging_duration + ); + + let new_staging_duration = staging_duration + 100; + counter.up_stage_update_staging_duration(new_staging_duration); + assert_eq!( + counter.up_get_delay_status().staging_duration.unwrap(), + staging_duration + ); + assert_eq!( + counter + .up_get_delay_status() + .new_staging_duration_timestamp + .unwrap(), + staging_timestamp + ); + + ctx.block_timestamp = staging_timestamp; + testing_env!(ctx); + + counter.up_apply_update_staging_duration(); + assert_eq!( + counter.up_get_delay_status().staging_duration.unwrap(), + new_staging_duration + ); + } + + #[test] + #[should_panic(expected = "Upgradable: Update duration too early: staging ends on ")] + fn test_panic_update_delay_duration() { + let (mut counter, mut ctx) = setup_basic(); + + ctx.predecessor_account_id = "eli.test".to_string().try_into().unwrap(); + testing_env!(ctx.clone()); + + assert_eq!(counter.up_staged_code(), None); + + let staging_duration: u64 = std::time::Duration::from_secs(60) + .as_nanos() + .try_into() + .unwrap(); + let staging_timestamp = ctx.block_timestamp + staging_duration; + + counter.up_init_staging_duration(staging_duration); + assert_eq!( + counter.up_get_delay_status().staging_duration.unwrap(), + staging_duration + ); + + let new_staging_duration = staging_duration + 100; + counter.up_stage_update_staging_duration(new_staging_duration); + assert_eq!( + counter.up_get_delay_status().staging_duration.unwrap(), + staging_duration + ); + assert_eq!( + counter + .up_get_delay_status() + .new_staging_duration_timestamp + .unwrap(), + staging_timestamp + ); + + ctx.block_timestamp = staging_timestamp - 1; + testing_env!(ctx); + + counter.up_apply_update_staging_duration(); + } }