跳转至主要内容

创建并部署一个去中心化金融应用程序

soliditydeFiweb3.jstruffleganache智能合约
中级
strykerin
www.github.com(opens in a new tab)
2020年12月31日
14 分钟阅读 minute read

在本教程中,我们将使用 Solidity 构建一个去中心化金融应用程序。用户可以将 ERC20 代币存入智能合约,然后铸造流动性矿池代币并将这些代币转给他们。 用户之后可以通过消耗智能合约上的流动性矿池代币来赎回他们的 ERC20 代币,然后 ERC20 代币将会转回给他们。

安装 Truffle 和 Ganache

如果这是您第一次编写智能合约,您需要搭建您的环境。 我们将使用两个工具:Truffle(opens in a new tab)Ganache(opens in a new tab)

Truffle 是用于开发以太坊智能合约的开发环境和测试框架。 使用 Truffle 可以很容易地在区块链中创建和部署智能合约。 Ganache 可以帮助我们创建一个本地以太坊区块链,用以测试智能合约。 它模拟真实的网络功能,前 10 个帐户存入了 100 个测试以太币,如此可以随意部署和测试智能合约。 Ganache 可提供桌面应用程序和命令行工具。 在本文中,我们将使用有图形界面的桌面应用程序。

Ganache 图形界面桌面应用程序(opens in a new tab)Ganache UI desktop application

要创建项目,请运行以下命令:

mkdir your-project-name
cd your-project-name
truffle init

这将为智能合约的开发和部署创建一个空白项目, 创建的项目结构如下:

  • contracts:存放 solidity 智能合约的文件夹

  • migrations:存放部署脚本的文件夹

  • test:存放智能合约测试文件的文件夹

  • truffle-config.js:Truffle 配置文件

创建 ERC20 代币

首先,我们需要创建 ERC20 代币,用于在智能合约上质押。 要创建同质化代币,我们首先需要安装 OpenZeppelin 库。 该库包含 ERC20 和 ERC721 等标准的实现。 如需安装,请运行以下命令:

npm install @openzeppelin/contracts

使用 OpenZepelin 库,我们可以通过写入 contracts/MyToken.sol 创建我们的 ERC20 代币,需用到以下 solidity 代码:

1pragma solidity ^0.8.0;
2
3import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
4
5contract MyToken is ERC20 {
6 constructor() public ERC20("MyToken", "MTKN"){
7 _mint(msg.sender, 1000000000000000000000000);
8 }
9}
显示全部
复制

在上述代码中:

  • 第 3 行:我们从包含此代币标准实现的 openzepelin 导入 ERC20.sol 合约。

  • 第 5 行:我们继承了 ERC20.sol 合约。

  • 第 6 行:我们要调用 ERC20.sol 构造函数,并将名称和符号参数分别设为 "MyToken""MTKN"

  • 第 7 行:我们为正在部署智能合约的账户铸造和转移 100 万代币(我们默认为 ERC20 代币使用 18 位小数)。这意味着,如果我们想要铸造 1 个代币,则需将其表示为 1000000000000000000,即 1 和 18 个零)。

我们可以看到,ERC20.sol 构造函数实现下方,_decimals 字段设置为 18:

1string private _name;
2string private _symbol;
3uint8 private _decimals;
4
5constructor (string memory name_, string memory symbol_) public {
6 _name = name_;
7 _symbol = symbol_;
8 _decimals = 18;
9}
显示全部
复制

编译 ERC20 代币

要编译我们的智能合约,我们必须首先检查我们的 solidity 编译器版本。 可以通过运行以下命令来检查:

truffle version

默认版本是 Solidity v0.5.16。 由于我们的代币基于 solidity 版本 0.6.2 编写,如果我们运行命令来编译合约,我们会遇到编译器错误。 要使用指定的 solidity 编译器版本,请转到文件 truffle-config.js,设为所需的编译器版本,如下所示:

1// Configure your compilers
2compilers: {
3 solc: {
4 version: "^0.8.0", // Fetch exact version from solc-bin (default: truffle's version)
5 // docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
6 // settings: { // See the solidity docs for advice about optimization and evmVersion
7 // optimizer: {
8 // enabled: false,
9 // runs: 200
10 // },
11 // evmVersion: "byzantium"
12 // }
13 }
14}
显示全部

现在,我们可以通过运行以下命令来编译智能合约:

truffle compile

部署 ERC20 代币

编译后,我们现在便可以部署代币了。

migrations 文件夹中,创建一个名为 2_deploy_Tokens.js 的文件。 我们将在该文件中部署 ERC20 代币和 FarmToken 智能合约。 以下代码用于部署我们的 MyToken.sol 合约:

1const MyToken = artifacts.require("MyToken")
2
3module.exports = async function (deployer, network, accounts) {
4 // Deploy MyToken
5 await deployer.deploy(MyToken)
6 const myToken = await MyToken.deployed()
7}

打开 Ganache 并选择“快速启动”选项,启动本地以太坊区块链。 要部署合约,请运行:

truffle migrate

用于部署合同的地址是 Ganache 显示的地址列表中的第一个地址。 为核实这一点,我们可以打开 Ganache 桌面应用程序,然后可以核实第一个帐户的以太币余额已经减少,这是我们部署智能合约的以太币成本:

Ganache 桌面应用程序(opens in a new tab)Ganache desktop application

为了验证 100 万 MyToken 代币是否已发送到部署者地址,我们可以使用 Truffle 控制台与我们部署的智能合约进行交互。

Truffle 控制台是一个基础型交互式控制台,可连接到任何以太坊客户端。(opens in a new tab)

要与智能合约交互,请运行以下命令:

truffle console

现在,我们可以在终端写入以下命令:

  • 获取智能合约:myToken = await MyToken.depolyed()

  • 从 Ganache 获取账户组:accounts = await web3.eth.getAccounts()

  • 获取第一个帐户的余额:balance = await myToken.balance Of(accounts[0])

  • 格式化具有 18 位小数的余额:web3.utils.fromWei(balance.toString())

通过运行上述命令,我们会看到第一个地址实际上有 100 万 MyToken:

第一个地址有 1000000 MyToken(opens in a new tab)

第一个地址有 1000000 MyToken

创建 FarmToken 智能合约

FarmToken 智能合约将包含 3 个函数:

  • balance():获取 FarmToken 智能合约上的 MyToken 余额。

  • deposit(uint256 _amount):代表用户将 MyToken 转移到 FarmToken 智能合约,然后铸造 FarmToken 并转账给用户。

  • withdraw(uint256 _amount):消耗用户的 FarmToken 并将 MyToken 转到用户的地址。

我们来看看 FarmToken 构造函数:

