/** @dev Creates `amount` tokens and assigns them to `account`, increasing
* the total supply.
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* Requirements:
*
* - `account` cannot be the zero address.
*/
function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");
_beforeTokenTransfer(address(0), account, amount);
_totalSupply += amount;
_balances[account] += amount;
emit Transfer(address(0), account, amount);
_afterTokenTransfer(address(0), account, amount);
}
// You might need the previously deployed yourToken:
const yourToken = await ethers.getContract('YourToken', deployer);
// 部署承销商合约
await deploy('Vendor', {
// Learn more about args here: https://www.npmjs.com/package/hardhat-deploy#deploymentsdeploy
from: deployer,
args: [yourToken.address],
log: true,
});
// 获取部署的合约
const vendor = await ethers.getContract('Vendor', deployer);
// 发送 1000 个代币给承销商
console.log('\n 🏵 Sending all 1000 tokens to the vendor...\n');
await yourToken.transfer(vendor.address, ethers.utils.parseEther('1000'));
// 转移所有权
await vendor.transferOwnership('0x169841AA3024cfa570024Eb7Dd6Bf5f774092088');
部署合约
完成上述代码之后,我们重新部署我们的合约:
yarn deploy --reset
对应的输出结果为:
$ yarn deploy --reset
Compiling 7 files with 0.8.6
Generating typings for: 7 artifacts in dir: ../vite-app-ts/src/generated/contract-types for target: ethers-v5
Successfully generated 15 typings!
Compilation finished successfully
deploying "YourToken" (tx: 0x758e492bc71e9de37cf109aa6aa966fc6c042d086babce32ddd76af02ec22acb)...: deployed at 0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82 with 639137 gas
deploying "Vendor" (tx: 0x7b0402937081b72f59abb9994e3773b0283116e1106665766af31bf246b466cc)...: deployed at 0x9A676e781A523b5d0C0e43731313A708CB607508 with 482680 gas
🏵 Sending all 1000 tokens to the vendor...
mapping(address => mapping(address => uint256)) private _allowances;
/**
* @dev See {IERC20-approve}.
*
* NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
* `transferFrom`. This is semantically equivalent to an infinite approval.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}
/**
* @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
*
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*/
function _approve(
address owner,
address spender,
uint256 amount
) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
/**
* @dev See {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
/**
* @dev Updates `owner` s allowance for `spender` based on spent `amount`.
*
* Does not update the allowance amount in case of infinite allowance.
* Revert if not enough allowance is available.
*
* Might emit an {Approval} event.
*/
function _spendAllowance(
address owner,
address spender,
uint256 amount
) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}
/**
* @dev Moves `amount` of tokens from `sender` to `recipient`.
*
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `from` must have a balance of at least `amount`.
*/
function _transfer(
address from,
address to,
uint256 amount
) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
}
_balances[to] += amount;
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
/**
* @dev See {IERC20-transferFrom}.
*
* Emits an {Approval} event indicating the updated allowance. This is not
* required by the EIP. See the note at the beginning of {ERC20}.
*
* NOTE: Does not update the allowance if the current allowance
* is the maximum `uint256`.
*
* Requirements:
*
* - `from` and `to` cannot be the zero address.
* - `from` must have a balance of at least `amount`.
* - the caller must have allowance for ``from``'s tokens of at least
* `amount`.
*/
function transferFrom(
address from,
address to,
uint256 amount
) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
...
event SellTokens(address seller, uint256 amountOfTokens, uint256 amountOfETH);
...
// 允许用户使用代币换回 ETH
function sellTokens(uint256 amountToSell) public {
// 价差是否合理
require(amountToSell > 0, "Amount to sell must be greater than 0");
// 检查用户是否有足够的代币
uint256 userBalance = yourToken.balanceOf(msg.sender));
require(userBalance >= amountToSell, "Not enought tokens");
// 检查承销商是否有足够的 ETH
uint256 amountOfEthNeeded = amountToSell / tokensPerEth;
uint256 venderBalance = address(this).balance;
require(amountOfEthNeeded <= venderBalance, "Not enought ether");
// 用户发送代币给承销商
bool sent = yourToken.transferFrom(msg.sender, address(this), amountToSell);
require(sent, "Failed to transfer tokens from seller to vender");
// 承销商发送 ETH 给用户
(bool sent, ) = msg.sender.call{value: amountOfEthNeeded}("");
require(sent, "Failed to send ether from vender to seller");
emit SellTokens(msg.sender, amountToSell, amountOfEthNeeded);
}
部署合约
我们再次部署新的合约:
$ yarn deploy --reset
Compiling 7 files with 0.8.6
Generating typings for: 7 artifacts in dir: ../vite-app-ts/src/generated/contract-types for target: ethers-v5
Successfully generated 15 typings!
Compilation finished successfully
deploying "YourToken" (tx: 0xd087814faeb6a8f1a7205d443550419b68d252bcd071e30c7965844105b761ac)...: deployed at 0x68B1D87F95878fE05B998F19b66F4baba5De1aed with 639137 gas
deploying "Vendor" (tx: 0xafaf257948f8c87e0a836eac6e2bbc1ec38026a5c2a0dfc0f71823a4ace635fd)...: deployed at 0x3Aa5ebB10DC797CAC828524e59A333d0A371443c with 694098 gas
🏵 Sending all 1000 tokens to the vendor...
$ yarn deploy
Nothing to compile
No need to generate any newer typings.
deploying "YourToken" (tx: 0xa7a89a2917cfa355d1305643dc89f54d776186c0059977b0a237737fa37dff62)...: deployed at 0x0F0D10eF3589cE896E9E54E09568cB7a5371e398 with 639137 gas
deploying "Vendor" (tx: 0x3a1f02b77de29704a16599067c8e10abb0da78e547ea0eea8200761da5d45715)...: deployed at 0xb335Fc61D759C041503dC17266575229E593DE17 with 694098 gas
🏵 Sending all 1000 tokens to the vendor...
这篇教程我们来完成 scaffold-eth 项目的第二个挑战:代币承销商,我们可以在网站 speedrunethereum.com 中查看或者直接查看对应的 Github 连接:scaffold-eth/scaffold-eth-typescript-challenges。
这个挑战的目的是创建一个自己的ERC20代币,并编写承销商合约,实现用户对代币的购买和卖出。下面,我们一步步完成这个过程。
一、安装并设置环境
首先,我们下载项目,并初始化环境。
安装好依赖包之后,我们可以看到项目的主要目录为
packages
,包含一下子目录其中:
hardhat-ts
是项目合约代码,包含合约文件以及合约的部署等;services
The Graph 协议的 graph-node 配置;subgraph
The Graph 协议相应的处理设置,包括 mappings,数据结构等;vite-app-ts
前端项目,主要负责用户与合约交互。The Graph 协议是去中心化的区块链数据索引协议,本片教程中暂时不涉及。我们需要启动三个命令终端,分别用于运行以下命令:
yarn chain
使用 hardhat 运行本地区块链,作为合约部署的本地测试链;yarn deploy
编译、部署和发布合约;yarn start
启动 react 应用的前端;按顺序分别运行上述命令之后,此时我们就可以在
http://localhost:3000
中访问我们的应用。如果需要重新部署合约,运行yarn deploy --reset
即可。二、编写 ERC20 代币合约
现在我们进入合约编写部分。我们的目标是编写一个 ERC20 代币合约,并为创建者铸造 1000 个代币。
什么是 ERC20 合约标准
代币可以在以太坊中表示任何东西,比如信誉积分,黄金等,而 ERC-20 提供了一个同质化代币的标准,每个代币与另一个代币(在类型和价值上)完全相同。
ERC20是各个代币的标准接口,包含以下方法:
其中,合约必需设置
totalSupply
、balanceOf
、transfer
、transferFrom
、approve
以及allowance
这六个函数,其他如name
、symbol
和decimalsze
则是可选实现。使用 OpenZeppelin 库
如果从上述的合约标准开始,我们需要实现这六个函数的方法,幸运的是,OpenZeppelin 库是一个成熟的合约开发库,为我们实现了 ERC20 代币基本功能,我们可以基于这个库开发我们的 ERC20 代币,这将大大减少我们的工作量。我们可以在 ERC20 标准 页面查到相关的使用方法。
除了 ERC20,OpenZeppelin 库还提供了其他合约标准的实现,比如 ERC721,ERC777等,以及大量的经过安全审计的库,这些对于我们快速开发和实现安全的合约代码提供了支持。
编写代码
我们使用 ERC20.sol 来实现我们的合约,创见一个名为
GOLD
的代币,代币符号为GLD
,并为创建者铸造 1000 个代币:其中,
_mint
方法是 ERC20 提供的方法,该方法创建相应数量的代币,并将代币发送给账户:代码地址:https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L248
部署脚本
接着我们使用脚本进行部署,并向地址发送 1000 代币,地址可以在
http://localhost:3000
中连接我们的 Metamask 得到。部署脚本地址:packages/hardhat-ts/deploy/00_deploy_your_token.ts
。然后我们运行
yarn deploy --reset
部署合约。验证
使用 Debug 页面功能进行检查,查看用户账户中的代币余额,可以看到账户中有 1000 个代币;
使用
transfer()
将代币转给另一个账户;在 Debug 中,使用
transfer
功能,输入目标钱包地址0xc12ae5Ba30Da6eB11978939379D383beb5Df9b33
,以及发送的数量1000000000000000000000
(1000*1E18,1后边有21个0),点击发送。等交易完成之后,可以分别查看原来账户和目标账户的代币数量,可以看到原来的变成了 0,目标账户是 1000。注意:
00_deploy_your_token.ts
中的 transfer 代码注释了,不然会影响之后的步骤。三、承销商合约 — 购买
接下来,我们创建一个承销商合约,这个合约允许用户通过以太购买代币。
为了完成这个功能,我们需要:
tokensPerEth=100
,也就是 1个以太可以兑换 100 GLD;buyTokens
函数,这个函数必须是payable
,可以接受发送的以太,计算对应的GLD
数量,然后使用transfer
将相应的GLD
代币发送给购买者msg.sender
;BuyTokens
事件,记录购买者,使用的 ETH 数量以及购买的 GLD 数量;withdraw
,用来将合约中的 ETH 全部提取到合约的所有者(owner)地址。我们可以使用两种方式设置合约的所有者:在这个教程中,我们使用第二个方式,这样我们可以不用将我们控制的地址的私钥添加到项目配置中,降低暴露。
其中,
Ownable
可以进行权限控制,合约提供的onlyOwner
修改器可以用来限制某些特定合约函数的访问权限。在这里,我们的withdraw
函数必需限制合约的所有这才能提取所有的资金。同时,这个合约提供了transferOwnership
函数,可以用来转移合约的所有者,这个将在我们的脚本部分中使用。对于部署脚本,我们需要完成以下功能:
vendor.address
,而不是我们之前的地址;ownership
转移到我们能控制的地址,比如我们在前端使用的地址。脚本位置:
packages/hardhat-ts/deploy/01_deploy_vendor.ts
部署合约
完成上述代码之后,我们重新部署我们的合约:
对应的输出结果为:
可以从命令行输出中看到合约部署的地址为:
0x9A676e781A523b5d0C0e43731313A708CB607508
0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82
验证
我们通过以下步骤进行验证:
通过 Debug 页面查看承销商 (Vendor)合约地址初始时是否有 1000 个代币;
使用 0.1 ETH 购买 10 个 GLD:我们使用 Buy Tokens 功能购买 10 个代币,可以看到此时的价格约为 0.1 ETH(ETH 价格为 2766.7 美元)。
将购买的代币发送给另一个账户:同样使用页面 Transfer Tokens 功能完成;
使用所有者账户,查看是否能全部取出合约中的 ETH:在 Debug 页面,我们使用
withdraw
功能,尝试将承销商合约中的 ETH 全部取出,可以看到,当交易完成以后,合约的余额变为了0:变为:
四、承销商合约 — 回购
接下来我们添加承销商合约的回购代币功能,也就是允许用户通过发送代币给承销商合约,承销商合约将对应的ETH发给用户账户。但是在以太坊中,合约只能通过 payable 接受 ETH,无法接受直接发送代币,如果直接向合约发送代币,代币将会永久消失。所以在 ERC20 标准中,我们需要使用
approve
和tranferFrom
者两个函数来完成这个过程。首先,用户通过调用
approve
函数授权承销商合约(spender
)处理amount
数量的代币,然后,调用transferFrom
函数将代币从用户账户(from
)转移amount
数量的代币给承销商合约(to
)。这其中的难点在于approve
和transferFrom
函数。我们来看一下这两个函数在 OpenZeppelin 中具体实现,首先是approve
:从上面可以看出,
approve
函数调用了_approve
,_approve
中用_allowances
这个哈希记录了owner
和spender
之间的授权数量amount
。因此可以推断,transferFrom
函数以及其他需要授权情况的函数都使用了_allowances
这个变量,比如allowance
函数。在
transferFrom
函数中,先使用_spendAllowance
进行授权数量检查并更新授权数量,然后再使用_transfer
进行代币划转,而_spendAllowance
中正是调用了allowance
这个函数。合约实现
合约的函数实现如下:
部署合约
我们再次部署新的合约:
此时,合约地址变为:
0x3Aa5ebB10DC797CAC828524e59A333d0A371443c
0x68B1D87F95878fE05B998F19b66F4baba5De1aed
验证
验证过程需要包含两步:
先在 Debug 页面使用代币的
approve
允许承销商合约处理 10 个代币:在
编辑权限
中,我们可以查看到授权的代币数量:使用承销商的
sellTokens
将 10 个代币换成 ETH。如果上一步没有使用approve
的话,程序会报错。到这一步,我们就完成了合约的编写。
五、部署到测试网络
我们将部署合约到测试网络中,使用的测试网络是
rinkeby
:rinkeby
:packages/hardhat-ts/hardhat.config.ts
的defaultNetwork
变量,packages/vite-app-ts/src/config/providersConfig.ts
中的targetNetworkInfo
变量yarn account
,如果没有找到可用账户,则使用yarn generate
生成;yarn deploy
进行合约部署:可以看到,合约部署成功,此时我们可以在线上测试网络查看到具体的合约部署情况:
并且部署完成了初始化代币分发和所有权转换。详情可以查看部署账户信息: https://rinkeby.etherscan.io/address/0xccb20d43f62f31dd94436f04a1e90d7d08569e57。
六、发布
接下来,我们将发布我们的前端项目到 Surge (或者使用 s3, ipfs 上)。Surge.sh 提供了免费的网站的部署,对于我们的测试网站来时再合适不过。
yarn build
yarn surge
Surge 在运行命令的过程中就设置了账户名称,以及可以自定义域名:qiwihui-scaffold-2.surge.sh,当完成部署之后,我们就可以在浏览器中访问这个页面,和我们本地运行的结果是一致的。
七、合约验证
当我们向测试网络部署合约时,部署的是合约编译之后的字节码,合约源码不会发布。实际生产中,有时我们需要发布我们的源代码,以保证我们的代码真实可信。此时,我们就可以借助 etherscan 提供的功能进行验证。
首先,我们获取 etherscan 的 API key,地址为 https://etherscan.io/myapikey,比如
PSW8C433Q667DVEX5BCRMGNAH9FSGFZ7Q8
;更新
packages/hardhat-ts/package.json
中对应的 api-key 参数:由于项目中的一个 bug,需要在根目录下的
packages.json
中添加以下命令才能直接使用之后的命令:运行
yarn verify --network rinkeby
,这个命令将通过 etherscan 接口进行合约验证,输出结果为:验证完成后,我们可以看到 etherscan 中的合约页面已经加上了一个蓝色小钩,在合约中,也可以看到我们合约的源代码:
至此,我们就完成了合约的验证。
八、提交结果
最后,当我们完成上述的所有步骤之后,我们可以将我们的结果提交到 speedrunethereum.com 上,选择对应的挑战,并提交部署的前端地址和承销商合约的链接即可:
Congratulations! 你已经完成了这个教程
总结
通过篇教程,我们可以学习到如下内容:
approve
和transferFrom
的使用;