Skip to content
This repository has been archived by the owner on Nov 30, 2023. It is now read-only.

Latest commit

 

History

History
388 lines (266 loc) · 13 KB

2.md

File metadata and controls

388 lines (266 loc) · 13 KB

第二课

Remix

Remix IDE

编译智能合约

编译面版介绍

  1. 选择编译器版本, 大多是时候 IDE 会根据文件头部的声明自动选择

  2. 选择要编译的语言, 默认 Solidity 即可

  3. 根据以太坊硬分叉选择 EVM 的版本, 每个硬分叉都为 EVM 增加了新的特性, 如果不是想测试旧版本的 EVM 的话, 默认 default 即可

  4. 是否自动编译, 勾选的话, 每次修改完都会自动编译一次

  5. 是否启用编译优化以及编译优化等级, 一般选用 200 即可, 优化后的合约部署成本更低

  6. 是否忽略警告

  7. 编译合约

  8. 选择要编译的合约合约名(文件名.sol)

  9. 查看编译细节, 见编译细节面板介绍

  10. 复制合约 ABI(Application Binary Interface), ABI 是一个很大的 json, 一般用于外部和合约交互

  11. 复制合约字节码, 字节码就是合约编译的结果

编译细节面板介绍

编译细节面板包含诸多细节, 一般只需要关心FUNCTIONHASHES, 这就是上一课所说的function selector.如果手动与合约交互, 可能会用到这个

部署、调试智能合约

部署、调试面版介绍

  1. 选择运行的网络环境

    • JavaScript VM

      使用在浏览器内存中运行的一个虚拟的区块链(每发起一笔交易, 都会为其生成一个新的区块, 但智能合约方面的行为与 EVM 完全一致)作为运行环境. 如果选择此选项, ACCOUNT中会自动生成若干个余额为 100 ETH 的账户用于测试

    • Injected Web3

      使用浏览器中的 Web3 插件(一般是 MetaMask)当前的环境作为运行环境. 比如, 如果要链接到 GXChain2.0 的 Testnet, 需要先在 MetaMask 中添加并切换到自定义网络地址, 然后在 Remix 中选择 Injected Web3

    • Web3 Provider

      使用手动输入的以太坊节点 RPC 地址作为运行环境, 如 http://127.0.0.1:8545

  2. 选择调用合约的账户, 点击加号可以增加一个账户(只在JavaScript VM时可用)

  3. 设置交易的 GasLimit, 有时候合约部署失败, 可能是因为这个数值低了, 需要注意

  4. 设置交易附带的转账金额及单位, 只有 payable 的方法可以接收转账, 如果不是 payable, value 必须为 0

  5. 选择需要部署的合约合约名 - 文件名.sol, 当文件中有多个合约的时候, 请确认合约名字是否是要部署的合约

  6. 部署合约到当前的运行环境

  7. 如果合约已经部署, 可以填入合约的地址, 并点击At Address即可

  8. 已部署的合约列表, 可以在此处与合约交互, 见合约交互面板介绍

    • 🗑️可以立即清除所有合约

    • x清除指定的合约

    • 📋复制合约地址

  9. 当一笔交易发出后, 可以在命令行查看交易的具体参数

合约交互面板介绍

  1. 黄色按钮代表此方法会修改以太坊状态树, 需要发起一笔交易才能调用

  2. 蓝色按钮代表此方法不会修改以太坊状态树, 只需要进行一次CALL就可以调用

    • 📋复制底层调用参数(function selector + 调用参数)
  3. CALL的返回结果返回参数下标: 类型: 值, 只有CALL并且方法有返回参数时, 才可以在这里获取返回参数. 发起交易不能获得合约的返回值. 比如有一个函数function func() external pure returns(uint256, string memory);, 调用结果可以是

    0: uint256: 1
    1: string: xxx
    

手动认证智能合约

Etherscan

Hardhat

Hardhat Document

示例项目: hardhat-erc20-example

