• 译文出自:登链翻译计划 [1]

  • 译者:翻译小组 [2]

  • 校对:Tiny 熊 [3]

正如我们之前所说,这个合约的最终目标是实现一个质押 dApp,当满足一些条件,用户就可以质押 ETH。如果没有达到这些条件,用户可以撤回他们的 ETH 。

这些条件是:

  • 至少向质押合约质押 1 个 ETH

  • 在 deadline(30 秒) 内达到 1 个 ETH 的质押阈值

需要掌握的重要概念

  • 调用外部合约 [4] - 区块链上的每个合约都像一个公共的 REST API 。如果合约被声明为 publicexternal,你可以从 web3 app 或直接从另一个合约调用它们。

  • 函数修改器 (Function Modifier)[5] - 修改器是可以在函数调用之前和 / 或之后运行的代码。它们可以用来限制访问,验证输入,或防范重入性攻击 [6]。

  • 错误处理 [7] - 错误处理很重要,因为它可以还原智能合约的状态(准确地说是让改变不生效),并通知用户还原的原因。你可以把这种还原比作数据库的 rollback

  • 发送 ETH (转账、发送、调用)[8] - Solidity 有本地方法可以将 ETH 从一个合约转账到另一个合约 / 地址。

练习实现

  • 声明一个 deadline,它是区块时间延后 30 秒

  • 创建一个 public timeLeft() 函数,用于返回剩余时间,直到时间到 deadline 为止

  • 创建一个修改器 (Modifier),用于检查外部合约是否已经完成

  • 创建一个修改器 (Modifier),用于动态检查 deadline 是否到了

  • 只允许用户在时间没到 deadline (有效期内) 且没有执行外部合约的情况下质押 ETH

  • 只有当时间没有到 deadline 且 balances 没有达到阀值,用户才可以撤回资金

  • 创建一个 execute() 方法,将资金从质押合约转移到外部合约并执行另一个合约外部函数

当你在本地测试合约是一定要注意:区块链的状态只有在区块被打包时才会更新。区块编号和区块时间都只有在交易完成后才会更新。这意味着 timeLeft() 只有在交易完成后才会更新。如果你想模拟真实场景,可以改变 Hardhat 配置来模拟区块自动挖矿。如果你想了解更多,请看 mining-mode 文档 [9]。

合约代码更新

    // 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;        // 用户质押余额        mapping(address => uint256) public balances;        // Staking threshold        uint256 public constant threshold = 1 ether;        // Staking deadline        uint256 public deadline = block.timestamp   30 seconds;        // 合约事件        event Stake(address indexed sender, uint256 amount);        // Contract's Modifiers        /**       * @notice Modifier that require the deadline to be reached or not        * @param requireReached Check if the deadline has reached or not        */        modifier deadlineReached( bool requireReached ) {          uint256 timeRemaining = timeLeft();          if( requireReached ) {            require(timeRemaining == 0, "Deadline is not reached yet");          } else {            require(timeRemaining > 0, "Deadline is already reached");          }         _;        }        /**       * @notice Modifier that require the external contract to not be completed        */        modifier stakeNotCompleted() {          bool completed = exampleExternalContract.completed();          require(!completed, "staking process already completed");         _;        }        /**       * @notice Contract Constructor        * @param exampleExternalContractAddress Address of the external contract that will hold stacked funds        */        constructor(address exampleExternalContractAddress) {          exampleExternalContract = ExampleExternalContract(exampleExternalContractAddress);        }        function execute() public stakeNotCompleted deadlineReached(false) {          uint256 contractBalance = address(this).balance;          // check the contract has enough ETH to reach the treshold          require(contractBalance >= threshold, "Threshold not reached");          // Execute the external contract, transfer all the balance to the contract          // (bool sent, bytes memory data) = exampleExternalContract.complete{value: contractBalance}();          (bool sent,) = address(exampleExternalContract).call{value: contractBalance}(abi.encodeWithSignature("complete()"));          require(sent, "exampleExternalContract.complete failed");        }        /**       * @notice Stake method that update the user's balance        */        function stake() public payable deadlineReached(false) stakeNotCompleted {          // 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);        }        /**       * @notice Allow users to withdraw their balance from the contract only if deadline is reached but the stake is not completed        */        function withdraw() public deadlineReached(true) stakeNotCompleted {          uint256 userBalance = balances[msg.sender];          // check if the user has balance to withdraw          require(userBalance > 0, "You don't have balance to withdraw");          // reset the balance of the user          balances[msg.sender] = 0;          // Transfer balance back to the user          (bool sent,) = msg.sender.call{value: userBalance}("");          require(sent, "Failed to send user balance back to the user");        }        /**       * @notice The number of seconds remaining until the deadline is reached        */        function timeLeft() public view returns (uint256 timeleft) {          if( block.timestamp >= deadline ) {            return 0;          } else {            return deadline - block.timestamp;          }        }      }  

为什么与练习 1 中的代码不同?

  • 我认为在这个实例中,变量 openForWithdraw 是不必要的。可以根据质押合约和外部合约的状态直接判定是否可以撤回资金。

  • 简单起见,本例中 withdraw 函数不接受外部地址作为参数,只有质押者本人可以撤回资金。

  • 我们已经将 Solidity 更新到 0.8.4 版本,Hardhat 更新到 2.6.1 版本。有些 scaffold-eth (比如这个)可能仍然依赖于旧版本的 Solidity,我认为出于安全、优化和功能完整的考虑,使用最新的版本是很重要的。

回顾一下

函数修改器 (Function Modifiers):首先,你可以看到我们已经创建了两个修改器。正如你已经从 Solidity 的例子中学到的,函数修改器是可以在一个函数调用之前或之后运行的代码。在上面的例子中,我们添加了带参数的函数修改器 !

当你定义了一个函数修改器 (Function Modifiers) 后,你可以在函数名称后附加上修改器的名称。如果修改器回退了(不满足条件 revert),函数会在运行之前就回退 !

stake() 函数:与练习 1 相同。

timeLeft() 函数:使用 block.timestamp 的值来计算剩余秒数。

withdraw() 函数:在修改器(modifiers)通过后,检查用户是否有余额,如果没有余额就返回。为了防止重入性攻击 [10],你应该在任何调用之前先修改合约的状态。这就是为什么我们要把用户的余额保存在一个变量中,并把用户的余额更新为 0。

execute() 函数:在修改器(modifiers)通过后,调用外部合约 complete() 函数,并检查一切是否成功。

现在用 yarn deploy 部署更新后的合约,并在本地进行测试, 检查一下:

  1. 在你进行交易的时候 timeLeft 是否在变化?

  2. 你能在时间到 deadline 之后质押 ETH 吗?

  3. 如果合约被执行,能在 deadline 前或后撤回资金吗?

  4. 即使质押金额没有达到阀值,也可以执行合约吗?

  5. 可以多次执行合约吗?

https://www.youtube.com/watch?v=193ZeR17dtk


本翻译由 CellETF[11] 赞助支持。

来源:https://stermi.medium.com/how-to-write-your-first-decentralized-app-scaffold-eth-challenge-1-staking-dapp-b0b6a6f4d242

参考资料

[1]

登链翻译计划 :https://github.com/lbc-team/Pioneer

[2]

翻译小组 :https://learnblockchain.cn/people/412

[3]

Tiny 熊 :https://learnblockchain.cn/people/15

[4]

调用外部合约 :https://solidity-by-example.org/calling-contract/

[5]

函数修改器 (Function Modifier):https://solidity-by-example.org/function-modifier/

[6]

重入性攻击 :https://solidity-by-example.org/hacks/re-entrancy/

[7]

错误处理 :https://solidity-by-example.org/error/

[8]

发送 ETH (转账、发送、调用):https://solidity-by-example.org/sending-ether/

[9]

mining-mode 文档 :https://hardhat.org/hardhat-network/reference/#mining-modes

[10]

重入性攻击 :https://solidity-by-example.org/hacks/re-entrancy/

[11]

CellETF:https://celletf.io/?utm_souce=learnblockchain

scaffold-eth 挑战:实现锁定机制和资金撤回(Part2)