如何用Vyper编写安全的智能合约

  • QuickNode
  • 发布于 2024-10-22 21:34
  • 阅读 24

本文介绍了如何使用Vyper语言开发和部署一个权限控制的智能合约。教程详细说明了Vyper的语言特性、开发环境配置以及合约代码的编写和部署过程,适合有一定Python和Ethereum知识的开发者。通过实例,读者能够学习到如何运用Vyper实现合约逻辑,并了解安全注意事项。

已知安全漏洞

更新时间:2023年7月30日

Vyper团队发现了Vyper版本0.2.15、0.2.16和0.3.0中的一些漏洞。如果你在这些版本中有活跃合约,请参考下面的推文并联系Vyper团队。请明智行事,切勿分享任何私钥或种子短语,即使是有声望的团队成员要求也不要🙏。

原始文章

概述

嗨,读者们!如果你对以太坊以及智能合约一般有所了解,你可能听说过Solidity。在本指南中,我们将向你展示Solidity不太为人知的表亲Vyper。我们将介绍Vyper是什么,你为什么想使用它,以及如何将用Vyper编写的智能合约部署到以太坊测试网络。

前提条件:

  • 文本编辑器/IDE
  • 安装Python 3
  • 具备Python的工作知识
  • 一些Goerli ETH
  • MetaMask

什么是Vyper?

Vyper是一种用于编写智能合约的语言,你可以将其部署到基于EVM的区块链上。它是Python 3的一个子集。语言子集的定义是,子集的所有语法都是父语言的有效语法。换句话说,所有Vyper代码都是有效的Python代码,但反之则不然。Python 3仍然具有Vyper不支持的特性。

Vyper的采用程度仅为Solidity的一个小部分,生态系统中的大多数主要参与者更喜欢Solidity。然而,有一个dApp打破了这种模式。Uniswap目前是以太坊上最大的去中心化交易所(DEX),他们的v1是用Vyper编写的。其他一些使用Vyper的著名项目包括Yearn v2和Curve。虽然Vyper没有广泛的采用,但可以放心的是,如果Uniswap可以将20亿美元的交易信任于该语言,那么你也可以!

如果你想了解如何看待Vyper,可以将其视为没有类的Python 3与没有修饰符的Solidity的结合体。

我们将构建什么?

在这个项目中,我们将构建一个作为有权限的涂鸦墙的智能合约。这意味着已经被列入白名单的人可以在墙上写字,而其他人则不能。

为此,我们需要提前做一些准备工作,以便设置和配置项目。

配置项目

首先,如果我们在Vyper中编写智能合约,安装编译器将是个好主意。除了编译器,我们还将安装web3 python库eth-account

pip install vyper web3 eth-account

安装完这些包后,我们需要获取一个Goerli节点。

为此,我们可以使用任何以太坊客户端,如Geth或OpenEthereum(前称Parity)。由于这对于部署单个合约而言稍显复杂,我们可以在这里创建一个免费的QuickNode账户,并轻松生成一个以太坊端点。在创建好你的端点后,复制你的HTTP提供者URL。

Quicknode Goerli端点的截图

现在我们已经具备了开始项目所需的所有工具,我们可以初始化工作目录并开始编码!运行以下命令以开始。

mkdir vyper_intro
cd vyper_intro
touch deployer.py
touch graffiti_wall.vy

上面的命令将创建一个名为vyper_intro的文件夹,让你移动到该目录,并创建两个文件:deployer.pygraffiti_wall.vy

编写智能合约

我们可以开始编写我们的Vyper代码。在你最喜欢的文本编辑器中打开vyper_intro文件夹。

我们希望我们的项目有一个白名单,只有批准地址可以在我们的“涂鸦墙”上写字。我们需要一个方法,允许合约的所有者将地址添加到白名单。另外,我们还需要一个方法,允许白名单中的用户在涂鸦墙上写字。

为此,我们将首先添加从上一段知道的3个变量。

approved_addresses: public(address[5])
graffiti_wall: public(String[100])
owner: public(address)

在这里,我们声明变变量approved_addressesgraffiti_wallowner。这些都是公共函数。你可以看到Vyper是静态类型的,并且我们有一个保存5个地址的数组。