Hardhat 项目构建

  1. 初始化(可通过 Fork 示例项目跳过以下步骤)

    1. npm i -g hardhat安装 hardhat

    2. 使用创建目录, 并在新目录下运行npx hardhat, 初始化项目

    3. npm i prettier -D安装 prettier, 并通过.prettierrc.js配置

    4. 通过hardhat.config.js配置 hardhat

  2. 编写合约(在 contracts 目录下)

  3. 编写部署脚本(在 deploy 目录下)

    示例脚本

  4. 编写需要的任务脚本(在 tasks 目录下)

    示例脚本

  5. 编写单元测试脚本(在 test 目录下)

    示例脚本

Hardhat 的使用

设置环境变量

# infura 的 api key
export INFURA_API_KEY=xxx
# 部署合约的账户的助记词
export MNEMONIC="test test test test test test test test test test test junk"
# 部署合约的账户的地址, 需要和助记词匹配
export DEV_ADDR=0x...abc
# etherscan 的 api key, 用于验证合约
export ETHERSCAN_KEY=xxx

安装依赖

npm i

编译

npm run build

以上命令实际上运行的是npx hardhat compile. 运行后, hardhat 会自动加载hardhat.config.js中的配置, 对contracts目录下的所有文件进行编译, 编译后的结果(包括 ABI, 字节码等)都将存储在artifacts文件夹下. hardhat 提供了各种方法加载合约 ABI 或字节码, 因此一般不需要关心artifacts文件夹下的具体内容

部署

# 部署到goerli测试网络
npm run deploy:goerli
# 部署到bsc
npm run deploy:bsc
# 部署到本地测试节点
npm run node

npm run deploy:goerli实际上运行的是npx hardhat --network goerli deploy, 其中

  • --network 声明了网络的名称(网络信息配置在hardhat.config.js中)
  • deploy 是 hardhat 内置的一个任务, 调用以后会运行 deploy 目录下的所有脚本

npm run node实际上运行的是npx hardhat node, 这个命令会让 hardhat 在本地运行一个虚拟的区块链, 默认端口是 8545, 并且在区块链开始运行的同时, 会运行一次 deploy 任务(也就是说会把所有合约部署好)

合约部署完成后, 会将合约的名字、部署后的地址等信息放在deployments目录下, 只要不删除此目录下的内容, 之后就可以直接通过合约的名字获取合约地址, ABI 等信息, 比如

task("balance:erc20", "Prints an account's ERC20 balance")
  .addParam("account", "User account")
  .setAction(async (taskArgs, { deployments, web3 }) => {
    // ...
    const { getArtifact, get } = deployments;
    // 通过合约名字获取合约地址
    const addr = await get("MyERC20");
    const contract = new web3.eth.Contract(
      // 通过合约名字获取合约 ABI, 用于初始化 web3 contract 对象
      (await getArtifact("MyERC20")).abi,
      addr,
      from
    );
    // ...
  });

运行任务脚本

任务脚本可以完成一系列预设的逻辑, 通过以下方式运行

npx hardhat --network networkName taskName --taskOption xxx

运行单元测试脚本

npm run test

自动认证智能合约

认证合约时, 请先确保可以正常访问api.etherscan.io

npx hardhat verify --network mainnet 0x...abc "LV Coin" "LV" "18"

参数依次是合约地址 构造参数1 构造参数2, 不用关心构造参数的类型, hardhat 会自动处理

课堂作业

// SPDX-License-Identifier: MIT

pragma solidity 0.6.2;

// Remix
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/math/SafeMath.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/token/ERC20/SafeERC20.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/utils/ReentrancyGuard.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/access/Ownable.sol";

// Hardhat
// import "@openzeppelin/contracts/math/SafeMath.sol";
// import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
// import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
// import "@openzeppelin/contracts/access/Ownable.sol";

