From 864ff8a6e4da3da62f11015421f67d69473d6f61 Mon Sep 17 00:00:00 2001 From: seaona Date: Thu, 8 Oct 2026 17:27:28 +0200 Subject: [PATCH] gas --- contracts/SponsorshipProbe.sol | 14 ++ src/components/transactions/index.js | 1 + .../transactions/sponsorship-probe.js | 22 +++ src/components/transactions/sponsorship.js | 172 ++++++++++++++++++ src/index.js | 2 + 5 files changed, 211 insertions(+) create mode 100644 contracts/SponsorshipProbe.sol create mode 100644 src/components/transactions/sponsorship-probe.js create mode 100644 src/components/transactions/sponsorship.js diff --git a/contracts/SponsorshipProbe.sol b/contracts/SponsorshipProbe.sol new file mode 100644 index 00000000..b6762070 --- /dev/null +++ b/contracts/SponsorshipProbe.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// Used to test Paid by MetaMask on a sponsored network. +/// `burn` writes one storage slot per iteration and succeeds. +contract SponsorshipProbe { + mapping(uint256 => uint256) public slots; + + function burn(uint256 iterations) external { + for (uint256 i = 0; i < iterations; i++) { + slots[i] = i + 1; + } + } +} diff --git a/src/components/transactions/index.js b/src/components/transactions/index.js index ee8456c9..38a8cc7c 100644 --- a/src/components/transactions/index.js +++ b/src/components/transactions/index.js @@ -3,5 +3,6 @@ export * from './erc20'; export * from './erc721'; export * from './erc1155'; export * from './send'; +export * from './sponsorship'; export * from './swapComparison'; export * from './batchUsdcSwap'; diff --git a/src/components/transactions/sponsorship-probe.js b/src/components/transactions/sponsorship-probe.js new file mode 100644 index 00000000..080e4e4c --- /dev/null +++ b/src/components/transactions/sponsorship-probe.js @@ -0,0 +1,22 @@ +// Compiled from contracts/SponsorshipProbe.sol (solc 0.8.20, optimizer on). +// Each burn iteration writes a fresh storage slot, about 22,000 gas. + +export const sponsorshipProbeAbi = [ + { + inputs: [{ internalType: 'uint256', name: 'iterations', type: 'uint256' }], + name: 'burn', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + name: 'slots', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, +]; + +export const sponsorshipProbeBytecode = + '0x608060405234801561000f575f80fd5b506101498061001d5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c8063387dd9e91461003857806342966c6814610069575b5f80fd5b6100576100463660046100b7565b5f6020819052908152604090205481565b60405190815260200160405180910390f35b61007c6100773660046100b7565b61007e565b005b5f5b818110156100b3576100938160016100e2565b5f82815260208190526040902055806100ab816100fb565b915050610080565b5050565b5f602082840312156100c7575f80fd5b5035919050565b634e487b7160e01b5f52601160045260245ffd5b808201808211156100f5576100f56100ce565b92915050565b5f6001820161010c5761010c6100ce565b506001019056fea2646970667358221220ee70631fccc4a34563b5efe275f5808a50f5af0440ba77a09493bbb5ecf5fd3664736f6c63430008140033'; diff --git a/src/components/transactions/sponsorship.js b/src/components/transactions/sponsorship.js new file mode 100644 index 00000000..9b3a28cc --- /dev/null +++ b/src/components/transactions/sponsorship.js @@ -0,0 +1,172 @@ +import { ethers } from 'ethers'; +import globalContext from '../..'; +import { + sponsorshipProbeAbi, + sponsorshipProbeBytecode, +} from './sponsorship-probe'; + +// Cold SSTORE of a zero slot is 20,000, plus the cold-account access. +const GAS_PER_ITERATION = 22000; +const MAX_ITERATIONS = 500; + +const probeInterface = new ethers.utils.Interface(sponsorshipProbeAbi); + +export function sponsorshipComponent(parentContainer) { + parentContainer.insertAdjacentHTML( + 'beforeend', + `
+
+
+

+ Paid by MetaMask +

