Solidity笔记

前言

本方将记录我学习solidity过程中的一些知识点。随着学习的深入,内容会持续更新。
学习过程中参考的技术文档:
solidity
solidity-by-example
以太坊电子书
hardhat
ethers.js

三个特殊函数

  • constructor: 构造函数,仅在合约部署时执行一次,当指定为payable时,表示允许部署时向合约转以太坊;
  • receive: 用于接收转账,外部调用calldata参数为空,且msg.value不为空时,该函数被默认调用,该函数的可见性一般定义为external,但必须指定为payable;
  • fallback: 当外部调用指定的函数签名不匹配当前合约的所有函数时或calldata为空时,默认调用该函数,一般用于错误处理。可见性必须定义为external,当该函数指定为payable时可以替代receive函数;

注:这些特殊函数定义时都不需要指定function关键词;

三种变量类型

  • memory: 这种类型的变量叫临时变量,其生命周期仅存在于函数执行时,函数执行结束后这种类型的变量被销毁。
  • calldata: 这种类型也是一种临时变量。EOA调用合约方法或合约间方法调用时,传递到方法的参数即是这种类型,这种类型的数据只可以读取,不能修改,如果要修改只能复制到memory类型变量进而对memory类型进行修改。当外部调用传递大量数据时这种将函数的参数定义为calldata相当有用,因为它减少了实参数据到memory的复制过程,从而节省了gas消耗。
  • sotrage: 永久的存在区块链的,其成本比较高昂。

函数的四种修饰符

  • view: 函数内部只能读取合约中定义的状态变量,而不修改;
  • prue: 函数内部即不能读取也不能修改合约中定义的状态变量;
  • payable: 外部调用该类型函数时允许发送ether;
  • nonpaybe: 外部调用该类型函数时不允许发送ether,当外部调用该类型的方法进行ether转账时,交易会失败扣除相应gas费用后ehter会返回给调用方;
    注:如果没有payable修饰符,当外部调用者调用合约方法时误将ether转入合约,而合约未设置账户进取功能,那么这笔ether将被锁定无法提取。

this

  • 以太坊合约中this可以显式转换为address类型,转换后可以调用address的所有方法,如:合约余额address(this).balance;
  • this还可以用于调用合约自身的方法,如下
import "hardhat/console.sol";
contract Example() {
    function a() public view {
        console.log( this.b() ); // 3
    }
    function b() public pure returns(uint) {
        return 3;
    }
}

selfdestruct

当合约代码不再使用时可以调用该函数销毁合约,被销毁的合约将从区块链中删除。确保删除后不会再有其它调用者向该合约地址转账,销毁后的所有转账将会被冻结。该函数调用时接收payable地址,用于接收该合约中剩余ether。

contract Contract {
    uint _countdown = 10;

    constructor() payable { }

    function tick() public {
        _countdown--;
        if(_countdown == 0) {
            // NOTE: we must cast to payable here
            // some solidity methods protect 
            // against accidentally sending ether
            selfdestruct(payable(msg.sender));
        }
    }
}

异常

触发机制

  • send函数会向调用者抛出异常,该异常会在调用链传递,直到有try/catch对其进行捕获;
  • 底层函数call/delegatecall/staticcall返回的第一个值用于标识执行结果,如果为false则表明调用异常;
    注:作为EVM设计的一部分,如果调用的账户不存在则其返回的第一个值是true,因此调用底层函数时需要先检查账户是否存在。

错误类型

  • Panic(uint256),该类型的错误一般应用于底层,一般编译器可以检查出来;
  • Error(string),该类型的错误一般应用于业务逻辑条件判断;

抛出错误的方法

  • assert:产生Panic(uint)类错误
  • require:产生Error(string)类错误
  • revert:产生Error(string)类错误,并且支持定制类错误CustomError()
    更详细的错误解析看这里

合约调用

三方库调用

使用ethers.js或web3.js通过JSON-RPC调用合约方法。

  1. 编写合约并部署
// Switch.sol
contract Switch {
    bool isOn;

    function change(bool _isOn) external {
        isOn = _isOn;
    }
}
  1. 根据合约ABI及合约地址调用合约
// turnOnSwitch.js
require('dotenv').config();
const ethers = require('ethers');
//此处用合约生成的ABI
const contractABI = [];

//根据本地配置来初始化provider
const provider = new ethers.providers.AlchemyProvider(
  'sepolia',
  process.env.TESTNET_ALCHEMY_KEY
);

const wallet = new ethers.Wallet(process.env.TESTNET_PRIVATE_KEY, provider);

async function main() {
  const counterContract = new ethers.Contract(
    '合约地址',
    contractABI,
    wallet
  );

  await counterContract.change(true);
}