💡 公共函数是合约接口的一部分,可以通过内部调用或消息调用。对于公共状态变量,会自动生成一个getter函数。

  • ❗ 确保对该方法的调用进行身份验证,因为任何人都可以访问它。

有了这个,我们可以开始编写我们的函数。

@external
def __init__():
    self.owner = msg.sender
    assert chain.id == 3

@external
def add_approved_address(new_address: address):
    assert msg.sender == self.owner
    i: uint256 = 0
    for approved_address in self.approved_addresses:
        if approved_address != 0x0000000000000000000000000000000000000000:
            i += 1
    self.approved_addresses[i] = new_address

@external
def set_graffiti(graffiti: String[100]):
    is_approved: bool = False
    for approved_address in self.approved_addresses:
      if approved_address == msg.sender:
        is_approved = True

    if is_approved:
      self.graffiti_wall = graffiti
      log GraffitiSet(graffiti)

在这里,我们可以看到我们写的3个函数,所有函数都有@external装饰器,这是一个可见性装饰器。可见性装饰器是每个方法都需要的,在这种情况下,@external意味着这些功能可以被其他智能合约或交易调用。

逐个步行我们的函数,我们可以看到第一个 __init__ 函数将合约的拥有者分配为部署它的地址,但前提是我们部署的链是Goerli(Goerli的链ID为3)。__init__ 是构造函数,这意味着它在合约创建时自动调用,并且之后不会再调用。

第二个函数,add_approved_address将一个地址作为参数,并将其添加到我们在第一个代码块中声明的approved_addresses变量中。有一段值得提及的代码,即if approved_address != 0x0000000000000000000000000000000000000000:。我们需要添加这个,因为如果你查询(空)初始化的数组,它将返回0x00地址。

第三个函数,set_graffiti接受一个字符串作为参数,并检查与该函数交互的人是否在我们的批准地址列表中。如果他们在列表中,我们将用该人写的内容替换当前的消息。替换消息后,我们将触发一个名为GraffitiSet的事件。

我们需要写这个GraffitiSet事件。

💡 索引参数可以被侦听器搜索。每个索引参数由 indexed 关键字标识。在这里,每个索引参数都是一个字符串。你可以有任意数量的索引参数,但索引参数不会直接传递给侦听器,尽管一些信息(例如发送者)可能在侦听器的结果对象中可用。

这个事件可以传递一个字符串,你可以在之前的set_graffiti函数中看到我们这样做。为了让区块链上的其他人能够接收事件,我们必须使用log关键字。在set_graffiti函数中,你可以看到我们使用log GraffitiSet(graffiti),这将允许用户在这个智能合约上设置一个侦听器,并广播事件以及我们传递给日志的字符串。

总的来说,你的合约应该如下所示:

approved_addresses: public(address[5])
graffiti_wall: public(String[100])
owner: public(address)

event GraffitiSet:
    graffiti: indexed(String[100])

@external
def __init__():
    self.owner = msg.sender
    assert chain.id == 3

@external
def add_approved_address(new_address: address):
    assert msg.sender == self.owner
    i: uint256 = 0
    for approved_address in self.approved_addresses:
        if approved_address != 0x0000000000000000000000000000000000000000:
            i += 1
    self.approved_addresses[i] = new_address

@external
def set_graffiti(graffiti: String[100]):
    is_approved: bool = False
    for approved_address in self.approved_addresses:
      if approved_address == msg.sender:
        is_approved = True

    if is_approved:
      self.graffiti_wall = graffiti
      log GraffitiSet(graffiti)

所有智能合约编写完毕后,我们可以继续将其部署到Goerli测试网。

部署智能合约

在这里,我们将编写部署智能合约所需的代码。如前所述,你需要有一些Goerli ETH可供使用。

打开你的deployer.py文件并写入以下代码。

from eth_account import Account
from web3 import Web3
import secrets
priv = secrets.token_hex(32)
private_key = "0x" + priv
print ("SAVE BUT DO NOT SHARE THIS:", private_key)
acct = Account.from_key(private_key)
print("Address:", acct.address)

在这里,我们导入了部署合约所需的所有库,并使用secrets.token_hex()方法生成私钥。保存这个文件,并使用以下命令运行脚本:

python3 deployer.py

