• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

竹千代

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

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}("")

true/false。

一般失败要回滚,需要开发中自己搭配require(success, '...')

所有剩余 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、缺少可追溯能力

posted on 2024-03-24 16:50  竹千代  阅读(72)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3