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
14 changes: 14 additions & 0 deletions contracts/SponsorshipProbe.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
1 change: 1 addition & 0 deletions src/components/transactions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
22 changes: 22 additions & 0 deletions src/components/transactions/sponsorship-probe.js
Original file line number Diff line number Diff line change
@@ -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';
172 changes: 172 additions & 0 deletions src/components/transactions/sponsorship.js
Original file line number Diff line number Diff line change
@@ -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',
`<div class="col-xl-4 col-lg-6 col-md-12 col-sm-12 col-12 d-flex align-items-stretch">
<div class="card full-width">
<div class="card-body">
<h4 class="card-title">
Paid by MetaMask
</h4>
<p class="info-text alert alert-secondary">
Use on Monad with an upgraded account to test Gas Sponsorship.
</p>

<button
class="btn btn-primary btn-lg btn-block mb-3"
id="deploySponsorshipProbeButton"
disabled
>
Deploy gas burner
</button>

<p class="info-text alert alert-secondary">
Contract: <span id="sponsorshipAddress" style="word-break: break-all;">Not deployed</span>
</p>

<div class="form-group">
<label for="sponsorshipIterations">
Storage writes (~22k gas each)
</label>
<input
class="form-control"
type="number"
id="sponsorshipIterations"
value="100"
min="1"
max="${MAX_ITERATIONS}"
/>
</div>

<button
class="btn btn-primary btn-lg btn-block mb-3"
id="burnGasButton"
disabled
>
Burn gas
</button>

<p class="info-text alert alert-secondary">
Status: <span id="sponsorshipStatus">Not clicked</span>
</p>
</div>
</div>
</div>`,
);

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;
}
};
}
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from './components/connections';
import {
sendComponent,
sponsorshipComponent,
erc20Component,
erc1155Component,
eip747Component,
Expand Down Expand Up @@ -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);
Expand Down
Loading