PolkaVM 是 Polkadot 2.0 的关键组件,提供了一个在 RISC-V 上的执行环境。对于 Solidity 开发者来说,Polkadot 提供了 Revive,一个将 Solidity 代码编译成 PolkaVM 字节码的工具。我们已经成功测试了许多 Solidity 项目,包括 Uniswap V2,展示了成功的部署和测试。除了这个开发流程,也可以用任何语言编写合约,然后将它们编译成 PolkaVM 字节码。
本文使用这个 repo(🔗 链接:github.com/papermoonio… 作为示例,来说明如何在 Rust 中实现一个与 ERC20 接口兼容的 ERC20 合约。开发者在理解了仓库中的源代码后,可以用 Rust 实现合约。
环境安装
按照 README.md 中的说明安装所需的环境。首先,你需要安装 Rust 来构建代码。然后,polkatool 用于链接二进制文件。我们使用 TypeScript 来测试合约的部署和交互,因此 Node.js、TypeScript 和 Yarn 也是必需的。
项目结构
这是一个典型的 Rust 项目,但目标是 RISC-V。你应该检查 .cargo 文件夹中的 config.toml 文件。这个配置允许我们使用不同的目标来编译代码。
[build]target = "riscv64emac-unknown-none-polkavm.json"[unstable]build-std = ["core", "alloc"]build-std-features = ["panic_immediate_abort"]
"riscv64emac-unknown-none-polkavm.json" 目标文件位于仓库中,定义了编译参数,例如使用的 LLVM 和链接器。
build.sh 脚本用于构建项目并调用 polkatool link 来生成 PolkaVM 字节码。
用 TypeScript 编写的部署和测试代码位于 ts 文件夹中。
构造函数
让我们看一下 erc20.rs。有一个 deploy 函数,它充当 Solidity 中的构造函数。为了避免在合约中引入编码/解码代码,我们使用 ethabi 库来解码构造函数参数。这些参数包括两个字符串(名称和符号),以及两个 Uint256 值(小数位数和总供应量)。
#[no_mangle]#[polkavm_derive::polkavm_export]pub extern "C" fn deploy() { input!(data: &[u8; 256],); let mut sender = [0_u8; 20]; api::origin(&mut sender); let param_types = &[ ParamType::String, ParamType::String, ParamType::Uint(256), ParamType::Uint(256), ]; let decode_result = decode(param_types, &data[..]).unwrap(); if let ( Token::String(name), Token::String(symbol), Token::Uint(decimals), Token::Uint(total_supply), ) = ( &decode_result, &decode_result, &decode_result, &decode_result, ) { set_string(NAME_LENGTH, NAME, name.as_bytes()); set_string(SYMBOL_LENGTH, SYMBOL, symbol.as_bytes()); let mut data = [0_u8; 32]; decimals.to_big_endian(&mut data); api::set_storage(StorageFlags::empty(), DECIMALS, &data); let supply = U256::from(10).pow(*decimals).saturating_mul(*total_supply); supply.to_big_endian(&mut data); api::set_storage(StorageFlags::empty(), TOTAL_SUPPLY, &data); api::set_storage(StorageFlags::empty(), &get_balance_key(&sender), &data); } else { panic!("Failed to decode input data"); }}
在解析了这四个参数后,合约通过调用 api::set_storage 将它们存储在区块链中。这些参数有四个键,因为区块链将所有内容存储为键值对。
const NAME: &[u8] = b"name";const SYMBOL: &[u8] = b"symbol";const NAME_LENGTH: &[u8] = b"name_length";const SYMBOL_LENGTH: &[u8] = b"symbol_length";const DECIMALS: &[u8] = b"decimals";const TOTAL_SUPPLY: &[u8] = b"total_supply";
还有两个额外的键用于存储名称和符号的长度。这允许我们在用户从合约获取数据时使用正确大小的缓冲区来检索字符串。
内存分配
在合约实现中,我们使用了像 String 和 Vec 这样的动态大小数据结构。为了避免从 std 中进行分配,合约使用了 PolkaVM 仓库中的一个简单分配器。这允许我们在合约中使用任意大小的字符串和 Vec<u8>。与基于固定 256 字节长度堆栈的 EVM 中的动态数据存储相比,PolkaVM 更加节省存储空间。
use simplealloc::SimpleAlloc;#[global_allocator]pub static mut GLOBAL: SimpleAlloc<{ 1024 * 10 }> = SimpleAlloc::new();[dependencies]simplealloc = { version = "0.23.0", git = "https://github.com/paritytech/polkavm.git" }
Call
合约部署后,唯一的入口点是 Call 函数。为了模拟 Solidity 中的 Calldata,合约将前 4 个字节作为选择器。然后,它将这个选择器与不同函数(如 name、transfer、allowance 等)的选择器进行比较。如果一个函数有参数,合约会从输入中读取更多内容,并使用 ethabi 库中的 decode 函数来解析它们。
#[no_mangle]#[polkavm_derive::polkavm_export]pub extern "C" fn call() { input!(selector: &[u8; 4],); let length = api::call_data_size(); if length > 256 { panic!("Input data too long"); } let mut sender = [0_u8; 20]; api::origin(&mut sender); match selector { &NAME_SELECTOR => { api::return_value(ReturnFlags::empty(), &get_string(NAME_LENGTH, NAME)[..]) } &SYMBOL_SELECTOR => { api::return_value(ReturnFlags::empty(), &get_string(SYMBOL_LENGTH, SYMBOL)[..]) } &DECIMALS_SELECTOR => { let mut data = [0_u8; 32]; let _ = api::get_storage(StorageFlags::empty(), DECIMALS, &mut &mut data[..]); api::return_value(ReturnFlags::empty(), &data[..]) } &TOTAL_SUPPLY_SELECTOR => { let mut data = [0_u8; 32]; let _ = api::get_storage(StorageFlags::empty(), TOTAL_SUPPLY, &mut &mut data[..]); api::return_value(ReturnFlags::empty(), &data[..]) } &BALANCE_OF_SELECTOR => { input!(buffer: &[u8; 4 + 32],); let param_types = &[ParamType::Address]; let decode_result = decode(param_types, &buffer[4..]).unwrap(); if let Token::Address(address) = &decode_result { let mut data = [0_u8; 32]; let _ = api::get_storage( StorageFlags::empty(), &get_balance_key(&address.to_fixed_bytes()), &mut &mut data[..], ); api::return_value(ReturnFlags::empty(), &data[..]) } else { panic!("Failed to decode input data"); } } // ... }}
部署和测试
Rust 代码完成后,我们可以通过运行 build.sh 来生成 PolkaVM 代码。在 ts 文件夹中,有一个基于 ethers.js 的简单应用程序,它连接到 Westend Asset Hub ETH RPC 端点。这个应用程序部署合约并测试所有的 ERC20 接口。
连接和合约部署
const url = "https://westend-asset-hub-eth-rpc.polkadot.io"; const provider = new ethers.JsonRpcProvider(url); config(); let privateKey = process.env.AH_PRIV_KEY || ""; const walletClient = new Wallet(privateKey, provider); const contractAddress = await deploy(provider, walletClient) const contract = new Contract( contractAddress, ABI, walletClient, );
**检查 ERC20 中的所有存储,**例如 name、symbol 和 balance 等
const name = await contract.name(); const symbol = await contract.symbol(); const decimals = await contract.decimals(); const totalSupply = await contract.totalSupply(); const balance = await contract.balanceOf(walletAddress); const allowance = await contract.allowance(walletAddress, recipientAddress);
**调用 transfer、approve、**transferFrom 并检查余额
const transferAmount = BigInt(1e18); const transferTx = await contract.transfer(recipientAddress, transferAmount); await transferTx.wait(); const myBalanceAfterTransfer = await contract.balanceOf(walletAddress);const approveAmount = BigInt(2e18); const approveTx = await contract.approve(recipientAddress, approveAmount); await approveTx.wait(); const approveAllowance = await contract.allowance( walletAddress, recipientAddress, );const transferFromTx = await contract2.transferFrom( walletAddress, walletAddress, approveAmount / BigInt(2), ); await transferFromTx.wait(); const allowanceAfterTransferFrom = await contract.allowance( walletAddress, recipientAddress, );
在整个测试过程中,一切都与 Solidity 中的标准 ERC20 合约相同。
总结
PolkaVM 作为一个通用的合约平台,可以使用很多语言来实现合约。这个 repo 是一个供开发者尝试用 Rust 实现合约的示例。
免责声明:由PaperMoon提供并包含在本文中的材料仅用于学习目的。它们不构成财务或投资建议,也不应被解读为任何商业决策的指导。我们建议读者在做出任何投资或商业相关的决定之前,进行独立研究并咨询专业人士。PaperMoon对根据本文内容采取的任何行动不承担任何责任。