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
120 changes: 120 additions & 0 deletions cli/token_benchmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env -S deno run --env-file --allow-all

import { env } from '../tools/lib/index.ts'
import { abis } from '../codegen/abis.ts'
import { MyToken } from '../codegen/addresses.ts'
import { privateKeyToAccount } from 'viem/accounts'
import { generatePrivateKey } from 'viem/accounts'
import { parseEther } from 'viem'

const code = await env.wallet.getCode({ address: MyToken })
if (!code) {
console.error(`Deploy contract first with "deno task deploy"`)
Deno.exit(1)
} else {
console.log(`MyToken code size: ${code.length / 2 - 1} bytes`)
}

const ITERATIONS = 300

// Generate address pool (first half senders, second half receivers)
console.log('Generating address...')
const addressPool = Array.from({ length: ITERATIONS * 2 }, () => {
const privateKey = generatePrivateKey()
const account = privateKeyToAccount(privateKey)
return { privateKey, address: account.address, account }
})
const senderAccounts = addressPool.slice(0, ITERATIONS)
const receiverAddresses = addressPool.slice(ITERATIONS).map((a) => a.address)
console.log(
`Generated ${ITERATIONS} sender accounts and ${ITERATIONS} receiver addresses`,
)

// Prefund all addresses to bring accounts into existence
{
console.log('Prefunding all addresses...')
const nonce = await env.wallet.getTransactionCount(env.wallet.account)
const fundHashes = await Promise.all(
addressPool.map((account, index) => {
return env.wallet.sendTransaction({
to: account.address,
value: parseEther('1'),
nonce: nonce + addressPool.length - 1 - index,
})
}),
)
console.log(`Sent ${fundHashes.length} funding transactions`)
await env.wallet.waitForTransactionReceipt({ hash: fundHashes[0] })
console.log('All addresses funded')
}

{
// Simulate and execute mint with env.wallet
const mintRequest = {
address: MyToken,
abi: abis.MyToken,
functionName: 'mint',
args: [env.wallet.account.address, 10_000_000_000_000_000_000_000_000n],
} as const
const mintHash = await env.wallet.writeContract(mintRequest)
const mintReceipt = await env.wallet.waitForTransactionReceipt({
hash: mintHash,
})

// Simulate and execute transfer with env.wallet
const transferRequest = {
address: MyToken,
abi: abis.MyToken,
functionName: 'transfer',
args: [receiverAddresses[0], 1n],
} as const
const transferHash = await env.wallet.writeContract(transferRequest)
const transferReceipt = await env.wallet.waitForTransactionReceipt({
hash: transferHash,
})

// Build all transactions for the loop
console.log('Building all transactions...')
const allRequests = []
for (let i = 0; i < ITERATIONS; i++) {
const sender = senderAccounts[i]
const receiverAddress = receiverAddresses[i]

// Mint transaction from sender (nonce 0)
allRequests.push(
env.wallet.writeContract({
...mintRequest,
nonce: 0,
account: sender.account,
gas: mintReceipt.gasUsed,
gasPrice: mintReceipt.effectiveGasPrice,
}),
)

// // Transfer transaction from sender (nonce 1)
allRequests.push(
env.wallet.writeContract({
...transferRequest,
args: [receiverAddress, 1n],
nonce: 1,
account: sender.account,
gas: transferReceipt.gasUsed,
gasPrice: mintReceipt.effectiveGasPrice,
}),
)
}

// Send all transactions at once
console.log('Sending all transactions...')
const allHashes = await Promise.all(allRequests)
console.log(
`Sent ${allHashes.length} transactions (${ITERATIONS} mint + ${ITERATIONS} transfer)`,
)

console.log('Waiting for last transaction...')
env.wallet.waitForTransactionReceipt({
hash: allHashes[allHashes.length - 1],
})

console.log(`\nSuccessfully processed ${ITERATIONS} mint + transfer pairs`)
}
30 changes: 30 additions & 0 deletions cli/token_deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { deploy } from '../tools/lib/index.ts'
import { readBytecode } from '../utils/index.ts'

await deploy({
name: { name: 'MyToken', mappedTo: 'MyTokenEvm' },
args: [],
})

await deploy({
name: { name: 'MyToken', mappedTo: 'MyTokenPvm' },
args: [],
bytecodeType: 'polkavm', // Specify `pvm` for PVM bytecode deployment
})
await deploy({
name: { name: 'MyToken', mappedTo: 'MyTokenInk' },
args: [],
bytecode: readBytecode('./ink/ink_erc20/target/ink/ink_erc20.polkavm'),
})

await deploy({
name: { name: 'MyToken', mappedTo: 'MyTokenRustWithAlloc' },
args: [],
bytecode: readBytecode('./rust/contract_with_alloc/contract.polkavm'),
})

await deploy({
name: { name: 'MyToken', mappedTo: 'MyTokenRustNoAlloc' },
args: [],
bytecode: readBytecode('./rust/contract_no_alloc/contract.polkavm'),
})
197 changes: 197 additions & 0 deletions cli/token_execute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/env -S deno run --env-file --allow-all
import { env } from '../tools/lib/index.ts'
import { abis } from '../codegen/abis.ts'
import {
MyTokenEvm,
MyTokenInk,
MyTokenPvm,
MyTokenRustNoAlloc,
MyTokenRustWithAlloc,
} from '../codegen/addresses.ts'
import { Hex, parseEther } from 'viem'
import Table from 'cli-table3'

const recipient: Hex = '0x3d26c9637dFaB74141bA3C466224C0DBFDfF4A63'

