From 24c41f10bb29857cc7fcb316d47ac055c15537f7 Mon Sep 17 00:00:00 2001 From: Cyrill Leutwiler Date: Fri, 1 Sep 2023 14:33:15 +0200 Subject: [PATCH] Contracts: `seal0::balance` should return the free balance (#1254) --- .../frame/contracts/fixtures/balance.wat | 42 ++++++++++++++++ substrate/frame/contracts/fixtures/drain.wat | 35 +++++++++++-- substrate/frame/contracts/src/exec.rs | 8 ++- substrate/frame/contracts/src/tests.rs | 50 +++++++++++++++++++ 4 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 substrate/frame/contracts/fixtures/balance.wat diff --git a/substrate/frame/contracts/fixtures/balance.wat b/substrate/frame/contracts/fixtures/balance.wat new file mode 100644 index 000000000000..d86d5c4b1c60 --- /dev/null +++ b/substrate/frame/contracts/fixtures/balance.wat @@ -0,0 +1,42 @@ +(module + (import "seal0" "seal_balance" (func $seal_balance (param i32 i32))) + (import "env" "memory" (memory 1 1)) + + ;; [0, 8) reserved for $seal_balance output + + ;; [8, 16) length of the buffer for $seal_balance + (data (i32.const 8) "\08") + + ;; [16, inf) zero initialized + + (func $assert (param i32) + (block $ok + (br_if $ok + (get_local 0) + ) + (unreachable) + ) + ) + + (func (export "deploy")) + + (func (export "call") + (call $seal_balance (i32.const 0) (i32.const 8)) + + ;; Balance should be encoded as a u64. + (call $assert + (i32.eq + (i32.load (i32.const 8)) + (i32.const 8) + ) + ) + + ;; Assert the free balance to be zero. + (call $assert + (i64.eq + (i64.load (i32.const 0)) + (i64.const 0) + ) + ) + ) +) diff --git a/substrate/frame/contracts/fixtures/drain.wat b/substrate/frame/contracts/fixtures/drain.wat index 9f126898fac8..cb8ff0aed61f 100644 --- a/substrate/frame/contracts/fixtures/drain.wat +++ b/substrate/frame/contracts/fixtures/drain.wat @@ -1,14 +1,20 @@ (module (import "seal0" "seal_balance" (func $seal_balance (param i32 i32))) + (import "seal0" "seal_minimum_balance" (func $seal_minimum_balance (param i32 i32))) (import "seal0" "seal_transfer" (func $seal_transfer (param i32 i32 i32 i32) (result i32))) (import "env" "memory" (memory 1 1)) ;; [0, 8) reserved for $seal_balance output - ;; [8, 16) length of the buffer + ;; [8, 16) length of the buffer for $seal_balance (data (i32.const 8) "\08") - ;; [16, inf) zero initialized + ;; [16, 24) reserved for $seal_minimum_balance + + ;; [24, 32) length of the buffer for $seal_minimum_balance + (data (i32.const 24) "\08") + + ;; [32, inf) zero initialized (func $assert (param i32) (block $ok @@ -33,13 +39,32 @@ ) ) - ;; Try to self-destruct by sending full balance to the 0 address. + ;; Get the minimum balance. + (call $seal_minimum_balance (i32.const 16) (i32.const 24)) + + ;; Minimum balance should be encoded as a u64. + (call $assert + (i32.eq + (i32.load (i32.const 24)) + (i32.const 8) + ) + ) + + ;; Make the transferred value exceed the balance by adding the minimum balance. + (i64.store (i32.const 0) + (i64.add + (i64.load (i32.const 0)) + (i64.load (i32.const 16)) + ) + ) + + ;; Try to self-destruct by sending more balance to the 0 address. ;; The call will fail because a contract transfer has a keep alive requirement (call $assert (i32.eq (call $seal_transfer - (i32.const 16) ;; Pointer to destination address - (i32.const 32) ;; Length of destination address + (i32.const 32) ;; Pointer to destination address + (i32.const 48) ;; Length of destination address (i32.const 0) ;; Pointer to the buffer with value to transfer (i32.const 8) ;; Length of the buffer with value to transfer ) diff --git a/substrate/frame/contracts/src/exec.rs b/substrate/frame/contracts/src/exec.rs index 38c15a807c5d..894667280b71 100644 --- a/substrate/frame/contracts/src/exec.rs +++ b/substrate/frame/contracts/src/exec.rs @@ -30,7 +30,7 @@ use frame_support::{ storage::{with_transaction, TransactionOutcome}, traits::{ fungible::{Inspect, Mutate}, - tokens::Preservation, + tokens::{Fortitude, Preservation}, Contains, OriginTrait, Randomness, Time, }, weights::Weight, @@ -1368,7 +1368,11 @@ where } fn balance(&self) -> BalanceOf { - T::Currency::balance(&self.top_frame().account_id) + T::Currency::reducible_balance( + &self.top_frame().account_id, + Preservation::Preserve, + Fortitude::Polite, + ) } fn value_transferred(&self) -> BalanceOf { diff --git a/substrate/frame/contracts/src/tests.rs b/substrate/frame/contracts/src/tests.rs index 9d03a29b038c..0c0a2f7f9327 100644 --- a/substrate/frame/contracts/src/tests.rs +++ b/substrate/frame/contracts/src/tests.rs @@ -5891,3 +5891,53 @@ fn root_cannot_instantiate() { ); }); } + +#[test] +fn balance_api_returns_free_balance() { + let (wasm, _code_hash) = compile_module::("balance").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the BOB contract without any extra balance. + let addr = Contracts::bare_instantiate( + ALICE, + 0, + GAS_LIMIT, + None, + Code::Upload(wasm.to_vec()), + vec![], + vec![], + DebugInfo::Skip, + CollectEvents::Skip, + ) + .result + .unwrap() + .account_id; + + let value = 0; + // Call BOB which makes it call the balance runtime API. + // The contract code asserts that the returned balance is 0. + assert_ok!(Contracts::call( + RuntimeOrigin::signed(ALICE), + addr.clone(), + value, + GAS_LIMIT, + None, + vec![] + )); + + let value = 1; + // Calling with value will trap the contract. + assert_err_ignore_postinfo!( + Contracts::call( + RuntimeOrigin::signed(ALICE), + addr.clone(), + value, + GAS_LIMIT, + None, + vec![] + ), + >::ContractTrapped + ); + }); +}