1pragma solidity ^0.6.2;
2
3import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
4import "@openzeppelin/contracts/utils/Address.sol";
5import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
7
8contract FarmToken is ERC20 {
9 using Address for address;
10 using SafeMath for uint256; // As of Solidity v0.8.0, mathematical operations can be done safely without the need for SafeMath
11 using SafeERC20 for IERC20;
12
13 IERC20 public token;
14
15 constructor(address _token)
16 public
17 ERC20("FarmToken", "FRM")
18 {
19 token = IERC20(_token);
20 }
显示全部
复制
  • 第 3-6 行:我们要从 openzepelin 导入下列合约:IERC20.sol、Address.sol、SafeERC20.sol 和 ERC20.sol。

  • 第 8 行:FarmToken 将继承 ERC20 合约。

  • 14-19 行:FarmToken 构造函数将接收 MyToken 合约地址作为参数,我们将把此地址分配给我们名为 token 的公共变量。

我们来实现 balance() 函数。 它将不会收到任何参数,将返回此智能合约上的 MyToken 余额。 可按如下所示实现:

1function balance() public view returns (uint256) {
2 return token.balanceOf(address(this));
3}
复制

对于 deposit(uint256 _amount) 函数,将接收用户想要存入的金额作为参数,铸造 FarmTokens 并转给用户:

1function deposit(uint256 _amount) public {
2 // Amount must be greater than zero
3 require(_amount > 0, "amount cannot be 0");
4
5 // Transfer MyToken to smart contract
6 token.safeTransferFrom(msg.sender, address(this), _amount);
7
8 // Mint FarmToken to msg sender
9 _mint(msg.sender, _amount);
10}
显示全部
复制

对于 withdraw(uint256 _amount) 函数,将接收用户想要消耗的 FarmTokens 数额作为参数,然后将相同数额的 MyToken 返还给用户:

1function withdraw(uint256 _amount) public {
2 // Burn FarmTokens from msg sender
3 _burn(msg.sender, _amount);
4
5 // Transfer MyTokens from this smart contract to msg sender
6 token.safeTransfer(msg.sender, _amount);
7}
复制

现在我们将部署我们的智能合约。 为此,我们将返回文件 2_deploy_Tokens.js 并添加要部署的新合约:

1const MyToken = artifacts.require("MyToken")
2const FarmToken = artifacts.require("FarmToken")
3
4module.exports = async function (deployer, network, accounts) {
5 // Deploy MyToken
6 await deployer.deploy(MyToken)
7 const myToken = await MyToken.deployed()
8
9 // Deploy Farm Token
10 await deployer.deploy(FarmToken, myToken.address)
11 const farmToken = await FarmToken.deployed()
12}
显示全部

请注意,在部署 FarmToken 时,我们将已部署的 MyToken 合约的地址作为参数传递。

现在,运行 truffle compiletruffle migrate 来部署我们的合约。

我们来测试一下我们的智能合约。 不要使用 truffle console 与我们的智能合约交互,我们将创建一个脚本来自动执行此过程。 创建一个名为 scripts 的文件夹,并添加以下文件 getMyTokenBalance.js。 该文件将核查 FarmToken 智能合约中的 MyToken 余额:

1const MyToken = artifacts.require("MyToken")
2const FarmToken = artifacts.require("FarmToken")
3
4module.exports = async function (callback) {
5 myToken = await MyToken.deployed()
6 farmToken = await FarmToken.deployed()
7 balance = await myToken.balanceOf(farmToken.address)
8 console.log(web3.utils.fromWei(balance.toString()))
9 callback()
10}
显示全部

要执行此脚本,请运行以下 cli 命令:

truffle exec .\scripts\getMyTokenBalance.js

我们将得到预期结果 0。 如果您收到尚未部署 FarmToken 的错误,则表明 truffle 网络尚未收到最新版本的合约代码。 只需关闭 ganache,然后再快速启动,并确保运行 truffle migrate

现在,让我们把 MyToken 押在智能合约上。 由于 deposit(uint256 _amount) 函数调用了来自 ERC20 的 safeTransferFrom 函数,用户必须首先批准智能合约,才能以用户的名义转账 MyToken。 因此,在下面的脚本中,我们将首先批准此步骤,然后调用函数:

1const MyToken = artifacts.require("MyToken")
2const FarmToken = artifacts.require("FarmToken")
3
4module.exports = async function (callback) {
5 const accounts = await new web3.eth.getAccounts()
6 const myToken = await MyToken.deployed()
7 const farmToken = await FarmToken.deployed()
8
9 // Returns the remaining number of tokens that spender will be allowed to spend on behalf of owner through transferFrom.
10 // This is zero by default.
11 const allowanceBefore = await myToken.allowance(
12 accounts[0],
13 farmToken.address
14 )
15 console.log(
16 "Amount of MyToken FarmToken is allowed to transfer on our behalf Before: " +
17 allowanceBefore.toString()
18 )
19
20 // In order to allow the Smart Contract to transfer to MyToken (ERC-20) on the accounts[0] behalf,
21 // we must explicitly allow it.
22 // We allow farmToken to transfer x amount of MyToken on our behalf
23 await myToken.approve(farmToken.address, web3.utils.toWei("100", "ether"))
24
25 // Validate that the farmToken can now move x amount of MyToken on our behalf
26 const allowanceAfter = await myToken.allowance(accounts[0], farmToken.address)
27 console.log(
28 "Amount of MyToken FarmToken is allowed to transfer on our behalf After: " +
29 allowanceAfter.toString()
30 )
31
32 // Verify accounts[0] and farmToken balance of MyToken before and after the transfer
33 balanceMyTokenBeforeAccounts0 = await myToken.balanceOf(accounts[0])
34 balanceMyTokenBeforeFarmToken = await myToken.balanceOf(farmToken.address)
35 console.log("*** My Token ***")
36 console.log(
37 "Balance MyToken Before accounts[0] " +
38 web3.utils.fromWei(balanceMyTokenBeforeAccounts0.toString())
39 )
40 console.log(
41 "Balance MyToken Before TokenFarm " +
42 web3.utils.fromWei(balanceMyTokenBeforeFarmToken.toString())
43 )
44
45 console.log("*** Farm Token ***")
46 balanceFarmTokenBeforeAccounts0 = await farmToken.balanceOf(accounts[0])
47 balanceFarmTokenBeforeFarmToken = await farmToken.balanceOf(farmToken.address)
48 console.log(
49 "Balance FarmToken Before accounts[0] " +
50 web3.utils.fromWei(balanceFarmTokenBeforeAccounts0.toString())
51 )
52 console.log(
53 "Balance FarmToken Before TokenFarm " +
54 web3.utils.fromWei(balanceFarmTokenBeforeFarmToken.toString())
55 )
56 // Call Deposit function from FarmToken
57 console.log("Call Deposit Function")
58 await farmToken.deposit(web3.utils.toWei("100", "ether"))
59 console.log("*** My Token ***")
60 balanceMyTokenAfterAccounts0 = await myToken.balanceOf(accounts[0])
61 balanceMyTokenAfterFarmToken = await myToken.balanceOf(farmToken.address)
62 console.log(
63 "Balance MyToken After accounts[0] " +
64 web3.utils.fromWei(balanceMyTokenAfterAccounts0.toString())
65 )
66 console.log(
67 "Balance MyToken After TokenFarm " +
68 web3.utils.fromWei(balanceMyTokenAfterFarmToken.toString())
69 )
70
71 console.log("*** Farm Token ***")
72 balanceFarmTokenAfterAccounts0 = await farmToken.balanceOf(accounts[0])
73 balanceFarmTokenAfterFarmToken = await farmToken.balanceOf(farmToken.address)
74 console.log(
75 "Balance FarmToken After accounts[0] " +
76 web3.utils.fromWei(balanceFarmTokenAfterAccounts0.toString())
77 )
78 console.log(
79 "Balance FarmToken After TokenFarm " +
80 web3.utils.fromWei(balanceFarmTokenAfterFarmToken.toString())
81 )
82
83 // End function
84 callback()
85}
显示全部

要运行此脚本:truffle exec .\script\transferMyTokenToFarmToken.js。 您将在控制台上看到如下输出:

transferMyTokenToFarmToken.js 的输出(opens in a new tab)

transferMyTokenToFarmToken.js 的输出

正如我们所见,我们已成功将 MyToken 存入智能合约,因为第一个帐户现在已经有了 FarmToken。

要取款:

1const MyToken = artifacts.require("MyToken")
2const FarmToken = artifacts.require("FarmToken")
3
4module.exports = async function (callback) {
5 const accounts = await new web3.eth.getAccounts()
6 const myToken = await MyToken.deployed()
7 const farmToken = await FarmToken.deployed()
8
9 // Verify accounts[0] and farmToken balance of MyToken before and after the transfer
10 balanceMyTokenBeforeAccounts0 = await myToken.balanceOf(accounts[0])
11 balanceMyTokenBeforeFarmToken = await myToken.balanceOf(farmToken.address)
12 console.log("*** My Token ***")
13 console.log(
14 "Balance MyToken Before accounts[0] " +
15 web3.utils.fromWei(balanceMyTokenBeforeAccounts0.toString())
16 )
17 console.log(
18 "Balance MyToken Before TokenFarm " +
19 web3.utils.fromWei(balanceMyTokenBeforeFarmToken.toString())
20 )
21
22 console.log("*** Farm Token ***")
23 balanceFarmTokenBeforeAccounts0 = await farmToken.balanceOf(accounts[0])
24 balanceFarmTokenBeforeFarmToken = await farmToken.balanceOf(farmToken.address)
25 console.log(
26 "Balance FarmToken Before accounts[0] " +
27 web3.utils.fromWei(balanceFarmTokenBeforeAccounts0.toString())
28 )
29 console.log(
30 "Balance FarmToken Before TokenFarm " +
31 web3.utils.fromWei(balanceFarmTokenBeforeFarmToken.toString())
32 )
33
34 // Call Deposit function from FarmToken
35 console.log("Call Withdraw Function")
36 await farmToken.withdraw(web3.utils.toWei("100", "ether"))
37
38 console.log("*** My Token ***")
39 balanceMyTokenAfterAccounts0 = await myToken.balanceOf(accounts[0])
40 balanceMyTokenAfterFarmToken = await myToken.balanceOf(farmToken.address)
41 console.log(
42 "Balance MyToken After accounts[0] " +
43 web3.utils.fromWei(balanceMyTokenAfterAccounts0.toString())
44 )
45 console.log(
46 "Balance MyToken After TokenFarm " +
47 web3.utils.fromWei(balanceMyTokenAfterFarmToken.toString())
48 )
49
50 console.log("*** Farm Token ***")
51 balanceFarmTokenAfterAccounts0 = await farmToken.balanceOf(accounts[0])
52 balanceFarmTokenAfterFarmToken = await farmToken.balanceOf(farmToken.address)
53 console.log(
54 "Balance FarmToken After accounts[0] " +
55 web3.utils.fromWei(balanceFarmTokenAfterAccounts0.toString())
56 )
57 console.log(
58 "Balance FarmToken After TokenFarm " +
59 web3.utils.fromWei(balanceFarmTokenAfterFarmToken.toString())
60 )
61
62 // End function
63 callback()
64}
显示全部

要运行此脚本:truffle exec .\scripts\withdrawMyTokenFromTokenFarm.js。 正如我们在下面的输出中所看到的,我们已经成功地取回了 MyToken,并消耗掉了 FarmToken:

withdrawMyTokenFromTokenFarm.js 的输出(opens in a new tab)

withdrawMyTokenFromTokenFarm.js 的输出

参考文献

合约 - OpenZepelin 文档(opens in a new tab)

智能合约的适用工具 | Truffle 套装(opens in a new tab)

Ganache | Truffle 套装(opens in a new tab)

什么是去中心化金融? 初学者指南(2021 年更新)(99bitcoins.com)(opens in a new tab)

去中心化金融 - DeFi Llama 的去中心化金融排行榜(opens in a new tab)

上次修改时间: @aqing(opens in a new tab), 2023年8月15日

本教程对你有帮助吗?