替代文本:SAVE BUT DO NOT SHARE THIS: 0xae4d38cbdd44521036658724403d3f2b33dbb26b2e9c93a93be083515979a2e3 Address: 0xd466687E156D088E8e97e13bb838113A4676EC06

在这里,你可以看到我们生成了一个私钥及其地址。修改你的Python代码,将这个私钥硬编码。我将使用终端中分享的私钥,但你应使用你的脚本生成的密钥。

from eth_account import Account
from web3 import Web3
import secrets
priv = secrets.token_hex(32)
private_key = "0x" + priv
private_key = "YOUR_PRIVATE_KEY_HERE"
print ("SAVE BUT DO NOT SHARE THIS:", private_key)
acct = Account.from_key(private_key)
print("Address:", acct.address)

这里你可以看到我们将private_key从随机生成的值重新赋值为你从运行脚本中生成的值。

接下来,我们需要将智能合约编译为字节码,以便部署到测试网。

为此,运行以下命令:

vyper graffiti_wall.vy

它将输出一个看起来像这样(相当长的字符串):

0x33600a55600346186102a85761029056600436101561000d57610275565b60046000601c376000513461027b57632502eb8081186100a6576004358060a01c61027b5760e052600a54331861027b5760006101005261014060006005818352015b6101405154610120526000610120511461007f5761010080516001818183011061027b57808201905090508152505b815160010180835281141561005057505060e051600161010051600581101561027b570255005b63f92d4699811861019557600435600401606481351161027b57808035602001808260e0375050506000610180526101c060006005818352015b6101c051546101a052336101a051186100fa576001610180525b81516001018083528114156100e057505061018051156101935760e0806005602082510160c060006005818352015b8260c051602002111561013b5761015a565b60c05160200285015160c0518501558151600101808352811415610129575b50505050505060e08051602082012090507f1feb66a2165328c0029dd757c8c304e5a76ebb0f047f8eefaf16e9bbfa6e336660006101a0a25b005b635506011181186101b9576001600435600581101561027b57025460e052602060e0f35b63bbbaf2c3811861025c5760e08060208082528083018060058082602082540160c060006005818352015b8260c05160200211156101f657610215565b60c05185015460c05160200285015281516001018083528114156101e4575b5050505050508051806020830101818260206001820306601f8201039050033682375050805160200160206001820306601f820103905090509050810190509050905060e0f35b638da5cb5b811861027357600a5460e052602060e0f35b505b60006000fd5b600080fd5b61001061029003610010600039610010610290036000f35b600080fd

我们将其分配给Python脚本中的一个变量,称为bytecode。配合这个新变量,我们将使用QuickNode的HTTP提供者连接到区块链,然后将合约部署到测试网。总之,你的脚本应该如下所示:

from eth_account import Account
from web3 import Web3
import secrets
priv = secrets.token_hex(32)
private_key = "0x" + priv
private_key = "YOUR_PRIVATE_KEY_HERE"
print ("SAVE BUT DO NOT SHARE THIS:", private_key)
acct = Account.from_key(private_key)
print("Address:", acct.address)

bytecode = "0x33600a55600346186102a85761029056600436101561000d57610275565b60046000601c376000513461027b57632502eb8081186100a6576004358060a01c61027b5760e052600a54331861027b5760006101005261014060006005818352015b6101405154610120526000610120511461007f5761010080516001818183011061027b57808201905090508152505b815160010180835281141561005057505060e051600161010051600581101561027b570255005b63f92d4699811861019557600435600401606481351161027b57808035602001808260e0375050506000610180526101c060006005818352015b6101c051546101a052336101a051186100fa576001610180525b81516001018083528114156100e057505061018051156101935760e0806005602082510160c060006005818352015b8260c051602002111561013b5761015a565b60c05160200285015160c0518501558151600101808352811415610129575b50505050505060e08051602082012090507f1feb66a2165328c0029dd757c8c304e5a76ebb0f047f8eefaf16e9bbfa6e336660006101a0a25b005b635506011181186101b9576001600435600581101561027b57025460e052602060e0f35b63bbbaf2c3811861025c5760e08060208082528083018060058082602082540160c060006005818352015b8260c05160200211156101f657610215565b60c05185015460c05160200285015281516001018083528114156101e4575b5050505050508051806020830101818260206001820306601f8201039050033682375050805160200160206001820306601f820103905090509050810190509050905060e0f35b638da5cb5b811861027357600a5460e052602060e0f35b505b60006000fd5b600080fd5b61001061029003610010600039610010610290036000f35b600080fd"
w3 = Web3(Web3.HTTPProvider('https://cool-frosty-moon.Goerli.quiknode.pro/your-token-here'))

