☰
Vyper 实战入门:Simple Open Auction 公开拍卖合约逐行解析
2026/10/13 1:41:14 网站建设 项目流程
  • 编程语言
  • 编译器
  • 区块链

【免费下载链接】vyper

Pythonic Smart Contract Language for the EVM

项目地址:https://gitcode.com/gh_mirrors/vy/vyper
点击查看免费下载

本文以 Vyper 官方文档中的 Simple Open Auction(简单公开拍卖)示例为主线,完整拆解一个可运行的 Vyper 拍卖合约:从public状态变量与自动生成的 getter、@deploy构造函数,到@payable出价、withdrawal 取款模式,再到endAuction的"检查—执行—交互"三阶段收尾流程。读完后,你将掌握用 Vyper 编写带状态管理、ETH 转移与再入防护的合约的基本套路,并能对照仓库中的源码与测试理解每个机制背后的实现。

原文档明确提示:This is example code for learning purposes. Do not use in production without thorough review and testing.(这是学习用途的示例代码,未经充分审查和测试请勿用于生产环境。)

完整合约源码见 examples/auctions/simple_open_auction.vy,本文所有行号均基于该文件。

1. 合约总体结构与业务模型

示例属于 Vyper by Example 系列的开篇。业务模型很直接:参与者在限定的时间窗口内提交出价(bid),拍卖结束后,由部署时指定的受益人(beneficiary)收到最高出价的 ETH。

从代码结构看,整个合约只有 4 个函数:1 个构造函数和 3 个可外部调用的方法(bid、withdraw、endAuction),加上 7 个状态变量。Vyper 语法与 Python 相似,但它是独立语言,有自己的类型系统、装饰器(如@deploy、@external)和关键字。文件首行通过#pragma version >0.3.10声明了所需的编译器版本范围,这一行也说明了当前仓库版本对示例的兼容性前提。

# Open Auction # Auction params # Beneficiary receives money from the highest bidder beneficiary: public(address) auctionStart: public(uint256) auctionEnd: public(uint256) # Current state of auction highestBidder: public(address) highestBid: public(uint256) # Set to true at the end, disallows any change ended: public(bool) # Keep track of refunded bids so we can follow the withdraw pattern pendingReturns: public(HashMap[address, uint256])

7 个状态变量按职责分为两组:

变量类型职责
beneficiaryaddress最高出价款的接收方
auctionStart/auctionEnduint256以块时间戳(秒)表示的出价窗口起止
highestBidder/highestBidaddress/uint256当前最高出价人与出价(wei)
endedbool标记拍卖是否已正式结束,之后禁止任何变更
pendingReturnsHashMap[address, uint256]键值对记录每位被超越者的待退款金额,用于执行 withdrawal 模式

public标注与 getter 自动生成

所有变量都写成public(类型)的形式。原文档解释:public让变量可被外部合约调用,并自动生成一个 getter 函数(如contract.beneficiary());不加public则为私有声明,仅同合约内的方法可访问。

从源码看,getter 的生成发生在语义分析阶段:vyper/semantics/analysis/getters.py 中的generate_public_variable_getters会遍历 AST 中所有is_public的VariableDecl节点,按变量的getter_signature构造一个同名的external+view函数(返回self.<var>,HashMap/DynArray变量还会为每个下标生成带参数arg0、arg1… 的查询函数),并挂到模块命名空间。这解释了为何测试中可以直接调用auction_contract.beneficiary()、auction_contract.pendingReturns(k1)等无参或带下标的只读方法——它们并不是手写方法,而是编译器合成的 getter。

2. 构造函数:@deploy与block.timestamp

@deploy def __init__(_beneficiary: address, _auction_start: uint256, _bidding_time: uint256): self.beneficiary = _beneficiary self.auctionStart = _auction_start # auction start time can be in the past, present or future self.auctionEnd = self.auctionStart + _bidding_time assert block.timestamp < self.auctionEnd # auction end time should be in the future
  • 构造参数为_beneficiary: address、_auction_start: uint256、_bidding_time: uint256,其中_bidding_time是出价窗口时长(秒),而不是结束时间本身。
  • 先存下受益人与开始时间,再由self.auctionStart + _bidding_time计算self.auctionEnd;注意开始时间允许是过去、现在或未来。
  • 末尾的assert block.timestamp < self.auctionEnd强制要求结束时点必须在未来,部署时若传入非法参数,整笔部署交易会 revert。
  • block是 Vyper 内置对象,提供当前块信息(时间戳、编号、hash 等);另一个内置对象msg提供调用方信息,后文的bid()会用到。

3.bid():@payable出价与assert校验

@external @payable def bid(): # Check if bidding period has started. assert block.timestamp >= self.auctionStart # Check if bidding period is over. assert block.timestamp < self.auctionEnd # Check if bid is high enough assert msg.value > self.highestBid # Track the refund for the previous high bidder self.pendingReturns[self.highestBidder] += self.highestBid # Track new high bid self.highestBidder = msg.sender self.highestBid = msg.value

