diff --git a/cli/token_benchmark.ts b/cli/token_benchmark.ts new file mode 100755 index 0000000..3d0aaf2 --- /dev/null +++ b/cli/token_benchmark.ts @@ -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`) +} diff --git a/cli/token_deploy.ts b/cli/token_deploy.ts new file mode 100644 index 0000000..7a7d748 --- /dev/null +++ b/cli/token_deploy.ts @@ -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'), +}) diff --git a/cli/token_execute.ts b/cli/token_execute.ts new file mode 100755 index 0000000..e3d8aa7 --- /dev/null +++ b/cli/token_execute.ts @@ -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()) diff --git a/contracts/MyToken.sol b/contracts/MyToken.sol new file mode 100644 index 0000000..4012ef0 --- /dev/null +++ b/contracts/MyToken.sol @@ -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); + } +} diff --git a/deno.json b/deno.json index 8a01541..792b9c8 100644 --- a/deno.json +++ b/deno.json @@ -8,20 +8,26 @@ "deploy": { "description": "Deploy contracts to the network specified in the .env file, and update addresses in the codegen directory", "command": "deno run --env-file --allow-all tools/deploy.ts" + }, + "token_benchmark": { + "description": "Deploy various contracts and compare execution costs for common token operations", + "command": "./scripts/build_tokens.sh && deno run --env-file --allow-all cli/token_deploy.ts && deno run --env-file --allow-all cli/token_execute.ts" } }, + "imports": { "@openzeppelin/contracts": "npm:@openzeppelin/contracts@^5.4.0", - "@parity/resolc": "npm:@parity/resolc@0.4.1", - "@std/cli": "jsr:@std/cli@^1.0.23", - "@std/path": "jsr:@std/path@^1.1.2", + "@parity/resolc": "npm:@parity/resolc@0.5.0", + "@std/cli": "jsr:@std/cli@^1.0.24", + "@std/path": "jsr:@std/path@^1.1.3", "@std/log": "jsr:@std/log@^0.224.14", - "ink": "npm:ink@^6.3.1", - "react": "npm:react@^19.2.0", - "react/jsx-runtime": "npm:react@^19.2.0/jsx-runtime", - "solc": "npm:solc@^0.8.30", - "viem": "npm:viem@^2.38.4", - "viem/accounts": "npm:viem@^2.38.4/accounts" + "cli-table3": "npm:cli-table3@^0.6.5", + "ink": "npm:ink@^6.5.1", + "react": "npm:react@^19.2.1", + "react/jsx-runtime": "npm:react@^19.2.1/jsx-runtime", + "solc": "npm:solc@^0.8.31", + "viem": "npm:viem@^2.41.2", + "viem/accounts": "npm:viem@^2.41.2/accounts" }, "compilerOptions": { "strict": true, diff --git a/deno.lock b/deno.lock index 3fa2416..d172d93 100644 --- a/deno.lock +++ b/deno.lock @@ -1,26 +1,27 @@ { "version": "5", "specifiers": { - "jsr:@std/cli@^1.0.23": "1.0.23", + "jsr:@std/cli@^1.0.24": "1.0.24", "jsr:@std/fmt@^1.0.5": "1.0.8", "jsr:@std/fs@^1.0.11": "1.0.19", - "jsr:@std/internal@^1.0.10": "1.0.12", "jsr:@std/internal@^1.0.12": "1.0.12", "jsr:@std/io@~0.225.2": "0.225.2", "jsr:@std/log@~0.224.14": "0.224.14", - "jsr:@std/path@^1.1.2": "1.1.2", + "jsr:@std/path@^1.1.3": "1.1.3", "npm:@openzeppelin/contracts@^5.4.0": "5.4.0", - "npm:@parity/resolc@0.4.1": "0.4.1", - "npm:ink@^6.3.1": "6.3.1_react@19.2.0", - "npm:react@^19.2.0": "19.2.0", - "npm:solc@~0.8.30": "0.8.30", - "npm:viem@^2.38.4": "2.38.4_ws@8.18.3" + "npm:@parity/resolc@0.5.0": "0.5.0", + "npm:cli-table3@*": "0.6.5", + "npm:cli-table3@~0.6.5": "0.6.5", + "npm:ink@^6.5.1": "6.5.1_react@19.2.1", + "npm:react@^19.2.1": "19.2.1", + "npm:solc@~0.8.31": "0.8.31", + "npm:viem@^2.41.2": "2.41.2_ws@8.18.3" }, "jsr": { - "@std/cli@1.0.23": { - "integrity": "bf95b7a9425ba2af1ae5a6359daf58c508f2decf711a76ed2993cd352498ccca", + "@std/cli@1.0.24": { + "integrity": "b655a5beb26aa94f98add6bc8889f5fb9bc3ee2cc3fc954e151201f4c4200a5e", "dependencies": [ - "jsr:@std/internal@^1.0.12" + "jsr:@std/internal" ] }, "@std/fmt@1.0.8": { @@ -43,10 +44,10 @@ "jsr:@std/io" ] }, - "@std/path@1.1.2": { - "integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038", + "@std/path@1.1.3": { + "integrity": "b015962d82a5e6daea980c32b82d2c40142149639968549c649031a230b1afb3", "dependencies": [ - "jsr:@std/internal@^1.0.10" + "jsr:@std/internal" ] } }, @@ -61,6 +62,9 @@ "is-fullwidth-code-point@5.1.0" ] }, + "@colors/colors@1.5.0": { + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" + }, "@noble/ciphers@1.3.0": { "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==" }, @@ -76,14 +80,14 @@ "@openzeppelin/contracts@5.4.0": { "integrity": "sha512-eCYgWnLg6WO+X52I16TZt8uEjbtdkgLC0SUX/xnAksjjrQI4Xfn4iBRoI5j55dmlOhDv1Y7BoR3cU7e3WWhC6A==" }, - "@parity/resolc@0.4.1": { - "integrity": "sha512-GdeJnCVlnRrrHjQk0vr+8mmkiJ2LiLwe7CEQBSKslHqK3t7dhjJORR1kfxoksTM4LSyftHUYmPszeIY0r1pv3w==", + "@parity/resolc@0.5.0": { + "integrity": "sha512-zSkg5xfkK/rLj+vvf2jinSFX/ih/gJ44DOV4r6Gksz7k0t1Jpg3GtchCnCfGA/aQZXoRWRsCradX3JqE6wgl4w==", "dependencies": [ "@types/node", "commander@13.1.0", "package-json", "resolve-pkg", - "solc" + "solc@0.8.30" ], "bin": true }, @@ -122,8 +126,8 @@ "@scure/base" ] }, - "@types/node@22.18.12": { - "integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==", + "@types/node@22.19.1": { + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "dependencies": [ "undici-types" ] @@ -131,12 +135,15 @@ "abitype@1.1.0": { "integrity": "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==" }, - "ansi-escapes@7.1.1": { - "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", + "ansi-escapes@7.2.0": { + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", "dependencies": [ "environment" ] }, + "ansi-regex@5.0.1": { + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, "ansi-regex@6.2.2": { "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==" }, @@ -158,11 +165,20 @@ "restore-cursor" ] }, - "cli-truncate@4.0.0": { - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "cli-table3@0.6.5": { + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dependencies": [ + "string-width@4.2.3" + ], + "optionalDependencies": [ + "@colors/colors" + ] + }, + "cli-truncate@5.1.1": { + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", "dependencies": [ - "slice-ansi@5.0.0", - "string-width" + "slice-ansi", + "string-width@8.1.0" ] }, "code-excerpt@4.0.0": { @@ -196,11 +212,14 @@ "emoji-regex@10.6.0": { "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==" }, + "emoji-regex@8.0.0": { + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "environment@1.1.0": { "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==" }, - "es-toolkit@1.41.0": { - "integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==" + "es-toolkit@1.42.0": { + "integrity": "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==" }, "escape-string-regexp@2.0.0": { "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" @@ -223,8 +242,8 @@ "ini@1.3.8": { "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "ink@6.3.1_react@19.2.0": { - "integrity": "sha512-3wGwITGrzL6rkWsi2gEKzgwdafGn4ZYd3u4oRp+sOPvfoxEHlnoB5Vnk9Uy5dMRUhDOqF3hqr4rLQ4lEzBc2sQ==", + "ink@6.5.1_react@19.2.1": { + "integrity": "sha512-wF3j/DmkM8q5E+OtfdQhCRw8/0ahkc8CUTgEddxZzpEWPslu7YPL3t64MWRoI9m6upVGpfAg4ms2BBvxCdKRLQ==", "dependencies": [ "@alcalzone/ansi-tokenize", "ansi-escapes", @@ -242,9 +261,9 @@ "react", "react-reconciler", "signal-exit", - "slice-ansi@7.1.2", + "slice-ansi", "stack-utils", - "string-width", + "string-width@8.1.0", "type-fest", "widest-line", "wrap-ansi", @@ -252,8 +271,8 @@ "yoga-layout" ] }, - "is-fullwidth-code-point@4.0.0": { - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==" + "is-fullwidth-code-point@3.0.0": { + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-fullwidth-code-point@5.1.0": { "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", @@ -274,8 +293,8 @@ "js-sha3@0.8.0": { "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" }, - "ky@1.13.0": { - "integrity": "sha512-JeNNGs44hVUp2XxO3FY9WV28ymG7LgO4wju4HL/dCq1A8eKDcFgVrdCn1ssn+3Q/5OQilv5aYsL0DMt5mmAV9w==" + "ky@1.14.0": { + "integrity": "sha512-Rczb6FMM6JT0lvrOlP5WUOCB7s9XKxzwgErzhKlKde1bEV90FXplV1o87fpt4PU/asJFiqjYJxAJyzJhcrxOsQ==" }, "memorystream@0.3.1": { "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==" @@ -333,15 +352,15 @@ ], "bin": true }, - "react-reconciler@0.32.0_react@19.2.0": { - "integrity": "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ==", + "react-reconciler@0.33.0_react@19.2.1": { + "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", "dependencies": [ "react", "scheduler" ] }, - "react@19.2.0": { - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==" + "react@19.2.1": { + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==" }, "registry-auth-token@5.1.0": { "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", @@ -371,8 +390,8 @@ "signal-exit" ] }, - "scheduler@0.26.0": { - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" + "scheduler@0.27.0": { + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" }, "semver@5.7.2": { "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", @@ -385,13 +404,6 @@ "signal-exit@3.0.7": { "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, - "slice-ansi@5.0.0": { - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dependencies": [ - "ansi-styles", - "is-fullwidth-code-point@4.0.0" - ] - }, "slice-ansi@7.1.2": { "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dependencies": [ @@ -412,24 +424,58 @@ ], "bin": true }, + "solc@0.8.31": { + "integrity": "sha512-wpccgDgu/aE/rRcF2F/LeN+4knK0734XTcjppyaQOticjYd/Giq1AJE3XPQZKEViAsY3sNaFKl7QpMRYrK35vg==", + "dependencies": [ + "command-exists", + "commander@8.3.0", + "follow-redirects", + "js-sha3", + "memorystream", + "semver@5.7.2", + "tmp" + ], + "bin": true + }, "stack-utils@2.0.6": { "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dependencies": [ "escape-string-regexp" ] }, + "string-width@4.2.3": { + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": [ + "emoji-regex@8.0.0", + "is-fullwidth-code-point@3.0.0", + "strip-ansi@6.0.1" + ] + }, "string-width@7.2.0": { "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dependencies": [ - "emoji-regex", + "emoji-regex@10.6.0", + "get-east-asian-width", + "strip-ansi@7.1.2" + ] + }, + "string-width@8.1.0": { + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dependencies": [ "get-east-asian-width", - "strip-ansi" + "strip-ansi@7.1.2" + ] + }, + "strip-ansi@6.0.1": { + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": [ + "ansi-regex@5.0.1" ] }, "strip-ansi@7.1.2": { "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dependencies": [ - "ansi-regex" + "ansi-regex@6.2.2" ] }, "strip-json-comments@2.0.1": { @@ -447,8 +493,8 @@ "undici-types@6.21.0": { "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, - "viem@2.38.4_ws@8.18.3": { - "integrity": "sha512-qnyPNg6Lz1EEC86si/1dq7GlOyZVFHSgAW+p8Q31R5idnAYCOdTM2q5KLE4/ykMeMXzY0bnp5MWTtR/wjCtWmQ==", + "viem@2.41.2_ws@8.18.3": { + "integrity": "sha512-LYliajglBe1FU6+EH9mSWozp+gRA/QcHfxeD9Odf83AdH5fwUS7DroH4gHvlv6Sshqi1uXrYFA2B/EOczxd15g==", "dependencies": [ "@noble/curves", "@noble/hashes", @@ -463,15 +509,15 @@ "widest-line@5.0.0": { "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", "dependencies": [ - "string-width" + "string-width@7.2.0" ] }, "wrap-ansi@9.0.2": { "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dependencies": [ "ansi-styles", - "string-width", - "strip-ansi" + "string-width@7.2.0", + "strip-ansi@7.1.2" ] }, "ws@8.18.3": { @@ -483,15 +529,16 @@ }, "workspace": { "dependencies": [ - "jsr:@std/cli@^1.0.23", + "jsr:@std/cli@^1.0.24", "jsr:@std/log@~0.224.14", - "jsr:@std/path@^1.1.2", + "jsr:@std/path@^1.1.3", "npm:@openzeppelin/contracts@^5.4.0", - "npm:@parity/resolc@0.4.1", - "npm:ink@^6.3.1", - "npm:react@^19.2.0", - "npm:solc@~0.8.30", - "npm:viem@^2.38.4" + "npm:@parity/resolc@0.5.0", + "npm:cli-table3@~0.6.5", + "npm:ink@^6.5.1", + "npm:react@^19.2.1", + "npm:solc@~0.8.31", + "npm:viem@^2.41.2" ] } } diff --git a/ink/ink_erc20/.gitignore b/ink/ink_erc20/.gitignore new file mode 100644 index 0000000..bf910de --- /dev/null +++ b/ink/ink_erc20/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file diff --git a/ink/ink_erc20/Cargo.toml b/ink/ink_erc20/Cargo.toml new file mode 100644 index 0000000..2f08199 --- /dev/null +++ b/ink/ink_erc20/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "ink_erc20" +version = "6.0.0-beta.1" +authors = ["Use Ink "] +edition = "2024" +publish = false + +[dependencies] +ink = { version = "6.0.0-beta.1", default-features = false } + +[dev-dependencies] +ink_e2e = { version = "6.0.0-beta.1" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", +] +ink-as-dependency = [] +e2e-tests = [] + +[package.metadata.ink-lang] +abi = "sol" diff --git a/ink/ink_erc20/lib.rs b/ink/ink_erc20/lib.rs new file mode 100644 index 0000000..2d27547 --- /dev/null +++ b/ink/ink_erc20/lib.rs @@ -0,0 +1,134 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +mod ink_erc20 { + use ink::{U256, storage::Mapping}; + + /// The zero address, used in minting and burning operations. + const ZERO_ADDRESS: Address = Address::repeat_byte(0); + + /// A simple ERC-20 contract. + #[ink(storage)] + #[derive(Default)] + pub struct InkErc20 { + /// Total token supply. + total_supply: U256, + /// Mapping from owner to number of owned token. + balances: Mapping, + } + + /// Event emitted when a token transfer occurs. + #[ink(event)] + pub struct Transfer { + #[ink(topic)] + from: Address, + #[ink(topic)] + to: Address, + value: U256, + } + + /// The ERC-20 error types. + #[derive(Debug, PartialEq, Eq)] + #[ink::error] + pub enum Error { + /// Returned if not enough balance to fulfill a request is available. + InsufficientBalance, + } + + /// The ERC-20 result type. + pub type Result = core::result::Result; + + impl InkErc20 { + /// Creates a new ERC-20 contract with the specified initial supply. + #[ink(constructor)] + pub fn new() -> Self { + Self::default() + } + + /// Returns the total token supply. + #[ink(message)] + pub fn total_supply(&self) -> U256 { + self.total_supply + } + + /// Returns the account balance for the specified `owner`. + /// + /// Returns `0` if the account is non-existent. + #[ink(message)] + pub fn balance_of(&self, owner: Address) -> U256 { + self.balance_of_impl(&owner) + } + + /// Returns the account balance for the specified `owner`. + /// + /// Returns `0` if the account is non-existent. + /// + /// # Note + /// + /// Prefer to call this method over `balance_of` since this + /// works using references which are more efficient. + #[inline] + fn balance_of_impl(&self, owner: &Address) -> U256 { + self.balances.get(owner).unwrap_or_default() + } + + /// Transfers `value` amount of tokens from the caller's account to account `to`. + /// + /// On success a `Transfer` event is emitted. + /// + /// # Errors + /// + /// Returns `InsufficientBalance` error if there are not enough tokens on + /// the caller's account balance. + #[ink(message)] + pub fn transfer(&mut self, to: Address, amount: U256) -> Result<()> { + let from = self.env().caller(); + self.transfer_from_to(&from, &to, amount) + } + + /// Transfers `value` amount of tokens from the caller's account to account `to`. + /// + /// On success a `Transfer` event is emitted. + /// + /// # Errors + /// + /// Returns `InsufficientBalance` error if there are not enough tokens on + /// the caller's account balance. + fn transfer_from_to(&mut self, from: &Address, to: &Address, value: U256) -> Result<()> { + let from_balance = self.balance_of_impl(from); + if from_balance < value { + return Err(Error::InsufficientBalance); + } + // We checked that from_balance >= value + #[allow(clippy::arithmetic_side_effects)] + self.balances.insert(from, &(from_balance - value)); + let to_balance = self.balance_of_impl(to); + self.balances + .insert(to, &(to_balance.checked_add(value).unwrap())); + self.env().emit_event(Transfer { + from: *from, + to: *to, + value, + }); + Ok(()) + } + + /// Mints `value` amount of new tokens to account `to`. + /// + /// This function is permissionless and can be called by anyone. + /// + /// On success a `Transfer` event is emitted with `from` set to the zero address. + #[ink(message)] + pub fn mint(&mut self, to: Address, value: U256) { + let to_balance = self.balance_of_impl(&to); + self.balances + .insert(&to, &(to_balance.checked_add(value).unwrap())); + self.total_supply = self.total_supply.checked_add(value).unwrap(); + self.env().emit_event(Transfer { + from: ZERO_ADDRESS, + to, + value, + }); + } + } +} diff --git a/rust/contract_no_alloc/.cargo/config.toml b/rust/contract_no_alloc/.cargo/config.toml new file mode 100644 index 0000000..8ae662c --- /dev/null +++ b/rust/contract_no_alloc/.cargo/config.toml @@ -0,0 +1,7 @@ +# Use a standard rust riscv64 target for cargo check and rust-anaylser +# cargo pvm will use `polkavm_linker::TargetJsonArgs::default()` +[build] +target = "riscv64imac-unknown-none-elf" + + + diff --git a/rust/contract_no_alloc/.gitignore b/rust/contract_no_alloc/.gitignore new file mode 100644 index 0000000..2dbb5ec --- /dev/null +++ b/rust/contract_no_alloc/.gitignore @@ -0,0 +1,2 @@ +/target +/*.polkavm diff --git a/rust/contract_no_alloc/Cargo.lock b/rust/contract_no_alloc/Cargo.lock new file mode 100644 index 0000000..2ce27fa --- /dev/null +++ b/rust/contract_no_alloc/Cargo.lock @@ -0,0 +1,157 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "const-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c06f1eb05f06cf2e380fdded278fbf056a38974299d77960555a311dcf91a52" +dependencies = [ + "keccak-const", + "sha2-const-stable", +] + +[[package]] +name = "contract_no_alloc" +version = "0.1.0" +dependencies = [ + "pallet-revive-uapi", + "picoalloc", + "polkavm-derive", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "keccak-const" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d8d8ce877200136358e0bbff3a77965875db3af755a11e1fa6b1b3e2df13ea" + +[[package]] +name = "pallet-revive-proc-macro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed97af646322cfc2d394c4737874bf6df507d25dd421a2939304eee02d89c742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pallet-revive-uapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0bf9c852c4426130520d546fe9ea0d932914c42ed7ae2970b5e428a3efe7e1" +dependencies = [ + "bitflags", + "const-crypto", + "hex-literal", + "pallet-revive-proc-macro", + "polkavm-derive", +] + +[[package]] +name = "picoalloc" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1912f9b1d3aea43590e3986afdcf4ed1d9662edae24744a095738157d65b6a" + +[[package]] +name = "picosimd" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af35c838647fef3d6d052e27006ef88ea162336eee33063c50a63f163c18cdeb" + +[[package]] +name = "polkavm-common" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed1b408db93d4f49f5c651a7844682b9d7a561827b4dc6202c10356076c055c9" +dependencies = [ + "picosimd", +] + +[[package]] +name = "polkavm-derive" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb4463fb0b9dbfafdc1d1a1183df4bf7afa3350d124f29d5700c6bee54556b5" +dependencies = [ + "polkavm-derive-impl-macro", +] + +[[package]] +name = "polkavm-derive-impl" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993ff45b972e09babe68adce7062c3c38a84b9f50f07b7caf393a023eaa6c74a" +dependencies = [ + "polkavm-common", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "polkavm-derive-impl-macro" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4f5352e13c1ca5f0e4d7b4a804fbb85b0e02c45cae435d101fe71081bc8ed8" +dependencies = [ + "polkavm-derive-impl", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" diff --git a/rust/contract_no_alloc/Cargo.toml b/rust/contract_no_alloc/Cargo.toml new file mode 100644 index 0000000..f9530f6 --- /dev/null +++ b/rust/contract_no_alloc/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "contract_no_alloc" +version = "0.1.0" +edition = "2021" +publish = false + +[[bin]] +name = "contract" +path = "src/contract.rs" + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 + +[dependencies] +polkavm-derive = { version = "0.30.0" } +picoalloc = "5.2.0" +pallet-revive-uapi = { version = "0.9.0", default-features = false } + + + + diff --git a/rust/contract_no_alloc/contract.sol b/rust/contract_no_alloc/contract.sol new file mode 100644 index 0000000..3f90238 --- /dev/null +++ b/rust/contract_no_alloc/contract.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +interface MyToken { + event Transfer(address indexed from, address indexed to, uint256 value); + error InsufficientBalance(); + + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + + function transfer(address to, uint256 amount) external; + function mint(address to, uint256 amount) external; +} diff --git a/rust/contract_no_alloc/src/contract.rs b/rust/contract_no_alloc/src/contract.rs new file mode 100644 index 0000000..378762b --- /dev/null +++ b/rust/contract_no_alloc/src/contract.rs @@ -0,0 +1,211 @@ +#![no_main] +#![no_std] + +use pallet_revive_uapi::{HostFn, HostFnImpl as api, ReturnFlags, StorageFlags}; +// +// Function selectors +const TRANSFER_SELECTOR: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb]; // transfer(address,uint256) +const MINT_SELECTOR: [u8; 4] = [0x40, 0xc1, 0x0f, 0x19]; // mint(address,uint256) + +// Event signature hash for Transfer(address,address,uint256) +const TRANSFER_EVENT_SIGNATURE: [u8; 32] = [ + 0xdd, 0xf2, 0x52, 0xad, 0x1b, 0xe2, 0xc8, 0x9b, 0x69, 0xc2, 0xb0, 0x68, 0xfc, 0x37, 0x8d, 0xaa, + 0x95, 0x2b, 0xa7, 0xf1, 0x63, 0xc4, 0xa1, 0x16, 0x28, 0xf5, 0x5a, 0x4d, 0xf5, 0x23, 0xb3, 0xef, +]; + +// Error selector for InsufficientBalance() +const INSUFFICIENT_BALANCE_ERROR: [u8; 4] = [0xf4, 0xd6, 0x78, 0xb8]; + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + // Safety: The unimp instruction is guaranteed to trap + unsafe { + core::arch::asm!("unimp"); + core::hint::unreachable_unchecked(); + } +} + +/// Storage key for totalSupply (slot 0) +#[inline] +fn total_supply_key() -> [u8; 32] { + [0u8; 32] // Slot 0 +} + +/// Helper function to compute storage key for balances[address] +/// Storage slot for balances mapping is 1 (totalSupply is at slot 0) +/// Follows Solidity convention: keccak256(leftPad32(key) ++ leftPad32(slot)) +fn balance_key(addr: &[u8; 20]) -> [u8; 32] { + let mut input = [0u8; 64]; // 32 bytes (padded address) + 32 bytes (slot) + + // First 32 bytes: address left-padded to 32 bytes (12 zeros + 20 address bytes) + input[12..32].copy_from_slice(addr); + + // Last 32 bytes: slot 1 for balances mapping (slot 0 is totalSupply) + input[63] = 1; + + let mut key = [0u8; 32]; + api::hash_keccak_256(&input, &mut key); + key +} + +/// Get totalSupply from storage +fn get_total_supply() -> u128 { + let key = total_supply_key(); + let mut supply_bytes = [0u8; 16]; + let mut supply_slice = &mut supply_bytes[..]; + + match api::get_storage(StorageFlags::empty(), &key, &mut supply_slice) { + Ok(_) => u128::from_be_bytes(supply_bytes), + Err(_) => 0u128, + } +} + +// #[inline(always)] +fn to_word(v: u128) -> [u8; 32] { + let mut out = [0u8; 32]; + out[16..].copy_from_slice(&v.to_be_bytes()); + out +} + +/// Set totalSupply in storage +#[inline] +fn set_total_supply(amount: u128) { + let key = total_supply_key(); + let bytes = amount.to_be_bytes(); + api::set_storage(StorageFlags::empty(), &key, &bytes); +} + +/// Get the balance for a given address from storage +#[inline] +fn get_balance(addr: &[u8; 20]) -> u128 { + let key = balance_key(addr); + let mut balance_bytes = [0u8; 16]; + let mut balance_slice = &mut balance_bytes[..]; + + match api::get_storage(StorageFlags::empty(), &key, &mut balance_slice) { + Ok(_) => u128::from_be_bytes(balance_bytes), + Err(_) => 0u128, + } +} + +/// Set the balance for a given address in storage +#[inline] +fn set_balance(addr: &[u8; 20], amount: u128) { + let key = balance_key(addr); + let bytes = amount.to_be_bytes(); + api::set_storage(StorageFlags::empty(), &key, &bytes); +} + +/// Emit a Transfer event +#[inline] +fn emit_transfer(from: &[u8; 20], to: &[u8; 20], value: u128) { + let mut from_topic = [0u8; 32]; + from_topic[12..32].copy_from_slice(from); + + let mut to_topic = [0u8; 32]; + to_topic[12..32].copy_from_slice(to); + + let topics = [TRANSFER_EVENT_SIGNATURE, from_topic, to_topic]; + let data = to_word(value); + api::deposit_event(&topics, &data); +} + +/// Revert with an InsufficientBalance error +#[inline] +fn revert_insufficient_balance() -> ! { + api::return_value(ReturnFlags::REVERT, &INSUFFICIENT_BALANCE_ERROR); +} + +/// Get the caller's address +#[inline] +fn get_caller() -> [u8; 20] { + let mut caller = [0u8; 20]; + api::caller(&mut caller); + caller +} + +/// Decode address from ABI-encoded data (32 bytes, address is in the last 20 bytes) +#[inline] +fn decode_address(data: &[u8]) -> [u8; 20] { + let mut addr = [0u8; 20]; + addr.copy_from_slice(&data[12..32]); + addr +} + +/// Decode u128 from ABI-encoded data (32 bytes) +#[inline] +fn decode_u128(data: &[u8]) -> u128 { + u128::from_be_bytes(data[16..32].try_into().unwrap()) +} + +/// This is the constructor which is called once per contract. +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +/// This is the regular entry point when the contract is called. +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let call_data_len = api::call_data_size() as usize; + + // Fixed buffer for call data + let mut call_data = [0u8; 256]; + if call_data_len > call_data.len() { + panic!("Call data too large"); + } + + api::call_data_copy(&mut call_data[..call_data_len], 0); + + if call_data_len < 4 { + panic!("Call data too short"); + } + + let selector: [u8; 4] = call_data[0..4].try_into().unwrap(); + + match selector { + TRANSFER_SELECTOR => { + // ABI encoding: selector(4) + address(32) + uint256(32) + if call_data_len < 68 { + panic!("Invalid transfer call data"); + } + + let to = decode_address(&call_data[4..36]); + let amount = decode_u128(&call_data[36..68]); + + let caller = get_caller(); + let sender_balance = get_balance(&caller); + + if sender_balance < amount { + revert_insufficient_balance(); + } + + let new_sender_balance = sender_balance - amount; + let recipient_balance = get_balance(&to); + let new_recipient_balance = recipient_balance + amount; + + set_balance(&caller, new_sender_balance); + set_balance(&to, new_recipient_balance); + emit_transfer(&caller, &to, amount); + } + MINT_SELECTOR => { + // ABI encoding: selector(4) + address(32) + uint256(32) + if call_data_len < 68 { + panic!("Invalid mint call data"); + } + + let to = decode_address(&call_data[4..36]); + let amount = decode_u128(&call_data[36..68]); + + let new_recipient_balance = get_balance(&to).saturating_add(amount); + set_balance(&to, new_recipient_balance); + + let new_supply = get_total_supply().saturating_add(amount); + set_total_supply(new_supply); + + let zero_address = [0u8; 20]; + emit_transfer(&zero_address, &to, amount); + } + _ => panic!("Unknown function selector"), + } +} diff --git a/rust/contract_with_alloc/.cargo/config.toml b/rust/contract_with_alloc/.cargo/config.toml new file mode 100644 index 0000000..8ae662c --- /dev/null +++ b/rust/contract_with_alloc/.cargo/config.toml @@ -0,0 +1,7 @@ +# Use a standard rust riscv64 target for cargo check and rust-anaylser +# cargo pvm will use `polkavm_linker::TargetJsonArgs::default()` +[build] +target = "riscv64imac-unknown-none-elf" + + + diff --git a/rust/contract_with_alloc/.gitignore b/rust/contract_with_alloc/.gitignore new file mode 100644 index 0000000..2dbb5ec --- /dev/null +++ b/rust/contract_with_alloc/.gitignore @@ -0,0 +1,2 @@ +/target +/*.polkavm diff --git a/rust/contract_with_alloc/Cargo.lock b/rust/contract_with_alloc/Cargo.lock new file mode 100644 index 0000000..e52c15d --- /dev/null +++ b/rust/contract_with_alloc/Cargo.lock @@ -0,0 +1,626 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "alloy-core" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f1ab91967646311bb7dd32db4fee380c69fe624319dcd176b89fb2a420c6b5" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", +] + +[[package]] +name = "alloy-primitives" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777d58b30eb9a4db0e5f59bc30e8c2caef877fee7dc8734cf242a51a60f22e05" +dependencies = [ + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "itoa", + "paste", + "ruint", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-macro" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68b32b6fa0d09bb74b4cefe35ccc8269d711c26629bc7cd98a47eeb12fe353f" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2afe6879ac373e58fd53581636f2cce843998ae0b058ebe1e4f649195e2bd23c" +dependencies = [ + "alloy-sol-macro-input", + "const-hex", + "heck", + "indexmap", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", + "syn-solidity", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ba01aee235a8c699d07e5be97ba215607564e71be72f433665329bec307d28" +dependencies = [ + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "syn", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-types" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e960c4b52508ef2ae1e37cae5058e905e9ae099b107900067a503f8c454036f" +dependencies = [ + "alloy-primitives", + "alloy-sol-macro", + "const-hex", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c06f1eb05f06cf2e380fdded278fbf056a38974299d77960555a311dcf91a52" +dependencies = [ + "keccak-const", + "sha2-const-stable", +] + +[[package]] +name = "const-hex" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" +dependencies = [ + "cfg-if", + "cpufeatures", + "proptest", + "serde_core", +] + +[[package]] +name = "contract_with_alloc" +version = "0.1.0" +dependencies = [ + "alloy-core", + "pallet-revive-uapi", + "picoalloc", + "polkavm-derive", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "derive_more" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "keccak-const" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d8d8ce877200136358e0bbff3a77965875db3af755a11e1fa6b1b3e2df13ea" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "pallet-revive-proc-macro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed97af646322cfc2d394c4737874bf6df507d25dd421a2939304eee02d89c742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pallet-revive-uapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0bf9c852c4426130520d546fe9ea0d932914c42ed7ae2970b5e428a3efe7e1" +dependencies = [ + "bitflags 1.3.2", + "const-crypto", + "hex-literal", + "pallet-revive-proc-macro", + "polkavm-derive", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "picoalloc" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1912f9b1d3aea43590e3986afdcf4ed1d9662edae24744a095738157d65b6a" + +[[package]] +name = "picosimd" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af35c838647fef3d6d052e27006ef88ea162336eee33063c50a63f163c18cdeb" + +[[package]] +name = "polkavm-common" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed1b408db93d4f49f5c651a7844682b9d7a561827b4dc6202c10356076c055c9" +dependencies = [ + "picosimd", +] + +[[package]] +name = "polkavm-derive" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb4463fb0b9dbfafdc1d1a1183df4bf7afa3350d124f29d5700c6bee54556b5" +dependencies = [ + "polkavm-derive-impl-macro", +] + +[[package]] +name = "polkavm-derive-impl" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993ff45b972e09babe68adce7062c3c38a84b9f50f07b7caf393a023eaa6c74a" +dependencies = [ + "polkavm-common", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "polkavm-derive-impl-macro" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4f5352e13c1ca5f0e4d7b4a804fbb85b0e02c45cae435d101fe71081bc8ed8" +dependencies = [ + "polkavm-derive-impl", + "syn", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +dependencies = [ + "bitflags 2.10.0", + "num-traits", + "rand 0.9.2", + "rand_chacha", + "rand_xorshift", + "unarray", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "ruint" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" +dependencies = [ + "proptest", + "rand 0.8.5", + "rand 0.9.2", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-solidity" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab4e6eed052a117409a1a744c8bda9c3ea6934597cf7419f791cb7d590871c4c" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/rust/contract_with_alloc/Cargo.toml b/rust/contract_with_alloc/Cargo.toml new file mode 100644 index 0000000..93e350d --- /dev/null +++ b/rust/contract_with_alloc/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "contract_with_alloc" +version = "0.1.0" +edition = "2021" +publish = false + +[[bin]] +name = "contract" +path = "src/contract.rs" + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 + +[dependencies] +polkavm-derive = { version = "0.30.0" } +alloy-core = { version = "0.8.0", default-features = false, features = ["sol-types"] } +picoalloc = "5.2.0" +pallet-revive-uapi = { version = "0.9.0", default-features = false } + + + + diff --git a/rust/contract_with_alloc/contract.sol b/rust/contract_with_alloc/contract.sol new file mode 100644 index 0000000..3f90238 --- /dev/null +++ b/rust/contract_with_alloc/contract.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +interface MyToken { + event Transfer(address indexed from, address indexed to, uint256 value); + error InsufficientBalance(); + + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + + function transfer(address to, uint256 amount) external; + function mint(address to, uint256 amount) external; +} diff --git a/rust/contract_with_alloc/src/contract.rs b/rust/contract_with_alloc/src/contract.rs new file mode 100644 index 0000000..d256548 --- /dev/null +++ b/rust/contract_with_alloc/src/contract.rs @@ -0,0 +1,176 @@ +#![no_main] +#![no_std] + +use alloy_core::{ + primitives::{Address, U256}, + sol, + sol_types::{SolCall, SolError, SolEvent}, +}; +use pallet_revive_uapi::{HostFn, HostFnImpl as api, ReturnFlags, StorageFlags}; + +extern crate alloc; +use alloc::vec; + +sol!("contract.sol"); +use crate::MyToken::transferCall; + +#[global_allocator] +static mut ALLOC: picoalloc::Mutex>> = { + static mut ARRAY: picoalloc::Array<1024> = picoalloc::Array([0u8; 1024]); + + picoalloc::Mutex::new(picoalloc::Allocator::new(unsafe { + picoalloc::ArrayPointer::new(&raw mut ARRAY) + })) +}; + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + // Safety: The unimp instruction is guaranteed to trap + unsafe { + core::arch::asm!("unimp"); + core::hint::unreachable_unchecked(); + } +} + +/// Storage key for totalSupply (slot 0) +#[inline] +fn total_supply_key() -> [u8; 32] { + [0u8; 32] // Slot 0 +} + +/// Helper function to compute storage key for balances[address] +/// Storage slot for balances mapping is 1 (totalSupply is at slot 0) +/// Follows Solidity convention: keccak256(leftPad32(key) ++ leftPad32(slot)) +fn balance_key(addr: &[u8; 20]) -> [u8; 32] { + let mut input = [0u8; 64]; // 32 bytes (padded address) + 32 bytes (slot) + + // First 32 bytes: address left-padded to 32 bytes (12 zeros + 20 address bytes) + input[12..32].copy_from_slice(addr); + + // Last 32 bytes: slot 1 for balances mapping (slot 0 is totalSupply) + input[63] = 1; + + let mut key = [0u8; 32]; + api::hash_keccak_256(&input, &mut key); + key +} + +/// Get totalSupply from storage +fn get_total_supply() -> U256 { + let key = total_supply_key(); + let mut supply_bytes = vec![0u8; 32]; + let mut supply_output = supply_bytes.as_mut_slice(); + + match api::get_storage(StorageFlags::empty(), &key, &mut supply_output) { + Ok(_) => U256::from_be_bytes::<32>(supply_output[0..32].try_into().unwrap()), + Err(_) => U256::ZERO, + } +} + +/// Set totalSupply in storage +#[inline] +fn set_total_supply(amount: U256) { + let key = total_supply_key(); + api::set_storage(StorageFlags::empty(), &key, &amount.to_be_bytes::<32>()); +} + +/// Get the balance for a given address from storage +#[inline] +fn get_balance(addr: &[u8; 20]) -> U256 { + let key = balance_key(addr); + let mut balance_bytes = vec![0u8; 32]; + let mut balance_output = balance_bytes.as_mut_slice(); + + match api::get_storage(StorageFlags::empty(), &key, &mut balance_output) { + Ok(_) => U256::from_be_bytes::<32>(balance_output[0..32].try_into().unwrap()), + Err(_) => U256::ZERO, + } +} + +/// Set the balance for a given address in storage +#[inline] +fn set_balance(addr: &[u8; 20], amount: U256) { + let key = balance_key(addr); + api::set_storage(StorageFlags::empty(), &key, &amount.to_be_bytes::<32>()); +} + +/// Emit a Transfer event +#[inline] +fn emit_transfer(from: Address, to: Address, value: U256) { + let event = MyToken::Transfer { from, to, value }; + let topics = [ + MyToken::Transfer::SIGNATURE_HASH.0, + event.from.into_word().0, + event.to.into_word().0, + ]; + let data = event.value.to_be_bytes::<32>(); + api::deposit_event(&topics, &data); +} + +/// Revert with an InsufficientBalance error +#[inline] +fn revert_insufficient_balance() -> ! { + let error = MyToken::InsufficientBalance {}; + let encoded_error = ::abi_encode(&error); + api::return_value(ReturnFlags::REVERT, &encoded_error); +} + +/// Get the caller's address +#[inline] +fn get_caller() -> [u8; 20] { + let mut caller = [0u8; 20]; + api::caller(&mut caller); + caller +} + +/// This is the constructor which is called once per contract. +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +/// This is the regular entry point when the contract is called. +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + let call_data_len = api::call_data_size(); + let mut call_data = vec![0u8; call_data_len as usize]; + api::call_data_copy(&mut call_data, 0); + + let selector: [u8; 4] = call_data[0..4].try_into().unwrap(); + + match selector { + MyToken::transferCall::SELECTOR => { + let transferCall { to, amount } = MyToken::transferCall::abi_decode(&call_data, true) + .expect("Failed to decode transfer call"); + + let caller = get_caller(); + let sender_balance = get_balance(&caller); + + if sender_balance < amount { + revert_insufficient_balance(); + } + + let new_sender_balance = sender_balance - amount; + + let recipient_balance = get_balance(&to.into_array()); + let new_recipient_balance = recipient_balance + amount; + + set_balance(&caller, new_sender_balance); + set_balance(&to.into_array(), new_recipient_balance); + emit_transfer(Address::from(caller), to, amount); + } + MyToken::mintCall::SELECTOR => { + let MyToken::mintCall { to, amount } = MyToken::mintCall::abi_decode(&call_data, true) + .expect("Failed to decode mint call"); + + let new_recipient_balance = get_balance(&to.into_array()).saturating_add(amount); + set_balance(&to.0 .0, new_recipient_balance); + + let new_supply = get_total_supply().saturating_add(amount); + set_total_supply(new_supply); + + emit_transfer(Address::ZERO, to, amount); + } + _ => panic!("Unknown function selector"), + } +} diff --git a/scripts/build_tokens.sh b/scripts/build_tokens.sh new file mode 100755 index 0000000..68c44a3 --- /dev/null +++ b/scripts/build_tokens.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +set -e + +echo "Building token contracts..." + +# Check if pop is installed +if ! command -v pop &>/dev/null; then + echo "Error: pop is not installed." + echo "Please install it by running:" + echo " cargo install --force --locked pop-cli" + exit 1 +fi + +# Build ink token contract +echo "Building ink token contract..." +cd ink/ink_erc20 +pop build --release +cd ../.. + +# Check if cargo-pvm-contract is installed +if ! cargo pvm-contract --version &>/dev/null; then + echo "cargo-pvm-contract is not installed. Installing..." + cargo install --force --locked cargo-pvm-contract +fi + +# Build PVM contracts +echo "Building PVM contract without alloc..." +cd rust/contract_no_alloc +cargo pvm-contract build +cd ../.. + +echo "Building PVM contract with alloc..." +cd rust/contract_with_alloc +cargo pvm-contract build +cd ../.. + +# Build PVM and resolc contracts +echo "Building PVM and resolc contracts..." +deno task build --filter MyToken + +echo "All token contracts built successfully!" diff --git a/scripts/node-env.sh b/scripts/node-env.sh index b127af5..d7beb2d 100755 --- a/scripts/node-env.sh +++ b/scripts/node-env.sh @@ -237,12 +237,8 @@ function dev-node() { "$POLKADOT_SDK_DIR/target/$bin_folder/revive-dev-node" build-spec --dev >"$HOME/.revive/revive-dev-node-chainspec-base.json" { set +x; } 2>/dev/null - # Apply patch from retester-chainspec-patch.json - jq -s '.[0] * .[1]' \ - "$HOME/.revive/revive-dev-node-chainspec-base.json" \ - "$SCRIPT_DIR/retester-chainspec-patch.json" \ - >"$HOME/.revive/revive-dev-node-chainspec.json" - rm -f "$HOME/.revive/revive-dev-node-chainspec-base.json" + # Apply retester patch + patch_chain_spec "$HOME/.revive/revive-dev-node-chainspec-base.json" "$HOME/.revive/revive-dev-node-chainspec.json" --retester fi ;; proxy) @@ -813,22 +809,87 @@ function retester_ci { fi } -# Helper function to endow development accounts in chain spec -# This is shared between westend() and passet() functions -# Usage: endow_dev_accounts -function endow_dev_accounts() { +# Helper function to apply various chain spec patches +# Usage: patch_chain_spec [--retester] [--dev-balance] [--dev-stakers] +# Examples: +# patch_chain_spec input.json output.json --retester +# patch_chain_spec input.json output.json --dev-balance --dev-stakers +# patch_chain_spec input.json output.json --retester --dev-balance --dev-stakers +# +# Account details for --dev-balance: +# Endow 0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac +# PassPhrase: bottom drive obey lake curtain smoke basket hold race lonely fit walk +# SS58: 5HYRCKHYJN9z5xUtfFkyMj4JUhsAwWyvuU8vKB1FcnYTf9ZQ +# Private key (ecdsa): 0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133 +function patch_chain_spec() { local input_spec="$1" local output_spec="$2" + shift 2 + + # Parse flags + local apply_retester="false" + local apply_dev_balance="false" + local apply_dev_stakers="false" + + for arg in "$@"; do + case "$arg" in + --retester) + apply_retester="true" + ;; + --dev-balance) + apply_dev_balance="true" + ;; + --dev-stakers) + apply_dev_stakers="true" + ;; + *) + echo "Unknown flag: $arg" + return 1 + ;; + esac + done + + local temp_spec="$input_spec" + + # Apply retester patch if requested (requires merge with external file) + if [ "$apply_retester" = "true" ]; then + echo "Applying retester patch" + local retester_temp="${input_spec}.retester.tmp" + jq -s '.[0] * .[1]' \ + "$temp_spec" \ + "$SCRIPT_DIR/retester-chainspec-patch.json" \ + >"$retester_temp" + if [ "$temp_spec" != "$input_spec" ]; then + rm -f "$temp_spec" + fi + temp_spec="$retester_temp" + fi + + # Build jq filter for remaining patches + local jq_filter='.' + + if [ "$apply_dev_balance" = "true" ]; then + echo "Endowing dev accounts" + jq_filter+=' | .genesis.runtimeGenesis.patch.balances.balances += [["5HYRCKHYJN9z5xUtfFkyMj4JUhsAwWyvuU8vKB1FcnYTf9ZQ", 100000000000000001000000000]]' + fi + + if [ "$apply_dev_stakers" = "true" ]; then + echo "Setting dev stakers" + jq_filter+=' | .genesis.runtimeGenesis.patch.staking.devStakers = [0, 0]' + fi - # Endow 0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac - # PassPhrase: bottom drive obey lake curtain smoke basket hold race lonely fit walk - # SS58: 5HYRCKHYJN9z5xUtfFkyMj4JUhsAwWyvuU8vKB1FcnYTf9ZQ - # Private key (ecdsa): 0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133 - jq '.genesis.runtimeGenesis.patch.balances.balances += [ - ["5HYRCKHYJN9z5xUtfFkyMj4JUhsAwWyvuU8vKB1FcnYTf9ZQ", 100000000000000001000000000] - ] - | .genesis.runtimeGenesis.patch.staking.devStakers = [0, 0] - ' "$input_spec" >"$output_spec" + # Apply jq filter if any patches were requested + if [ "$jq_filter" != "." ]; then + jq "$jq_filter" "$temp_spec" >"$output_spec" + if [ "$temp_spec" != "$input_spec" ]; then + rm -f "$temp_spec" + fi + else + # No patches to apply, just move/copy the result + if [ "$temp_spec" != "$output_spec" ]; then + mv -f "$temp_spec" "$output_spec" + fi + fi } # Helper function to send desktop notifications @@ -881,16 +942,15 @@ function wait_for_eth_rpc() { # Manages the Westend Asset Hub runtime for testing Polkadot Revive contracts # Builds a custom chain spec with development accounts endowed with funds -# Usage: westend [bacon|build|run] +# Usage: westend [bacon|build|run] [--retester] # Examples: -# westend bacon - Watch and rebuild the runtime on changes -# westend build - Build the runtime and generate chain spec with endowed accounts -# westend run - Run the already built runtime with polkadot-omni-node -# westend - Build and run the runtime (default) +# westend bacon - Watch and rebuild the runtime on changes +# westend build - Build the runtime and generate chain spec with endowed accounts +# westend build --retester - Build and generate retester chain spec using polkadot-omni-node +# westend run - Run the already built runtime with polkadot-omni-node +# westend run --retester - Run with retester chain spec +# westend - Build and run the runtime (default) function westend() { - # Capture the first argument as the command - arg=$1 - # Validate the polkadot-sdk directory if ! validate_polkadot_sdk_dir "$POLKADOT_SDK_DIR"; then return 1 @@ -899,27 +959,71 @@ function westend() { # Set default logging levels (can be overridden by environment variable) RUST_LOG="${RUST_LOG:-error,sc_rpc_server=info,runtime::revive=debug}" + # Parse arguments to detect --retester flag + retester_spec="false" + args=() + arg="" + + for var in "$@"; do + if [ "$var" = "--retester" ]; then + retester_spec="true" + elif [ -z "$arg" ] && [[ "$var" =~ ^(bacon|build|run)$ ]]; then + arg="$var" + else + args+=("$var") + fi + done + # Build the runtime and create a chain spec with endowed dev accounts build() { - # Build the asset-hub-westend-runtime - set -x - cargo build --quiet --manifest-path "$POLKADOT_SDK_DIR/Cargo.toml" -p asset-hub-westend-runtime - { set +x; } 2>/dev/null + if [ "$retester_spec" = "true" ]; then + # Build using polkadot-omni-node for retester + set -x + cargo build --quiet --manifest-path "$POLKADOT_SDK_DIR/Cargo.toml" -p asset-hub-westend-runtime + { set +x; } 2>/dev/null - # Create chain spec using chain-spec-builder - chain-spec-builder -c /tmp/ah-westend-spec.json \ - create \ - --para-id 1000 \ - --relay-chain dontcare \ - --runtime "$POLKADOT_SDK_DIR/target/debug/wbuild/asset-hub-westend-runtime/asset_hub_westend_runtime.wasm" \ - named-preset development + # Generate base chain spec using polkadot-omni-node + mkdir -p "$HOME/.revive" + set -x + polkadot-omni-node chain-spec-builder \ + --chain-spec-path "/tmp/ah-westend-spec-base.json" \ + create \ + --relay-chain dontcare \ + --para-id 1000 \ + --runtime "$POLKADOT_SDK_DIR/target/debug/wbuild/asset-hub-westend-runtime/asset_hub_westend_runtime.wasm" \ + named-preset development + { set +x; } 2>/dev/null - # Use helper function to endow accounts - endow_dev_accounts /tmp/ah-westend-spec.json ~/ah-westend-spec.json + patch_chain_spec "/tmp/ah-westend-spec-base.json" "$HOME/.revive/ah-westend-spec.json" --retester --dev-stakers + else + # Build the asset-hub-westend-runtime + set -x + cargo build --quiet --manifest-path "$POLKADOT_SDK_DIR/Cargo.toml" -p asset-hub-westend-runtime + { set +x; } 2>/dev/null + + # Create chain spec using chain-spec-builder + chain-spec-builder -c /tmp/ah-westend-spec.json \ + create \ + --para-id 1000 \ + --relay-chain dontcare \ + --runtime "$POLKADOT_SDK_DIR/target/debug/wbuild/asset-hub-westend-runtime/asset_hub_westend_runtime.wasm" \ + named-preset development + + # Use helper function to endow accounts + patch_chain_spec /tmp/ah-westend-spec.json ~/ah-westend-spec.json --dev-balance --dev-stakers + fi } # Run the polkadot-omni-node with the westend chain spec run() { + # Determine which chain spec to use + local chain_spec + if [ "$retester_spec" = "true" ]; then + chain_spec="$HOME/.revive/ah-westend-spec.json" + else + chain_spec="$HOME/ah-westend-spec.json" + fi + # Check if lnav is installed and pipe output to it if available if command -v lnav &>/dev/null; then set -x @@ -928,7 +1032,7 @@ function westend() { --log="$RUST_LOG" \ --instant-seal \ --no-prometheus \ - --chain ~/ah-westend-spec.json 2>&1 | lnav + --chain "$chain_spec" "${args[@]}" 2>&1 | lnav { set +x; } 2>/dev/null else set -x @@ -937,7 +1041,7 @@ function westend() { --log="$RUST_LOG" \ --instant-seal \ --no-prometheus \ - --chain ~/ah-westend-spec.json + --chain "$chain_spec" "${args[@]}" { set +x; } 2>/dev/null fi } @@ -1046,7 +1150,7 @@ function passet() { named-preset development # Use helper function to endow accounts - endow_dev_accounts /tmp/passet-spec.json ~/passet-spec.json + patch_chain_spec /tmp/passet-spec.json ~/passet-spec.json --dev-balance --dev-stakers } # Run the polkadot-omni-node with the passet chain spec @@ -1415,27 +1519,45 @@ function passet_stack() { # Runs the complete Westend Asset Hub stack (westend node + eth-rpc) in tmux window # This starts both the Westend node and Ethereum RPC bridge in separate panes -# Usage: westend_stack [--proxy] +# Usage: westend_stack [--proxy] [--build] [--retester] # Examples: -# westend_stack - Run both services without proxy -# westend_stack --proxy - Run both services with proxy +# westend_stack - Run both services without proxy +# westend_stack --proxy - Run both services with proxy +# westend_stack --build - Build westend and eth-rpc before starting +# westend_stack --retester - Use retester chainspec +# westend_stack --build --retester - Build with retester and run function westend_stack() { # Kill existing 'servers' window if it exists tmux kill-window -t servers 2>/dev/null # Parse arguments use_proxy="false" + build_flag="false" + retester_flag="" for arg in "$@"; do case "$arg" in --proxy) use_proxy="true" ;; + --build) + build_flag="true" + ;; + --retester) + retester_flag="--retester" + ;; esac done + # Build binaries if requested + if [ "$build_flag" = "true" ]; then + echo "Building westend and eth-rpc..." + westend build $retester_flag + eth-rpc build + fi + # Create new 'servers' window running westend node - tmux new-window -d -n servers "$CURRENT_SHELL -c 'source $SHELL_RC; westend run; exec \$SHELL'" + tmux new-window -d -n servers "$CURRENT_SHELL -c 'source $SHELL_RC; westend run $retester_flag; exec \$SHELL'" # Split the window and run eth-rpc with or without proxy if [ "$use_proxy" = "false" ]; then diff --git a/tools/build.ts b/tools/build.ts index 23eb99e..36f47fe 100644 --- a/tools/build.ts +++ b/tools/build.ts @@ -1,12 +1,30 @@ /// -import * as resolc from '@parity/resolc' import solc from 'solc' import { basename, join } from '@std/path' import * as log from '@std/log' import { parseArgs } from '@std/cli' -type CompileInput = Parameters[0] +type CompileInput = Record + +interface SolcOutput { + errors?: Array<{ + severity: string + formattedMessage: string + }> + contracts: Record< + string, + Record + > +} + const LOG_LEVEL = (Deno.env.get('LOG_LEVEL')?.toUpperCase() ?? 'INFO') as log.LevelName log.setup({ @@ -95,28 +113,202 @@ function writeCachedHash(hashFile: string, hash: string): void { Deno.writeTextFileSync(hashFile, hash) } +let resolcBin = Deno.env.get('RESOLC_BIN') || '' let resolcVersion = '' + +async function checkResolcExists() { + // If no RESOLC_BIN specified, find it in PATH + if (!resolcBin) { + // Try to find resolc, preferring cargo/system installations over node_modules + const pathsToCheck = [ + `${Deno.env.get('HOME')}/.cargo/bin/resolc`, + '/usr/local/bin/resolc', + '/usr/bin/resolc', + ] + + for (const path of pathsToCheck) { + try { + await Deno.stat(path) + resolcBin = path + break + } catch { + // Continue to next path + } + } + + // If not found in standard locations, use which command + if (!resolcBin) { + try { + const whichCommand = new Deno.Command('which', { + args: ['resolc'], + stdout: 'piped', + stderr: 'piped', + }) + const whichOutput = await whichCommand.output() + if (whichOutput.success) { + const foundPath = new TextDecoder().decode( + whichOutput.stdout, + ).trim() + // Skip if it's in node_modules + if (!foundPath.includes('node_modules')) { + resolcBin = foundPath + } + } + } catch { + // Continue + } + } + + if (!resolcBin) { + logger.error( + `Could not find resolc executable. Please install resolc or set RESOLC_BIN environment variable.`, + ) + Deno.exit(1) + } + } + + try { + const command = new Deno.Command(resolcBin, { + args: ['--version'], + stdout: 'piped', + stderr: 'piped', + }) + const output = await command.output() + if (!output.success) { + logger.error( + `Failed to run ${resolcBin}: ${ + new TextDecoder().decode(output.stderr) + }`, + ) + Deno.exit(1) + } + resolcVersion = new TextDecoder().decode(output.stdout).trim() + } catch (error) { + logger.error( + `Could not find ${resolcBin} executable. Please install resolc or set RESOLC_BIN environment variable.`, + ) + logger.error(`Error: ${error}`) + Deno.exit(1) + } +} + async function pvmCompile(file: Deno.DirEntry, sources: CompileInput) { if (resolcVersion === '') { - if (Deno.env.get('REVIVE_BIN') === undefined) { - resolcVersion = ` @parity/resolc: ${resolc.version().trim()}` - } else { - resolcVersion = new TextDecoder() - .decode( - ( - await new Deno.Command('resolc', { - args: ['--version'], - stdout: 'piped', - }).output() - ).stdout, + await checkResolcExists() + } + logger.info(`Compiling ${file.name} with ${resolcBin} ${resolcVersion}`) + logger.debug(`Using resolc binary: ${resolcBin}`) + + const input = { + language: 'Solidity', + sources, + settings: { + optimizer: { + enabled: true, + mode: 'z', + }, + remappings: [ + `@openzeppelin/=${ + join(rootDir, 'node_modules/@openzeppelin/') + }/`, + ], + outputSelection: { + '*': { + '*': [ + 'abi', + 'metadata', + 'evm.bytecode', + 'evm.deployedBytecode', + ], + }, + }, + }, + } + + const command = new Deno.Command(resolcBin, { + args: ['--standard-json'], + stdin: 'piped', + stdout: 'piped', + stderr: 'piped', + }) + + const child = command.spawn() + const writer = child.stdin.getWriter() + await writer.write(new TextEncoder().encode(JSON.stringify(input))) + await writer.close() + + const { stdout, stderr, success } = await child.output() + const stderrText = new TextDecoder().decode(stderr) + const stdoutText = new TextDecoder().decode(stdout) + + if (stderrText.trim().length > 0) { + logger.error(`resolc stderr: ${stderrText}`) + } + + if (!success) { + logger.error(`resolc command failed with exit code`) + Deno.exit(1) + } + + try { + const result = JSON.parse(stdoutText) + + // Check for errors in the compilation output + if (result.errors) { + for (const error of result.errors) { + if (error.severity === 'error') { + logger.error(error.formattedMessage || error.message) + } else if (error.severity === 'warning') { + logger.warn(error.formattedMessage || error.message) + } + } + + if ( + result.errors.some((err: { severity: string }) => + err.severity === 'error' ) - .trim() + ) { + Deno.exit(1) + } } + + return result + } catch (e) { + logger.error(`Failed to parse resolc output: ${e}`) + logger.error(`Output was: ${stdoutText}`) + Deno.exit(1) + } +} + +function tryResolveImport(importPath: string): string { + // Try node_modules first for package imports + if (importPath.startsWith('@')) { + const nodeModulesPath = join(rootDir, 'node_modules', importPath) + try { + Deno.statSync(nodeModulesPath) + return nodeModulesPath + } catch { + // Continue to other resolution strategies + } + } + + // Try relative to contracts directory + const contractsPath = join(contractsDir, importPath) + try { + Deno.statSync(contractsPath) + return contractsPath + } catch { + // Continue to other resolution strategies + } + + // Try relative to root directory + const rootPath = join(rootDir, importPath) + try { + Deno.statSync(rootPath) + return rootPath + } catch { + throw new Error(`Could not resolve import: ${importPath}`) } - logger.info(`Compiling ${file.name} with revive ${resolcVersion}`) - return await resolc.compile(sources, { - bin: Deno.env.get('REVIVE_BIN'), - }) } let solcVersion = '' @@ -129,6 +321,10 @@ function evmCompile(file: Deno.DirEntry, sources: CompileInput) { language: 'Solidity', sources, settings: { + optimizer: { + enabled: true, + runs: 200, + }, outputSelection: { '*': { '*': ['*'], @@ -140,7 +336,7 @@ function evmCompile(file: Deno.DirEntry, sources: CompileInput) { return solc.compile(JSON.stringify(input), { import: (relativePath: string) => { const source = Deno.readTextFileSync( - resolc.tryResolveImport(relativePath), + tryResolveImport(relativePath), ) return { contents: source } }, @@ -216,7 +412,7 @@ for (const file of input) { const evmOut = JSON.parse( evmCompile(file, inputSources), - ) as resolc.SolcOutput + ) as SolcOutput if (evmOut.errors) { for (const error of evmOut.errors) { diff --git a/tools/lib/index.ts b/tools/lib/index.ts index 6ae7047..d845997 100644 --- a/tools/lib/index.ts +++ b/tools/lib/index.ts @@ -46,25 +46,36 @@ let firstDeploy = true * This function deploys a contract identified by its name, with the specified * arguments and optional value, and updates the addresses file with the contract's address. * - * @param options.name - The name of the contract to deploy. - * @param [options.id] - An optional identifier that will be used to identify the contract in the generated addresses file, default to [options.name]. + * @param options.name - The name of the contract to deploy, or an object with name and mappedTo. * @param options.args - The arguments required by the contract's constructor. * @param [options.value] - An optional value (in wei) to send with the deployment. + * @param [options.bytecodeType] - The type of bytecode to deploy ('evm' or 'polkavm'). */ export async function deploy({ - id, name, args, value, bytecodeType, + bytecode, }: { - id?: string - name: K + name: K | { name: K; mappedTo: string } args: ContractConstructorArgs value?: bigint bytecodeType?: 'evm' | 'polkavm' + bytecode?: Hex }): Promise { - if (filter && !name.toLowerCase().includes(filter.toLowerCase())) { + let contractName: K + let mappedTo: string | undefined + + if (typeof name === 'string') { + contractName = name + mappedTo = undefined + } else { + contractName = name.name + mappedTo = name.mappedTo + } + + if (filter && !contractName.toLowerCase().includes(filter.toLowerCase())) { return '0x' } @@ -106,18 +117,19 @@ export const chain = defineChain({ writeFileSync(join(codegenDir, 'chain.ts'), chain, 'utf8') } - console.log(`🚀 Deploying ${name}`) + const id = mappedTo ?? contractName + console.log(`🚀 Deploying ${id}`) - id ??= name const receipt = await env.deploy({ - name, + name: contractName, args, value, bytecodeType, + bytecode, }) if (receipt.status === 'reverted') { - console.error(`❌ Contract "${name}" reverted`) + console.error(`❌ Contract "${contractName}" reverted`) Deno.exit(1) } const address = receipt.contractAddress @@ -152,7 +164,7 @@ export const chain = defineChain({ ].join('\n') const exportLine = - `export const ${id} = { address: addresses.${id}, abi: abis.${id} }` + `export const ${id} = { address: addresses.${id}, abi: abis.${contractName} }` const regex = new RegExp(`^export const ${id} = .*`, 'm') if (regex.test(contracts)) { @@ -164,7 +176,7 @@ export const chain = defineChain({ } console.log( - `✅ ${name} deployed: ${address} at block ${receipt.blockNumber}\n tx hash: ${receipt.transactionHash}`, + `✅ ${id} deployed: ${address} at block ${receipt.blockNumber}\n tx hash: ${receipt.transactionHash}`, ) return address } diff --git a/utils/index.ts b/utils/index.ts index be713f6..3097387 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -92,14 +92,9 @@ export async function createEnv({ name: string, bytecodeType: 'evm' | 'polkavm' = 'evm', ): Hex { - const bytecode = bytecodeType == 'evm' - ? Deno.readFileSync(`codegen/evm/${name}.bin`) - : Deno.readFileSync(`codegen/pvm/${name}.polkavm`) - return `0x${ - Array.from(bytecode) - .map((b: number) => b.toString(16).padStart(2, '0')) - .join('') - }` as Hex + return bytecodeType == 'evm' + ? readBytecode(`codegen/evm/${name}.bin`) + : readBytecode(`codegen/pvm/${name}.polkavm`) } const chain = defineChain({ @@ -118,7 +113,11 @@ export async function createEnv({ testnet: true, }) - const transport = http(rpcUrl) + const transport = http(rpcUrl, { + // enable batching + // timeout: 60_000, + // batch: { wait: 100 }, + }) const [account] = await createWalletClient({ transport, chain, @@ -168,6 +167,16 @@ export async function createEnv({ params: [txHash, params], }) }, + postDispatchWeight( + transactionHash: Hex, + ) { + return client.request({ + method: 'substrate_postDispatchWeight' as never, + params: [ + transactionHash, + ], + }) + }, traceBlock( blockNumber: bigint, tracer: Tracer, @@ -203,15 +212,17 @@ export async function createEnv({ args, value, bytecodeType, + bytecode, }: { name: K args: ContractConstructorArgs value?: bigint bytecodeType?: 'evm' | 'polkavm' + bytecode?: Hex }) { const hash = await wallet.deployContract({ abi: abis[name] as Abi, - bytecode: getByteCode(name, bytecodeType), + bytecode: bytecode ?? getByteCode(name, bytecodeType), args: args as readonly unknown[], value, }) @@ -221,3 +232,13 @@ export async function createEnv({ return { chain, deploy, getByteCode, wallet, debugClient } } + +export function readBytecode( + filepath: string, +): Hex { + return `0x${ + Array.from(Deno.readFileSync(filepath)) + .map((b: number) => b.toString(16).padStart(2, '0')) + .join('') + }` as Hex +}