-
选择编译器版本, 大多是时候 IDE 会根据文件头部的声明自动选择
-
选择要编译的语言, 默认 Solidity 即可
-
根据以太坊硬分叉选择 EVM 的版本, 每个硬分叉都为 EVM 增加了新的特性, 如果不是想测试旧版本的 EVM 的话, 默认 default 即可
-
是否自动编译, 勾选的话, 每次修改完都会自动编译一次
-
是否启用编译优化以及编译优化等级, 一般选用 200 即可, 优化后的合约部署成本更低
-
是否忽略警告
-
编译合约
-
选择要编译的合约
合约名(文件名.sol)
-
查看编译细节, 见编译细节面板介绍
-
复制合约 ABI(Application Binary Interface), ABI 是一个很大的 json, 一般用于外部和合约交互
-
复制合约字节码, 字节码就是合约编译的结果
编译细节面板包含诸多细节, 一般只需要关心FUNCTIONHASHES
, 这就是上一课所说的function selector
.如果手动与合约交互, 可能会用到这个
-
选择运行的网络环境
-
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
-
-
选择调用合约的账户, 点击加号可以增加一个账户(只在
JavaScript VM
时可用) -
设置交易的
GasLimit
, 有时候合约部署失败, 可能是因为这个数值低了, 需要注意 -
设置交易附带的转账金额及单位, 只有
payable
的方法可以接收转账, 如果不是payable
, value 必须为 0 -
选择需要部署的合约
合约名 - 文件名.sol
, 当文件中有多个合约的时候, 请确认合约名字是否是要部署的合约 -
部署合约到当前的运行环境
-
如果合约已经部署, 可以填入合约的地址, 并点击
At Address
即可 -
已部署的合约列表, 可以在此处与合约交互, 见合约交互面板介绍
-
🗑️
可以立即清除所有合约 -
x
清除指定的合约 -
📋
复制合约地址
-
-
当一笔交易发出后, 可以在命令行查看交易的具体参数
-
黄色按钮代表此方法会修改以太坊状态树, 需要发起一笔交易才能调用
-
蓝色按钮代表此方法不会修改以太坊状态树, 只需要进行一次
CALL
就可以调用📋
复制底层调用参数(function selector
+ 调用参数)
-
CALL
的返回结果返回参数下标: 类型: 值
, 只有CALL
并且方法有返回参数时, 才可以在这里获取返回参数. 发起交易不能获得合约的返回值. 比如有一个函数function func() external pure returns(uint256, string memory);
, 调用结果可以是0: uint256: 1 1: string: xxx
示例项目: hardhat-erc20-example
-
初始化(可通过 Fork 示例项目跳过以下步骤)
-
npm i -g hardhat
安装 hardhat -
使用创建目录, 并在新目录下运行
npx hardhat
, 初始化项目 -
npm i prettier -D
安装 prettier, 并通过.prettierrc.js
配置 -
通过
hardhat.config.js
配置 hardhat
-
-
编写合约(在 contracts 目录下)
-
编写部署脚本(在 deploy 目录下)
-
编写需要的任务脚本(在 tasks 目录下)
-
编写单元测试脚本(在 test 目录下)
# 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);
}
}
特别申明: 本课程只做技术上的探讨, 不负任何法律上的责任
实现一个和视频中功能一样的 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");
}
}
}