Skip to content

Commit 9e6f057

Browse files
mooorikarim-en
andauthored
feat(upgradable)!: add hash parameter to up_deploy_code (#117)
* Factor out up_hash_code() * Add hash parameter to up_deploy_code * Update existing tests * Test up_deploy_code with hash parameter * Fix crate name * Fix versions of dependencies to downgrade in CI * Another dependency downgrade fix * Oblige to pass hash * Fix tesrt & clippy * Fix clippy --------- Co-authored-by: karim-en <karim@aurora.dev>
1 parent 4f8466b commit 9e6f057

File tree

4 files changed

+161
-27
lines changed

4 files changed

+161
-27
lines changed

near-plugins-derive/src/upgradable.rs

+20-2
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,13 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {
140140
fn up_set_staging_duration_unchecked(&self, staging_duration: near_sdk::Duration) {
141141
self.up_storage_write(__UpgradableStorageKey::StagingDuration, &::near_sdk::borsh::to_vec(&staging_duration).unwrap());
142142
}
143+
144+
/// Computes the `sha256` hash of `code` and panics if the conversion to `CryptoHash` fails.
145+
fn up_hash_code(code: &[u8]) -> ::near_sdk::CryptoHash {
146+
let hash = near_sdk::env::sha256(code);
147+
std::convert::TryInto::try_into(hash)
148+
.expect("sha256 should convert to CryptoHash")
149+
}
143150
}
144151

145152
#[near]
@@ -176,11 +183,11 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {
176183

177184
fn up_staged_code_hash(&self) -> Option<::near_sdk::CryptoHash> {
178185
self.up_staged_code()
179-
.map(|code| std::convert::TryInto::try_into(::near_sdk::env::sha256(code.as_ref())).unwrap())
186+
.map(|code| Self::up_hash_code(code.as_ref()))
180187
}
181188

182189
#[#cratename::access_control_any(roles(#(#acl_roles_code_deployers),*))]
183-
fn up_deploy_code(&mut self, function_call_args: Option<#cratename::upgradable::FunctionCallArgs>) -> near_sdk::Promise {
190+
fn up_deploy_code(&mut self, hash: String, function_call_args: Option<#cratename::upgradable::FunctionCallArgs>) -> near_sdk::Promise {
184191
let staging_timestamp = self.up_get_timestamp(__UpgradableStorageKey::StagingTimestamp)
185192
.unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: staging timestamp isn't set"));
186193

@@ -195,6 +202,17 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {
195202
}
196203

197204
let code = self.up_staged_code().unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: No staged code"));
205+
let expected_hash = ::near_sdk::base64::encode(Self::up_hash_code(code.as_ref()));
206+
if hash != expected_hash {
207+
near_sdk::env::panic_str(
208+
format!(
209+
"Upgradable: Cannot deploy due to wrong hash: expected hash: {}",
210+
expected_hash,
211+
)
212+
.as_str(),
213+
)
214+
}
215+
198216
let promise = ::near_sdk::Promise::new(::near_sdk::env::current_account_id())
199217
.deploy_contract(code);
200218
match function_call_args {

near-plugins-derive/tests/common/upgradable_contract.rs

+2
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,13 @@ impl UpgradableContract {
7171
pub async fn up_deploy_code(
7272
&self,
7373
caller: &Account,
74+
hash: String,
7475
function_call_args: Option<FunctionCallArgs>,
7576
) -> near_workspaces::Result<ExecutionFinalResult> {
7677
caller
7778
.call(self.contract.id(), "up_deploy_code")
7879
.args_json(json!({
80+
"hash": hash,
7981
"function_call_args": function_call_args,
8082
}))
8183
.max_gas()

near-plugins-derive/tests/upgradable.rs

+123-24
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,13 @@ impl Setup {
109109
}
110110

111111
/// Asserts staged code equals `expected_code`.
112-
async fn assert_staged_code(&self, expected_code: Option<Vec<u8>>) {
112+
async fn assert_staged_code(&self, expected_code: Option<&Vec<u8>>) {
113113
let staged = self
114114
.upgradable_contract
115115
.up_staged_code(&self.unauth_account)
116116
.await
117117
.expect("Call to up_staged_code should succeed");
118-
assert_eq!(staged, expected_code);
118+
assert_eq!(staged.as_ref(), expected_code);
119119
}
120120

121121
/// Asserts the staging duration of the `Upgradable` contract equals the `expected_duration`.
@@ -210,6 +210,14 @@ fn convert_code_to_crypto_hash(code: &[u8]) -> CryptoHash {
210210
.expect("Code should be converted to CryptoHash")
211211
}
212212

213+
/// Computes the hash `code` according the to requirements of the `hash` parameter of
214+
/// `Upgradable::up_deploy_code`.
215+
fn convert_code_to_deploy_hash(code: &[u8]) -> String {
216+
use near_sdk::base64::Engine;
217+
let hash = near_sdk::env::sha256(code);
218+
near_sdk::base64::prelude::BASE64_STANDARD.encode(hash)
219+
}
220+
213221
/// Smoke test of contract setup.
214222
#[tokio::test]
215223
async fn test_setup() -> anyhow::Result<()> {
@@ -332,7 +340,7 @@ async fn test_staging_empty_code_clears_storage() -> anyhow::Result<()> {
332340
.up_stage_code(&dao, code.clone())
333341
.await?;
334342
assert_success_with_unit_return(res);
335-
setup.assert_staged_code(Some(code)).await;
343+
setup.assert_staged_code(Some(&code)).await;
336344

337345
// Verify staging empty code removes it.
338346
let res = setup
@@ -436,11 +444,72 @@ async fn test_deploy_code_without_delay() -> anyhow::Result<()> {
436444
.up_stage_code(&dao, code.clone())
437445
.await?;
438446
assert_success_with_unit_return(res);
439-
setup.assert_staged_code(Some(code)).await;
447+
setup.assert_staged_code(Some(&code)).await;
448+
449+
// Deploy staged code.
450+
let res = setup
451+
.upgradable_contract
452+
.up_deploy_code(&dao, convert_code_to_deploy_hash(&code), None)
453+
.await?;
454+
assert_success_with_unit_return(res);
455+
456+
Ok(())
457+
}
458+
459+
#[tokio::test]
460+
async fn test_deploy_code_with_hash_success() -> anyhow::Result<()> {
461+
let worker = near_workspaces::sandbox().await?;
462+
let dao = worker.dev_create_account().await?;
463+
let setup = Setup::new(worker.clone(), Some(dao.id().clone()), None).await?;
464+
465+
// Stage some code.
466+
let code = vec![1, 2, 3];
467+
let res = setup
468+
.upgradable_contract
469+
.up_stage_code(&dao, code.clone())
470+
.await?;
471+
assert_success_with_unit_return(res);
472+
setup.assert_staged_code(Some(&code)).await;
440473

441474
// Deploy staged code.
442-
let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?;
475+
let hash = convert_code_to_deploy_hash(&code);
476+
let res = setup
477+
.upgradable_contract
478+
.up_deploy_code(&dao, hash, None)
479+
.await?;
480+
assert_success_with_unit_return(res);
481+
482+
Ok(())
483+
}
484+
485+
/// Verifies failure of `up_deploy_code(hash, ...)` when `hash` does not correspond to the
486+
/// hash of staged code.
487+
#[tokio::test]
488+
async fn test_deploy_code_with_hash_invalid_hash() -> anyhow::Result<()> {
489+
let worker = near_workspaces::sandbox().await?;
490+
let dao = worker.dev_create_account().await?;
491+
let setup = Setup::new(worker.clone(), Some(dao.id().clone()), None).await?;
492+
493+
// Stage some code.
494+
let code = vec![1, 2, 3];
495+
let res = setup
496+
.upgradable_contract
497+
.up_stage_code(&dao, code.clone())
498+
.await?;
443499
assert_success_with_unit_return(res);
500+
setup.assert_staged_code(Some(&code)).await;
501+
502+
// Deployment is aborted if an invalid hash is provided.
503+
let res = setup
504+
.upgradable_contract
505+
.up_deploy_code(&dao, "invalid_hash".to_owned(), None)
506+
.await?;
507+
let actual_hash = convert_code_to_deploy_hash(&code);
508+
let expected_err = format!(
509+
"Upgradable: Cannot deploy due to wrong hash: expected hash: {}",
510+
actual_hash
511+
);
512+
assert_failure_with(res, &expected_err);
444513

445514
Ok(())
446515
}
@@ -465,10 +534,13 @@ async fn test_deploy_code_and_call_method() -> anyhow::Result<()> {
465534
.up_stage_code(&dao, code.clone())
466535
.await?;
467536
assert_success_with_unit_return(res);
468-
setup.assert_staged_code(Some(code)).await;
537+
setup.assert_staged_code(Some(&code)).await;
469538

470539
// Deploy staged code.
471-
let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?;
540+
let res = setup
541+
.upgradable_contract
542+
.up_deploy_code(&dao, convert_code_to_deploy_hash(&code), None)
543+
.await?;
472544
assert_success_with_unit_return(res);
473545

474546
// The newly deployed contract defines the function `is_upgraded`. Calling it successfully
@@ -502,7 +574,7 @@ async fn test_deploy_code_with_migration() -> anyhow::Result<()> {
502574
.up_stage_code(&dao, code.clone())
503575
.await?;
504576
assert_success_with_unit_return(res);
505-
setup.assert_staged_code(Some(code)).await;
577+
setup.assert_staged_code(Some(&code)).await;
506578

507579
// Deploy staged code and call the new contract's `migrate` method.
508580
let function_call_args = FunctionCallArgs {
@@ -513,7 +585,11 @@ async fn test_deploy_code_with_migration() -> anyhow::Result<()> {
513585
};
514586
let res = setup
515587
.upgradable_contract
516-
.up_deploy_code(&dao, Some(function_call_args))
588+
.up_deploy_code(
589+
&dao,
590+
convert_code_to_deploy_hash(&code),
591+
Some(function_call_args),
592+
)
517593
.await?;
518594
assert_success_with_unit_return(res);
519595

@@ -545,7 +621,7 @@ async fn test_deploy_code_with_migration_failure_rollback() -> anyhow::Result<()
545621
.up_stage_code(&dao, code.clone())
546622
.await?;
547623
assert_success_with_unit_return(res);
548-
setup.assert_staged_code(Some(code)).await;
624+
setup.assert_staged_code(Some(&code)).await;
549625

550626
// Deploy staged code and call the new contract's `migrate_with_failure` method.
551627
let function_call_args = FunctionCallArgs {
@@ -556,7 +632,11 @@ async fn test_deploy_code_with_migration_failure_rollback() -> anyhow::Result<()
556632
};
557633
let res = setup
558634
.upgradable_contract
559-
.up_deploy_code(&dao, Some(function_call_args))
635+
.up_deploy_code(
636+
&dao,
637+
convert_code_to_deploy_hash(&code),
638+
Some(function_call_args),
639+
)
560640
.await?;
561641
assert_failure_with(res, "Failing migration on purpose");
562642

@@ -591,21 +671,23 @@ async fn test_deploy_code_in_batch_transaction_pitfall() -> anyhow::Result<()> {
591671
.up_stage_code(&dao, code.clone())
592672
.await?;
593673
assert_success_with_unit_return(res);
594-
setup.assert_staged_code(Some(code)).await;
674+
setup.assert_staged_code(Some(&code)).await;
595675

596676
// Construct the function call actions to be executed in a batch transaction.
597677
// Note that we are attaching a call to `migrate_with_failure`, which will fail.
598678
let fn_call_deploy = near_workspaces::operations::Function::new("up_deploy_code")
599-
.args_json(json!({ "function_call_args": FunctionCallArgs {
679+
.args_json(json!({
680+
"hash": convert_code_to_deploy_hash(&code),
681+
"function_call_args": FunctionCallArgs {
600682
function_name: "migrate_with_failure".to_string(),
601683
arguments: Vec::new(),
602684
amount: NearToken::from_yoctonear(0),
603685
gas: Gas::from_tgas(2),
604686
} }))
605-
.gas(Gas::from_tgas(201));
687+
.gas(Gas::from_tgas(220));
606688
let fn_call_remove_code = near_workspaces::operations::Function::new("up_stage_code")
607689
.args_borsh(Vec::<u8>::new())
608-
.gas(Gas::from_tgas(90));
690+
.gas(Gas::from_tgas(80));
609691

610692
let res = dao
611693
.batch(setup.contract.id())
@@ -658,13 +740,16 @@ async fn test_deploy_code_with_delay() -> anyhow::Result<()> {
658740
.up_stage_code(&dao, code.clone())
659741
.await?;
660742
assert_success_with_unit_return(res);
661-
setup.assert_staged_code(Some(code)).await;
743+
setup.assert_staged_code(Some(&code)).await;
662744

663745
// Let the staging duration pass.
664746
fast_forward_beyond(&worker, staging_duration).await;
665747

666748
// Deploy staged code.
667-
let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?;
749+
let res = setup
750+
.upgradable_contract
751+
.up_deploy_code(&dao, convert_code_to_deploy_hash(&code), None)
752+
.await?;
668753
assert_success_with_unit_return(res);
669754

670755
Ok(())
@@ -688,13 +773,16 @@ async fn test_deploy_code_with_delay_failure_too_early() -> anyhow::Result<()> {
688773
.up_stage_code(&dao, code.clone())
689774
.await?;
690775
assert_success_with_unit_return(res);
691-
setup.assert_staged_code(Some(code)).await;
776+
setup.assert_staged_code(Some(&code)).await;
692777

693778
// Let some time pass but not enough.
694779
fast_forward_beyond(&worker, sdk_duration_from_secs(1)).await;
695780

696781
// Verify trying to deploy staged code fails.
697-
let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?;
782+
let res = setup
783+
.upgradable_contract
784+
.up_deploy_code(&dao, convert_code_to_deploy_hash(&code), None)
785+
.await?;
698786
assert_failure_with(res, ERR_MSG_DEPLOY_CODE_TOO_EARLY);
699787

700788
// Verify `code` wasn't deployed by calling a function that is defined only in the initial
@@ -717,13 +805,17 @@ async fn test_deploy_code_permission_failure() -> anyhow::Result<()> {
717805
.up_stage_code(&dao, code.clone())
718806
.await?;
719807
assert_success_with_unit_return(res);
720-
setup.assert_staged_code(Some(code)).await;
808+
setup.assert_staged_code(Some(&code)).await;
721809

722810
// Only the roles passed as `code_deployers` to the `Upgradable` derive macro may successfully
723811
// call this method.
724812
let res = setup
725813
.upgradable_contract
726-
.up_deploy_code(&setup.unauth_account, None)
814+
.up_deploy_code(
815+
&setup.unauth_account,
816+
convert_code_to_deploy_hash(&code),
817+
None,
818+
)
727819
.await?;
728820
assert_insufficient_acl_permissions(
729821
res,
@@ -762,7 +854,10 @@ async fn test_deploy_code_empty_failure() -> anyhow::Result<()> {
762854
// The staging timestamp is set when staging code and removed when unstaging code. So when there
763855
// is no code staged, there is no staging timestamp. Hence the error message regarding a missing
764856
// staging timestamp is expected.
765-
let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?;
857+
let res = setup
858+
.upgradable_contract
859+
.up_deploy_code(&dao, "".to_owned(), None)
860+
.await?;
766861
assert_failure_with(res, ERR_MSG_NO_STAGING_TS);
767862

768863
Ok(())
@@ -1003,14 +1098,18 @@ async fn test_acl_permission_scope() -> anyhow::Result<()> {
10031098
.up_stage_code(&code_stager, code.clone())
10041099
.await?;
10051100
assert_success_with_unit_return(res);
1006-
setup.assert_staged_code(Some(code)).await;
1101+
setup.assert_staged_code(Some(&code)).await;
10071102

10081103
// Verify `code_stager` is not authorized to deploy staged code. Only grantees of at least one
10091104
// of the roles passed as `code_deployers` to the `Upgradable` derive macro are authorized to
10101105
// deploy code.
10111106
let res = setup
10121107
.upgradable_contract
1013-
.up_deploy_code(&setup.unauth_account, None)
1108+
.up_deploy_code(
1109+
&setup.unauth_account,
1110+
convert_code_to_deploy_hash(&code),
1111+
None,
1112+
)
10141113
.await?;
10151114
assert_insufficient_acl_permissions(
10161115
res,

near-plugins/src/upgradable.rs

+16-1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ pub trait Upgradable {
102102

103103
/// Allows an authorized account to deploy the staged code. It panics if no code is staged.
104104
///
105+
/// # Verifying the hash of staged code
106+
///
107+
/// Some workflows (e.g. when a DAO interacts with an `Upgradable` contract) are facilitated if
108+
/// deployment succeeds only in case the hash of staged code corresponds to a given hash. This
109+
/// behavior can be enabled with the `hash` parameter. In case it is `h`, the deployment
110+
/// succeeds only if `h` equals the base64 encoded string of the staged code's `sha256` hash. In
111+
/// particular, the encoding according to [`near_sdk::base64::encode`] is expected. Note that
112+
/// `near_sdk` uses a rather dated version of the `base64` crate whose API differs from current
113+
/// versions.
114+
///
115+
///
105116
/// # Attaching a function call
106117
///
107118
/// If `function_call_args` are provided, code is deployed in a batch promise that contains the
@@ -143,7 +154,11 @@ pub trait Upgradable {
143154
/// [asynchronous design]: https://docs.near.org/concepts/basics/transactions/overview
144155
/// [state migration]: https://docs.near.org/develop/upgrade#migrating-the-state
145156
/// [storage staked]: https://docs.near.org/concepts/storage/storage-staking#btw-you-can-remove-data-to-unstake-some-tokens
146-
fn up_deploy_code(&mut self, function_call_args: Option<FunctionCallArgs>) -> Promise;
157+
fn up_deploy_code(
158+
&mut self,
159+
hash: String,
160+
function_call_args: Option<FunctionCallArgs>,
161+
) -> Promise;
147162

148163
/// Initializes the duration of the delay for deploying the staged code. It defaults to zero if
149164
/// code is staged before the staging duration is initialized. Once the staging duration has

0 commit comments

Comments
 (0)