signed_txn = w3.eth.account.sign_transaction(dict(
    nonce=w3.eth.get_transaction_count(acct.address),
    gasPrice=20000000000,
    gas=900000,
    to=None,
    value=0,
    data=bytecode,
    chainId=3, #Goerli
  ),
  private_key,
)

print(signed_txn)

tx = w3.eth.send_raw_transaction(signed_txn.rawTransaction)

print(tx.hex())

在这里,你可以看到我们新添加的字节码,以及使用web3.py发送交易所需的代码。你可以看到我们初始化了w3变量,这使我们能够连接到区块链并发送交易。然后我们使用sign_transaction方法来生成一个交易,然后使用send_raw_transaction方法将交易发送到区块链。

交易的内容大约涉及设置你愿意支付的Gas价格,你正在发送到哪个网络,以及应包括哪些数据。此代码将智能合约(字节码)发送到None,这是我们在以太坊网络上部署智能合约的方式。你还可以通过此交易数据指定要部署到哪个网络。在此交易中,我们使用的链ID为3,这是Goerli网络的ID。这也没有利用EIP-1559为用户提供的一些控制选项。如果你想了解更多关于如何发送EIP-1559交易的信息,可以在这里了解。

保存文件,然后运行我们之前使用过的命令。

python3 deployer.py

这主要是关于交易的信息。我们真正关心的是交易的哈希,以便我们可以在etherscan上查找。你可以在输出的最底部找到哈希。在我的例子中,它是:0x22e17f92dea5f3208e179dbc7125271fb9cb77fc3a9da86903b7d6772640d733。打开Goerli Etherscan并粘贴你的交易哈希。我的交易如图所示。

从那里,你可以在交易的“to”字段中看到你部署的合约。然后,转到合约选项卡。

你需要打开MetaMask,并使用你之前生成的私钥导入一个新账户。

设置好你的MetaMask账户后(确保你在Goerli测试网络上),在“写入合约”选项卡下点击“连接到Web3”按钮。连接你的钱包后,将你的钱包地址粘贴到add_approved_address方法中,然后点击“写入”。你现在应该会看到一个MetaMask弹出窗口,询问你确认。一旦该交易完成,你就可以转到第二个方法。

在这里,你可以粘贴任何字符串。我将发送“gm graffiti frens”。一旦这两个交易都得到了批准,请转到“读取合约”选项卡,检查索引为0的approved_addresses。你应该可以看到你在之前步骤中粘贴的地址。在这里,你可以确认我之前提到的关于0x00地址的说法。如果你查询数字1-4,你会发现返回的地址是0x00。如果你查询高于4的数字,你会得到一个错误,因为我们在智能合约中只分配了5个地址的空间。

你还可以检查graffiti_wall以获取消息。你可以在这里查看我那里的样子。如果你想测试合约的安全性,可以切换到不同的账户,并尝试相同的方法。你将无法将任何人添加到获得批准的地址数组中,因为你不是拥有者,也无法在墙上写字,因为你不在白名单上。

最后需要检查的是,我们的事件是否已正确触发。去Etherscan的“事件”选项卡,你应该会有1个GraffitiSet事件记录在这里。

结论

恭喜你完成了本文的内容!在本教程中,你使用Vyper编写了一个智能合约。在此过程中,你学习了如何创建可外部调用的函数,如何为智能合约增加安全层,以及如何将其部署到测试网络!如果你想了解更多关于智能合约的Python工具,可以查看如何使用brownie进行部署

订阅我们的新闻通讯,获取更多关于以太坊的文章和指南。如果你有任何反馈,请通过Twitter与我们联系。你也可以随时在我们的Discord社区服务器上与我们聊天,那里有一些你见过的最酷的开发者 :)

  • 原文链接: quicknode.com/guides/eth...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。