资讯详情

资讯详情

Vyper 众筹合约实战:Goal 达成、退款与资金流转的 Crowdfund 合约逐行解析

编程语言编译器区块链【免费下载链接】vyperPythonic Smart Contract Language for the EVM项目地址https://gitcode.com/gh_mirrors/vy/vyper点击查看免费下载本篇基于 Vyper 官方教程vyper-by-example章节中的 Crowdfund 案例完整讲解一个达标付款给受益人、未达标全额退款的众筹合约设计包括状态变量与public可见性语义、payable参与方法、finalize()结算与refund()提现模式并结合仓库源码印证send()内建函数的底层实现和self.balance内置属性的编译方式以及用仓库中的功能测试复现成功/失败两条资金路径。读完后你可以独立写出并验证一个完整的众筹合约。一、案例背景与业务规则该案例位于官方文档 docs/vyper-by-example/crowdfund.rst在 vyper-by-example 索引 中归入 Escrow Crowdfunding 分类对应的可编译示例源码是 examples/crowdfund.vy。业务规则非常清晰参与者funder在众筹期间调用participate()往合约里打款若众筹截止时合约余额 ≥ 预设目标goal任何人可调用finalize()把全部资金转给受益人beneficiary若众筹截止时余额 goal参与者可调用refund()取回自己的全部贡献。文档开篇即给出警告This is example code for learning purposes. Do not use in production without thorough review and testing.即这是教学代码生产环境使用前必须自行完整审查和测试。二、完整合约源码合约声明#pragma version 0.3.10完整代码见 examples/crowdfund.vyfunders: HashMap[address, uint256] beneficiary: address deadline: public(uint256) goal: public(uint256) timelimit: public(uint256) finalized: bool # Setup global variables deploy def __init__(_beneficiary: address, _goal: uint256, _timelimit: uint256): self.beneficiary _beneficiary self.deadline block.timestamp _timelimit self.timelimit _timelimit assert _goal 0, Goal must be non-zero self.goal _goal # Participate in this crowdfunding campaign external payable def participate(): assert block.timestamp self.deadline, deadline has expired assert not self.finalized self.funders[msg.sender] msg.value # Enough money was raised! Send funds to the beneficiary external def finalize(): assert block.timestamp self.deadline, deadline has not expired yet assert self.balance self.goal, goal has not been reached assert self.balance 0 self.finalized True send(self.beneficiary, self.balance) # Let participants withdraw their fund external def refund(): assert block.timestamp self.deadline and self.balance self.goal assert not self.finalized assert self.funders[msg.sender] 0 value: uint256 self.funders[msg.sender] self.funders[msg.sender] 0 send(msg.sender, value)下面按文档的讲解顺序逐段展开。三、状态变量HashMap 记账与public可见性funders: HashMap[address, uint256] beneficiary: address deadline: public(uint256) goal: public(uint256) timelimit: public(uint256) finalized: bool各变量职责funders以地址为键、以uint256为值的映射记录每个参与者的累计贡献额单位 wei。用映射而不是列表是因为参与者是动态加入的且退款时按地址查账beneficiary众筹成功后资金的最终接收者deadline/timelimit众筹的绝对截止时刻时间戳与持续时长二者共同确定众筹窗口goal众筹目标金额wei达标判断的依据finalized布尔标志防止重复结算。可见性规则文档原话的含义像deadline、goal、timelimit这样声明为public(uint256)的变量编译器会自动为其生成一个同名的外部 getter 函数链下或跨合约都可以直接调用读取Vyper 没有对应的private()装饰器——变量不加public()时默认就是私有的只能合约内部访问。这是与有 public 就有 private的直觉相反的一个设计点官方文档专门用 note 强调了这一点。这也解释了为什么 tests/functional/examples/crowdfund/test_crowdfund_example.py 里测试可以直接写c.deadline()、c.goal()、c.timelimit()这样的调用——它们就是public变量自动生成的 getter。四、构造函数deploy与截止时刻的确定deploy def __init__(_beneficiary: address, _goal: uint256, _timelimit: uint256): self.beneficiary _beneficiary self.deadline block.timestamp _timelimit self.timelimit _timelimit assert _goal 0, Goal must be non-zero self.goal _goal构造函数接收三个参数受益人地址、以 wei 为单位的众筹目标、众筹时长秒。要点使用deploy装饰器标记构造函数deploy def __init__是当前版本的标准写法合约部署时执行一次self.deadline block.timestamp _timelimit是核心设计把相对时长_timelimit在部署瞬间锚定为绝对时间戳之后participate()、finalize()、refund()的准入判断都基于这个确定值而不是每次都重新算相对时间assert _goal 0, Goal must be non-zero在部署阶段就拦截非法参数——目标为零的合约没有任何意义提前失败比链上运行期失败更安全。测试中的部署调用印证了参数含义# tests/functional/examples/crowdfund/test_crowdfund_example.py return get_contract(contract_code, *[env.accounts[1], 50, 60])即受益人为accounts[1]、目标 50wei、时长 60秒。部署后的第一个断言assert c.timelimit() 60 assert c.deadline() - env.timestamp 59 # 部署块已消耗 1 个时间单位确认了deadline 部署时间戳 timelimit的计算逻辑。五、participate()payable打款与映射记账external payable def participate(): assert block.timestamp self.deadline, deadline has expired assert not self.finalized self.funders[msg.sender] msg.valuepayable装饰器允许调用者在调用本方法时附带发送 ETH没有payable的外部函数收到msg.value 0会直接失败msg.sender是发起本次调用的地址msg.value是本次随调用打来的 wei 数额二者都是与block类似的链上内置变量在任何 Vyper 合约中都可用self.funders[msg.sender] msg.value一行完成记账同一地址多次参与会自动累加天然支持一人多次打款两条assert是准入护栏截止时间前才允许参与block.timestamp self.deadline且合约不能被标记为已结算。Vyper 的assert附带字符串消息条件不满足时整笔交易回滚并携带该 revert 消息方便链下定位原因。六、finalize()达标结算与self.balance、send()external def finalize(): assert block.timestamp self.deadline, deadline has not expired yet assert self.balance self.goal, goal has not been reached assert self.balance 0 self.finalized True send(self.beneficiary, self.balance)方法逻辑必须已过截止时间合约余额self.balance必须 ≥goal置位finalized True之后任何人再调用participate()都会因assert not self.finalized失败通过send()把合约内全部余额转给beneficiary——注意是整锅端这也是该简化示例的一个特点见第八节安全讨论。文档在此处特别指出self.balance是一个从未被显式赋值、却始终可访问的内置变量。它在编译器中的来源可以确认语义层address类型的成员属性在 vyper/semantics/types/primitives.py#L414-L423 中定义为AddressT._type_members {balance: UINT(256), codehash: ..., codesize: ..., ...}。self本身就是合约的 address所以self.balance被识别为uint256类型的属性访问代码生成层在 vyper/codegen_venom/expr.py#L792-L794 中attr balance的属性访问被直接降阶为builder.balance(addr)指令最终对应 EVM 的BALANCE操作码。因此self.balance读取的是当前区块里合约地址的实时余额而不是合约自己维护的计数器——参与者每打一笔msg.value它就会自动增长。send()的底层实现send()是 Vyper 内建函数签名与行为见 docs/built-in-functions.rst#L285-L307。其编译实现在 vyper/builtins/functions.py#L1226-L1238class Send(BuiltinFunctionT): _id send _inputs [(to, AddressT()), (value, UINT256_T)] # default gas stipend is 0 _kwargs {gas: KwargSettings(UINT256_T, 0)} process_inputs def build_IR(self, expr, args, kwargs, context): to, value args gas kwargs[gas] ... send_op ensure_eval_once(send_builtin, [call, gas, to, value, 0, 0, 0, 0]) return IRnode.from_list([assert, send_op], error_msgsend failed)两个关键点send底层就是一次CALL且第三参数gas 额度默认是0——即send(to, value)不显式转发 gas。文档同时提醒向非空地址转账非零 ETH 时EVM 会自动附加 2300 gas 的 stipend但如果value 0连这个 stipend 也没有此时若接收方有代码就必然失败。本合约里finalize()与refund()转的都是真实正数余额正常账户都能成功IR 层包了一层assert ... send failed转账失败会让整笔交易 revert。对finalize()而言这是保护转不出钱就不结算对refund()而言则意味着受益人/收款地址是普通 EOA 时才可靠。七、refund()提现模式withdraw patternexternal def refund(): assert block.timestamp self.deadline and self.balance self.goal assert not self.finalized assert self.funders[msg.sender] 0 value: uint256 self.funders[msg.sender] self.funders[msg.sender] 0 send(msg.sender, value)退款条件与finalize()严格互补条件finalize()refund()时间block.timestamp self.deadline同左且二者都要求已过截止余额self.balance self.goalself.balance self.goal状态设置finalized True要求not self.finalized即两个方法不可能同时通过准入——余额要么达标要么不达标finalized标志又阻止了退款窗口被二次利用。这保证众筹只能走一条终局成功付款或失败退款不会既付给受益人又退给参与者。方法体采用经典的先置零、后转账check-effects-interaction 的简化形态也是 withdraw pattern 的常见写法assert self.funders[msg.sender] 0——没贡献的人不能退款测试里a0调用refund即因该 assert 失败把额度读进局部变量value先把存储里的份额清零再send(msg.sender, value)。先清零再转账的意义在于即使接收地址是一个会 reenter重入调用的恶意合约二次调用refund()时第 3 行 assert 也会因份额已为 0 而失败用户不会重复提现。仓库测试完整覆盖了这段逻辑见下节。八、测试验证成功与退款两条路径测试文件 tests/functional/examples/crowdfund/test_crowdfund_example.py 基于get_contractfixture定义见 tests/conftest.py#L290编译部署同一份examples/crowdfund.vy并驱动模拟链时间env.timestamp N。成功路径test_crowdfund_examplec.participate(value5) # a0 先投 5 c.participate(value49) # 再投 49合计 54 goal 50 env.timestamp 100 # 跨过 deadline c.finalize() assert post_bal - pre_bal 54 # 受益人 a1 收到全部 54 wei退款路径test_crowdfund_example2c.participate(value1, sendera3) c.participate(value2, sendera4) c.participate(value3, sendera5) c.participate(value4, sendera6) env.timestamp 100 with tx_failed(): c.refund(sendera0) # a0 无贡献 - assert 失败 c.refund(sendera3) with tx_failed(): c.refund(sendera3) # 份额已清零 - 二次退款失败 c.refund(sendera4) c.refund(sendera5) c.refund(sendera6) assert [y - x for x, y in zip(pre_bals, post_bals)] [1, 2, 3, 4] # 各拿回自己的原额这组断言精确验证了第七节说的三件事无贡献者退款失败、退款只能发生一次、退款金额恰好等于各自映射中记录的累计贡献。九、设计取舍与安全提醒结合文档警告与源码结构对这个教学示例做如下评估示例定位明确文件头注释THIS IS EXAMPLE CODE, NOT MEANT TO BE USED IN PRODUCTION! CAVEAT EMPTOR!与文档 warning 一致以下讨论均基于教学前提无权限控制finalize()与refund()任何地址都可调用。这是刻意为之避免单点信任但也意味着截止后任何人都能触发结算send的 0-gas 调用如第六节源码所示默认不转发 gas。若beneficiary是合约其receive路径需要执行超过 stipend 的逻辑时转账会失败、finalize()整体回滚。生产化时通常改用raw_call或显式gas参数处理assert self.balance 0的兜底在 EVM 语义下若goal 1且上一行balance goal成立则第三行必然成立它是防御性冗余保证不会以 0 值调用send0 值调用不享受 2300 gas stipend接收方为合约时必然失败时间依赖合约完全依赖block.timestamp判定窗口其值由出块方给出、存在一定可控性这是链上时间锁的通用局限教学场景可接受扩展方向该合约没有事件日志event、没有多币种抽象、没有中途停止机制。若要工程化可参考同目录的 docs/vyper-by-example/safe-remote-purchase.rst同属 Escrow 类案例与 docs/vyper-by-example/blind-auction.rst 中更完整的状态机写法。十、小结Crowdfund 示例虽然只有 50 余行却覆盖了 Vyper 的几个高频特性HashMap[address, uint256]按地址记账、public变量自动 getter、deploy def __init__构造函数、payablemsg.sender/msg.value收款、self.balance内置属性、send()内建转账以及达标结算 / 失败退款的二选一终局设计与先置零后转账的退款写法。配合 examples/crowdfund.vy、tests/functional/examples/crowdfund/test_crowdfund_example.py 与 vyper/builtins/functions.py 中的Send实现这套材料足以作为编写链上资金类合约的起点模板。赞分享编程语言编译器区块链【免费下载链接】vyperPythonic Smart Contract Language for the EVM项目地址https://gitcode.com/gh_mirrors/vy/vyper点击查看免费下载相关推荐深入解析Vyper项目中的众筹合约实现深入解析Vyper项目中的众筹合约实现 众筹合约概述 在区块链开发领域众筹合约是一种常见的智能合约应用场景。Vyper作为区块链上的一种合约编程语言以其简洁编程语言编译器区块链Vyper 盲拍合约实战blindedBid 哈希、押金退款与时间窗口状态机blind_auction 示例全解Vyper 盲拍合约实战blindedBid 哈希、押金退款与时间窗口状态机blind_auction 示例全解 本文基于 Vyper 官方示例文档 do编程语言编译器区块链Dapp-Learning 众筹合约实战从 Solidity 合约编写、Hardhat 部署到 OpenZeppelin Crowdsale 类型全解析Dapp Learning 众筹合约实战从 Solidity 合约编写、Hardhat 部署到 OpenZeppelin Crowdsale 类型全解析 本文示例工程区块链上一篇Display Driver Uninstaller (DDU)5步精通显卡驱动深度清理终极秘籍下一篇手柄玩转PC游戏AntiMicroX让你的旧手柄重获新生 创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →