Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions prdoc/pr_10309.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
title: '[pallet-revive] evm remove contract storage slot when writing all zero bytes'
doc:
- audience: Runtime Dev
description: |-
fixes https://github.com/paritytech/contract-issues/issues/216

When writing all zero bytes to a storage item, the item shall be deleted and deposit refunded.
crates:
- name: pallet-revive
bump: patch
2 changes: 1 addition & 1 deletion substrate/frame/revive/src/tests/pvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5159,7 +5159,7 @@ fn consume_all_gas_works() {
}

#[test]
fn existential_deposit_shall_not_charged_twice() {
fn existential_deposit_shall_not_be_charged_twice() {
let (code, _) = compile_module("dummy").unwrap();

let salt = [0u8; 32];
Expand Down
105 changes: 103 additions & 2 deletions substrate/frame/revive/src/tests/sol/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
use crate::{
address::AddressMapper,
test_utils::{builder::Contract, ALICE, BOB, BOB_ADDR},
tests::{builder, test_utils, ExtBuilder, RuntimeEvent, Test},
tests::{builder, test_utils, test_utils::get_contract, ExtBuilder, RuntimeEvent, Test},
Code, Config, Error, Key, System, H256, U256,
};
use frame_support::assert_err_ignore_postinfo;

use alloy_core::sol_types::{SolCall, SolInterface};
use frame_support::traits::{fungible::Mutate, Get};
use pallet_revive_fixtures::{compile_module_with_type, FixtureType, Host};
use pallet_revive_fixtures::{compile_module_with_type, Caller, FixtureType, Host};
use pretty_assertions::assert_eq;
use test_case::test_case;

Expand Down Expand Up @@ -606,3 +606,104 @@ fn logs_denied_for_static_call(caller_type: FixtureType, callee_type: FixtureTyp
assert_eq!(decoded_result.success, false);
});
}

#[test_case(FixtureType::Solc, FixtureType::Solc; "solc->solc")]
#[test_case(FixtureType::Solc, FixtureType::Resolc; "solc->resolc")]
#[test_case(FixtureType::Resolc, FixtureType::Solc; "resolc->solc")]
#[test_case(FixtureType::Resolc, FixtureType::Resolc; "resolc->resolc")]
fn empty_storage_item_is_zero(caller_type: FixtureType, callee_type: FixtureType) {
let (caller_code, _) = compile_module_with_type("Caller", caller_type).unwrap();
let (host_code, _) = compile_module_with_type("Host", callee_type).unwrap();

ExtBuilder::default().build().execute_with(|| {
<Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);

let Contract { addr: host_addr, .. } =
builder::bare_instantiate(Code::Upload(host_code)).build_and_unwrap_contract();

let Contract { addr: caller_addr, .. } =
builder::bare_instantiate(Code::Upload(caller_code)).build_and_unwrap_contract();

let index = 13u64;

// read non-existent storage item
let result = builder::bare_call(caller_addr)
.data(
Caller::delegateCall {
_callee: host_addr.0.into(),
_data: Host::sloadOpCall { slot: index }.abi_encode().into(),
_gas: u64::MAX,
}
.abi_encode(),
)
.build_and_unwrap_result();

let result = Caller::delegateCall::abi_decode_returns(&result.data).unwrap();
assert!(result.success, "delegateCall did not succeed");
let decoded = Host::sloadOpCall::abi_decode_returns(&result.output).unwrap();
assert_eq!(decoded, 0u64, "sloadOpCall should return zero for empty storage item");
})
}

#[test_case(FixtureType::Solc, FixtureType::Solc; "solc->solc")]
#[test_case(FixtureType::Solc, FixtureType::Resolc; "solc->resolc")]
#[test_case(FixtureType::Resolc, FixtureType::Solc; "resolc->solc")]
#[test_case(FixtureType::Resolc, FixtureType::Resolc; "resolc->resolc")]
fn storage_item_zero_shall_refund_deposit(caller_type: FixtureType, callee_type: FixtureType) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again do we need to to a nested call for testing this scenario here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it a nested call to make sure the caller_type does not matter. Maybe I was being overzealous, tell me what you think

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah let's keep it simple please, we can have it aon both Solc and Resolc but we dont need a caller & callee