main();

合约间调用

通常EOA通过发送交易的形式与合约交互,其中交易中有个字段data用于指明要调用 的目标合约的方法及参数,该字段也就是我们常说的calldata。不同合约间方法调用通常也需要传递calldata。合约方法调用一般有如下两种情形:

  • 调用者组织calldata。
//通过abi.encodeWithSignature方法生成calldata
contract Example {
   function sendData(address x) external {
       (bool s, ) = x.call(
          abi.encodeWithSignature("receiveData(uint256)", 5)
        );
       require(s);
   } 
}
  • 通过接口定义来调用
interface A {
		function receiveData(uint) external;
}

contract Example {
   function sendData(address x) external {
       (bool s, ) = A(x).receiveData(5);
       require(s);
   } 
}

hardhat开发流程

hardhat是一个工具集,其中囊括了智能合约开发过程中所涉及的一切,包括合约的编译、部署、测试和调试等。

hardhat有如下特点:

  1. 本地测试,包括本地网络设置;
  2. Solidity编译及错误检查;
  3. 与其它工具、插件的集成,比如:ethers.js、Mocha等;
  4. 以更直观的方式让开发人员与智能合约交互;

下面以代码示例说明hardhat开发、部署、测试等过程

项目依赖安装

npm install --save-dev hardhat
npm install @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers dotenv

上述dotenv主要用于解析本地环境变量,一般项目的最佳实践是将一些敏感信息以环境变量的形式提供。

初始化项目

npx hardhat init

初始化后项目目录结构如下:
在这里插入图片描述

  • 创建.env文件,将一些敏感信息以环境变量形式存储,如:
    在这里插入图片描述

编辑相关文件

  • 编辑配置文件hardhat.config.js
require("@nomiclabs/hardhat-waffle");
require("dotenv").config();  //此处解析了本地.env文件中的环境变量

module.exports = {
  solidity: "0.8.4",
  networks: {
    sepolia: {
      url: process.env.SEPOLIA_URL,
      accounts: [process.env.PRIVATE_KEY]
    },
  }
};
  • 在contracts目录下添加合约文件Faucet.sol
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

contract Faucet {
  
  function withdraw(uint _amount) public {
    // users can only withdraw .1 ETH at a time, feel free to change this!
    require(_amount <= 100000000000000000);
    payable(msg.sender).transfer(_amount);
  }

  // fallback function
  receive() external payable {}
}
  • 在scripts目录下添加部署脚本deploy.js
const ethers = require('ethers');
require('dotenv').config();

async function main() {

  const url = process.env.SEPOLIA_URL;

  let artifacts = await hre.artifacts.readArtifact("Faucet");

  const provider = new ethers.providers.JsonRpcProvider(url);

  let privateKey = process.env.PRIVATE_KEY;

  let wallet = new ethers.Wallet(privateKey, provider);

  // Create an instance of a Faucet Factory
  let factory = new ethers.ContractFactory(artifacts.abi, artifacts.bytecode, wallet);

  let faucet = await factory.deploy();

  console.log("Faucet address:", faucet.address);

  await faucet.deployed();
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
});

编译合约

npx hardhat compile

注:合约编译完成后会在工程目录下生成artifacts目录,其中存放了合约编译后的ABI及bytecode

部署合约到指定网络

//hardhat默认会在内存中生成一个本地网络,如果不指定--network则连接本地网络
//此处--network指定要将合约部署到那个网络,相关的网络配置在hardhat.config.js中,比如此处我们配置的sepolia
npx hardhat run scripts/deploy.js --network sepolia

注:部署完成后我们输出了合约地址。
在这里插入图片描述

测试

在tests目录下存放测试安例。如下 :

const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { expect } = require('chai');

describe('Faucet', 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 deployContractAndSetVariables() {
    const Faucet = await ethers.getContractFactory('Faucet');
    const faucet = await Faucet.deploy();

    const [owner] = await ethers.getSigners();

    console.log('Signer 1 address: ', owner.address);
    return { faucet, owner };
  }

  it('should deploy and set the owner correctly', async function () {
    const { faucet, owner } = await loadFixture(deployContractAndSetVariables);

    expect(await faucet.owner()).to.equal(owner.address);
  });
});

执行测试:

npx hardhat test

通过脚本调用合约方法

// add the game address here and update the contract name if necessary
const contractAddr = "合约地址";
const contractName = "Faucet";

async function main() {
    // attach to the game
    const contract= await hre.ethers.getContractAt(contractName, contractAddr );

    const tx = await contract.withdraw(20000);
    
    const receipt = await tx.wait();
    console.log(receipt);
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值