contract MyUniswap is Ownable, ReentrancyGuard {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    // tokenA的地址
    IERC20 public tokenA;
    // tokenB的地址
    IERC20 public tokenB;

    // x个A可以换 x * priceAToB 个B
    // x个B可以换 x / priceAToB 个A
    uint256 public priceAToB;

    // 记录是否以及初始化
    bool public initialized = false;

    // 初始化函数, 只有owner可以调用. 设置两种代币的地址, 初始化金额, 价格等参数
    function initialize(address _tokenA, uint256 tokenAInitAmount, address _tokenB, uint256 tokenBInitAmount, uint256 _priceAToB) external onlyOwner {
        require(initialized == false, "invalid initialize");
        require(_tokenA != address(0), "invalid _tokenA");
        require(_tokenB != address(0), "invalid _tokenB");
        require(_priceAToB > 0, "invalid _priceAToB");
        tokenA = IERC20(_tokenA);
        tokenB = IERC20(_tokenB);
        priceAToB = _priceAToB;
        tokenA.safeTransferFrom(msg.sender, address(this), tokenAInitAmount);
        tokenB.safeTransferFrom(msg.sender, address(this), tokenBInitAmount);
        initialized = true;
    }

    // 用A换B
    function swapAToB(uint256 amount) external nonReentrant {
        uint256 amountOut = estimateAToB(amount);
        tokenA.safeTransferFrom(msg.sender, address(this), amount);
        tokenB.safeTransfer(msg.sender, amountOut);
    }

    // 用B换A
    function swapBToA(uint256 amount) external nonReentrant {
        uint256 amountOut = estimateBToA(amount);
        require(amountOut > 0, "invalid amountOut");
        tokenB.safeTransferFrom(msg.sender, address(this), amount);
        tokenA.safeTransfer(msg.sender, amountOut);
    }

    // 预估可以用A换出多少个B
    function estimateAToB(uint256 amount) public view returns(uint256 amountOut) {
        require(initialized, "invalid initialize");
        require(amount > 0, "invalid amount");
        amountOut = amount.mul(priceAToB);
    }

    // 预估可以用B换出多少个A
    function estimateBToA(uint256 amount) public view returns(uint256 amountOut) {
        require(initialized, "invalid initialize");
        require(amount > 0, "invalid amount");
        amountOut = amount.div(priceAToB);
    }
}

央视新闻: 警惕虚拟货币交易骗局(视频的 22:36)

特别申明: 本课程只做技术上的探讨, 不负任何法律上的责任

实现一个和视频中功能一样的 ERC20 合约, 要求如下:

  • 选择 Remix 或 Hardhat 完成合约编写
  • 转账, 授权, 增发, 销毁, 查询余额等功能一切正常
  • 只有合约的部署者(或者拥有某个权限的地址)可以在MyUniswap合约中出售代币
  • 编写完成后, 将MyUniswap和你到合约部署到任意测试网络
  • (可选)在 etherscan 上认证你的合约

提示:

  • 可以通过ERC20PresetMinterPauser实现大部分 ERC20 的功能, 例子
  • 可以通过重写_beforeTokenTransfer方法来实现自定义转账逻辑, 比如
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual override {
        super._beforeTokenTransfer(from, to, amount);
        // your code ...
    }
  • 如果想认证合约, 建议使用 Hardhat, 因为如果依赖了大量 OpenZepplin 的库的话, 手动认证合约会异常困难
  • Ropsten 水龙头

请将合约代码, 测试网络名称, 合约地址发到[email protected], 同时附上名字

参考答案
// SPDX-License-Identifier: MIT

pragma solidity 0.6.2;

// Remix
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/presets/ERC20PresetMinterPauser.sol";

// Hardhat
// import "@openzeppelin/contracts/presets/ERC20PresetMinterPauser.sol";

contract MyERC20 is ERC20PresetMinterPauser {
    bytes32 public SELL_ROLE = keccak256("SELL_ROLE");
    address public MyUniswap;

    constructor(
        string memory name,
        string memory symbol,
        uint8 decimals
    ) public ERC20PresetMinterPauser(name, symbol) {
        super._setupDecimals(decimals);
        grantRole(SELL_ROLE, msg.sender);
    }

    function setMyUniswapAddress(address _MyUniswap) external {
        require(hasRole(SELL_ROLE, msg.sender), "require sell role");
        MyUniswap = _MyUniswap;
    }

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual override {
      super._beforeTokenTransfer(from, to, amount);

      if (to == MyUniswap) {
          require(hasRole(SELL_ROLE, from), "require sell role");
      }
    }
}