Skip to content

Conversation

@filip-parity
Copy link

Fix fund_pranked_accounts overwriting dealt account balances.
Track accounts explicitly funded via vm.deal() and skip auto-funding them in fund_pranked_accounts. Previously, pranking an account that had spent its balance to zero would incorrectly set it to u128::MAX, breaking balance assertions in tests.

@filip-parity filip-parity requested a review from pkhry January 19, 2026 09:46
@pkhry
Copy link

pkhry commented Jan 19, 2026

Can you add a repro test case?

@filip-parity
Copy link
Author

Can you add a repro test case?

Added testcase for this

@pkhry
Copy link

pkhry commented Jan 21, 2026

what is going to happen when vm.deal was executed exclusively within foundry side?

/// their balance is 0. This is intentional - vm.deal() is an explicit user action that
/// should be respected, while internal contract creations should still get auto-funding.
pub(crate) fn fund_pranked_accounts(&self, account: Address) {
// Fuzzed prank addresses have no balance, so they won't exist in revive, and
Copy link
Collaborator

Choose a reason for hiding this comment

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

how does it work in REVM if prank addresses do not have any balance?

Copy link
Author

Choose a reason for hiding this comment

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

It works, in REVM they don't need balance to execute

@filip-parity
Copy link
Author

what is going to happen when vm.deal was executed exclusively within foundry side?

Balance changes don't sync back when executed in pallet-revive, it's a different issue though, we can look separately into this

@pkhry
Copy link

pkhry commented Jan 22, 2026

what is going to happen when vm.deal was executed exclusively within foundry side?

Balance changes don't sync back when executed in pallet-revive, it's a different issue though, we can look separately into this

they do.
the question was: what's going to happen if we call the vm.deal inside a callback?

ie

proxy.call() // calls the test contract, original scope
// inside test contract
vm.deal(addr1, 2 ether); // executed inside callback to original scope.
//
vm.prank(addr1)
addr1.balance // if the addr1 didn't exist on pallet-revive side it's balance would be 0. instead of the balance that was set inside the vm.deal() call.

but i digress, it can be changed in other PR.

@smiasojed
Copy link
Collaborator

hint from Claude AI, I did not check it in details but is worth to consider, I think:

pub(crate) fn fund_pranked_accounts(&self, account: Address) {
        // In pallet-revive, contracts need funds to cover storage deposits for their code.
        // If a pranked account is a contract with insufficient balance, it becomes non-functional
        // and any storage writes will fail with StorageDepositNotEnoughFunds.
        //
        // Unlike EVM where balance is just tracked, pallet-revive requires storage deposits
        // to be properly held (reserved) on the contract account. Just setting the free balance
        // is not enough - we must create the hold as would happen in a real transaction.
        //
        // Only fund if the pranked account is a contract.
        // EOAs don't need storage deposit funds since they don't execute code.
        let account_h160 = H160::from_slice(account.as_slice());

        // Check if account is a contract and get its info
        let Some(contract_info) = AccountInfo::<Runtime>::load_contract(&account_h160) else {
            return; // EOA or no contract - no funding needed
        };

        // Get the required deposit from contract info (includes code + storage deposits)
        let required_deposit: u128 = contract_info.total_deposit();
        if required_deposit == 0 {
            return;
        }

        // Convert to AccountId32 for balance operations
        let account_id = AccountId32Mapper::<Runtime>::to_fallback_account_id(&account_h160);

        // Convert HoldReason to RuntimeHoldReason
        let hold_reason: RuntimeHoldReason = HoldReason::StorageDepositReserve.into();

        // Fund the account with enough balance to cover the deposit + some buffer for ED
        // We need free balance available to then hold it
        let current_balance = Pallet::<Runtime>::evm_balance(&account_h160);
        let required_total = SpU256::from(required_deposit);
        if current_balance < required_total {
            Pallet::<Runtime>::set_evm_balance(&account_h160, required_total)
                .expect("Could not fund contract with required deposit");
        }

        // Create the hold on the contract account, as would happen in a real transaction.
        // This properly reserves the storage deposit funds.
        if let Err(e) = Balances::hold(&hold_reason, &account_id, required_deposit) {
            tracing::warn!(
                target: "cheatcodes",
                "Failed to hold storage deposit for contract {:?}: {:?}",
                account_h160,
                e
            );
        }
    }

pub mocked_calls: HashMap<Address, BTreeMap<MockCallDataContext, VecDeque<MockCallReturnData>>>,
pub mocked_functions: HashMap<Address, HashMap<Bytes, Address>>,
/// Records of accounts that were explicitly dealt to via vm.deal().
pub eth_deals: Vec<DealRecord>,
Copy link

Choose a reason for hiding this comment

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

why do we need this field if we can just pass it to fund_pranked_accounts?

Copy link
Author

Choose a reason for hiding this comment

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

Addressed


// Skip accounts that were explicitly dealt to via vm.deal()
if mock_inner.eth_deals.iter().any(|deal| deal.address == account) {
return;
Copy link

Choose a reason for hiding this comment

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

maybe we should check if the balance aligns between foundry's REVM and pallet-revive and then align them if they are divergent in case of e.g vm.deal execution within a callback as it will not set the balance within pallet-revive but will be present in eth_deals.

Copy link
Author

Choose a reason for hiding this comment

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

Also addressed, and we default to max if there is no deal

@filip-parity
Copy link
Author

hint from Claude AI, I did not check it in details but is worth to consider, I think:

pub(crate) fn fund_pranked_accounts(&self, account: Address) {
        // In pallet-revive, contracts need funds to cover storage deposits for their code.
        // If a pranked account is a contract with insufficient balance, it becomes non-functional
        // and any storage writes will fail with StorageDepositNotEnoughFunds.
        //
        // Unlike EVM where balance is just tracked, pallet-revive requires storage deposits
        // to be properly held (reserved) on the contract account. Just setting the free balance
        // is not enough - we must create the hold as would happen in a real transaction.
        //
        // Only fund if the pranked account is a contract.
        // EOAs don't need storage deposit funds since they don't execute code.
        let account_h160 = H160::from_slice(account.as_slice());

        // Check if account is a contract and get its info
        let Some(contract_info) = AccountInfo::<Runtime>::load_contract(&account_h160) else {
            return; // EOA or no contract - no funding needed
        };

        // Get the required deposit from contract info (includes code + storage deposits)
        let required_deposit: u128 = contract_info.total_deposit();
        if required_deposit == 0 {
            return;
        }

        // Convert to AccountId32 for balance operations
        let account_id = AccountId32Mapper::<Runtime>::to_fallback_account_id(&account_h160);

        // Convert HoldReason to RuntimeHoldReason
        let hold_reason: RuntimeHoldReason = HoldReason::StorageDepositReserve.into();

        // Fund the account with enough balance to cover the deposit + some buffer for ED
        // We need free balance available to then hold it
        let current_balance = Pallet::<Runtime>::evm_balance(&account_h160);
        let required_total = SpU256::from(required_deposit);
        if current_balance < required_total {
            Pallet::<Runtime>::set_evm_balance(&account_h160, required_total)
                .expect("Could not fund contract with required deposit");
        }

        // Create the hold on the contract account, as would happen in a real transaction.
        // This properly reserves the storage deposit funds.
        if let Err(e) = Balances::hold(&hold_reason, &account_id, required_deposit) {
            tracing::warn!(
                target: "cheatcodes",
                "Failed to hold storage deposit for contract {:?}: {:?}",
                account_h160,
                e
            );
        }
    }

Should this be addressed in the scope of this PR?

@filip-parity filip-parity requested a review from pkhry January 23, 2026 12:10
@filip-parity filip-parity merged commit e3ea87e into master Jan 23, 2026
60 of 78 checks passed
@filip-parity filip-parity deleted the filip/fund-pranked-accounts-dealt-tracking branch January 23, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants