如何编写你的第一个去中心化应用程序 - scaffold-eth 挑战 1:Staking dApp

2025-06-07

如何编写你的第一个去中心化应用程序 - scaffold-eth 挑战 1:Staking dApp

在这篇博文中,我将介绍 scaffold-eth 的首个快速运行项目:创建一个Staking dApp。如果你想了解更多关于 scaffold-eth 以及我目前在 web3 领域的探索,可以阅读我之前的文章:我的 Web3 开发之旅:scaffold-eth

dApp 的目标

该项目的最终目标是模拟以太坊 2.0 的质押合约。要求非常简单:

  • 允许任何人堆叠以太币并追踪其余额
  • 如果时间和堆栈数量截止日期已到,则不允许用户提取他们的资金(这些资金将用于未来的项目,如以太坊 PoS)

你要学什么?

  • 设置 scaffold-eth 项目
  • 编写质押合约
  • 调用外部合约
  • 为您的 Solidity 合约创建单元测试
  • 在本地机器上使用 React 应用并测试你的 Contract
  • 在以太坊测试网上部署权益合约!

或许并非如此,但您可以将其视为您(和我)旅程的第一块垫脚石。

您应该始终牢记一些始终有用的链接:

设置项目

首先,我们需要进行设置。克隆 scaffold-eth 仓库,切换到 challenge 1 分支,并安装所有需要的依赖项。

git clone https://github.com/austintgriffith/scaffold-eth.git challenge-1-decentralized-staking  
cd challenge-1-decentralized-staking  
git checkout challenge-1-decentralized-staking  
yarn install
Enter fullscreen mode Exit fullscreen mode

可用 CLI 命令概述

这些命令并不是针对这个挑战而特有的,而是对每个 scaffold-eth 项目都是通用的!

yarn chain

此命令将启动您的本地安全帽网络并将其配置为在http://localhost:8545上运行

yarn start

此命令将在http://localhost:3000上启动您的本地 React 网站

yarn deploy

此命令将部署所有合约并刷新 React 应用。更准确地说,此命令将运行两个 JavaScript 脚本(部署和发布)。

因此,打开三个不同的终端并启动这些命令。每次更改合约时,只需重新启动部署命令即可。

练习 1:实现 stake() 方法

在练习的这一部分,我们希望允许用户在我们的合约中质押一些 ETH 并跟踪他们的余额。

需要掌握的重要概念

  • 可支付方法——当一个函数被声明为可支付时,就意味着允许用户向其发送 ETH。
  • Mapping——它是Solidity 支持的变量类型之一。它允许你将一个与一个关联起来。
  • 事件——事件允许合约通知其他实体(合约、Web3 应用程序等)某些事件已发生。声明事件时,最多可以指定 3 个索引参数。当参数声明为索引参数时,第三方应用可以过滤该特定参数的事件。

演习实施

  • 声明一个映射来跟踪余额
  • 声明恒定阈值为 1 以太
  • 声明一个 Stake 事件,该事件将记录质押者地址和质押金额
  • 实现一个可支付stake()函数来更新质押者的余额

合约代码已更新

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "hardhat/console.sol";
import "./ExampleExternalContract.sol";
/**
* @title Stacker Contract
* @author scaffold-eth
* @notice A contract that allow users to stack ETH
*/
contract Staker {
// External contract that will old stacked funds
ExampleExternalContract public exampleExternalContract;
// Balances of the user's stacked funds
mapping(address => uint256) public balances;
// Staking threshold
uint256 public constant threshold = 1 ether;
// Contract's Events
event Stake(address indexed sender, uint256 amount);
/**
* @notice Contract Constructor
* @param exampleExternalContractAddress Address of the external contract that will hold stacked funds
*/
constructor(address exampleExternalContractAddress) public {
exampleExternalContract = ExampleExternalContract(exampleExternalContractAddress);
}
/**
* @notice Stake method that update the user's balance
*/
function stake() public payable {
// update the user's balance
balances[msg.sender] += msg.value;
// emit the event to notify the blockchain that we have correctly Staked some fund for the user
emit Stake(msg.sender, msg.value);
}
}

一些澄清:

  • uintuint256是一样的(只是别名)
  • 当变量声明为 public 时,Solidity 会自动为你创建一个 getter 方法。这意味着它将暴露一个yourVariableName()方法供调用
  • 当你声明一个变量但没有初始化它时,它将根据变量类型初始化为其默认值
  • Solidity 公开了一些实用单位,例如wei、ethers 或时间单位

让我们回顾一下:

  • 我们已经声明了我们的余额,它将跟踪每个用户地址的累积余额
  • 我们已经宣布了我们的门槛
  • 我们已经宣布了 Stake 事件,该事件将通知区块链用户已存入一定数量的
  • 我们已经将 Stake 功能实现为一种公共支付方法,它将更新用户的余额并发出 Stake 事件。

有一点可能很奇怪,那就是我们只是更新了 的值,而没有初始化 的默认值balances[msg.sender]。这是可能的,因为当一个变量未初始化时,它将以其类型默认值创建。在本例中 (uint256) 的默认值为 0。

现在,部署合约,从 Faucet 获取一些资金并尝试将一些 ETH 投入合约。

  • 你能从水龙头获得一些资金吗?
  • 您可以通过点击 Stake 按钮向合约发送 0.5 ETH 吗?
  • 该事件是否在 UI 上触发?
  • 您的权益余额更新了吗?
  • 合约余额是否更新?

如果您已检查所有这些标记,我们可以继续练习的第 2 部分。

练习 2:实现锁定机制并撤回

正如我们之前所说,本合约的最终目标是创建一个 Staking dApp,允许公众用户在满足某些条件的情况下持有部分 ETH。如果这些条件不满足,他们就可以提取资金。

这些条件是:

  • 至少需要 1 ETH 被存入 Staker 合约
  • 在 30 秒的时间内达到 1 ETH 堆栈阈值

需要掌握的重要概念

  • 调用外部合约——区块链上的每个合约都像一个公共 REST API。您可以从您的 Web3 应用调用它们,或者如果它们声明为public或,则直接从另一个合约调用它们。external
  • 函数修饰符——修饰符是可以在函数调用之前和/或之后运行的代码。它们可用于限制访问、验证输入或防止重入攻击
  • 错误处理——错误处理非常重要,因为它允许你恢复智能合约的状态(确切地说是不应用)。你可以将恢复过程想象成数据库操作rollback。错误处理还允许你通知用户恢复的原因。
  • 发送以太币(transfer、send、call) —— Solidity 有原生方法可以将 ETH 从一个合约转移到另一个合约/用户地址。简而言之:使用call ;)

演习实施

  • 从合约部署时间起声明 30 秒的期限
  • 创建一个公共timeLeft()函数,返回截止时间前剩余的时间
  • 创建一个修改器来检查外部合同是否完成
  • 创建一个修饰符,它将动态地(使用参数)检查是否已经到达最后期限
  • 仅当截止日期尚未到来且我们尚未执行外部合约时,才允许用户质押 ETH
  • 仅当未达到余额阈值的截止日期时才允许用户提取资金
  • 创建一个execute()方法,将资金从Staker合约转移到外部合约,并从另一个合约执行外部函数

在本地测试合约时,需要注意一点:区块链状态仅在区块被挖出时更新。区块编号和区块时间戳仅在交易完成时更新。这意味着 timeLeft() 函数仅在交易完成后才会更新。如果您想模拟“真实”体验,可以更改安全帽配置以模拟区块自动挖矿。如果您想了解更多信息,请查看他们的挖矿模式文档。

合约代码已更新


为什么代码与原始挑战的代码不同?

  • 我认为在这种情况下变量openForWithdraw是不必要的。可以直接从 Staker 合约和外部合约的状态启用提现。
  • 在这种情况下,withdraw为了简化操作,我们不会使用外部地址。您将是唯一可以提现的人!
  • 我们已将 Solidity 更新至 版本0.8.4,并将 Hardhat 更新至 版本2.6.1。某些 scaffold-eth(例如本例)可能仍依赖于旧版本的 Solidity,我认为出于安全、优化和功能完整性的考虑,使用最新版本至关重要。

让我们回顾一下代码

函数修饰符:首先,您可以看到我们创建了两个修饰符。正如您从 Solidity 示例中学到的,函数修饰符是可以在函数调用之前和/或之后运行的代码。在我们的例子中,我们甚至添加了参数函数修饰符!

定义了函数修饰符后,可以使用它们,只需在函数名称后附加修饰符的名称即可。如果修饰符为 rever,则函数在运行之前就会被还原!

stake() 函数:与之前相同

timeLeft() 函数:非常简单,我们使用该block.timestamp值来计算截止日期前剩余的秒数。

withdrawal() 函数:在我们的修饰符标志通过后,我们会检查用户是否有余额,否则将撤销。为了防止重入攻击,您应该在每次调用之前修改合约状态。因此,我们将用户余额保存在一个变量中,并将用户余额更新为 0。

execute() 函数:在我们的修饰符标志传递之后,我们调用外部合约complete()函数并检查一切是否成功。

现在部署更新后的合约yarn deploy并在本地进行测试。

  1. 您是否看到交易完成后剩余时间立即发生变化?
  2. 截止日期之后你还能质押 ETH 吗?
  3. 如果合同已经履行,您可以在期限之前或期限之后撤回吗?
  4. 即使未达到门槛,您还能执行合同吗?
  5. 合同可以多次执行吗?

练习第 3 部分:测试覆盖率

我知道我知道,您只想部署您的合约和前端并立即在您选择的测试网络上开始测试它,但是……我们需要确保一切都按预期工作,而无需在 UI 上进行猴子点击!

因此,在文章的下一部分中,我将介绍每个开发人员都应该做的事情:用测试覆盖你的合同逻辑!

胡扯

Waffle是一个用于编写和测试智能合约的库,可以与 ethers-js 完美协同工作。

Waffle 内置了丰富的工具来实现这一点。Waffle 中的测试是使用MochaChai编写的。你可以使用其他测试环境,但 Waffle 的匹配器仅适用于chai

为了测试我们的合同,我们将使用Chai 匹配器来验证我们期望的条件是否已经满足。

编写完所有测试后,您只需键入内容yarn test,所有测试将根据您的合同自动运行。

我不会解释如何使用该库(您可以简单地看一下下面的代码来了解概况),我将更专注于“我们应该测试什么”。

我们已经按照一些逻辑实现了我们的智能合约:

  • 我们正在跟踪用户余额mapping(address => uint256) public balances
  • 我们有最低限度uint256 public constant threshold = 1 ether
  • 我们有一个最大值uint256 public deadline = block.timestamp + 120 seconds
  • stake()如果外部合约尚未达成completed用户可以调用该函数deadline
  • execute如果外部合同尚未达成,用户completed可以调用该方法deadline
  • 如果deadline已经达成协议,并且外部合同没有completed
  • timeLeft()返回剩余秒数,直到deadline达到,之后它应该始终返回0

测试中应该涵盖的内容

附言:这是我个人的测试方法,如果您有任何建议,请在 Twitter 上联系我!

当我编写测试时,我的想法是针对某个函数,覆盖所有边缘情况。试着在编写测试时回答以下问题:

  • 我是否已经涵盖了所有极端情况
  • 该功能是否按预期恢复?
  • 该函数是否发出了所需的事件
  • 有了特定的输入,函数会产生预期的输出吗?合约的状态会按照我们的预期形成吗?
  • 该函数会返回(如果它返回某些内容)我们所期望的内容吗?

如何在测试中模拟区块链挖矿

还记得我们说过,为了正确模拟,timeLeft()我们必须创建交易,或者只是从水龙头请求资金(这也是一笔交易)吗?好吧,为了在测试中解决这个问题,我实现了一个小实用程序(您可以简单地将其复制/粘贴到其他项目中),它可以做同样的事情:

increaseWorldTimeInSeconds 函数

调用increaseWorldTimeInSeconds(10, true)此函数时,EVM 内部时间戳会比当前时间提前 10 秒。之后,如果您指定了此函数,它还会挖出一个区块来创建交易。

下次调用您的合同时,应该更新block.timestamp所使用的合同。timeLeft()

测试execute()函数

我们先回顾一个测试,然后我会把整个代码贴出来,只解释一些特定的代码。这些代码涵盖了execute()我们代码的功能。

  • 第一个测试检查如果execute()在未达到阈值时调用该函数,它将使用正确的错误消息恢复交易
  • 第二个测试是连续两次调用该execute()函数。质押过程已经完成,交易应该被回滚,以防止再次发生。
  • 第三个测试是尝试在时间截止时间之后调用该函数。由于只能在截止时间之前execute()调用该函数,因此交易应该会被回滚。execute()
  • 最后一个测试是测试如果所有要求都满足,函数execute()不会回滚,并且达到了预期的结果。函数调用后,外部合约completed变量应该为true,外部合约balance应该等于用户质押的金额,我们的合约余额应该等于0(我们已将所有余额转入外部合约)。

如果一切按预期进行,运行yarn test应该会给出这个输出

文本执行成功!

测试覆盖完整代码

以下是完整的测试覆盖率代码

你有没有注意到,测试代码覆盖率远高于合约本身?这正是我们想要看到的!测试所有东西!

最后一步:将您的合约部署到月球(测试网)

好的,现在是时候了。我们已经实现了智能合约,测试了前端 UI,并且测试涵盖了所有边缘情况。我们已准备好在测试网上部署它。

按照scaffold-eth 文档,我们需要遵循以下步骤:

  1. defaultNetwork将in更改packages/hardhat/hardhat.config.js为您想要使用的测试网(在我的情况下是 rinkeby)
  2. infuriaProjectId使用在Infuria上创建的更新
  3. 生成部署账户with yarn generate。此命令应生成两个.txt文件。一个代表账户地址,另一个代表所生成账户的种子短语。
  4. 运行yarn account即可查看帐户的详细信息,例如不同网络的 eth 余额。
  5. 确保mnemonic.txt相关帐户文件未通过您的 git 存储库推送,否则任何人都可以获得您的合约的所有权!
  6. 为你的部署者账户充值一些资金。你可以使用即时钱包将资金发送到你刚刚在控制台上看到的二维码。
  7. 使用 部署您的合约yarn deploy

如果一切顺利的话你应该会在控制台上看到类似这样的内容

yarn run v1.22.10  
$ yarn workspace [@scaffold](http://twitter.com/scaffold)-eth/hardhat deploy  
$ hardhat run scripts/deploy.js && hardhat run scripts/publish.js📡 Deploying...🛰  Deploying: ExampleExternalContract  
 📄 ExampleExternalContract deployed to: 0x96918Bd0EeAF5BBe10deD67f796ef44b2f5cb2A3  
 🛰  Deploying: Staker  
 📄 Staker deployed to: 0x96918Bd0EeAF5BBe10deD67f796ef44b2f5cb2A3  
 💾  Artifacts (address, abi, and args) saved to:  packages/hardhat/artifacts/ 💽 Publishing ExampleExternalContract to ../react-app/src/contracts  
 📠 Published ExampleExternalContract to the frontend.  
 💽 Publishing Staker to ../react-app/src/contracts  
 📠 Published Staker to the frontend.  
✨  Done in 11.09s.
Enter fullscreen mode Exit fullscreen mode

部署元数据存储在文件夹中,并通过命令中的标志/deployments自动复制到(参见)。/packages/react-app/src/contracts/hardhat_contracts.json--export-allyarn deploy/packages/hardhat/packagen.json

如果您想检查已部署的合约,您可以在 Etherscan Rinkeby 网站上搜索它们:

更新您的前端应用程序并将其部署在 Surge 上!

我们将使用Surge方法,但您也可以在AWS S3IPFS上部署您的应用程序,这取决于您!

在不久的将来,我还将尝试添加一些基本方法,以手动(通过 CLI)和通过 GitHub Actions CI/CD将其部署到Vercel 。

scaffold -eth 文档总是随手可得,但我会总结一下你应该做的事情:

  1. 如果您要在主网上部署,则应该在 Etherscan 上验证您的合约。此过程将增强您应用程序的可信度和信任度。如果您有兴趣,请遵循scaffold-eth指南。
  2. 关闭调试模式(它会打印大量的 console.log,相信我,这可不是你想在 Chrome 开发者控制台中看到的!)。打开App.jsx,找到const DEBUG = true;并将其转换为false
  3. 查看App.jsx并删除所有未使用的代码,确保只发送您真正需要的代码!
  4. 确保你的 React 应用指向正确的网络(即你刚刚用于部署合约的网络)。查找const targetNetwork = NETWORKS[“localhost”];并替换localhost为你的合约的网络。在本例中,它将是rinkeby
  5. 确保你使用的是自己的节点,而不是 Scaffold-eth 中的节点,因为它们是公开的,并且无法保证它们会被关闭或限制速率。请查看第 58 行和第 59 行App.jsx
  6. 如果您想使用他们的服务,请更新constants.js并交换InfuraEtherscanBlocknative API 密钥。

准备好了吗?出发!

现在使用 构建你的 React App yarn build,当构建脚本完成后,使用 将其部署到 Surge yarn surge

如果一切顺利,你应该会看到类似这样的画面。你的 dApp 现已在 Surge 上线!

在 Surge.sh 上部署成功!

您可以在这里查看我们部署的 dApp:https://woozy-cable.surge.sh/

回顾与结论

这就是我们迄今为止学到的和做的

  • 克隆 scaffold-eth 挑战 repo
  • 学习了几个基本概念(记得继续阅读 Solidity 示例、Hardhat 文档、Solidity 文档、Waffle 文档)
  • 从零开始创建智能合约
  • 为我们的合同创建一个完整的测试套件
  • 在安全帽网络上本地测试了我们的合约
  • 在 Rinkeby 上部署我们的合约
  • 在 Surge 上部署了我们的 dApp

如果一切按预期进行,您已准备好实现重大飞跃并将所有内容部署到以太坊主网上!

该项目的 GitHub Repo:scaffold-eth-challenge-1-decentralized-staking

你喜欢这篇内容吗?关注我,了解更多!

文章来源:https://dev.to/stermi/scaffold-eth-challenge-1-stake-dapp-4ofb
PREV
开发者在申请 Google 职位之前请阅读此文
NEXT
How to create an ERC20 Token and a Solidity Vendor Contract to sell/buy your own token