DeFi现在是加密货币领域的一个主要讨论话题。DeFi代表着 "去中心化的金融",这意味着没有中央机构盯着和控制资金的转移。这也意味着,DeFi中的交易是P2P(点对点),这意味着没有中央机构负责转账,资金直接从一个实体发送到另一个实体。
在这篇文章中,我们将学习如何开始使用DeFi,在Polygon链上使用Next.js作为前端制作一个全栈DeFi应用。这个应用程序将向用户出售和购买OKToken(一种虚构的代币)。然而,每笔购买交易都会从每个MATIC可以获得的代币数量中减少一个代币(出售会使这个数字增加一个)。这不是一个理想的示范,但这样你可以了解如何在Solidity智能合约中使用自己的逻辑,并学习使用Polygon创建自己的全栈DeFi应用程序。
内容
要求
要开始学习本教程,请确保你有以下条件。
- 安装了Node.js
- 安装了VS Code
- 具有React和Next.js的工作知识
- 对Solidity和Hardhat等工具有一定了解
现在你已经检查了这些要求,让我们继续创建我们的Hardhat项目,与我们的Solidity智能合约一起工作。
创建一个Hardhat项目
导航到一个安全的目录,在终端运行以下命令来初始化你的Hardhat项目。
npx hardhat
一旦你运行该命令,你应该在终端看到以下Hardhat初始化向导。