Copy link
Contributor Author

@0xRVE 0xRVE Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should keep it this way, it is not that complicated. And there is value in testing the delegatecall here because the storage is owned by Caller.sol while the code is owned by Host.sol.

EDIT.
added the simple test and it turns out that delegatecall and direct call behave differently:

  • delegatecall PVM does not refund storage deposit
  • direct call PVM refunds storage deposit

Copy link
Contributor Author

@0xRVE 0xRVE Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now both PVM and EVM refund storage deposit when writing all zeros to a storage item.
This changed since c9d82a0, on this commit PVM did not refund storage deposit.
I did not change the code since then (only added tests).

Unable to reproduce this issue (locally or in CI). Even after making a local checkout of c9d82a0 I cannot reproduce.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's expected this will use set_storage_or_clear

you can test it out with

RUST_LOG=runtime::revive=trace cargo test tests::sol::host::storage_item_zero_shall_refund_deposit_simple

let (caller_code, _) = compile_module_with_type("Caller", caller_type).unwrap();
let (host_code, _) = compile_module_with_type("Host", callee_type).unwrap();

ExtBuilder::default().build().execute_with(|| {
<Test as Config>::Currency::set_balance(&ALICE, 100_000_000_000);

let Contract { addr: host_addr, .. } =
builder::bare_instantiate(Code::Upload(host_code)).build_and_unwrap_contract();

let Contract { addr: caller_addr, .. } =
builder::bare_instantiate(Code::Upload(caller_code)).build_and_unwrap_contract();

let index = 13u64;

let base_deposit = get_contract(&caller_addr).total_deposit();

// write storage item
let result = builder::bare_call(caller_addr)
.data(
Caller::delegateCall {
_callee: host_addr.0.into(),
_data: Host::sstoreOpCall { slot: index, value: 17u64 }.abi_encode().into(),
_gas: u64::MAX,
}
.abi_encode(),
)
.build_and_unwrap_result();
let result = Caller::delegateCall::abi_decode_returns(&result.data).unwrap();
assert!(result.success, "delegateCall did not succeed");

// 32 for key, 32 for value, 2 for item
assert_eq!(get_contract(&caller_addr).total_deposit(), base_deposit + 32 + 32 + 2);

// write storage item to all zeros
let result = builder::bare_call(caller_addr)
.data(
Caller::delegateCall {
_callee: host_addr.0.into(),
_data: Host::sstoreOpCall { slot: index, value: 0u64 }.abi_encode().into(),
_gas: u64::MAX,
}
.abi_encode(),
)
.build_and_unwrap_result();

let result = Caller::delegateCall::abi_decode_returns(&result.data).unwrap();
assert!(result.success, "delegateCall did not succeed");

if callee_type == FixtureType::Resolc {
// Resolc will not refund
assert_eq!(get_contract(&caller_addr).total_deposit(), base_deposit + 32 + 32 + 2);
} else {
// solc will refund
assert_eq!(get_contract(&caller_addr).total_deposit(), base_deposit);
}
});
}
12 changes: 6 additions & 6 deletions substrate/frame/revive/src/vm/evm/instructions/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,16 +146,16 @@ fn store_helper<'ext, E: Ext>(
let charged_amount = interpreter.ext.charge_or_halt(cost_before)?;
let key = Key::Fix(index.to_big_endian());
let take_old = false;
let Ok(write_outcome) =
set_function(interpreter.ext, &key, Some(value.to_big_endian().to_vec()), take_old)
let value_to_store = if value.is_zero() { None } else { Some(value.to_big_endian().to_vec()) };
let Ok(write_outcome) = set_function(interpreter.ext, &key, value_to_store.clone(), take_old)
else {
return ControlFlow::Break(Error::<E::T>::ContractTrapped.into());
};

interpreter
.ext
.gas_meter_mut()
.adjust_gas(charged_amount, adjust_cost(32, write_outcome.old_len()));
interpreter.ext.gas_meter_mut().adjust_gas(
charged_amount,
adjust_cost(if value_to_store.is_some() { 32 } else { 0 }, write_outcome.old_len()),
);

ControlFlow::Continue(())
}
Expand Down
Loading