diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 5722603fe62a5..766bc9515b8ab 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -1215,6 +1215,7 @@ impl pallet_tips::Config for Runtime { parameter_types! { pub const DepositPerItem: Balance = deposit(1, 0); pub const DepositPerByte: Balance = deposit(0, 1); + pub const DefaultDepositLimit: Balance = deposit(1024, 1024 * 1024); pub Schedule: pallet_contracts::Schedule = Default::default(); } @@ -1233,6 +1234,7 @@ impl pallet_contracts::Config for Runtime { type CallFilter = Nothing; type DepositPerItem = DepositPerItem; type DepositPerByte = DepositPerByte; + type DefaultDepositLimit = DefaultDepositLimit; type CallStack = [pallet_contracts::Frame; 5]; type WeightPrice = pallet_transaction_payment::Pallet; type WeightInfo = pallet_contracts::weights::SubstrateWeight; diff --git a/frame/contracts/README.md b/frame/contracts/README.md index cf89c9514142d..13c5e7253c1d8 100644 --- a/frame/contracts/README.md +++ b/frame/contracts/README.md @@ -1,6 +1,6 @@ -# Contract Module +# Contracts Module -The Contract module provides functionality for the runtime to deploy and execute WebAssembly smart-contracts. +The Contracts module provides functionality for the runtime to deploy and execute WebAssembly smart-contracts. - [`Call`](https://paritytech.github.io/substrate/master/pallet_contracts/pallet/enum.Call.html) - [`Config`](https://paritytech.github.io/substrate/master/pallet_contracts/pallet/trait.Config.html) @@ -63,9 +63,9 @@ directly. This makes sure that by default `pallet-contracts` does not expose any When setting up the `Schedule` for your runtime make sure to set `InstructionWeights::fallback` to a non zero value. The default is `0` and prevents the upload of any non deterministic code. -An indeterministic code can be deployed on-chain by passing `Determinism::AllowIndeterministic` +An indeterministic code can be deployed on-chain by passing `Determinism::Relaxed` to `upload_code`. A deterministic contract can then delegate call into it if and only if it -is ran by using `bare_call` and passing `Determinism::AllowIndeterministic` to it. **Never use +is ran by using `bare_call` and passing `Determinism::Relaxed` to it. **Never use this argument when the contract is called from an on-chain transaction.** ## Interface diff --git a/frame/contracts/fixtures/call_with_limit.wat b/frame/contracts/fixtures/call_with_limit.wat index abb8708267271..04da59551a8ce 100644 --- a/frame/contracts/fixtures/call_with_limit.wat +++ b/frame/contracts/fixtures/call_with_limit.wat @@ -1,8 +1,8 @@ -;; This expects [account_id, gas_limit] as input and calls the account_id with the supplied gas_limit. +;; This expects [account_id, ref_time, proof_size] as input and calls the account_id with the supplied 2D Weight limit. ;; It returns the result of the call as output data. (module (import "seal0" "seal_input" (func $seal_input (param i32 i32))) - (import "seal0" "seal_call" (func $seal_call (param i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32))) + (import "seal2" "call" (func $seal_call (param i32 i32 i64 i64 i32 i32 i32 i32 i32 i32) (result i32))) (import "seal0" "seal_return" (func $seal_return (param i32 i32 i32))) (import "env" "memory" (memory 1 1)) @@ -13,24 +13,25 @@ (func (export "deploy")) (func (export "call") - ;; Receive the encoded call + gas_limit + ;; Receive the encoded account_id, ref_time, proof_size (call $seal_input (i32.const 4) ;; Pointer to the input buffer - (i32.const 0) ;; Size of the length buffer + (i32.const 0) ;; Pointer to the length of the input buffer ) (i32.store (i32.const 0) (call $seal_call + (i32.const 0) ;; Set no flag. (i32.const 4) ;; Pointer to "callee" address. - (i32.const 32) ;; Length of "callee" address. - (i64.load (i32.const 36)) ;; How much gas to devote for the execution. + (i64.load (i32.const 36)) ;; How much ref_time to devote for the execution. + (i64.load (i32.const 44)) ;; How much proof_size to devote for the execution. + (i32.const 0xffffffff) ;; u32 max sentinel value: pass no deposit limit. (i32.const 0) ;; Pointer to the buffer with value to transfer - (i32.const 0) ;; Length of the buffer with value to transfer. (i32.const 0) ;; Pointer to input data buffer address - (i32.const 0) ;; Length of input data buffer + (i32.const 0) ;; Length of input data buffer (i32.const 0xffffffff) ;; u32 max sentinel value: do not copy output - (i32.const 0) ;; Ptr to output buffer len - ) + (i32.const 0) ;; Length is ignored in this case + ) ) (call $seal_return (i32.const 0) (i32.const 0) (i32.const 4)) ) diff --git a/frame/contracts/fixtures/caller_contract.wat b/frame/contracts/fixtures/caller_contract.wat index f9caf49f29ca8..929171b9a26f6 100644 --- a/frame/contracts/fixtures/caller_contract.wat +++ b/frame/contracts/fixtures/caller_contract.wat @@ -1,9 +1,9 @@ (module (import "seal0" "seal_input" (func $seal_input (param i32 i32))) (import "seal0" "seal_balance" (func $seal_balance (param i32 i32))) - (import "seal0" "seal_call" (func $seal_call (param i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32))) - (import "seal0" "seal_instantiate" (func $seal_instantiate - (param i32 i32 i64 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32) + (import "seal2" "call" (func $seal_call (param i32 i32 i64 i64 i32 i32 i32 i32 i32 i32) (result i32))) + (import "seal2" "instantiate" (func $seal_instantiate + (param i32 i64 i64 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32) )) (import "env" "memory" (memory 1 1)) @@ -43,18 +43,18 @@ (set_local $exit_code (call $seal_instantiate (i32.const 24) ;; Pointer to the code hash. - (i32.const 32) ;; Length of the code hash. - (i64.const 0) ;; How much gas to devote for the execution. 0 = all. + (i64.const 0) ;; How much ref_time weight to devote for the execution. 0 = all. + (i64.const 0) ;; How much proof_size weight to devote for the execution. 0 = all. + (i32.const 0xffffffff) ;; u32 max sentinel value: pass no deposit limit. (i32.const 0) ;; Pointer to the buffer with value to transfer - (i32.const 8) ;; Length of the buffer with value to transfer. (i32.const 9) ;; Pointer to input data buffer address (i32.const 7) ;; Length of input data buffer - (i32.const 4294967295) ;; u32 max sentinel value: do not copy address - (i32.const 0) ;; Length is ignored in this case + (i32.const 4294967295) ;; u32 max sentinel value: do not copy address + (i32.const 0) ;; Length is ignored in this case (i32.const 4294967295) ;; u32 max sentinel value: do not copy output - (i32.const 0) ;; Length is ignored in this case - (i32.const 0) ;; salt_ptr - (i32.const 0) ;; salt_le + (i32.const 0) ;; Length is ignored in this case + (i32.const 0) ;; salt_ptr + (i32.const 0) ;; salt_le ) ) @@ -63,22 +63,47 @@ (i32.eq (get_local $exit_code) (i32.const 2)) ;; ReturnCode::CalleeReverted ) - ;; Fail to deploy the contract due to insufficient gas. + ;; Fail to deploy the contract due to insufficient ref_time weight. (set_local $exit_code (call $seal_instantiate (i32.const 24) ;; Pointer to the code hash. - (i32.const 32) ;; Length of the code hash. - (i64.const 1) ;; Supply too little gas + (i64.const 1) ;; Supply too little ref_time weight + (i64.const 0) ;; How much proof_size weight to devote for the execution. 0 = all. + (i32.const 0xffffffff) ;; u32 max sentinel value: pass no deposit limit. (i32.const 0) ;; Pointer to the buffer with value to transfer - (i32.const 8) ;; Length of the buffer with value to transfer. (i32.const 8) ;; Pointer to input data buffer address (i32.const 8) ;; Length of input data buffer (i32.const 4294967295) ;; u32 max sentinel value: do not copy address - (i32.const 0) ;; Length is ignored in this case + (i32.const 0) ;; Length is ignored in this case (i32.const 4294967295) ;; u32 max sentinel value: do not copy output - (i32.const 0) ;; Length is ignored in this case - (i32.const 0) ;; salt_ptr - (i32.const 0) ;; salt_le + (i32.const 0) ;; Length is ignored in this case + (i32.const 0) ;; salt_ptr + (i32.const 0) ;; salt_le + + ) + ) + + ;; Check for special trap exit status. + (call $assert + (i32.eq (get_local $exit_code) (i32.const 1)) ;; ReturnCode::CalleeTrapped + ) + + ;; Fail to deploy the contract due to insufficient ref_time weight. + (set_local $exit_code + (call $seal_instantiate + (i32.const 24) ;; Pointer to the code hash. + (i64.const 0) ;; How much ref_time weight to devote for the execution. 0 = all. + (i64.const 1) ;; Supply too little proof_size weight + (i32.const 0xffffffff) ;; u32 max sentinel value: pass no deposit limit. + (i32.const 0) ;; Pointer to the buffer with value to transfer + (i32.const 8) ;; Pointer to input data buffer address + (i32.const 8) ;; Length of input data buffer + (i32.const 4294967295) ;; u32 max sentinel value: do not copy address + (i32.const 0) ;; Length is ignored in this case + (i32.const 4294967295) ;; u32 max sentinel value: do not copy output + (i32.const 0) ;; Length is ignored in this case + (i32.const 0) ;; salt_ptr + (i32.const 0) ;; salt_le ) ) @@ -98,18 +123,18 @@ (set_local $exit_code (call $seal_instantiate (i32.const 24) ;; Pointer to the code hash. - (i32.const 32) ;; Length of the code hash. - (i64.const 0) ;; How much gas to devote for the execution. 0 = all. + (i64.const 0) ;; How much ref_time weight to devote for the execution. 0 = all. + (i64.const 0) ;; How much proof_size weight to devote for the execution. 0 = all. + (i32.const 0xffffffff) ;; u32 max sentinel value: pass no deposit limit. (i32.const 0) ;; Pointer to the buffer with value to transfer - (i32.const 8) ;; Length of the buffer with value to transfer. (i32.const 8) ;; Pointer to input data buffer address (i32.const 8) ;; Length of input data buffer - (i32.const 16) ;; Pointer to the address output buffer - (i32.sub (get_local $sp) (i32.const 4)) ;; Pointer to the address buffer length - (i32.const 4294967295) ;; u32 max sentinel value: do not copy output - (i32.const 0) ;; Length is ignored in this case - (i32.const 0) ;; salt_ptr - (i32.const 0) ;; salt_le + (i32.const 16) ;; Pointer to the address output buffer + (i32.sub (get_local $sp) (i32.const 4)) ;; Pointer to the address buffer length + (i32.const 4294967295) ;; u32 max sentinel value: do not copy output + (i32.const 0) ;; Length is ignored in this case + (i32.const 0) ;; salt_ptr + (i32.const 0) ;; salt_le ) ) @@ -139,15 +164,16 @@ ;; Call the new contract and expect it to return failing exit code. (set_local $exit_code (call $seal_call + (i32.const 0) ;; Set no flag (i32.const 16) ;; Pointer to "callee" address. - (i32.const 32) ;; Length of "callee" address. - (i64.const 0) ;; How much gas to devote for the execution. 0 = all. + (i64.const 0) ;; How much ref_time weight to devote for the execution. 0 = all. + (i64.const 0) ;; How much proof_size weight to devote for the execution. 0 = all. + (i32.const 0xffffffff) ;; u32 max sentinel value: pass no deposit limit. (i32.const 0) ;; Pointer to the buffer with value to transfer - (i32.const 8) ;; Length of the buffer with value to transfer. (i32.const 9) ;; Pointer to input data buffer address (i32.const 7) ;; Length of input data buffer - (i32.sub (get_local $sp) (i32.const 4)) ;; Ptr to output buffer - (i32.sub (get_local $sp) (i32.const 8)) ;; Ptr to output buffer len + (i32.sub (get_local $sp) (i32.const 4)) ;; Ptr to output buffer + (i32.sub (get_local $sp) (i32.const 8)) ;; Ptr to output buffer len ) ) @@ -167,18 +193,40 @@ ) ) - ;; Fail to call the contract due to insufficient gas. + ;; Fail to call the contract due to insufficient ref_time weight. (set_local $exit_code (call $seal_call + (i32.const 0) ;; Set no flag (i32.const 16) ;; Pointer to "callee" address. - (i32.const 32) ;; Length of "callee" address. - (i64.const 1) ;; Supply too little gas + (i64.const 1) ;; Supply too little ref_time weight + (i64.const 0) ;; How much proof_size weight to devote for the execution. 0 = all. + (i32.const 0xffffffff) ;; u32 max sentinel value: pass no deposit limit. (i32.const 0) ;; Pointer to the buffer with value to transfer - (i32.const 8) ;; Length of the buffer with value to transfer. (i32.const 8) ;; Pointer to input data buffer address (i32.const 8) ;; Length of input data buffer - (i32.const 4294967295) ;; u32 max sentinel value: do not copy output - (i32.const 0) ;; Length is ignored in this cas + (i32.const 4294967295) ;; u32 max sentinel value: do not copy output + (i32.const 0) ;; Length is ignored in this cas + ) + ) + + ;; Check for special trap exit status. + (call $assert + (i32.eq (get_local $exit_code) (i32.const 1)) ;; ReturnCode::CalleeTrapped + ) + + ;; Fail to call the contract due to insufficient proof_size weight. + (set_local $exit_code + (call $seal_call + (i32.const 0) ;; Set no flag + (i32.const 16) ;; Pointer to "callee" address. + (i64.const 0) ;; How much ref_time weight to devote for the execution. 0 = all. + (i64.const 1) ;; Supply too little proof_size weight + (i32.const 0xffffffff) ;; u32 max sentinel value: pass no deposit limit. + (i32.const 0) ;; Pointer to the buffer with value to transfer + (i32.const 8) ;; Pointer to input data buffer address + (i32.const 8) ;; Length of input data buffer + (i32.const 4294967295) ;; u32 max sentinel value: do not copy output + (i32.const 0) ;; Length is ignored in this cas ) ) @@ -202,15 +250,16 @@ ;; Call the contract successfully. (set_local $exit_code (call $seal_call + (i32.const 0) ;; Set no flag (i32.const 16) ;; Pointer to "callee" address. - (i32.const 32) ;; Length of "callee" address. - (i64.const 0) ;; How much gas to devote for the execution. 0 = all. + (i64.const 0) ;; How much ref_time weight to devote for the execution. 0 = all. + (i64.const 0) ;; How much proof_size weight to devote for the execution. 0 = all. + (i32.const 0xffffffff) ;; u32 max sentinel value: pass no deposit limit. (i32.const 0) ;; Pointer to the buffer with value to transfer - (i32.const 8) ;; Length of the buffer with value to transfer. (i32.const 8) ;; Pointer to input data buffer address (i32.const 8) ;; Length of input data buffer - (i32.sub (get_local $sp) (i32.const 4)) ;; Ptr to output buffer - (i32.sub (get_local $sp) (i32.const 8)) ;; Ptr to output buffer len + (i32.sub (get_local $sp) (i32.const 4)) ;; Ptr to output buffer + (i32.sub (get_local $sp) (i32.const 8)) ;; Ptr to output buffer len ) ) diff --git a/frame/contracts/fixtures/create_storage_and_call.wat b/frame/contracts/fixtures/create_storage_and_call.wat index 2a1e53f7ce08a..5592e7e96a980 100644 --- a/frame/contracts/fixtures/create_storage_and_call.wat +++ b/frame/contracts/fixtures/create_storage_and_call.wat @@ -2,7 +2,7 @@ (module (import "seal0" "seal_input" (func $seal_input (param i32 i32))) (import "seal0" "seal_set_storage" (func $seal_set_storage (param i32 i32 i32))) - (import "seal1" "seal_call" (func $seal_call (param i32 i32 i64 i32 i32 i32 i32 i32) (result i32))) + (import "seal2" "call" (func $seal_call (param i32 i32 i64 i64 i32 i32 i32 i32 i32 i32) (result i32))) (import "env" "memory" (memory 1 1)) (func $assert (param i32) @@ -20,7 +20,10 @@ ;; store length of input buffer (i32.store (i32.const 0) (i32.const 512)) - ;; copy input at address 4 + ;; copy input at address 4: + ;; first 4 bytes for the size of the storage to be created in callee + ;; next 32 bytes are for the callee address + ;; next bytes for the encoded deposit limit (call $seal_input (i32.const 4) (i32.const 0)) ;; create 4 byte of storage before calling @@ -34,8 +37,10 @@ (call $assert (i32.eqz (call $seal_call (i32.const 0) ;; No flags - (i32.const 8) ;; Pointer to "callee" address. - (i64.const 0) ;; How much gas to devote for the execution. 0 = all. + (i32.const 8) ;; Pointer to "callee" address + (i64.const 0) ;; How much ref_time to devote for the execution. 0 = all + (i64.const 0) ;; How much proof_limit to devote for the execution. 0 = all + (i32.const 40) ;; Pointer to the storage deposit limit (i32.const 512) ;; Pointer to the buffer with value to transfer (i32.const 4) ;; Pointer to input data buffer address (i32.const 4) ;; Length of input data buffer diff --git a/frame/contracts/fixtures/create_storage_and_instantiate.wat b/frame/contracts/fixtures/create_storage_and_instantiate.wat new file mode 100644 index 0000000000000..cd7202478437b --- /dev/null +++ b/frame/contracts/fixtures/create_storage_and_instantiate.wat @@ -0,0 +1,66 @@ +;; This instantiates another contract and passes some input to its constructor. +(module + (import "seal0" "seal_input" (func $seal_input (param i32 i32))) + (import "seal0" "seal_set_storage" (func $seal_set_storage (param i32 i32 i32))) + (import "seal2" "instantiate" (func $seal_instantiate + (param i32 i64 i64 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32) + )) + (import "seal0" "seal_return" (func $seal_return (param i32 i32 i32))) + (import "env" "memory" (memory 1 1)) + + ;; [0, 8) send 10_000 balance + (data (i32.const 48) "\10\27\00\00\00\00\00\00") + + (func $assert (param i32) + (block $ok + (br_if $ok + (get_local 0) + ) + (unreachable) + ) + ) + + (func (export "deploy")) + + (func (export "call") + ;; store length of input buffer + (i32.store (i32.const 0) (i32.const 512)) + ;; store length of contract address + (i32.store (i32.const 84) (i32.const 32)) + + ;; copy input at address 4 + (call $seal_input (i32.const 4) (i32.const 0)) + + ;; memory layout is: + ;; [0,4): size of input buffer + ;; [4,8): size of the storage to be created in callee + ;; [8,40): the code hash of the contract to instantiate + ;; [40,48): for the encoded deposit limit + ;; [48,52): value to transfer + ;; [52,84): address of the deployed contract + ;; [84,88): len of the address + + ;; instantiate a contract + (call $assert (i32.eqz +;; (i32.store +;; (i32.const 64) + (call $seal_instantiate + (i32.const 8) ;; Pointer to the code hash. + (i64.const 0) ;; How much ref_time weight to devote for the execution. 0 = all. + (i64.const 0) ;; How much proof_size weight to devote for the execution. 0 = all. + (i32.const 40) ;; Pointer to the storage deposit limit + (i32.const 48) ;; Pointer to the buffer with value to transfer + (i32.const 4) ;; Pointer to input data buffer address + (i32.const 4) ;; Length of input data buffer + (i32.const 52) ;; Pointer to where to copy address + (i32.const 84) ;; Pointer to address len ptr + (i32.const 0xffffffff) ;; u32 max sentinel value: do not copy output + (i32.const 0) ;; Length is ignored in this case + (i32.const 0) ;; salt_ptr + (i32.const 0) ;; salt_len + ) + )) + ;; return the deployed contract address + (call $seal_return (i32.const 0) (i32.const 52) (i32.const 32)) + ) +) diff --git a/frame/contracts/fixtures/store.wat b/frame/contracts/fixtures/store_call.wat similarity index 100% rename from frame/contracts/fixtures/store.wat rename to frame/contracts/fixtures/store_call.wat diff --git a/frame/contracts/fixtures/store_deploy.wat b/frame/contracts/fixtures/store_deploy.wat new file mode 100644 index 0000000000000..cc428e9623bfb --- /dev/null +++ b/frame/contracts/fixtures/store_deploy.wat @@ -0,0 +1,45 @@ +;; Stores a value of the passed size in constructor. +(module + (import "seal0" "seal_set_storage" (func $seal_set_storage (param i32 i32 i32))) + (import "seal0" "seal_input" (func $seal_input (param i32 i32))) + (import "env" "memory" (memory 16 16)) + + ;; [0, 32) storage key + (data (i32.const 0) "\01") + + ;; [32, 36) buffer where input is copied (expected size of storage item) + + ;; [36, 40) size of the input buffer + (data (i32.const 36) "\04") + + (func $assert (param i32) + (block $ok + (br_if $ok + (get_local 0) + ) + (unreachable) + ) + ) + + (func (export "deploy") + (call $seal_input (i32.const 32) (i32.const 36)) + + ;; assert input size == 4 + (call $assert + (i32.eq + (i32.load (i32.const 36)) + (i32.const 4) + ) + ) + + ;; place a value in storage, the size of which is specified by the call input. + ;; we don't care about the contents of the storage item + (call $seal_set_storage + (i32.const 0) ;; Pointer to storage key + (i32.const 0) ;; Pointer to value + (i32.load (i32.const 32)) ;; Size of value + ) + ) + + (func (export "call")) +) diff --git a/frame/contracts/src/benchmarking/mod.rs b/frame/contracts/src/benchmarking/mod.rs index 98a3dc36b7b68..6ebfb18509b1c 100644 --- a/frame/contracts/src/benchmarking/mod.rs +++ b/frame/contracts/src/benchmarking/mod.rs @@ -547,7 +547,7 @@ benchmarks! { seal_gas_left { let r in 0 .. API_BENCHMARK_RUNS; let instance = Contract::::new(WasmModule::getter( - "seal0", "seal_gas_left", r + "seal1", "gas_left", r ), vec![])?; let origin = RawOrigin::Signed(instance.caller.clone()); }: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![]) @@ -604,9 +604,9 @@ benchmarks! { let code = WasmModule::::from(ModuleDefinition { memory: Some(ImportedMemory::max::()), imported_functions: vec![ImportedFunction { - module: "seal0", - name: "seal_weight_to_fee", - params: vec![ValueType::I64, ValueType::I32, ValueType::I32], + module: "seal1", + name: "weight_to_fee", + params: vec![ValueType::I64, ValueType::I64, ValueType::I32, ValueType::I32], return_type: None, }], data_segments: vec![DataSegment { @@ -615,6 +615,7 @@ benchmarks! { }], call_body: Some(body::repeated(r, &[ Instruction::I64Const(500_000), + Instruction::I64Const(300_000), Instruction::I32Const(4), Instruction::I32Const(0), Instruction::Call(0), @@ -1584,16 +1585,26 @@ benchmarks! { let callee_bytes = callees.iter().flat_map(|x| x.account_id.encode()).collect(); let value: BalanceOf = 0u32.into(); let value_bytes = value.encode(); - let value_len = value_bytes.len(); + let value_len = BalanceOf::::max_encoded_len() as u32; + // Set an own limit every 2nd call + let own_limit = (u32::MAX - 100).into(); + let deposits = (0..r) + .map(|i| if i % 2 == 0 { 0u32.into() } else { own_limit } ) + .collect::>>(); + let deposits_bytes: Vec = deposits.iter().flat_map(|i| i.encode()).collect(); + let deposits_len = deposits_bytes.len() as u32; + let deposit_len = value_len.clone(); + let callee_offset = value_len + deposits_len; let code = WasmModule::::from(ModuleDefinition { memory: Some(ImportedMemory::max::()), imported_functions: vec![ImportedFunction { - module: "seal0", - name: "seal_call", + module: "seal2", + name: "call", params: vec![ ValueType::I32, ValueType::I32, ValueType::I64, + ValueType::I64, ValueType::I32, ValueType::I32, ValueType::I32, @@ -1609,16 +1620,21 @@ benchmarks! { value: value_bytes, }, DataSegment { - offset: value_len as u32, + offset: value_len, + value: deposits_bytes, + }, + DataSegment { + offset: callee_offset, value: callee_bytes, }, ], call_body: Some(body::repeated_dyn(r, vec![ - Counter(value_len as u32, callee_len as u32), // callee_ptr - Regular(Instruction::I32Const(callee_len as i32)), // callee_len - Regular(Instruction::I64Const(0)), // gas + Regular(Instruction::I32Const(0)), // flags + Counter(callee_offset, callee_len as u32), // callee_ptr + Regular(Instruction::I64Const(0)), // ref_time weight + Regular(Instruction::I64Const(0)), // proof_size weight + Counter(value_len, deposit_len as u32), // deposit_limit_ptr Regular(Instruction::I32Const(0)), // value_ptr - Regular(Instruction::I32Const(value_len as i32)), // value_len Regular(Instruction::I32Const(0)), // input_data_ptr Regular(Instruction::I32Const(0)), // input_data_len Regular(Instruction::I32Const(SENTINEL as i32)), // output_ptr @@ -1630,7 +1646,7 @@ benchmarks! { }); let instance = Contract::::new(code, vec![])?; let origin = RawOrigin::Signed(instance.caller.clone()); - }: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![]) + }: call(origin, instance.addr, 0u32.into(), Weight::MAX, Some(BalanceOf::::from(u32::MAX).into()), vec![]) // This is a slow call: We redeuce the number of runs. #[pov_mode = Measured] @@ -1742,17 +1758,17 @@ benchmarks! { }: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, bytes) // We assume that every instantiate sends at least the minimum balance. - // This is a slow call: We redeuce the number of runs. + // This is a slow call: we reduce the number of runs. #[pov_mode = Measured] seal_instantiate { - let r in 0 .. API_BENCHMARK_RUNS / 2; + let r in 1 .. API_BENCHMARK_RUNS / 2; let hashes = (0..r) .map(|i| { let code = WasmModule::::from(ModuleDefinition { memory: Some(ImportedMemory::max::()), call_body: Some(body::plain(vec![ - // we need to add this in order to make contracts unique - // so that they can be deployed from the same sender + // We need to add this in order to make contracts unique, + // so that they can be deployed from the same sender. Instruction::I32Const(i as i32), Instruction::Drop, Instruction::End, @@ -1765,27 +1781,24 @@ benchmarks! { .collect::, &'static str>>()?; let hash_len = hashes.get(0).map(|x| x.encode().len()).unwrap_or(0); let hashes_bytes = hashes.iter().flat_map(|x| x.encode()).collect::>(); - let hashes_len = hashes_bytes.len(); + let hashes_len = &hashes_bytes.len(); let value = Pallet::::min_balance(); assert!(value > 0u32.into()); let value_bytes = value.encode(); - let value_len = value_bytes.len(); + let value_len = BalanceOf::::max_encoded_len(); let addr_len = T::AccountId::max_encoded_len(); - - // offsets where to place static data in contract memory - let value_offset = 0; - let hashes_offset = value_offset + value_len; + // Offsets where to place static data in contract memory. + let hashes_offset = value_len; let addr_len_offset = hashes_offset + hashes_len; let addr_offset = addr_len_offset + addr_len; - let code = WasmModule::::from(ModuleDefinition { memory: Some(ImportedMemory::max::()), imported_functions: vec![ImportedFunction { - module: "seal0", - name: "seal_instantiate", + module: "seal2", + name: "instantiate", params: vec![ ValueType::I32, - ValueType::I32, + ValueType::I64, ValueType::I64, ValueType::I32, ValueType::I32, @@ -1802,7 +1815,7 @@ benchmarks! { }], data_segments: vec![ DataSegment { - offset: value_offset as u32, + offset: 0, value: value_bytes, }, DataSegment { @@ -1816,10 +1829,10 @@ benchmarks! { ], call_body: Some(body::repeated_dyn(r, vec![ Counter(hashes_offset as u32, hash_len as u32), // code_hash_ptr - Regular(Instruction::I32Const(hash_len as i32)), // code_hash_len - Regular(Instruction::I64Const(0)), // gas - Regular(Instruction::I32Const(value_offset as i32)), // value_ptr - Regular(Instruction::I32Const(value_len as i32)), // value_len + Regular(Instruction::I64Const(0)), // ref_time weight + Regular(Instruction::I64Const(0)), // proof_size weight + Regular(Instruction::I32Const(SENTINEL as i32)), // deposit limit ptr: use parent's limit + Regular(Instruction::I32Const(0)), // value_ptr Regular(Instruction::I32Const(0)), // input_data_ptr Regular(Instruction::I32Const(0)), // input_data_len Regular(Instruction::I32Const(addr_offset as i32)), // address_ptr @@ -1827,7 +1840,7 @@ benchmarks! { Regular(Instruction::I32Const(SENTINEL as i32)), // output_ptr Regular(Instruction::I32Const(0)), // output_len_ptr Regular(Instruction::I32Const(0)), // salt_ptr - Regular(Instruction::I32Const(0)), // salt_ptr_len + Regular(Instruction::I32Const(0)), // salt_len_ptr Regular(Instruction::Call(0)), Regular(Instruction::Drop), ])), diff --git a/frame/contracts/src/exec.rs b/frame/contracts/src/exec.rs index 46a611dbfd09b..d0a650e0a0ef6 100644 --- a/frame/contracts/src/exec.rs +++ b/frame/contracts/src/exec.rs @@ -23,7 +23,9 @@ use crate::{ }; use frame_support::{ crypto::ecdsa::ECDSAExt, - dispatch::{DispatchError, DispatchResult, DispatchResultWithPostInfo, Dispatchable}, + dispatch::{ + fmt::Debug, DispatchError, DispatchResult, DispatchResultWithPostInfo, Dispatchable, + }, storage::{with_transaction, TransactionOutcome}, traits::{ tokens::{Fortitude::Polite, Preservation::Expendable}, @@ -40,7 +42,7 @@ use sp_core::{ sr25519::{Public as SR25519Public, Signature as SR25519Signature}, }; use sp_io::{crypto::secp256k1_ecdsa_recover_compressed, hashing::blake2_256}; -use sp_runtime::traits::{Convert, Hash}; +use sp_runtime::traits::{Convert, Hash, Zero}; use sp_std::{marker::PhantomData, mem, prelude::*, vec::Vec}; pub type AccountIdOf = ::AccountId; @@ -137,6 +139,7 @@ pub trait Ext: sealing::Sealed { fn call( &mut self, gas_limit: Weight, + deposit_limit: BalanceOf, to: AccountIdOf, value: BalanceOf, input_data: Vec, @@ -160,6 +163,7 @@ pub trait Ext: sealing::Sealed { fn instantiate( &mut self, gas_limit: Weight, + deposit_limit: BalanceOf, code: CodeHash, value: BalanceOf, input_data: Vec, @@ -692,8 +696,9 @@ where args, value, gas_meter, - storage_meter, Weight::zero(), + storage_meter, + BalanceOf::::zero(), schedule, determinism, )?; @@ -719,12 +724,13 @@ where /// /// This does not take `self` because when constructing the first frame `self` is /// not initialized, yet. - fn new_frame( + fn new_frame( frame_args: FrameArgs, value_transferred: BalanceOf, gas_meter: &mut GasMeter, - storage_meter: &mut storage::meter::GenericMeter, gas_limit: Weight, + storage_meter: &mut storage::meter::GenericMeter, + deposit_limit: BalanceOf, schedule: &Schedule, determinism: Determinism, ) -> Result<(Frame, E, Option), ExecError> { @@ -781,7 +787,7 @@ where account_id, entry_point, nested_gas: gas_meter.nested(gas_limit)?, - nested_storage: storage_meter.nested(), + nested_storage: storage_meter.nested(deposit_limit), allows_reentry: true, }; @@ -794,6 +800,7 @@ where frame_args: FrameArgs, value_transferred: BalanceOf, gas_limit: Weight, + deposit_limit: BalanceOf, ) -> Result { if self.frames.len() == T::CallStack::size() { return Err(Error::::MaxCallDepthReached.into()) @@ -817,8 +824,9 @@ where frame_args, value_transferred, nested_gas, - nested_storage, gas_limit, + nested_storage, + deposit_limit, self.schedule, self.determinism, )?; @@ -859,8 +867,10 @@ where return Ok(output) } - // Storage limit is enforced as late as possible (when the last frame returns) so that - // the ordering of storage accesses does not matter. + // Storage limit is normally enforced as late as possible (when the last frame returns) + // so that the ordering of storage accesses does not matter. + // (However, if a special limit was set for a sub-call, it should be enforced right + // after the sub-call returned. See below for this case of enforcement). if self.frames.is_empty() { let frame = &mut self.first_frame; frame.contract_info.load(&frame.account_id); @@ -869,7 +879,7 @@ where } let frame = self.top_frame(); - let account_id = &frame.account_id; + let account_id = &frame.account_id.clone(); match (entry_point, delegated_code_hash) { (ExportedFunction::Constructor, _) => { // It is not allowed to terminate a contract inside its constructor. @@ -877,6 +887,13 @@ where return Err(Error::::TerminatedInConstructor.into()) } + // If a special limit was set for the sub-call, we enforce it here. + // This is needed because contract constructor might write to storage. + // The sub-call will be rolled back in case the limit is exhausted. + let frame = self.top_frame_mut(); + let contract = frame.contract_info.as_contract(); + frame.nested_storage.enforce_subcall_limit(contract)?; + // Deposit an instantiation event. Contracts::::deposit_event( vec![T::Hashing::hash_of(self.caller()), T::Hashing::hash_of(account_id)], @@ -893,9 +910,15 @@ where ); }, (ExportedFunction::Call, None) => { + // If a special limit was set for the sub-call, we enforce it here. + // The sub-call will be rolled back in case the limit is exhausted. + let frame = self.top_frame_mut(); + let contract = frame.contract_info.as_contract(); + frame.nested_storage.enforce_subcall_limit(contract)?; + let caller = self.caller(); Contracts::::deposit_event( - vec![T::Hashing::hash_of(caller), T::Hashing::hash_of(account_id)], + vec![T::Hashing::hash_of(caller), T::Hashing::hash_of(&account_id)], Event::Called { caller: caller.clone(), contract: account_id.clone() }, ); }, @@ -1107,6 +1130,7 @@ where fn call( &mut self, gas_limit: Weight, + deposit_limit: BalanceOf, to: T::AccountId, value: BalanceOf, input_data: Vec, @@ -1135,6 +1159,7 @@ where FrameArgs::Call { dest: to, cached_info, delegated_call: None }, value, gas_limit, + deposit_limit, )?; self.run(executable, input_data) }; @@ -1166,6 +1191,7 @@ where }, value, Weight::zero(), + BalanceOf::::zero(), )?; self.run(executable, input_data) } @@ -1173,6 +1199,7 @@ where fn instantiate( &mut self, gas_limit: Weight, + deposit_limit: BalanceOf, code_hash: CodeHash, value: BalanceOf, input_data: Vec, @@ -1190,6 +1217,7 @@ where }, value, gas_limit, + deposit_limit, )?; let account_id = self.top_frame().account_id.clone(); self.run(executable, input_data).map(|ret| (account_id, ret)) @@ -1917,7 +1945,7 @@ mod tests { let value = Default::default(); let recurse_ch = MockLoader::insert(Call, |ctx, _| { // Try to call into yourself. - let r = ctx.ext.call(Weight::zero(), BOB, 0, vec![], true); + let r = ctx.ext.call(Weight::zero(), BalanceOf::::zero(), BOB, 0, vec![], true); ReachedBottom::mutate(|reached_bottom| { if !*reached_bottom { @@ -1971,7 +1999,11 @@ mod tests { WitnessedCallerBob::mutate(|caller| *caller = Some(ctx.ext.caller().clone())); // Call into CHARLIE contract. - assert_matches!(ctx.ext.call(Weight::zero(), CHARLIE, 0, vec![], true), Ok(_)); + assert_matches!( + ctx.ext + .call(Weight::zero(), BalanceOf::::zero(), CHARLIE, 0, vec![], true), + Ok(_) + ); exec_success() }); let charlie_ch = MockLoader::insert(Call, |ctx, _| { @@ -2105,7 +2137,8 @@ mod tests { // ALICE is the origin of the call stack assert!(ctx.ext.caller_is_origin()); // BOB calls CHARLIE - ctx.ext.call(Weight::zero(), CHARLIE, 0, vec![], true) + ctx.ext + .call(Weight::zero(), BalanceOf::::zero(), CHARLIE, 0, vec![], true) }); ExtBuilder::default().build().execute_with(|| { @@ -2136,7 +2169,11 @@ mod tests { assert_eq!(*ctx.ext.address(), BOB); // Call into charlie contract. - assert_matches!(ctx.ext.call(Weight::zero(), CHARLIE, 0, vec![], true), Ok(_)); + assert_matches!( + ctx.ext + .call(Weight::zero(), BalanceOf::::zero(), CHARLIE, 0, vec![], true), + Ok(_) + ); exec_success() }); let charlie_ch = MockLoader::insert(Call, |ctx, _| { @@ -2287,6 +2324,7 @@ mod tests { .ext .instantiate( Weight::zero(), + BalanceOf::::zero(), dummy_ch, ::Currency::minimum_balance(), vec![], @@ -2351,6 +2389,7 @@ mod tests { assert_matches!( ctx.ext.instantiate( Weight::zero(), + BalanceOf::::zero(), dummy_ch, ::Currency::minimum_balance(), vec![], @@ -2443,13 +2482,26 @@ mod tests { let info = ctx.ext.contract_info(); assert_eq!(info.storage_byte_deposit, 0); info.storage_byte_deposit = 42; - assert_eq!(ctx.ext.call(Weight::zero(), CHARLIE, 0, vec![], true), exec_trapped()); + assert_eq!( + ctx.ext.call( + Weight::zero(), + BalanceOf::::zero(), + CHARLIE, + 0, + vec![], + true + ), + exec_trapped() + ); assert_eq!(ctx.ext.contract_info().storage_byte_deposit, 42); } exec_success() }); let code_charlie = MockLoader::insert(Call, |ctx, _| { - assert!(ctx.ext.call(Weight::zero(), BOB, 0, vec![99], true).is_ok()); + assert!(ctx + .ext + .call(Weight::zero(), BalanceOf::::zero(), BOB, 0, vec![99], true) + .is_ok()); exec_trapped() }); @@ -2479,7 +2531,7 @@ mod tests { fn recursive_call_during_constructor_fails() { let code = MockLoader::insert(Constructor, |ctx, _| { assert_matches!( - ctx.ext.call(Weight::zero(), ctx.ext.address().clone(), 0, vec![], true), + ctx.ext.call(Weight::zero(), BalanceOf::::zero(), ctx.ext.address().clone(), 0, vec![], true), Err(ExecError{error, ..}) if error == >::ContractNotFound.into() ); exec_success() @@ -2618,7 +2670,7 @@ mod tests { // call the contract passed as input with disabled reentry let code_bob = MockLoader::insert(Call, |ctx, _| { let dest = Decode::decode(&mut ctx.input_data.as_ref()).unwrap(); - ctx.ext.call(Weight::zero(), dest, 0, vec![], false) + ctx.ext.call(Weight::zero(), BalanceOf::::zero(), dest, 0, vec![], false) }); let code_charlie = MockLoader::insert(Call, |_, _| exec_success()); @@ -2665,15 +2717,17 @@ mod tests { fn call_deny_reentry() { let code_bob = MockLoader::insert(Call, |ctx, _| { if ctx.input_data[0] == 0 { - ctx.ext.call(Weight::zero(), CHARLIE, 0, vec![], false) + ctx.ext + .call(Weight::zero(), BalanceOf::::zero(), CHARLIE, 0, vec![], false) } else { exec_success() } }); // call BOB with input set to '1' - let code_charlie = - MockLoader::insert(Call, |ctx, _| ctx.ext.call(Weight::zero(), BOB, 0, vec![1], true)); + let code_charlie = MockLoader::insert(Call, |ctx, _| { + ctx.ext.call(Weight::zero(), BalanceOf::::zero(), BOB, 0, vec![1], true) + }); ExtBuilder::default().build().execute_with(|| { let schedule = ::Schedule::get(); @@ -2862,6 +2916,7 @@ mod tests { ctx.ext .instantiate( Weight::zero(), + BalanceOf::::zero(), fail_code, ctx.ext.minimum_balance() * 100, vec![], @@ -2875,6 +2930,7 @@ mod tests { .ext .instantiate( Weight::zero(), + BalanceOf::::zero(), success_code, ctx.ext.minimum_balance() * 100, vec![], @@ -2883,7 +2939,9 @@ mod tests { .unwrap(); // a plain call should not influence the account counter - ctx.ext.call(Weight::zero(), account_id, 0, vec![], false).unwrap(); + ctx.ext + .call(Weight::zero(), BalanceOf::::zero(), account_id, 0, vec![], false) + .unwrap(); exec_success() }); @@ -3387,7 +3445,14 @@ mod tests { assert_eq!(ctx.ext.nonce(), 1); // Should not change with a failed instantiation assert_err!( - ctx.ext.instantiate(Weight::zero(), fail_code, 0, vec![], &[],), + ctx.ext.instantiate( + Weight::zero(), + BalanceOf::::zero(), + fail_code, + 0, + vec![], + &[], + ), ExecError { error: >::ContractTrapped.into(), origin: ErrorOrigin::Callee @@ -3395,7 +3460,16 @@ mod tests { ); assert_eq!(ctx.ext.nonce(), 1); // Successful instantiation increments - ctx.ext.instantiate(Weight::zero(), success_code, 0, vec![], &[]).unwrap(); + ctx.ext + .instantiate( + Weight::zero(), + BalanceOf::::zero(), + success_code, + 0, + vec![], + &[], + ) + .unwrap(); assert_eq!(ctx.ext.nonce(), 2); exec_success() }); diff --git a/frame/contracts/src/lib.rs b/frame/contracts/src/lib.rs index 118da36ae88a1..5a82a4a42b4a9 100644 --- a/frame/contracts/src/lib.rs +++ b/frame/contracts/src/lib.rs @@ -15,9 +15,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! # Contract Pallet +//! # Contracts Pallet //! -//! The Contract module provides functionality for the runtime to deploy and execute WebAssembly +//! The Contracts module provides functionality for the runtime to deploy and execute WebAssembly //! smart-contracts. //! //! - [`Config`] @@ -73,7 +73,7 @@ //! //! ## Usage //! -//! The Contract module is a work in progress. The following examples show how this Contract module +//! The Contracts module is a work in progress. The following examples show how this module //! can be used to instantiate and call contracts. //! //! * [`ink!`](https://use.ink) is @@ -265,6 +265,10 @@ pub mod pallet { #[pallet::constant] type DepositPerByte: Get>; + /// Fallback value to limit the storage deposit if it's not being set by the caller. + #[pallet::constant] + type DefaultDepositLimit: Get>; + /// The amount of balance a caller has to pay for each storage item. /// /// # Note @@ -315,8 +319,8 @@ pub mod pallet { } fn integrity_test() { - // Total runtime memory is expected to have 128Mb upper limit - const MAX_RUNTIME_MEM: u32 = 1024 * 1024 * 128; + // Total runtime memory limit + let max_runtime_mem: u32 = T::Schedule::get().limits.runtime_memory; // Memory limits for a single contract: // Value stack size: 1Mb per contract, default defined in wasmi const MAX_STACK_SIZE: u32 = 1024 * 1024; @@ -350,10 +354,10 @@ pub mod pallet { // This gives us the following formula: // // `(MaxCodeLen * 18 * 4 + MAX_STACK_SIZE + max_heap_size) * max_call_depth < - // MAX_RUNTIME_MEM/2` + // max_runtime_mem/2` // // Hence the upper limit for the `MaxCodeLen` can be defined as follows: - let code_len_limit = MAX_RUNTIME_MEM + let code_len_limit = max_runtime_mem .saturating_div(2) .saturating_div(max_call_depth) .saturating_sub(max_heap_size) diff --git a/frame/contracts/src/schedule.rs b/frame/contracts/src/schedule.rs index 747540bce6359..aef2fa1ca7dc1 100644 --- a/frame/contracts/src/schedule.rs +++ b/frame/contracts/src/schedule.rs @@ -127,6 +127,10 @@ pub struct Limits { /// The maximum size of a storage value and event payload in bytes. pub payload_len: u32, + + /// The maximum node runtime memory. This is for integrity checks only and does not affect the + /// real setting. + pub runtime_memory: u32, } impl Limits { @@ -479,6 +483,7 @@ impl Default for Limits { br_table_size: 256, subject_len: 32, payload_len: 16 * 1024, + runtime_memory: 1024 * 1024 * 128, } } } diff --git a/frame/contracts/src/storage/meter.rs b/frame/contracts/src/storage/meter.rs index 51a0af574bcd5..b8bbd6dc4178b 100644 --- a/frame/contracts/src/storage/meter.rs +++ b/frame/contracts/src/storage/meter.rs @@ -23,7 +23,7 @@ use crate::{ }; use codec::Encode; use frame_support::{ - dispatch::DispatchError, + dispatch::{fmt::Debug, DispatchError}, ensure, traits::{ tokens::{Fortitude::Polite, Preservation::Protect, WithdrawConsequence}, @@ -32,7 +32,10 @@ use frame_support::{ DefaultNoBound, RuntimeDebugNoBound, }; use pallet_contracts_primitives::StorageDeposit as Deposit; -use sp_runtime::{traits::Saturating, FixedPointNumber, FixedU128}; +use sp_runtime::{ + traits::{Saturating, Zero}, + FixedPointNumber, FixedU128, +}; use sp_std::{marker::PhantomData, vec::Vec}; /// Deposit that uses the native currency's balance type. @@ -96,17 +99,24 @@ pub enum ReservingExt {} pub trait State: private::Sealed {} /// State parameter that constitutes a meter that is in its root state. -pub enum Root {} +#[derive(Default, Debug)] +pub struct Root; /// State parameter that constitutes a meter that is in its nested state. -pub enum Nested {} +/// Its value indicates whether the nested meter has its own limit. +#[derive(DefaultNoBound, RuntimeDebugNoBound)] +pub enum Nested { + #[default] + DerivedLimit, + OwnLimit, +} impl State for Root {} impl State for Nested {} /// A type that allows the metering of consumed or freed storage of a single contract call stack. #[derive(DefaultNoBound, RuntimeDebugNoBound)] -pub struct RawMeter { +pub struct RawMeter { /// The limit of how much balance this meter is allowed to consume. limit: BalanceOf, /// The amount of balance that was used in this meter and all of its already absorbed children. @@ -118,8 +128,10 @@ pub struct RawMeter { /// We only have one charge per contract hence the size of this vector is /// limited by the maximum call depth. charges: Vec>, - /// Type parameters are only used in impls. - _phantom: PhantomData<(E, S)>, + /// We store the nested state to determine if it has a special limit for sub-call. + nested: S, + /// Type parameter only used in impls. + _phantom: PhantomData, } /// This type is used to describe a storage change when charging from the meter. @@ -214,6 +226,9 @@ impl Diff { /// this we can do all the refunds before doing any charge. This way a plain account can use /// more deposit than it has balance as along as it is covered by a refund. This /// essentially makes the order of storage changes irrelevant with regard to the deposit system. +/// The only exception is when a special (tougher) deposit limit is specified for a cross-contract +/// call. In that case the limit is enforced once the call is returned, rolling it back if +/// exhausted. #[derive(RuntimeDebugNoBound, Clone)] struct Charge { deposit_account: DepositAccount, @@ -255,16 +270,24 @@ impl RawMeter where T: Config, E: Ext, - S: State, + S: State + Default + Debug, { - /// Create a new child that has its `limit` set to whatever is remaining of it. + /// Create a new child that has its `limit`. + /// Passing `0` as the limit is interpreted as to take whatever is remaining from its parent. /// /// This is called whenever a new subcall is initiated in order to track the storage /// usage for this sub call separately. This is necessary because we want to exchange balance /// with the current contract we are interacting with. - pub fn nested(&self) -> RawMeter { + pub fn nested(&self, limit: BalanceOf) -> RawMeter { debug_assert!(self.is_alive()); - RawMeter { limit: self.available(), ..Default::default() } + // If a special limit is specified higher than it is available, + // we want to enforce the lesser limit to the nested meter, to fail in the sub-call. + let limit = self.available().min(limit); + if limit.is_zero() { + RawMeter { limit: self.available(), ..Default::default() } + } else { + RawMeter { limit, nested: Nested::OwnLimit, ..Default::default() } + } } /// Absorb a child that was spawned to handle a sub call. @@ -397,7 +420,7 @@ where self.total_deposit = deposit.clone(); info.storage_base_deposit = deposit.charge_or_zero(); - // Usually, deposit charges are deferred to be able to coalesce them with refunds. + // Normally, deposit charges are deferred to be able to coalesce them with refunds. // However, we need to charge immediately so that the account is created before // charges possibly below the ed are collected and fail. E::charge( @@ -429,8 +452,10 @@ where /// /// # Note /// - /// We only need to call this **once** for every call stack and not for every cross contract - /// call. Hence this is only called when the last call frame returns. + /// We normally need to call this **once** for every call stack and not for every cross contract + /// call. However, if a dedicated limit is specified for a sub-call, this needs to be called + /// once the sub-call has returned. For this, the [`Self::enforce_subcall_limit`] wrapper is + /// used. pub fn enforce_limit( &mut self, info: Option<&mut ContractInfo>, @@ -448,6 +473,18 @@ where } Ok(()) } + + /// This is a wrapper around [`Self::enforce_limit`] to use on the exit from a sub-call to + /// enforce its special limit if needed. + pub fn enforce_subcall_limit( + &mut self, + info: Option<&mut ContractInfo>, + ) -> Result<(), DispatchError> { + match self.nested { + Nested::OwnLimit => self.enforce_limit(info), + Nested::DerivedLimit => Ok(()), + } + } } impl Ext for ReservingExt { @@ -462,7 +499,8 @@ impl Ext for ReservingExt { let max = T::Currency::reducible_balance(origin, Protect, Polite) .saturating_sub(min_leftover) .saturating_sub(Pallet::::min_balance()); - let limit = limit.unwrap_or(max); + let default = max.min(T::DefaultDepositLimit::get()); + let limit = limit.unwrap_or(default); ensure!( limit <= max && matches!(T::Currency::can_withdraw(origin, limit), WithdrawConsequence::Success), @@ -673,7 +711,7 @@ mod tests { assert_eq!(meter.available(), 1_000); // an empty charge does not create a `Charge` entry - let mut nested0 = meter.nested(); + let mut nested0 = meter.nested(BalanceOf::::zero()); nested0.charge(&Default::default()); meter.absorb(nested0, DepositAccount(BOB), None); @@ -695,7 +733,7 @@ mod tests { let mut nested0_info = new_info(StorageInfo { bytes: 100, items: 5, bytes_deposit: 100, items_deposit: 10 }); - let mut nested0 = meter.nested(); + let mut nested0 = meter.nested(BalanceOf::::zero()); nested0.charge(&Diff { bytes_added: 108, bytes_removed: 5, @@ -706,13 +744,13 @@ mod tests { let mut nested1_info = new_info(StorageInfo { bytes: 100, items: 10, bytes_deposit: 100, items_deposit: 20 }); - let mut nested1 = nested0.nested(); + let mut nested1 = nested0.nested(BalanceOf::::zero()); nested1.charge(&Diff { items_removed: 5, ..Default::default() }); nested0.absorb(nested1, DepositAccount(CHARLIE), Some(&mut nested1_info)); let mut nested2_info = new_info(StorageInfo { bytes: 100, items: 7, bytes_deposit: 100, items_deposit: 20 }); - let mut nested2 = nested0.nested(); + let mut nested2 = nested0.nested(BalanceOf::::zero()); nested2.charge(&Diff { items_removed: 7, ..Default::default() }); nested0.absorb(nested2, DepositAccount(CHARLIE), Some(&mut nested2_info)); @@ -760,7 +798,7 @@ mod tests { let mut meter = TestMeter::new(&ALICE, Some(1_000), 0).unwrap(); assert_eq!(meter.available(), 1_000); - let mut nested0 = meter.nested(); + let mut nested0 = meter.nested(BalanceOf::::zero()); nested0.charge(&Diff { bytes_added: 5, bytes_removed: 1, @@ -771,7 +809,7 @@ mod tests { let mut nested1_info = new_info(StorageInfo { bytes: 100, items: 10, bytes_deposit: 100, items_deposit: 20 }); - let mut nested1 = nested0.nested(); + let mut nested1 = nested0.nested(BalanceOf::::zero()); nested1.charge(&Diff { items_removed: 5, ..Default::default() }); nested1.charge(&Diff { bytes_added: 20, ..Default::default() }); nested1.terminate(&nested1_info); diff --git a/frame/contracts/src/tests.rs b/frame/contracts/src/tests.rs index 4e6c468f19c81..7b2bb04428a6f 100644 --- a/frame/contracts/src/tests.rs +++ b/frame/contracts/src/tests.rs @@ -16,6 +16,7 @@ // limitations under the License. use self::test_utils::hash; +use crate as pallet_contracts; use crate::{ chain_extension::{ ChainExtension, Environment, Ext, InitState, RegisteredChainExtension, @@ -44,6 +45,7 @@ use frame_support::{ }; use frame_system::{EventRecord, Phase}; use pretty_assertions::{assert_eq, assert_ne}; +use sp_core::ByteArray; use sp_io::hashing::blake2_256; use sp_keystore::{testing::MemoryKeystore, KeystoreExt}; use sp_runtime::{ @@ -53,8 +55,6 @@ use sp_runtime::{ }; use std::ops::Deref; -use crate as pallet_contracts; - type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -345,6 +345,8 @@ parameter_types! { }; pub static DepositPerByte: BalanceOf = 1; pub const DepositPerItem: BalanceOf = 2; + // We need this one set high enough for running benchmarks. + pub static DefaultDepositLimit: BalanceOf = 10_000_000; } impl Convert> for Test { @@ -402,6 +404,7 @@ impl Config for Test { type Schedule = MySchedule; type DepositPerByte = DepositPerByte; type DepositPerItem = DepositPerItem; + type DefaultDepositLimit = DefaultDepositLimit; type AddressGenerator = DefaultAddressGenerator; type MaxCodeLen = ConstU32<{ 123 * 1024 }>; type MaxStorageKeyLen = ConstU32<128>; @@ -1110,7 +1113,7 @@ fn cannot_self_destruct_through_draning() { #[test] fn cannot_self_destruct_through_storage_refund_after_price_change() { - let (wasm, _code_hash) = compile_module::("store").unwrap(); + let (wasm, _code_hash) = compile_module::("store_call").unwrap(); ExtBuilder::default().existential_deposit(200).build().execute_with(|| { let _ = Balances::deposit_creating(&ALICE, 1_000_000); let min_balance = ::Currency::minimum_balance(); @@ -2628,7 +2631,6 @@ fn gas_estimation_nested_call_fixed_limit() { ExtBuilder::default().existential_deposit(50).build().execute_with(|| { let min_balance = ::Currency::minimum_balance(); let _ = Balances::deposit_creating(&ALICE, 1000 * min_balance); - let _ = Balances::deposit_creating(&CHARLIE, 1000 * min_balance); let addr_caller = Contracts::bare_instantiate( ALICE, @@ -2662,6 +2664,7 @@ fn gas_estimation_nested_call_fixed_limit() { .iter() .cloned() .chain((GAS_LIMIT / 5).ref_time().to_le_bytes()) + .chain((GAS_LIMIT / 5).proof_size().to_le_bytes()) .collect(); // Call in order to determine the gas that is required for this call @@ -2678,22 +2681,36 @@ fn gas_estimation_nested_call_fixed_limit() { assert_ok!(&result.result); // We have a subcall with a fixed gas limit. This constitutes precharging. - assert!(result.gas_required.ref_time() > result.gas_consumed.ref_time()); + assert!(result.gas_required.all_gt(result.gas_consumed)); // Make the same call using the estimated gas. Should succeed. assert_ok!( Contracts::bare_call( ALICE, - addr_caller, + addr_caller.clone(), 0, result.gas_required, Some(result.storage_deposit.charge_or_zero()), - input, + input.clone(), false, Determinism::Enforced, ) .result ); + + // Make the same call using proof_size a but less than estimated. Should fail with OutOfGas. + let result = Contracts::bare_call( + ALICE, + addr_caller, + 0, + result.gas_required.sub_proof_size(1), + Some(result.storage_deposit.charge_or_zero()), + input, + false, + Determinism::Enforced, + ) + .result; + assert_err!(result, >::OutOfGas); }); } @@ -2774,7 +2791,7 @@ fn gas_estimation_call_runtime() { } #[test] -fn gas_call_runtime_reentrancy_guarded() { +fn call_runtime_reentrancy_guarded() { let (caller_code, _caller_hash) = compile_module::("call_runtime").unwrap(); let (callee_code, _callee_hash) = compile_module::("dummy").unwrap(); ExtBuilder::default().existential_deposit(50).build().execute_with(|| { @@ -3968,7 +3985,7 @@ fn set_code_hash() { #[test] fn storage_deposit_limit_is_enforced() { - let (wasm, _code_hash) = compile_module::("store").unwrap(); + let (wasm, _code_hash) = compile_module::("store_call").unwrap(); ExtBuilder::default().existential_deposit(200).build().execute_with(|| { let _ = Balances::deposit_creating(&ALICE, 1_000_000); let min_balance = ::Currency::minimum_balance(); @@ -3992,15 +4009,47 @@ fn storage_deposit_limit_is_enforced() { assert_eq!(get_contract(&addr).total_deposit(), min_balance); assert_eq!(::Currency::total_balance(&addr), min_balance); - // Create 100 bytes of storage with a price of per byte + // Create 1 byte of storage with a price of per byte, + // setting insufficient deposit limit, as it requires 3 Balance: + // 2 for the item added + 1 for the new storage item. assert_err_ignore_postinfo!( Contracts::call( RuntimeOrigin::signed(ALICE), addr.clone(), 0, GAS_LIMIT, - Some(codec::Compact(1)), - 100u32.to_le_bytes().to_vec() + Some(codec::Compact(2)), + 1u32.to_le_bytes().to_vec() + ), + >::StorageDepositLimitExhausted, + ); + + // To check that deposit limit fallbacks to DefaultDepositLimit, + // we customize it here. + DEFAULT_DEPOSIT_LIMIT.with(|c| *c.borrow_mut() = 3); + + // Create 1 byte of storage, should cost 3 Balance: + // 2 for the item added + 1 for the new storage item. + // Should pass as it fallbacks to DefaultDepositLimit. + assert_ok!(Contracts::call( + RuntimeOrigin::signed(ALICE), + addr.clone(), + 0, + GAS_LIMIT, + None, + 1u32.to_le_bytes().to_vec() + )); + + // Use 4 more bytes of the storage for the same item, which requires 4 Balance. + // Should fail as DefaultDepositLimit is 3 and hence isn't enough. + assert_err_ignore_postinfo!( + Contracts::call( + RuntimeOrigin::signed(ALICE), + addr.clone(), + 0, + GAS_LIMIT, + None, + 5u32.to_le_bytes().to_vec() ), >::StorageDepositLimitExhausted, ); @@ -4008,10 +4057,10 @@ fn storage_deposit_limit_is_enforced() { } #[test] -fn storage_deposit_limit_is_enforced_late() { +fn deposit_limit_in_nested_calls() { let (wasm_caller, _code_hash_caller) = compile_module::("create_storage_and_call").unwrap(); - let (wasm_callee, _code_hash_callee) = compile_module::("store").unwrap(); + let (wasm_callee, _code_hash_callee) = compile_module::("store_call").unwrap(); ExtBuilder::default().existential_deposit(200).build().execute_with(|| { let _ = Balances::deposit_creating(&ALICE, 1_000_000); @@ -4054,27 +4103,28 @@ fn storage_deposit_limit_is_enforced_late() { 100u32.to_le_bytes().to_vec() )); - // We do not remove any storage but require 14 bytes of storage for the new - // storage created in the immediate contract. + // We do not remove any storage but add a storage item of 12 bytes in the caller + // contract. This would cost 12 + 2 = 14 Balance. + // The nested call doesn't get a special limit, which is set by passing 0 to it. + // This should fail as the specified parent's limit is less than the cost: 13 < + // 14. assert_err_ignore_postinfo!( Contracts::call( RuntimeOrigin::signed(ALICE), addr_caller.clone(), 0, GAS_LIMIT, - Some(codec::Compact(5)), - 100u32 - .to_le_bytes() - .as_ref() - .iter() - .chain(<_ as AsRef<[u8]>>::as_ref(&addr_callee)) - .cloned() - .collect(), + Some(codec::Compact(13)), + (100u32, &addr_callee, 0u64).encode(), ), >::StorageDepositLimitExhausted, ); - - // Allow for the additional 14 bytes but demand an additional byte in the callee contract. + // Now we specify the parent's limit high enough to cover the caller's storage additions. + // However, we use a single byte more in the callee, hence the storage deposit should be 15 + // Balance. + // The nested call doesn't get a special limit, which is set by passing 0 to it. + // This should fail as the specified parent's limit is less than the cost: 14 + // < 15. assert_err_ignore_postinfo!( Contracts::call( RuntimeOrigin::signed(ALICE), @@ -4082,19 +4132,31 @@ fn storage_deposit_limit_is_enforced_late() { 0, GAS_LIMIT, Some(codec::Compact(14)), - 101u32 - .to_le_bytes() - .as_ref() - .iter() - .chain(<_ as AsRef<[u8]>>::as_ref(&addr_callee)) - .cloned() - .collect(), + (101u32, &addr_callee, 0u64).encode(), ), >::StorageDepositLimitExhausted, ); - // Refund in the callee contract but not enough to cover the 14 balance required by the - // caller. + // Now we specify the parent's limit high enough to cover both the caller's and callee's + // storage additions. However, we set a special deposit limit of 1 Balance for the nested + // call. This should fail as callee adds up 2 bytes to the storage, meaning that the nested + // call should have a deposit limit of at least 2 Balance. The sub-call should be rolled + // back, which is covered by the next test case. + assert_err_ignore_postinfo!( + Contracts::call( + RuntimeOrigin::signed(ALICE), + addr_caller.clone(), + 0, + GAS_LIMIT, + Some(codec::Compact(16)), + (102u32, &addr_callee, 1u64).encode(), + ), + >::StorageDepositLimitExhausted, + ); + + // Refund in the callee contract but not enough to cover the 14 Balance required by the + // caller. Note that if previous sub-call wouldn't roll back, this call would pass making + // the test case fail. We don't set a special limit for the nested call here. assert_err_ignore_postinfo!( Contracts::call( RuntimeOrigin::signed(ALICE), @@ -4102,59 +4164,198 @@ fn storage_deposit_limit_is_enforced_late() { 0, GAS_LIMIT, Some(codec::Compact(0)), - 87u32 - .to_le_bytes() - .as_ref() - .iter() - .chain(<_ as AsRef<[u8]>>::as_ref(&addr_callee)) - .cloned() - .collect(), + (87u32, &addr_callee, 0u64).encode(), ), >::StorageDepositLimitExhausted, ); let _ = Balances::make_free_balance_be(&ALICE, 1_000); - // Send more than the sender has balance. + // Require more than the sender's balance. + // We don't set a special limit for the nested call. assert_err_ignore_postinfo!( Contracts::call( RuntimeOrigin::signed(ALICE), addr_caller.clone(), 0, GAS_LIMIT, - Some(codec::Compact(50)), - 1_200u32 - .to_le_bytes() - .as_ref() - .iter() - .chain(<_ as AsRef<[u8]>>::as_ref(&addr_callee)) - .cloned() - .collect(), + None, + (1200u32, &addr_callee, 1u64).encode(), ), >::StorageDepositLimitExhausted, ); - // Same as above but allow for the additional balance. + // Same as above but allow for the additional deposit of 1 Balance in parent. + // We set the special deposit limit of 1 Balance for the nested call, which isn't + // enforced as callee frees up storage. This should pass. assert_ok!(Contracts::call( RuntimeOrigin::signed(ALICE), addr_caller.clone(), 0, GAS_LIMIT, Some(codec::Compact(1)), - 87u32 - .to_le_bytes() - .as_ref() - .iter() - .chain(<_ as AsRef<[u8]>>::as_ref(&addr_callee)) - .cloned() - .collect(), + (87u32, &addr_callee, 1u64).encode(), )); }); } +#[test] +fn deposit_limit_in_nested_instantiate() { + let (wasm_caller, _code_hash_caller) = + compile_module::("create_storage_and_instantiate").unwrap(); + let (wasm_callee, code_hash_callee) = compile_module::("store_deploy").unwrap(); + const ED: u64 = 5; + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let _ = Balances::deposit_creating(&ALICE, 1_000_000); + let _ = Balances::deposit_creating(&BOB, 1_000_000); + // Create caller contract + let addr_caller = Contracts::bare_instantiate( + ALICE, + 10_000u64, // this balance is later passed to the deployed contract + GAS_LIMIT, + None, + Code::Upload(wasm_caller), + vec![], + vec![], + false, + ) + .result + .unwrap() + .account_id; + // Deploy a contract to get its occupied storage size + let addr = Contracts::bare_instantiate( + ALICE, + 0, + GAS_LIMIT, + None, + Code::Upload(wasm_callee), + vec![0, 0, 0, 0], + vec![], + false, + ) + .result + .unwrap() + .account_id; + + let callee_info_len = ContractInfoOf::::get(&addr).unwrap().encoded_size() as u64; + + // We don't set a special deposit limit for the nested instantiation. + // + // The deposit limit set for the parent is insufficient for the instantiation, which + // requires: + // - callee_info_len + 2 for storing the new contract info, + // - ED for deployed contract account, + // - 2 for the storage item of 0 bytes being created in the callee constructor + // or (callee_info_len + 2 + ED + 2) Balance in total. + // + // Provided the limit is set to be 1 Balance less, + // this call should fail on the return from the caller contract. + assert_err_ignore_postinfo!( + Contracts::call( + RuntimeOrigin::signed(BOB), + addr_caller.clone(), + 0, + GAS_LIMIT, + Some(codec::Compact(callee_info_len + 2 + ED + 1)), + (0u32, &code_hash_callee, 0u64).encode(), + ), + >::StorageDepositLimitExhausted, + ); + // The charges made on instantiation should be rolled back. + assert_eq!(::Currency::free_balance(&BOB), 1_000_000); + + // Now we give enough limit for the instantiation itself, but require for 1 more storage + // byte in the constructor. Hence +1 Balance to the limit is needed. This should fail on the + // return from constructor. + assert_err_ignore_postinfo!( + Contracts::call( + RuntimeOrigin::signed(BOB), + addr_caller.clone(), + 0, + GAS_LIMIT, + Some(codec::Compact(callee_info_len + 2 + ED + 2)), + (1u32, &code_hash_callee, 0u64).encode(), + ), + >::StorageDepositLimitExhausted, + ); + // The charges made on the instantiation should be rolled back. + assert_eq!(::Currency::free_balance(&BOB), 1_000_000); + + // Now we set enough limit in parent call, but an insufficient limit for child instantiate. + // This should fail during the charging for the instantiation in + // `RawMeter::charge_instantiate()` + assert_err_ignore_postinfo!( + Contracts::call( + RuntimeOrigin::signed(BOB), + addr_caller.clone(), + 0, + GAS_LIMIT, + Some(codec::Compact(callee_info_len + 2 + ED + 2)), + (0u32, &code_hash_callee, callee_info_len + 2 + ED + 1).encode(), + ), + >::StorageDepositLimitExhausted, + ); + // The charges made on the instantiation should be rolled back. + assert_eq!(::Currency::free_balance(&BOB), 1_000_000); + + // Same as above but requires for single added storage + // item of 1 byte to be covered by the limit, which implies 3 more Balance. + // Now we set enough limit for the parent call, but insufficient limit for child + // instantiate. This should fail right after the constructor execution. + assert_err_ignore_postinfo!( + Contracts::call( + RuntimeOrigin::signed(BOB), + addr_caller.clone(), + 0, + GAS_LIMIT, + Some(codec::Compact(callee_info_len + 2 + ED + 3)), // enough parent limit + (1u32, &code_hash_callee, callee_info_len + 2 + ED + 2).encode(), + ), + >::StorageDepositLimitExhausted, + ); + // The charges made on the instantiation should be rolled back. + assert_eq!(::Currency::free_balance(&BOB), 1_000_000); + + // Set enough deposit limit for the child instantiate. This should succeed. + let result = Contracts::bare_call( + BOB, + addr_caller.clone(), + 0, + GAS_LIMIT, + Some(codec::Compact(callee_info_len + 2 + ED + 4).into()), + (1u32, &code_hash_callee, callee_info_len + 2 + ED + 3).encode(), + false, + Determinism::Enforced, + ); + + let returned = result.result.unwrap(); + // All balance of the caller except ED has been transferred to the callee. + // No deposit has been taken from it. + assert_eq!(::Currency::free_balance(&addr_caller), ED); + // Get address of the deployed contract. + let addr_callee = AccountId32::from_slice(&returned.data[0..32]).unwrap(); + // 10_000 should be sent to callee from the caller contract, plus ED to be sent from the + // origin. + assert_eq!(::Currency::free_balance(&addr_callee), 10_000 + ED); + // The origin should be charged with: + // - callee instantiation deposit = (callee_info_len + 2) + // - callee account ED + // - for writing an item of 1 byte to storage = 3 Balance + // + // Still, the latter is to be charged at the end of the call stack, hence + // only (callee_info_len + 2 + ED) is charged so far + assert_eq!( + ::Currency::free_balance(&BOB), + 1_000_000 - (callee_info_len + 2 + ED) + ); + // Check that deposit due to be charged still includes these 3 Balance + assert_eq!(result.storage_deposit.charge_or_zero(), (callee_info_len + 2 + ED + 3),) + }); +} + #[test] fn deposit_limit_honors_liquidity_restrictions() { - let (wasm, _code_hash) = compile_module::("store").unwrap(); + let (wasm, _code_hash) = compile_module::("store_call").unwrap(); ExtBuilder::default().existential_deposit(200).build().execute_with(|| { let _ = Balances::deposit_creating(&ALICE, 1_000_000); let _ = Balances::deposit_creating(&BOB, 1_000); @@ -4198,7 +4399,7 @@ fn deposit_limit_honors_liquidity_restrictions() { #[test] fn deposit_limit_honors_existential_deposit() { - let (wasm, _code_hash) = compile_module::("store").unwrap(); + let (wasm, _code_hash) = compile_module::("store_call").unwrap(); ExtBuilder::default().existential_deposit(200).build().execute_with(|| { let _ = Balances::deposit_creating(&ALICE, 1_000_000); let _ = Balances::deposit_creating(&BOB, 1_000); @@ -4241,7 +4442,7 @@ fn deposit_limit_honors_existential_deposit() { #[test] fn deposit_limit_honors_min_leftover() { - let (wasm, _code_hash) = compile_module::("store").unwrap(); + let (wasm, _code_hash) = compile_module::("store_call").unwrap(); ExtBuilder::default().existential_deposit(200).build().execute_with(|| { let _ = Balances::deposit_creating(&ALICE, 1_000_000); let _ = Balances::deposit_creating(&BOB, 1_000); @@ -4266,7 +4467,7 @@ fn deposit_limit_honors_min_leftover() { assert_eq!(get_contract(&addr).total_deposit(), min_balance); assert_eq!(::Currency::total_balance(&addr), min_balance); - // check that the minumum leftover (value send) is considered + // check that the minimum leftover (value send) is considered assert_err_ignore_postinfo!( Contracts::call( RuntimeOrigin::signed(BOB), diff --git a/frame/contracts/src/wasm/mod.rs b/frame/contracts/src/wasm/mod.rs index 4723f0c833654..5be28a301d8f4 100644 --- a/frame/contracts/src/wasm/mod.rs +++ b/frame/contracts/src/wasm/mod.rs @@ -370,7 +370,7 @@ mod tests { gas::GasMeter, storage::WriteOutcome, tests::{RuntimeCall, Test, ALICE, BOB}, - BalanceOf, CodeHash, Error, OldWeight, Pallet as Contracts, + BalanceOf, CodeHash, Error, Pallet as Contracts, }; use assert_matches::assert_matches; use frame_support::{ @@ -470,6 +470,7 @@ mod tests { fn call( &mut self, _gas_limit: Weight, + _deposit_limit: BalanceOf, to: AccountIdOf, value: u64, data: Vec, @@ -489,6 +490,7 @@ mod tests { fn instantiate( &mut self, gas_limit: Weight, + _deposit_limit: BalanceOf, code_hash: CodeHash, value: u64, data: Vec, @@ -587,7 +589,11 @@ mod tests { 16_384 } fn get_weight_price(&self, weight: Weight) -> BalanceOf { - BalanceOf::::from(1312_u32).saturating_mul(weight.ref_time().into()) + BalanceOf::::from(1312_u32) + .saturating_mul(weight.ref_time().into()) + .saturating_add( + BalanceOf::::from(103_u32).saturating_mul(weight.proof_size()), + ) } fn schedule(&self) -> &Schedule { &self.schedule @@ -1589,7 +1595,7 @@ mod tests { const CODE_GAS_PRICE: &str = r#" (module - (import "seal0" "seal_weight_to_fee" (func $seal_weight_to_fee (param i64 i32 i32))) + (import "seal1" "weight_to_fee" (func $seal_weight_to_fee (param i64 i64 i32 i32))) (import "env" "memory" (memory 1 1)) ;; size of our buffer is 32 bytes @@ -1606,7 +1612,7 @@ mod tests { (func (export "call") ;; This stores the gas price in the buffer - (call $seal_weight_to_fee (i64.const 2) (i32.const 0) (i32.const 32)) + (call $seal_weight_to_fee (i64.const 2) (i64.const 1) (i32.const 0) (i32.const 32)) ;; assert len == 8 (call $assert @@ -1616,11 +1622,11 @@ mod tests { ) ) - ;; assert that contents of the buffer is equal to the i64 value of 2 * 1312. + ;; assert that contents of the buffer is equal to the i64 value of 2 * 1312 + 103 = 2727. (call $assert (i64.eq (i64.load (i32.const 0)) - (i64.const 2624) + (i64.const 2727) ) ) ) @@ -1635,12 +1641,12 @@ mod tests { const CODE_GAS_LEFT: &str = r#" (module - (import "seal0" "seal_gas_left" (func $seal_gas_left (param i32 i32))) + (import "seal1" "gas_left" (func $seal_gas_left (param i32 i32))) (import "seal0" "seal_return" (func $seal_return (param i32 i32 i32))) (import "env" "memory" (memory 1 1)) - ;; size of our buffer is 32 bytes - (data (i32.const 32) "\20") + ;; Make output buffer size 20 bytes + (data (i32.const 20) "\14") (func $assert (param i32) (block $ok @@ -1652,19 +1658,19 @@ mod tests { ) (func (export "call") - ;; This stores the gas left in the buffer - (call $seal_gas_left (i32.const 0) (i32.const 32)) + ;; This stores the weight left to the buffer + (call $seal_gas_left (i32.const 0) (i32.const 20)) - ;; assert len == 8 + ;; Assert len <= 16 (max encoded Weight len) (call $assert - (i32.eq - (i32.load (i32.const 32)) - (i32.const 8) + (i32.le_u + (i32.load (i32.const 20)) + (i32.const 16) ) ) - ;; return gas left - (call $seal_return (i32.const 0) (i32.const 0) (i32.const 8)) + ;; Return weight left and its encoded value len + (call $seal_return (i32.const 0) (i32.const 0) (i32.load (i32.const 20))) (unreachable) ) @@ -1679,11 +1685,11 @@ mod tests { let output = execute(CODE_GAS_LEFT, vec![], &mut ext).unwrap(); - let gas_left = OldWeight::decode(&mut &*output.data).unwrap(); + let weight_left = Weight::decode(&mut &*output.data).unwrap(); let actual_left = ext.gas_meter.gas_left(); - // TODO: account for proof size weight - assert!(gas_left < gas_limit.ref_time(), "gas_left must be less than initial"); - assert!(gas_left > actual_left.ref_time(), "gas_left must be greater than final"); + + assert!(weight_left.all_lt(gas_limit), "gas_left must be less than initial"); + assert!(weight_left.all_gt(actual_left), "gas_left must be greater than final"); } /// Test that [`frame_support::weights::OldWeight`] en/decodes the same as our diff --git a/frame/contracts/src/wasm/runtime.rs b/frame/contracts/src/wasm/runtime.rs index bfb991ae51dfa..92ae2e3e6ea57 100644 --- a/frame/contracts/src/wasm/runtime.rs +++ b/frame/contracts/src/wasm/runtime.rs @@ -433,7 +433,7 @@ bitflags! { /// The kind of call that should be performed. enum CallType { /// Execute another instantiated contract - Call { callee_ptr: u32, value_ptr: u32, gas: u64 }, + Call { callee_ptr: u32, value_ptr: u32, deposit_ptr: u32, weight: Weight }, /// Execute deployed code in the context (storage, account ID, value) of the caller contract DelegateCall { code_hash_ptr: u32 }, } @@ -873,16 +873,22 @@ impl<'a, E: Ext + 'a> Runtime<'a, E> { }; let call_outcome = match call_type { - CallType::Call { callee_ptr, value_ptr, gas } => { + CallType::Call { callee_ptr, value_ptr, deposit_ptr, weight } => { let callee: <::T as frame_system::Config>::AccountId = self.read_sandbox_memory_as(memory, callee_ptr)?; + let deposit_limit: BalanceOf<::T> = if deposit_ptr == SENTINEL { + BalanceOf::<::T>::zero() + } else { + self.read_sandbox_memory_as(memory, deposit_ptr)? + }; let value: BalanceOf<::T> = self.read_sandbox_memory_as(memory, value_ptr)?; if value > 0u32.into() { self.charge_gas(RuntimeCosts::CallSurchargeTransfer)?; } self.ext.call( - Weight::from_parts(gas, 0), + weight, + deposit_limit, callee, value, input_data, @@ -926,7 +932,8 @@ impl<'a, E: Ext + 'a> Runtime<'a, E> { &mut self, memory: &mut [u8], code_hash_ptr: u32, - gas: u64, + weight: Weight, + deposit_ptr: u32, value_ptr: u32, input_data_ptr: u32, input_data_len: u32, @@ -937,8 +944,12 @@ impl<'a, E: Ext + 'a> Runtime<'a, E> { salt_ptr: u32, salt_len: u32, ) -> Result { - let gas = Weight::from_parts(gas, 0); self.charge_gas(RuntimeCosts::InstantiateBase { input_data_len, salt_len })?; + let deposit_limit: BalanceOf<::T> = if deposit_ptr == SENTINEL { + BalanceOf::<::T>::zero() + } else { + self.read_sandbox_memory_as(memory, deposit_ptr)? + }; let value: BalanceOf<::T> = self.read_sandbox_memory_as(memory, value_ptr)?; if value > 0u32.into() { self.charge_gas(RuntimeCosts::InstantiateSurchargeTransfer)?; @@ -947,7 +958,8 @@ impl<'a, E: Ext + 'a> Runtime<'a, E> { self.read_sandbox_memory_as(memory, code_hash_ptr)?; let input_data = self.read_sandbox_memory(memory, input_data_ptr, input_data_len)?; let salt = self.read_sandbox_memory(memory, salt_ptr, salt_len)?; - let instantiate_outcome = self.ext.instantiate(gas, code_hash, value, input_data, &salt); + let instantiate_outcome = + self.ext.instantiate(weight, deposit_limit, code_hash, value, input_data, &salt); if let Ok((address, output)) = &instantiate_outcome { if !output.flags.contains(ReturnFlags::REVERT) { self.write_sandbox_output( @@ -993,6 +1005,7 @@ pub mod env { /// /// NOTE: This is a implementation defined call and is NOT a part of the public API. /// This call is supposed to be called only by instrumentation injected code. + /// It deals only with the *ref_time* Weight. /// /// - `amount`: How much gas is used. fn gas(ctx: _, _memory: _, amount: u64) -> Result<(), TrapReason> { @@ -1002,8 +1015,9 @@ pub mod env { /// Set the value at the given key in the contract storage. /// - /// Equivalent to the newer version [`super::seal1::Api::set_storage`] with the exception of the - /// return type. Still a valid thing to call when not interested in the return value. + /// Equivalent to the newer [`seal1`][`super::api_doc::Version1::set_storage`] version with the + /// exception of the return type. Still a valid thing to call when not interested in the return + /// value. #[prefixed_alias] fn set_storage( ctx: _, @@ -1076,8 +1090,9 @@ pub mod env { /// Clear the value at the given key in the contract storage. /// - /// Equivalent to the newer version [`super::seal1::Api::clear_storage`] with the exception of - /// the return type. Still a valid thing to call when not interested in the return value. + /// Equivalent to the newer [`seal1`][`super::api_doc::Version1::clear_storage`] version with + /// the exception of the return type. Still a valid thing to call when not interested in the + /// return value. #[prefixed_alias] fn clear_storage(ctx: _, memory: _, key_ptr: u32) -> Result<(), TrapReason> { ctx.clear_storage(memory, KeyType::Fix, key_ptr).map(|_| ()) @@ -1299,7 +1314,47 @@ pub mod env { ctx.call( memory, CallFlags::ALLOW_REENTRY, - CallType::Call { callee_ptr, value_ptr, gas }, + CallType::Call { + callee_ptr, + value_ptr, + deposit_ptr: SENTINEL, + weight: Weight::from_parts(gas, 0), + }, + input_data_ptr, + input_data_len, + output_ptr, + output_len_ptr, + ) + } + + /// Make a call to another contract. + /// + /// Equivalent to the newer [`seal2`][`super::api_doc::Version2::call`] version but works with + /// *ref_time* Weight only. It is recommended to switch to the latest version, once it's + /// stabilized. + #[version(1)] + #[prefixed_alias] + fn call( + ctx: _, + memory: _, + flags: u32, + callee_ptr: u32, + gas: u64, + value_ptr: u32, + input_data_ptr: u32, + input_data_len: u32, + output_ptr: u32, + output_len_ptr: u32, + ) -> Result { + ctx.call( + memory, + CallFlags::from_bits(flags).ok_or(Error::::InvalidCallFlags)?, + CallType::Call { + callee_ptr, + value_ptr, + deposit_ptr: SENTINEL, + weight: Weight::from_parts(gas, 0), + }, input_data_ptr, input_data_len, output_ptr, @@ -1318,7 +1373,12 @@ pub mod env { /// - `flags`: See `crate::wasm::runtime::CallFlags` for a documentation of the supported flags. /// - `callee_ptr`: a pointer to the address of the callee contract. Should be decodable as an /// `T::AccountId`. Traps otherwise. - /// - `gas`: how much gas to devote to the execution. + /// - `ref_time_limit`: how much *ref_time* Weight to devote to the execution. + /// - `proof_size_limit`: how much *proof_size* Weight to devote to the execution. + /// - `deposit_ptr`: a pointer to the buffer with value of the storage deposit limit for the + /// call. Should be decodable as a `T::Balance`. Traps otherwise. Passing `SENTINEL` means + /// setting no specific limit for the call, which implies storage usage up to the limit of the + /// parent call. /// - `value_ptr`: a pointer to the buffer with value, how much value to send. Should be /// decodable as a `T::Balance`. Traps otherwise. /// - `input_data_ptr`: a pointer to a buffer to be used as input data to the callee. @@ -1336,14 +1396,16 @@ pub mod env { /// - `ReturnCode::CalleeTrapped` /// - `ReturnCode::TransferFailed` /// - `ReturnCode::NotCallable` - #[version(1)] - #[prefixed_alias] + #[version(2)] + #[unstable] fn call( ctx: _, memory: _, flags: u32, callee_ptr: u32, - gas: u64, + ref_time_limit: u64, + proof_size_limit: u64, + deposit_ptr: u32, value_ptr: u32, input_data_ptr: u32, input_data_len: u32, @@ -1353,7 +1415,12 @@ pub mod env { ctx.call( memory, CallFlags::from_bits(flags).ok_or(Error::::InvalidCallFlags)?, - CallType::Call { callee_ptr, value_ptr, gas }, + CallType::Call { + callee_ptr, + value_ptr, + deposit_ptr, + weight: Weight::from_parts(ref_time_limit, proof_size_limit), + }, input_data_ptr, input_data_len, output_ptr, @@ -1417,8 +1484,7 @@ pub mod env { /// # Note /// /// The values `_code_hash_len` and `_value_len` are ignored because the encoded sizes - /// of those types are fixed through - /// [`codec::MaxEncodedLen`]. The fields exist + /// of those types are fixed through [`codec::MaxEncodedLen`]. The fields exist /// for backwards compatibility. Consider switching to the newest version of this function. #[prefixed_alias] fn instantiate( @@ -1441,7 +1507,47 @@ pub mod env { ctx.instantiate( memory, code_hash_ptr, - gas, + Weight::from_parts(gas, 0), + SENTINEL, + value_ptr, + input_data_ptr, + input_data_len, + address_ptr, + address_len_ptr, + output_ptr, + output_len_ptr, + salt_ptr, + salt_len, + ) + } + + /// Instantiate a contract with the specified code hash. + /// + /// Equivalent to the newer [`seal2`][`super::api_doc::Version2::instantiate`] version but works + /// with *ref_time* Weight only. It is recommended to switch to the latest version, once it's + /// stabilized. + #[version(1)] + #[prefixed_alias] + fn instantiate( + ctx: _, + memory: _, + code_hash_ptr: u32, + gas: u64, + value_ptr: u32, + input_data_ptr: u32, + input_data_len: u32, + address_ptr: u32, + address_len_ptr: u32, + output_ptr: u32, + output_len_ptr: u32, + salt_ptr: u32, + salt_len: u32, + ) -> Result { + ctx.instantiate( + memory, + code_hash_ptr, + Weight::from_parts(gas, 0), + SENTINEL, value_ptr, input_data_ptr, input_data_len, @@ -1468,15 +1574,21 @@ pub mod env { /// # Parameters /// /// - `code_hash_ptr`: a pointer to the buffer that contains the initializer code. - /// - `gas`: how much gas to devote to the execution of the initializer code. + /// - `ref_time_limit`: how much *ref_time* Weight to devote to the execution. + /// - `proof_size_limit`: how much *proof_size* Weight to devote to the execution. + /// - `deposit_ptr`: a pointer to the buffer with value of the storage deposit limit for + /// instantiation. Should be decodable as a `T::Balance`. Traps otherwise. Passing `SENTINEL` + /// means setting no specific limit for the call, which implies storage usage up to the limit + /// of the parent call. /// - `value_ptr`: a pointer to the buffer with value, how much value to send. Should be /// decodable as a `T::Balance`. Traps otherwise. /// - `input_data_ptr`: a pointer to a buffer to be used as input data to the initializer code. /// - `input_data_len`: length of the input data buffer. - /// - `address_ptr`: a pointer where the new account's address is copied to. - /// - `address_len_ptr`: in-out pointer to where the length of the buffer is read from and the - /// actual length is written to. - /// - `output_ptr`: a pointer where the output buffer is copied to. + /// - `address_ptr`: a pointer where the new account's address is copied to. `SENTINEL` means + /// not to copy. + /// - `address_len_ptr`: pointer to where put the length of the address. + /// - `output_ptr`: a pointer where the output buffer is copied to. `SENTINEL` means not to + /// copy. /// - `output_len_ptr`: in-out pointer to where the length of the buffer is read from and the /// actual length is written to. /// - `salt_ptr`: Pointer to raw bytes used for address derivation. See `fn contract_address`. @@ -1494,13 +1606,15 @@ pub mod env { /// - `ReturnCode::CalleeTrapped` /// - `ReturnCode::TransferFailed` /// - `ReturnCode::CodeNotFound` - #[version(1)] - #[prefixed_alias] + #[version(2)] + #[unstable] fn instantiate( ctx: _, memory: _, code_hash_ptr: u32, - gas: u64, + ref_time_limit: u64, + proof_size_limit: u64, + deposit_ptr: u32, value_ptr: u32, input_data_ptr: u32, input_data_len: u32, @@ -1514,7 +1628,8 @@ pub mod env { ctx.instantiate( memory, code_hash_ptr, - gas, + Weight::from_parts(ref_time_limit, proof_size_limit), + deposit_ptr, value_ptr, input_data_ptr, input_data_len, @@ -1762,49 +1877,99 @@ pub mod env { /// Stores the price for the specified amount of gas into the supplied buffer. /// - /// The value is stored to linear memory at the address pointed to by `out_ptr`. - /// `out_len_ptr` must point to a u32 value that describes the available space at - /// `out_ptr`. This call overwrites it with the size of the value. If the available - /// space at `out_ptr` is less than the size of the value a trap is triggered. + /// Equivalent to the newer [`seal1`][`super::api_doc::Version2::weight_to_fee`] version but + /// works with *ref_time* Weight only. It is recommended to switch to the latest version, once + /// it's stabilized. + #[prefixed_alias] + fn weight_to_fee( + ctx: _, + memory: _, + gas: u64, + out_ptr: u32, + out_len_ptr: u32, + ) -> Result<(), TrapReason> { + let gas = Weight::from_parts(gas, 0); + ctx.charge_gas(RuntimeCosts::WeightToFee)?; + Ok(ctx.write_sandbox_output( + memory, + out_ptr, + out_len_ptr, + &ctx.ext.get_weight_price(gas).encode(), + false, + already_charged, + )?) + } + + /// Stores the price for the specified amount of weight into the supplied buffer. + /// + /// # Parameters + /// + /// - `out_ptr`: pointer to the linear memory where the returning value is written to. If the + /// available space at `out_ptr` is less than the size of the value a trap is triggered. + /// - `out_len_ptr`: in-out pointer into linear memory where the buffer length is read from and + /// the value length is written to. /// /// The data is encoded as `T::Balance`. /// /// # Note /// - /// It is recommended to avoid specifying very small values for `gas` as the prices for a single - /// gas can be smaller than one. - #[prefixed_alias] + /// It is recommended to avoid specifying very small values for `ref_time_limit` and + /// `proof_size_limit` as the prices for a single gas can be smaller than the basic balance + /// unit. + #[version(1)] + #[unstable] fn weight_to_fee( ctx: _, memory: _, - gas: u64, + ref_time_limit: u64, + proof_size_limit: u64, out_ptr: u32, out_len_ptr: u32, ) -> Result<(), TrapReason> { - let gas = Weight::from_parts(gas, 0); + let weight = Weight::from_parts(ref_time_limit, proof_size_limit); ctx.charge_gas(RuntimeCosts::WeightToFee)?; Ok(ctx.write_sandbox_output( memory, out_ptr, out_len_ptr, - &ctx.ext.get_weight_price(gas).encode(), + &ctx.ext.get_weight_price(weight).encode(), false, already_charged, )?) } - /// Stores the amount of gas left into the supplied buffer. + /// Stores the weight left into the supplied buffer. + /// + /// Equivalent to the newer [`seal1`][`super::api_doc::Version2::gas_left`] version but + /// works with *ref_time* Weight only. It is recommended to switch to the latest version, once + /// it's stabilized. + #[prefixed_alias] + fn gas_left(ctx: _, memory: _, out_ptr: u32, out_len_ptr: u32) -> Result<(), TrapReason> { + ctx.charge_gas(RuntimeCosts::GasLeft)?; + let gas_left = &ctx.ext.gas_meter().gas_left().ref_time().encode(); + Ok(ctx.write_sandbox_output( + memory, + out_ptr, + out_len_ptr, + gas_left, + false, + already_charged, + )?) + } + + /// Stores the amount of weight left into the supplied buffer. /// /// The value is stored to linear memory at the address pointed to by `out_ptr`. /// `out_len_ptr` must point to a u32 value that describes the available space at /// `out_ptr`. This call overwrites it with the size of the value. If the available /// space at `out_ptr` is less than the size of the value a trap is triggered. /// - /// The data is encoded as Gas. - #[prefixed_alias] + /// The data is encoded as Weight. + #[version(1)] + #[unstable] fn gas_left(ctx: _, memory: _, out_ptr: u32, out_len_ptr: u32) -> Result<(), TrapReason> { ctx.charge_gas(RuntimeCosts::GasLeft)?; - let gas_left = &ctx.ext.gas_meter().gas_left().ref_time().encode(); + let gas_left = &ctx.ext.gas_meter().gas_left().encode(); Ok(ctx.write_sandbox_output( memory, out_ptr,