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:
+
+
+
+
+
+
+
+
+
+
+ Status:
+
+
+
+
`,
+ );
+
+ 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 d5d915fe..f8081082 100644
--- a/src/index.js
+++ b/src/index.js
@@ -16,6 +16,7 @@ import {
} from './components/connections';
import {
sendComponent,
+ sponsorshipComponent,
erc20Component,
erc1155Component,
eip747Component,
@@ -184,6 +185,7 @@ const transactionsRow = document.createElement('div');
transactionsRow.className = 'row';
transactionsSection.appendChild(transactionsRow);
sendComponent(transactionsRow);
+sponsorshipComponent(transactionsRow);
erc20Component(transactionsRow);
erc721Component(transactionsRow);
erc1155Component(transactionsRow);