// fund recipient if needed
{
// endow the account wallet with some funds if needed
const endowment = parseEther('1')
const balance = await env.wallet.getBalance({ address: recipient })
if (balance == 0n) {
console.log(`funding ${recipient}`)
const hash = await env.wallet.sendTransaction({
to: recipient,
value: endowment,
})
await env.wallet.waitForTransactionReceipt({ hash })
}
}

const addresses = [
{ address: MyTokenEvm, name: 'EVM - solidity' },
{ address: MyTokenPvm, name: 'PVM - solidity' },
{ address: MyTokenInk, name: 'PVM - Ink!' },
{ address: MyTokenRustWithAlloc, name: 'PVM - Rust with alloc' },
{ address: MyTokenRustNoAlloc, name: 'PVM - Rust no alloc' },
] as const

type CodeSizeEntry = {
name: string
size: number
}

type StatEntry = {
operation: string
gas: bigint
weight: { ref_time: bigint; proof_size: bigint }
}

const codeSizes: CodeSizeEntry[] = []
const stats: StatEntry[] = []

for (const { name, address } of addresses) {
const code = await env.wallet.getCode({ address })
if (!code) {
console.error(`Deploy contract first with "deno task deploy"`)
Deno.exit(1)
}
codeSizes.push({
name,
size: code.length / 2 - 1,
})
}

// mint token
for (const { name, address } of addresses) {
const { request } = await env.wallet.simulateContract({
address,
abi: abis.MyToken,
functionName: 'mint',
args: [
'0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac',
10_000n,
],
})

const hash = await env.wallet.writeContract(request)
const receipt = await env.wallet.waitForTransactionReceipt({ hash })
const weight = await env.debugClient.postDispatchWeight(hash)

stats.push({
operation: `mint (${name})`,
gas: receipt.gasUsed,
weight,
})
}

// transfer token
for (const { name, address } of addresses) {
const { request } = await env.wallet.simulateContract({
address,
abi: abis.MyToken,
functionName: 'transfer',
args: [recipient, 1n],
})

const hash = await env.wallet.writeContract(request)
const receipt = await env.wallet.waitForTransactionReceipt({ hash })
const weight = await env.debugClient.postDispatchWeight(hash)

stats.push({
operation: `transfer (${name})`,
gas: receipt.gasUsed,
weight,
})
}

// Separate mint and transfer operations
const mintStats = stats.filter((s) => s.operation.includes('mint'))
const transferStats = stats.filter((s) => s.operation.includes('transfer'))

function createOperationTable(operationStats: StatEntry[], title: string) {
const minGas = operationStats.reduce(
(min, s) => s.gas < min ? s.gas : min,
operationStats[0].gas,
)
const minRefTime = operationStats.reduce(
(min, s) => s.weight.ref_time < min ? s.weight.ref_time : min,
operationStats[0].weight.ref_time,
)
const minProofSize = operationStats.reduce(
(min, s) => s.weight.proof_size < min ? s.weight.proof_size : min,
operationStats[0].weight.proof_size,
)

const table = new Table({
head: [
'Implementation',
'Gas Used',
'Gas %',
'Ref Time',
'Ref Time %',
'Proof Size',
'Proof Size %',
],
style: {
head: ['cyan'],
},
})

for (const stat of operationStats) {
const gasPercent = ((Number(stat.gas) / Number(minGas)) * 100).toFixed(
1,
)
const refTimePercent =
((Number(stat.weight.ref_time) / Number(minRefTime)) * 100).toFixed(
1,
)
const proofSizePercent =
((Number(stat.weight.proof_size) / Number(minProofSize)) * 100)
.toFixed(1)

// Extract implementation name (EVM or PVM) from operation
const implName = stat.operation.match(/\((.*?)\)/)?.[1] ||
stat.operation

table.push([
implName,
stat.gas.toString(),
`${gasPercent}%`,
stat.weight.ref_time.toString(),
`${refTimePercent}%`,
stat.weight.proof_size.toString(),
`${proofSizePercent}%`,
])
}

console.log(`\n## ${title}\n`)
console.log(table.toString())
}

createOperationTable(mintStats, 'Mint Operation')
createOperationTable(transferStats, 'Transfer Operation')

// Output code size table
const minCodeSize = codeSizes.reduce(
(min, c) => c.size < min ? c.size : min,
codeSizes[0].size,
)

const codeSizeTable = new Table({
head: ['Implementation', 'Code Size (bytes)', 'Size %'],
style: {
head: ['cyan'],
},
})

for (const entry of codeSizes) {
const sizePercent = ((entry.size / minCodeSize) * 100).toFixed(1)
codeSizeTable.push([
entry.name,
entry.size.toString(),
`${sizePercent}%`,
])
}

console.log('\n' + codeSizeTable.toString())
36 changes: 36 additions & 0 deletions contracts/MyToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

/// @title Minimal ERC-20-like token with only mint and transfer entry points.
contract MyToken {
uint256 public totalSupply;

mapping(address => uint256) private balances;

event Transfer(address indexed from, address indexed to, uint256 value);

error InsufficientBalance();

/// @notice Transfers `amount` tokens from the caller to `to`.
function transfer(address to, uint256 amount) external {
uint256 senderBalance = balances[msg.sender];
if (senderBalance < amount) {
revert InsufficientBalance();
}

unchecked {
balances[msg.sender] = senderBalance - amount;
balances[to] += amount;
}

emit Transfer(msg.sender, to, amount);
}

/// @notice Permissionless mint mirroring the ink! implementation.
/// Emits a Transfer event with the zero address as the sender.
function mint(address to, uint256 amount) external {
balances[to] += amount;
totalSupply += amount;
emit Transfer(address(0), to, amount);
}
}
Loading