从列表中,选择创建一个高级样本项目。然后你会被问到你想在哪里初始化Hardhat项目;不要改变字段,只需按回车键,这样项目就会在当前目录下被初始化。
然后会问你是否要安装Hardhat项目运行所需的依赖项。按y,因为我们将需要这些依赖项,现在安装它们是最好的主意。
依赖项的安装将开始,可能需要几秒钟或几分钟,取决于你运行的机器。现在,在终端运行以下命令来安装另一个我们需要的依赖项,以方便我们的Solidity合约开发。
npm install @openzeppelin/contracts
OpenZeppelin提供了智能合约标准,我们可以在自己的智能合约中使用,以轻松创建一个Ownable、ERC-20和ERC-721合约,以及更多。
一旦依赖关系安装成功,在代码编辑器中打开该目录。我将在本教程中使用VS Code。
我们将创建两个智能合约:第一个将是我们的ERC-20代币本身,第二个将是供应商合约,它将促进这些代币的购买和销售。
创建我们的智能合约
现在,去contracts 文件夹,创建一个新的 Solidity 文件,名为OKToken.sol ,它将包含我们的ERC-20代币合约。
为这个文件使用以下代码。
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract OKToken is ERC20 {
constructor() ERC20("OKT", "OKToken"){
_mint(msg.sender, 10000 * 10 ** 18);
}
}
在上面的代码中,我们正在从@openzeppelin/contracts 中导入ERC20.sol 文件,这将帮助我们轻松开始使用ERC-20代币。然后,在构造函数中,我们将为我们的代币提供符号"OKT" 和名称"OKToken" 。
这就是代币合约的全部内容!现在,让我们来处理供应商合同。在contracts 文件夹下,创建一个名为OKVendor.sol 的新文件,代码如下。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "./OKToken.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract OKVendor is Ownable {
OKToken yourToken;
uint256 public tokensPerNativeCurrency = 100;
event BuyTokens(address buyer, uint256 amountOfNativeCurrency, uint256 amountOfTokens);
constructor(address tokenAddress) {
yourToken = OKToken(tokenAddress);
}
function buyTokens() public payable returns (uint256 tokenAmount) {
require(msg.value > 0, "You need to send some NativeCurrency to proceed");
uint256 amountToBuy = msg.value * tokensPerNativeCurrency;
uint256 vendorBalance = yourToken.balanceOf(address(this));
require(vendorBalance >= amountToBuy, "Vendor contract has not enough tokens to perform transaction");
(bool sent) = yourToken.transfer(msg.sender, amountToBuy);
require(sent, "Failed to transfer token to user");
tokensPerNativeCurrency = tokensPerNativeCurrency - 1;
emit BuyTokens(msg.sender, msg.value, amountToBuy);
return amountToBuy;
}
function sellTokens(uint256 tokenAmountToSell) public {
require(tokenAmountToSell > 0, "Specify an amount of token greater than zero");
uint256 userBalance = yourToken.balanceOf(msg.sender);
require(userBalance >= tokenAmountToSell, "You have insufficient tokens");
uint256 amountOfNativeCurrencyToTransfer = tokenAmountToSell / tokensPerNativeCurrency;
uint256 ownerNativeCurrencyBalance = address(this).balance;
require(ownerNativeCurrencyBalance >= amountOfNativeCurrencyToTransfer, "Vendor has insufficient funds");
(bool sent) = yourToken.transferFrom(msg.sender, address(this), tokenAmountToSell);
require(sent, "Failed to transfer tokens from user to vendor");
(sent,) = msg.sender.call{value: amountOfNativeCurrencyToTransfer}("");
tokensPerNativeCurrency = tokensPerNativeCurrency + 1;
require(sent, "Failed to send NativeCurrency to the user");
}
function getNumberOfTokensInNativeCurrency() public view returns(uint256) {
return tokensPerNativeCurrency;
}
function withdraw() public onlyOwner {
uint256 ownerBalance = address(this).balance;
require(ownerBalance > 0, "No NativeCurrency present in Vendor");
(bool sent,) = msg.sender.call{value: address(this).balance}("");
require(sent, "Failed to withdraw");
}
}
这将帮助我们促进代币的购买和销售。
在上面的合同中,首先我们要导入我们的代币合同,为了与我们的代币合同互动,我们需要使用供应商合同和调用函数。
Ownable.sol 我们还从@openzeppelin/contracts 。这意味着智能合约的所有者可以转让其所有权,并可以访问只有所有者才能使用的功能。
在初始化智能合约后,我们定义了变量tokensPerNativeCurrency ,它指出了使用1个MATIC可以购买的代币数量。我们将根据所做的交易来改变这个数字。
然后,我们有一个构造函数,它将接受OKToken的合约地址,这样我们就可以与部署的合约进行通信,并对它们执行功能。
在buyTokens() 函数中,我们正在进行检查,以确保向智能合约发送适当数量的MATIC,并确保供应商合约拥有所需数量的代币。然后,我们从之前创建的OKToken实例中调用函数transfer() ,将代币转移到请求发送方。
在sellTokens() 函数中,我们正在执行检查,以确保请求发送方有足够的令牌,以及供应商合同是否有足够的MATIC来发回给请求发送方。然后,我们使用之前创建的OKToken实例中的transferFrom() 函数,将代币从请求发送方的钱包转移到智能合约中。然而,发送者需要批准这项交易;在提出请求之前,我们在客户端执行这项批准。我们将在制作这个应用程序的前端时涵盖这一部分。
最后,我们有withdraw() ,这个功能只有合约的所有者可以访问。它允许他们撤回合同上的所有MATIC。
现在我们已经准备好了智能合约,让我们把它们部署到Polygon Mumbai testnet!
部署我们的智能合约
我们将创建一个脚本,将我们的合约部署到Polygon Mumbai。一旦合同被部署,我们将以编程方式将存储在部署者钱包中的所有代币发送到供应商合同中。
首先进入hardhat.config.js ,在module.exports ,添加以下对象,以便Hardhat知道要连接到哪个网络。
networks: {
mumbai: {
url: "https://matic-mumbai.chainstacklabs.com",
accounts: ["PRIVATE KEY HERE"],
}
}
我们为网络提供一个名称(本例中为mumbai )并提供一个RPC URL。所提到的RPC URL是针对Polygon Mumbai的。如果你想使用Polygon Mainnet,你可以选择你的RPC URL。记得输入你自己的钱包私钥与一些测试MATIC,以支付智能合约部署过程中涉及的气体费用。
现在,在scripts 文件夹下,创建一个名为deploy.js 的新文件。粘贴以下内容。
const { BigNumber, utils } = require("ethers");
const hardhat = require("hardhat");
async function main() {
const OKToken = await hardhat.ethers.getContractFactory("OKToken");
const oktoken = await OKToken.deploy();
await oktoken.deployed();
console.log("[
在上述文件中,我们正在指示Hardhat如何部署我们的合约。main() 函数是这里的入口。首先,我们得到OKToken 合同并部署它。然后,我们得到OKVendor 合同,在构造函数中提供OKToken 合同地址,并部署该合同。然后,我们把所有的资金从OKToken 合同转移到OKVendor 合同。
在终端运行以下命令,运行脚本并将我们的合同部署到Polygon Mumbai网络中。
npx hardhat run --network mumbai scripts/deploy.js --show-stack-traces
注意,网络名称必须与hardhat.config.js 中提到的网络名称一致。运行该脚本后,合同应该被部署,你应该在终端看到以下内容。

如果你看到与此类似的输出,你的智能合约已经被成功部署和配置了。现在,让我们继续创建我们的Next.js应用程序。
创建一个Next.js DeFi应用程序
在同一目录下,在终端运行以下命令来创建你的Next.js应用程序。
npx create-next-app frontend
上述命令将创建一个新的应用程序,并自动安装必要的依赖项。
导航到frontend 文件夹,在终端中使用以下命令来安装额外的依赖,这将有助于我们与我们的智能合约进行交互。
yarn add @thirdweb-dev/react @thirdweb-dev/sdk ethers web3
我们正在安装@thirdweb-dev/react 和@thirdweb-dev/sdk ,这样我们就可以轻松地验证用户,并使用MetaMask将他们的钱包连接到我们的应用程序。ethers 是thirdweb的一个必要依赖,所以我们也需要安装它。最后,我们要安装web3 ,这样我们就可以与我们的智能合约进行交互。
添加thirdweb提供者
为了开始工作,我们需要将我们的应用程序包裹在一个thirdwebProvider ,以便thirdweb能够正常运行。
进入你的pages 文件夹下的_app.js 文件,添加以下内容。
import { thirdwebProvider, ChainId } from "@thirdweb-dev/react";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
return (
<thirdwebProvider desiredChainId={ChainId.Mumbai}>
<Component {...pageProps} />
</thirdwebProvider>
);
}
export default MyApp;
在上面的代码中,我们正在导入thirdwebProvider ,并将我们的应用程序包围在其中。我们还提供了Polygon Mumbai的连锁ID的desiredChainId 。如果你想这样做,你也可以使用Polygon Mainnet的链ID。
在你的Next.js应用根部创建一个新文件,名为contracts.js ,并添加以下内容。
export const oktoken = {
contractAddress: "0xE83DD81890C76BB8c4b8Bc6365Ad95E5e71495E5",
abi: [
{
inputs: [],
stateMutability: "nonpayable",
type: "constructor",
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "owner",
type: "address",
},
{
indexed: true,
internalType: "address",
name: "spender",
type: "address",
},
{
indexed: false,
internalType: "uint256",
name: "value",
type: "uint256",
},
],
name: "Approval",
type: "event",
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "from",
type: "address",
},
{
indexed: true,
internalType: "address",
name: "to",
type: "address",
},
{
indexed: false,
internalType: "uint256",
name: "value",
type: "uint256",
},
],
name: "Transfer",
type: "event",
},
{
inputs: [
{
internalType: "address",
name: "owner",
type: "address",
},
{
internalType: "address",
name: "spender",
type: "address",
},
],
name: "allowance",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "spender",
type: "address",
},
{
internalType: "uint256",
name: "amount",
type: "uint256",
},
],
name: "approve",
outputs: [
{
internalType: "bool",
name: "",
type: "bool",
},
],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "account",
type: "address",
},
],
name: "balanceOf",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "decimals",
outputs: [
{
internalType: "uint8",
name: "",
type: "uint8",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "spender",
type: "address",
},
{
internalType: "uint256",
name: "subtractedValue",
type: "uint256",
},
],
name: "decreaseAllowance",
outputs: [
{
internalType: "bool",
name: "",
type: "bool",
},
],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "spender",
type: "address",
},
{
internalType: "uint256",
name: "addedValue",
type: "uint256",
},
],
name: "increaseAllowance",
outputs: [
{
internalType: "bool",
name: "",
type: "bool",
},
],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "name",
outputs: [
{
internalType: "string",
name: "",
type: "string",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "symbol",
outputs: [
{
internalType: "string",
name: "",
type: "string",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "totalSupply",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "to",
type: "address",
},
{
internalType: "uint256",
name: "amount",
type: "uint256",
},
],
name: "transfer",
outputs: [
{
internalType: "bool",
name: "",
type: "bool",
},
],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "from",
type: "address",
},
{
internalType: "address",
name: "to",
type: "address",
},
{
internalType: "uint256",
name: "amount",
type: "uint256",
},
],
name: "transferFrom",
outputs: [
{
internalType: "bool",
name: "",
type: "bool",
},
],
stateMutability: "nonpayable",
type: "function",
},
],
};
export const okvendor = {
contractAddress: "0xAa3b8cbB24aF3EF68a0B1760704C969E57c53D7A",
abi: [
{
inputs: [
{
internalType: "address",
name: "tokenAddress",
type: "address",
},
],
stateMutability: "nonpayable",
type: "constructor",
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "address",
name: "buyer",
type: "address",
},
{
indexed: false,
internalType: "uint256",
name: "amountOfNativeCurrency",
type: "uint256",
},
{
indexed: false,
internalType: "uint256",
name: "amountOfTokens",
type: "uint256",
},
],
name: "BuyTokens",
type: "event",
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "previousOwner",
type: "address",
},
{
indexed: true,
internalType: "address",
name: "newOwner",
type: "address",
},
],
name: "OwnershipTransferred",
type: "event",
},
{
inputs: [],
name: "buyTokens",
outputs: [
{
internalType: "uint256",
name: "tokenAmount",
type: "uint256",
},
],
stateMutability: "payable",
type: "function",
},
{
inputs: [],
name: "getNumberOfTokensInNativeCurrency",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "owner",
outputs: [
{
internalType: "address",
name: "",
type: "address",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "renounceOwnership",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "uint256",
name: "tokenAmountToSell",
type: "uint256",
},
],
name: "sellTokens",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "tokensPerNativeCurrency",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "newOwner",
type: "address",
},
],
name: "transferOwnership",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "withdraw",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
],
};
记得用你自己的合约地址替换,这样Next.js应用就会尝试连接到正确的智能合约。
现在让我们开始对我们的应用程序进行编码。打开pages 文件夹下的index.js 文件,添加以下内容。
import { useAddress, useContract, useMetamask } from "@thirdweb-dev/react";
import Head from "next/head";
import Image from "next/image";
import { oktoken, okvendor } from "../contracts";
import styles from "../styles/Home.module.css";
import { useEffect, useState } from "react";
import Web3 from "web3";
const web3 = new Web3(Web3.givenProvider);
export default function Home() {
const [tokensPerCurrency, setTokensPerCurrency] = useState(0);
const [tokens, setTokens] = useState(0);
const address = useAddress();
const connectUsingMetamask = useMetamask();
const account = web3.defaultAccount;
const purchase = async () => {
const contract = new web3.eth.Contract(
okvendor.abi,
okvendor.contractAddress
);
const ethToSend = tokens / tokensPerCurrency;
const purchase = await contract.methods.buyTokens().send({
from: address,
value: web3.utils.toWei(ethToSend.toString(), "ether"),
});
console.log(purchase);
await fetchPrice();
};
const sell = async () => {
const vendorContract = new web3.eth.Contract(
okvendor.abi,
okvendor.contractAddress
);
const tokenContract = new web3.eth.Contract(
oktoken.abi,
oktoken.contractAddress
);
const approve = await tokenContract.methods
.approve(
okvendor.contractAddress,
web3.utils.toWei(tokens.toString(), "ether")
)
.send({
from: address,
});
const sellTokens = await vendorContract.methods.sellTokens(tokens).send({
from: address,
});
await fetchPrice();
};
const fetchPrice = async () => {
const contract = new web3.eth.Contract(
okvendor.abi,
okvendor.contractAddress
);
const priceFromContract = await contract.methods
.getNumberOfTokensInNativeCurrency()
.call();
setTokensPerCurrency(priceFromContract);
};
useEffect(() => {
fetchPrice();
}, []);
return (
<div>
<Head>
<title>Exchange OKTokens</title>
</Head>
{address ? (
<div>
<p>Tokens per currency: {tokensPerCurrency}</p>
<div>
<input
type="number"
value={tokens}
onChange={(e) => setTokens(e.target.value)}
/>
</div>
<button onClick={purchase}>Purchase</button>
<button onClick={sell}>Sell</button>
</div>
) : (
<div>
<button onClick={connectUsingMetamask}>Connect using MetaMask</button>
</div>
)}
</div>
);
}
这是一个很长的代码块,所以让我们看看代码正在一步步地做什么。
- 使用thirdweb设置的提供者初始化
web3包 - 使用thirdweb的钩子
useMetamask()来认证,使用useAddress()来检查认证状态,然后在用户没有使用MetaMask连接钱包的情况下渲染登录按钮 - 设置各种状态来映射我们应用程序中的文本框
- 创建一个
fetchPrice(),与我们的智能合约互动,检查一个MATIC可以得到多少代币,同时也创建一个useEffect,在页面加载时检查这个价格。 - 创建一个
purchase()函数,该函数初始化我们的供应商合同,并从合同中调用buyTokens()函数,然后随着这个交易发送一些MATIC。然后,我们调用fetchPrice(),这样就会显示最新的价格
最后,我们正在创建一个sell() 函数,该函数同时初始化代币和供应商合约。首先,我们与代币合约的approve() 函数互动,并允许供应商合约代表我们转移资金。然后,我们从供应商合同中调用sellTokens() 函数,最终出售代币并收到MATIC。我们也在调用fetchPrice() ,以获得交易后的最新价格。
我们简单的DeFi应用程序就完成了!你可以通过运行以下命令在你的浏览器中查看这个应用程序。
yarn dev
现在,一旦你访问http://localhost:3000,你应该看到以下屏幕,你应该能够进行交易。

总结
这是一个关于如何在Polygon基础上创建自己的全栈DeFi应用的简单教程。你可以在智能合约上实现你自己的逻辑,根据你的组织情况,使其变得更好。我建议对代码进行修补,以便你能以最佳方式学习。