关键点:

  • @payable装饰器允许调用者在方法调用之外随交易附带 ETH。出价者调用bid()时随交易发送的金额即为出价本身(不含 gas 费)。在类型层面,@payable将函数可变性标记为StateMutability.PAYABLE,见 vyper/semantics/types/function.py 的is_payable属性;反向地,向非 payable 函数传value会在语义分析期抛出CallViolation("Cannot send ether to nonpayable function")(同文件约 L784-L787)。
  • msg.sender/msg.value:msg内置变量分别给出调用者地址与随交易发送的 ETH 数量(wei)。
  • 三道assert顺序执行:窗口未开始、窗口已结束、新出价未严格大于当前最高出价,任一不成立即抛出错误并回滚整笔交易。这也意味着 0 wei 出价、以及"追平"当前最高价(equal bid)都会失败——仓库测试 tests/functional/examples/auctions/test_simple_open_auction.py 的test_bid正是用tx_failed()验证了这两种情况(value=0被拒、value=1之后再次value=1被拒),并验证更高出价会覆盖highestBidder/highestBid。
  • 退款记账:通过校验后,先把前最高出价人的金额累加进self.pendingReturns[self.highestBidder](遵循安全的取款模式),再更新highestBidder和highestBid。测试中对应断言:被超越者调用withdraw()前pendingReturns余额增加、取款后归零且账户余额上升。

4.withdraw():withdrawal 取款模式

@external def withdraw(): pending_amount: uint256 = self.pendingReturns[msg.sender] self.pendingReturns[msg.sender] = 0 send(msg.sender, pending_amount)

源码注释(合约第 50-53 行)解释了为什么不在bid()里直接退款:如果在bid()中向被超越者直接转账,一个恶意的出价合约可以拒收/阻塞这笔退款,从而卡死后续所有更高出价。采用 withdrawal 模式后,每个出价者自己拉取(pull)自己的退款:读出待退金额、先清零(防止重入时二次退款)、再发送资金。测试test_bid末尾验证了该闭环:withdraw(sender=k1)后账户余额增加且pendingReturns(k1) == 0。

从编译器实现看,send是一个内置函数:vyper/builtins/functions.py 中的Send类将其生成 IR["assert", ["call", gas, to, value, 0, 0, 0, 0]](默认 gas stipend 为 0),即send()在转账失败时会使整笔交易 revert,而不是静默吞掉失败——这是 Vyper 与传统 EVM 脚本中"send 不检查返回值"写法的区别。

5.endAuction():三阶段模式收尾

@external def endAuction(): # 1. Conditions assert block.timestamp >= self.auctionEnd assert not self.ended # 2. Effects self.ended = True # 3. Interaction send(self.beneficiary, self.highestBid)

合约内的大段注释(第 60-75 行)给出了一条重要工程准则:与外部合约交互(调用函数或发送 ETH)的函数应拆成三个阶段——

  1. Conditions(检查):断言当前时间已过auctionEnd;断言self.ended尚未置位。后者防止重复调用——若拍卖已结束,任何再次调用都可能被恶意利用,必须显式拦截。
  2. Effects(执行):将self.ended置为True,正式关闭拍卖。
  3. Interaction(交互):send(self.beneficiary, self.highestBid)把最高出价转给受益人。

注释进一步说明:三个阶段若混杂,外部合约可能回调(re-entrancy)进入本合约修改状态或重复触发副作用(例如重复打款);内部函数若包含外部交互,也应视为交互阶段的一部分。本示例中send恰好是最后一个语句,天然满足该模式。

对应测试test_end_auction验证了完整生命周期:未到期时endAuction()失败 → 时间推进EXPIRY秒后调用成功且受益人余额精确增加最高出价 → 结束后再bid()失败、再次endAuction()也失败(不能重复结束)。

6. 编译与运行验证

示例文件可直接用 Vyper 编译器编译(命令行入口见 vyper/cli/vyper_compile.py 对应的vyper compile命令,需先通过pip install vyper安装编译器;#pragma version >0.3.10决定当前安装版本是否满足该示例的编译前提):

vyper compile examples/auctions/simple_open_auction.vy --show-source-map --show-method-ids

行为层面的正确性由功能测试套件覆盖:tests/functional/examples/auctions/test_simple_open_auction.py 使用仓库内置的模拟链环境(tests/evm_backends 提供 pyevm 等后端),三个测试函数分别覆盖:

  • test_initial_state:部署后受益人、起止时间(auctionEnd == auctionStart + EXPIRY)、ended is False、最高出价为 0 等初始状态;
  • test_bid:0 出价/等额出价被拒、多次抬价、pendingReturns累计与withdraw()取款;
  • test_end_auction:到期前不可结束、到期后受益人收款、结束后不可再出价且不可重复结束。

7. 小结与扩展方向

这个示例用不到百行代码展示了 Vyper 合约的核心骨架:public状态变量 + 自动 getter、@deploy构造器、@external @payable收币入口、msg.sender/msg.value/block.timestamp内置变量、assert校验回滚、HashMap状态记账,以及 withdrawal 模式与"检查—执行—交互"三阶段两大安全模式。原文档也指出,这是一个功能最简(barebones)的实现,可以在此基础上继续改进。

若希望在此基础上深入学习,仓库提供了直接的同系列进阶案例:docs/vyper-by-example/blind-auction.rst(盲拍,涉及keccak256哈希密封与HashMap多键存储)以及 examples/auctions/blind_auction.vy 的完整实现;其余示例(代币、众筹、治理投票、DeFi 等)的入口在 docs/vyper-by-example/index.rst。

  • 编程语言
  • 编译器
  • 区块链

【免费下载链接】vyper

Pythonic Smart Contract Language for the EVM

项目地址:https://gitcode.com/gh_mirrors/vy/vyper
点击查看免费下载

相关推荐

上一篇:Laravel-Lang 项目使用教程
下一篇:React-Component/Form 开源项目教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询