+

+ Use on Monad with an upgraded account to test Gas Sponsorship. +

+ + + +

+ Contract: Not deployed +

+ +
+ + +
+ + + +

+ Status: Not clicked +

+
+
+
`, + ); + + const deployButton = document.getElementById('deploySponsorshipProbeButton'); + const burnGasButton = document.getElementById('burnGasButton'); + const iterationsInput = document.getElementById('sponsorshipIterations'); + const contractAddress = document.getElementById('sponsorshipAddress'); + const status = document.getElementById('sponsorshipStatus'); + + let probeContract; + + document.addEventListener('globalConnectionChange', (event) => { + if (event.detail.connected) { + deployButton.disabled = false; + } + }); + + document.addEventListener('disableAndClear', () => { + deployButton.disabled = true; + burnGasButton.disabled = true; + probeContract = undefined; + contractAddress.innerHTML = 'Not deployed'; + }); + + document.addEventListener('newChainIdInt', () => { + if (!probeContract) { + return; + } + probeContract = undefined; + burnGasButton.disabled = true; + contractAddress.innerHTML = 'Not deployed'; + status.innerHTML = 'Network changed. Deploy the gas burner again.'; + }); + + const readIterations = () => { + const iterations = Number.parseInt(iterationsInput.value, 10); + if ( + !Number.isInteger(iterations) || + iterations < 1 || + iterations > MAX_ITERATIONS + ) { + throw new Error( + `Iterations must be an integer from 1 to ${MAX_ITERATIONS}.`, + ); + } + return iterations; + }; + + deployButton.onclick = async () => { + status.innerHTML = 'Deploying'; + burnGasButton.disabled = true; + try { + const factory = new ethers.ContractFactory( + sponsorshipProbeAbi, + sponsorshipProbeBytecode, + globalContext.ethersProvider.getSigner(), + ); + const deployed = await factory.deploy(); + await deployed.deployTransaction.wait(); + if (!deployed.address) { + status.innerHTML = 'Deployment failed'; + burnGasButton.disabled = !probeContract; + return; + } + + probeContract = deployed; + burnGasButton.disabled = false; + contractAddress.innerHTML = deployed.address; + status.innerHTML = 'Deployed'; + console.log( + `Sponsorship probe mined at ${deployed.address} tx ${deployed.deployTransaction.hash}`, + ); + } catch (error) { + status.innerHTML = error.message || 'Deployment failed'; + burnGasButton.disabled = !probeContract; + console.log('error', error); + throw error; + } + }; + + burnGasButton.onclick = async () => { + try { + if (!probeContract) { + throw new Error('Deploy the gas burner first.'); + } + const iterations = readIterations(); + const approximateGas = iterations * GAS_PER_ITERATION; + status.innerHTML = `Waiting for confirmation. About ${approximateGas} gas, no gas limit on the request.`; + const txHash = await globalContext.provider.request({ + method: 'eth_sendTransaction', + params: [ + { + from: globalContext.accounts[0], + to: probeContract.address, + data: probeInterface.encodeFunctionData('burn', [iterations]), + }, + ], + }); + status.innerHTML = `Submitted ${txHash}. About ${approximateGas} gas was executed. Compare gasUsed on the explorer.`; + console.log('burn gas', txHash); + } catch (error) { + status.innerHTML = error.message || 'Burn failed'; + console.log('error', error); + throw error; + } + }; +} diff --git a/src/index.js b/src/index.js index eb6232e7..e9b2b4a1 100644 --- a/src/index.js +++ b/src/index.js @@ -15,6 +15,7 @@ import { } from './components/connections'; import { sendComponent, + sponsorshipComponent, erc20Component, erc1155Component, eip747Component, @@ -182,6 +183,7 @@ const transactionsRow = document.createElement('div'); transactionsRow.className = 'row'; transactionsSection.appendChild(transactionsRow); sendComponent(transactionsRow); +sponsorshipComponent(transactionsRow); erc20Component(transactionsRow); erc721Component(transactionsRow); erc1155Component(transactionsRow);