Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion app/vibenet/demos/account/AccountDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import {
safeGasLimit,
tryDecodeUsdvTransfer,
USDV_DECIMALS,
valueBearingCallCount,
} from './library/calls';
import {
type AccountType,
Expand Down Expand Up @@ -775,8 +776,9 @@ export function AccountDemo() {
deploy: !acct.deployed,
calls: calls.length,
keyChanges: keyChangeCount,
valueCalls: valueBearingCallCount(calls),
});
}, [acct, chain.mode, calls.length, keyChangeCount]);
}, [acct, chain.mode, calls, keyChangeCount]);

// Re-sync owner draft + stale results when the active account changes.
useEffect(() => {
Expand Down Expand Up @@ -1421,6 +1423,9 @@ export function AccountDemo() {
calls: plainCallCount,
keyChanges: accountChanges.filter((c) => c.type === 'config').length,
policyCalls: heavyCallCount,
// Value-bearing user calls carry the stipend + cold-account cost the node
// estimate misses (policy-wrapped sends still forward the ETH value).
valueCalls: valueBearingCallCount(rows),
fallback,
});
let gasLimit: bigint;
Expand Down
22 changes: 22 additions & 0 deletions app/vibenet/demos/account/library/calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,28 @@ export function buildCalls(rows: CallRow[], fallback: Address) {
return rows.map((r) => rowToCall(r, fallback));
}

/**
* Count call rows that transfer a non-zero native ETH value. Each such inner
* CALL needs the ~9k value-transfer surcharge (G_callvalue) and, for a
* not-yet-existent recipient, the ~25k new-account creation cost (G_newaccount)
* — gas the node's EIP-8130 `eth_estimateGas`
* omits (a phase whose inner CALL OOGs is still a valid inclusion, so the
* estimator converges below what the transfer actually needs to SUCCEED). The
* structural floor budgets these via `estimateTxGas`'s `valueCalls`. Rows whose
* value is empty, zero, or unparseable count as non-value-bearing.
*/
export function valueBearingCallCount(rows: CallRow[]): number {
return rows.reduce((n, r) => {
const v = r.value.trim();
if (!v) return n;
try {
return parseEther(v) > 0n ? n + 1 : n;
} catch {
return n;
}
}, 0);
}

// --- USDV (ERC-20 transfer) helpers -----------------------------------------

export const ERC20_TRANSFER_SELECTOR = '0xa9059cbb';
Expand Down
24 changes: 23 additions & 1 deletion app/vibenet/demos/account/library/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,13 @@ export function estimateTxGas(params: {
// adds the PolicyManager.execute frame, policy validation, spend-tracking
// SSTOREs, and the callback into executeBatch on top of the inner call.
policyCalls?: number;
// Number of calls that transfer a non-zero native ETH value. Each needs the
// ~9k value stipend plus (for a not-yet-existent recipient) the ~25k
// new-account creation cost. The node's `eth_estimateGas` misses this — it
// converges to the tx-envelope cost, ignoring that the inner value transfer
// must actually succeed — so the floor must carry it or a send to a cold
// recipient OOG-reverts on-chain (unused gas is refunded, an OOG is not).
valueCalls?: number;
// When true, this value is the SOLE gas source (the node's `eth_estimateGas`
// is unavailable, reverted, or the user chose "Send anyway"), so it is scaled
// up by {@link FALLBACK_SAFETY} to over-provision on purpose — unused gas is
Expand All @@ -226,7 +233,15 @@ export function estimateTxGas(params: {
// node's accurate estimate and permanently over-price every transaction.
fallback?: boolean;
}): number {
const { mode, deploy, calls, keyChanges, policyCalls = 0, fallback = false } = params;
const {
mode,
deploy,
calls,
keyChanges,
policyCalls = 0,
valueCalls = 0,
fallback = false,
} = params;
// ERC-4337: EntryPoint overhead (unchanged — different execution path).
// EIP-8130 native: base covers intrinsic gas for authenticator dispatch,
// AccountConfiguration lookup, and RLP calldata.
Expand All @@ -247,6 +262,12 @@ export function estimateTxGas(params: {
// node's estimate can be low when the simulated inner call reverts (an 8130
// reverting phase is still a valid inclusion), so the floor is the backstop.
const perPolicyCall = 55_000;
// Headroom for a value-bearing inner CALL the node estimate can't see: the 9k
// value-transfer surcharge (G_callvalue, which itself includes the 2,300 callee
// stipend) + 25k creation of a cold recipient account (G_newaccount). Additive
// on top of `perCall` (a value call is still a call). Both native and 4337 pay
// it — the EVM cost is the same regardless of execution path.
const perValueCall = 34_000;
// 2x safety multiplier, applied ONLY in fallback mode (see `fallback` above):
// when the node's `eth_estimateGas` is unavailable or reverts (staged config +
// policy bundles, cold-account creation on a value transfer to a fresh
Expand All @@ -263,6 +284,7 @@ export function estimateTxGas(params: {
calls * perCall +
keyChanges * perKeyChange +
policyCalls * perPolicyCall +
valueCalls * perValueCall +
wrap)
);
}
Expand Down