Solidity智能合约开发
一、开发
1、类型
// 基础类型 bool、 unit8、 uint256、 string
mapping:
address: 其他合约地址 // 高级类型 interface: 表示外部合约的函数签名,用于调用其他合约的函数。 示例,InterfaceName(address).method()
struct: 表示一种自定义的数据结构,用于打包多个变量成为一个对象。 示例,MyStruct memory s = MyStruct(...)
2、函数定义
// 类似装饰器,主要作用就是代码复用 modifier onlyOwner() { require(owner == msg.sender, "Owner emassiz!"); _; }
// 访问属性:
public: 既可以从内部、也可以从外部调用
private: 表示只能在当前合约内部调用, 子合约也不可以调用
external: 表示该方法只能从合约外部调用
internal: 表示该方法只能从合约内部、或继承的子合约调用
// 状态属性
pure: 既不访问、也不修改区块链状态
view: 只读区块链状态, 不会修改
payable:表示该函数可以接受主币
// 示例:
function allowance (address _owner, address _spender) public view returns (uint256){
......
}
function mint(address _to, uint256 _tokensToMint) public onlyOwner payable {
......
}
3、内置能力
// 查询当前时间 uint currentTime = block.timestamp; // 得到时间戳 // 生成伪随机数。 区块链是确定机制,无法直接做到,这种只适用于非价值类的。 因为block.timestamp和msg.sender都可以刻意构造。
uint256(keccak256(abi.encodePacked(block.timestamp,
block.prevrandao, // 替代 block.difficulty,从 Ethereum Shanghai 升级后启用
msg.sender
)))
// 生成类似uuid的唯一id function generateId() public view returns (bytes32) {
//abi.encodePacked(...),
将输入打包成紧凑字节数组;// keccak256(...),
对字节数组进行 Keccak-256 哈希(Ethereum 的标准哈希算法)
return keccak256(abi.encodePacked(block.timestamp, msg.sender, block.number));
// 还可以再继续取前几位, 转hex等
}
// 转账主币 some_address.call{value: amount}(""); (bool sent, ) = msg.sender.call{value: amount}(""); // 它的作用是:向调用合约的地址(msg.sender)发送 amount 数量的以太币,并捕获发送是否成功的布尔结果 sent // 转账token
调用
// 合约自身信息
合约地址: address(this).
合约余额: address(this).balance
方法 | 语法 | 返回值 | Gas 限制 | 安全性 |
---|---|---|---|---|
.transfer |
recipient.transfer(amount) |
无返回值,失败则自动 revert | 固定 2300 gas | ✅ 安全但限制 |
.send |
bool success = recipient.send(amount) |
true/false |
固定 2300 gas | ⚠️ 有失败风险 |
.call |
(bool success, ) = recipient.call{value: amount}("") |
|
所有剩余 gas(可控) | ✅ 推荐方式 |
二、单元测试
import { time, loadFixture, } from "@nomicfoundation/hardhat-toolbox/network-helpers"; import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; import { expect } from "chai"; import hre from "hardhat"; describe("Lock", function () { // We define a fixture to reuse the same setup in every test. // We use loadFixture to run this setup once, snapshot that state, // and reset Hardhat Network to that snapshot in every test. async function deployOneYearLockFixture() { const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; const ONE_GWEI = 1_000_000_000; const lockedAmount = ONE_GWEI; const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; // Contracts are deployed using the first signer/account by default const [owner, otherAccount] = await hre.ethers.getSigners(); const Lock = await hre.ethers.getContractFactory("Lock"); const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); return { lock, unlockTime, lockedAmount, owner, otherAccount }; } describe("Deployment", function () { it("Should set the right unlockTime", async function () { // loadFixture作用类似jest的beforeEach+自动重置状态 const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture); expect(await lock.unlockTime()).to.equal(unlockTime); }); }} // 前置操作 模拟部署: Contract.deploy(latestTime, { value: 1 })) // 切换合约调用账户(地址): const [owner, otherAccount] = await hre.ethers.getSigners(); lock.connect(otherAccount) // 断言 是否没经过require校验: await expect(Lock.func().to.be.revertedWith( "Unlock time should be in the future" ); 是否emit事件: expect(lock.withdraw()) .to.emit(lock, "Withdrawal") 是否完成转账: await expect(contract.withdraw()).to.changeEtherBalances( [owner, lock], [lockedAmount, -lockedAmount] );
三、gas费的统计与优化
最常用的工具是, hardhat-gas-reporter
执行单测获得模拟gas费数据的方式: REPORT_GAS=true npx hardhat test
四、部署
// 1、初次部署: 先通过openzepplin编写可升级合约 // 用initialize,而不用构造函数的原因: pragma solidity ^0.8.19; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract MyUpgradeableContract is Initializable { uint256 public value; function initialize(uint256 _value) public initializer { value = _value; } } // 2、初次部署,执行部署 const { ethers, upgrades } = require("hardhat"); async function main() { const MyUpgradeableContract = await ethers.getContractFactory("MyUpgradeableContract"); // 指定了initialize函数是初始化函数,[42]是给到的入参, const proxy = await upgrades.deployProxy(MyUpgradeableContract, [42], { initializer: "initialize" }); await proxy.deployed(); // 记录代理合约地址 console.log("Proxy deployed to:", proxy.address); } main().catch((error) => { console.error(error); process.exitCode = 1; }); // 3、升级再部署(无状态数据变化)
// 先写升级后的新合约代码, 可以利用继承来扩展实现
contract MyUpgradeableContractV2 is MyUpgradeContract {
function burn(uint256 amount) public {
_burn(msg.sender, amount);
}
}
const { ethers, upgrades } = require("hardhat"); async function main() { const NewImplementation = await ethers.getContractFactory("MyUpgradeableContractV2"); const proxyAddress = "YOUR_PROXY_ADDRESS"; // 指定初始部署后的地址; 表示修改升级,这里不再需要initialize await upgrades.upgradeProxy(proxyAddress, NewImplementation); console.log("Proxy upgraded"); }
// 4、升级再部署(有状态数据变化,比如新增了状态变量)
// 先在合约中新的、额外的初始化方法
function initializeV2() public reinitializer(2) {
// 新的初始化逻辑
}
// 升级脚本中,手动调用该额外初始化方法
const proxy = await ethers.getContractAt("MyUpgradeableContractV2", proxyAddress);
await proxy.initializeV2();
1、转账
native币的转账直接用原声的transfer方法
token合约币转账,需要使用合约方法
//以太币转账 address.transfer() // token币转账 IERC20(address).transferFrom()
2、部署
hardhat的api能直接处理好部署发布代理合约和实现合约
async function main() { // 获取合约工厂,保证contracts目录里面有MyContract合约 const MyContract = await ethers.getContractFactory("MyContract"); // 部署可升级合约 console.log("Deploying the upgradable MyContract..."); const myContract = await upgrades.deployProxy(MyContract, ["MyContractV1"], { initializer: "initialize", gasPrice: '...' }); console.log("MyContract deployed to:", myContract.address); }
3、调用
- 在合约中调用另外合约的方法
// 引入接口类型,然后直接用目标合约地址进行调用
// 因为合约本来就在链上,是可信任环境,不需要签名
Interface(address).methodA(param1, param2, ...)
- 从客户端调用合约的方法
// Infura等托管节点服务,它们会处理签名
// 如果没有,需要用web3.js先处理签名
const web3 = new Web3('https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY'); const contract = new web3.eth.Contract(contractABI, contractAddress); // 使用call方法。 用于读取合约状态或数据,不会改变合约状态,通常用于查询合约数据,例如获取合约中的变量值或执行只读函数 contract.methods.YOUR_CONTRACT_METHOD().call() .then(result => { console.log('Result:', result); }) .catch(error => { console.error('Error:', error); }); // 使用send方法。用于执行合约方法,会改变合约状态,通常用于执行会改变合约状态的函数。
// 这种适合用于有钱包插件环境,因为send会被钱包插件补货,内部给它做签名。调用方是完全不会知道privateKey contract.methods.YOUR_CONTRACT_METHOD().send({ from: '0xYOUR_ADDRESS', gas: 1000000 }) .then(receipt => { console.log('Transaction receipt:', receipt); }) .catch(error => { console.error('Error:', error); });
// 适用于服务端调用,调用方掌握了privateKey情况
const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
调用的错误一般总共有四类
- 连接到节点的JSON-RPC错误
- 交易错误,比如交易因gas不足而失败
- 合约错误,一般是触发了合约的require、revert等
- 调用错误
// json-rpc错误 { message: 'Internal JSON-RPC error.', code: -32603, // ...其他信息 }
// 交易错误
{
message: 'Transaction ran out of gas.',
transactionHash: '0x...',
gasUsed: 12345,
}
4、升级
合约因为功能变更、实现bug等情况,需要升级改动。必须要使用代理模式的合约才可以升级:
- 升级后,合约原本的storage属性不受影响
因为storage
变量存储在代理合约中,而合约逻辑存储在实现合约中。当你升级实现合约时,代理合约会将调用转发到新的实现合约,但storage
变量仍然存储在代理合约中,因此不会发生变化
- 新增的
storage
变量应该位于合约结构的末尾,以确保与现有storage
变量的布局兼容
storage
变量按照声明的顺序在合约中进行布局。如果你在现有变量之间插入新的storage
变量,可能会导致现有变量的布局发生变化,从而导致代理合约无法正确访问storage
变量
1、solidity不足,导致以太坊分叉
2、缺少权限检查,任何人都可以调用
3、缺少可追溯能力