ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WTF Solidity 极简入门:第 42 讲 分账合约 PaymentSplit 实战指南

WTF Solidity 极简入门:第 42 讲 分账合约 PaymentSplit 实战指南 WTF Solidity 极简入门第 42 讲 分账合约 PaymentSplit 实战指南【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity分账Payment Split是把收到的款项按事先约定的权重分配给一组受益人的典型链上业务场景。本讲基于 WTF-Solidity 仓库中的 42_PaymentSplit/PaymentSplit.sol 讲解一个由 OpenZeppelinPaymentSplitter合约简化而来的PaymentSplit分账合约读完你将掌握如何在合约部署时固化受益人名单与份额、合约如何按份额自动计算每位受益人应得的ETH以及如何通过release()函数以Pull Payment模式安全地领取款项。一、什么是分账分账就是按照一定比例分配钱财。在现实世界中合作项目、多人共创的收入分配经常出现分赃不均的纠纷而在区块链世界里Code is Law我们可以把每个人应分的比例提前写死在智能合约中合约收到收入后自动按既定比例进行分账从机制上杜绝事后扯皮。分账合约的用途非常广泛多人联合创作的版权收入分成、团队/DAO 国库资金分配、项目方与多个合作方的销售分成等都可以用同一套份额 按比例提取的模型来实现。二、分账合约PaymentSplit的设计特点PaymentSplit合约源码见 42_PaymentSplit/PaymentSplit.sol具有以下 4 个核心特点部署时固化分账规则在创建合约时定好分账受益人payees和每人的份额shares初始化之后不可修改。份额比例任意份额可以是相等的也可以是任意其他比例如1:3totalShares是所有份额之和。按份额比例可提取在该合约收到的所有ETH中每个受益人能够提取与其所分配份额成比例的金额。遵循Pull Payment模式付款不会自动打入受益人账户而是先保存在分账合约中受益人通过调用release()函数触发实际转账。这种先入池、后提取的模式与 41_WETH/WETH.sol 中 WETH 等资金的托管思路类似把入账和出账分离便于审计与重入防护。合约整体骨架如下// SPDX-License-Identifier: MIT pragma solidity ^0.8.21; /** * 分账合约 * dev 这个合约会把收到的ETH按事先定好的份额分给几个账户。收到ETH会存在分账合约中需要每个受益人调用release()函数来领取。 */ contract PaymentSplit{ // ... }仓库中的实际源码 42_PaymentSplit/PaymentSplit.sol 使用pragma solidity ^0.8.34;而 Languages/en/42_PaymentSplit_en/PaymentSplit.sol 为英文版同名实现逻辑完全一致读者可按自身编译环境选择版本。三、事件Events分账合约中共有 3 个事件用于链上记录分账全生命周期的关键动作事件语义参数PayeeAdded增加受益人事件address account受益人地址uint256 shares对应份额PaymentReleased受益人提款事件address to收款地址uint256 amount提取金额PaymentReceived合约收款事件address from付款方uint256 amount收款金额// 事件 event PayeeAdded(address account, uint256 shares); // 增加受益人事件 event PaymentReleased(address to, uint256 amount); // 受益人提款事件 event PaymentReceived(address from, uint256 amount); // 合约收款事件这三个事件分别对应设立分账规则、执行分账、合约进账三个阶段前端与索引器如 subgraph、区块浏览器可依赖这些事件追踪每一笔资金的流向。四、状态变量State Variables分账合约中共有 5 个状态变量用于记录受益地址、份额、以及已经支付出去的ETH变量类型说明totalSharesuint256总份额为所有shares的和totalReleaseduint256从分账合约向受益人支付出去的ETH总量为所有released的和payeesaddress[]受益人地址数组sharesmapping(address uint256)记录每个受益人的份额releasedmapping(address uint256)记录分账合约支付给每个受益人的累计金额uint256 public totalShares; // 总份额 uint256 public totalReleased; // 总支付 mapping(address uint256) public shares; // 每个受益人的份额 mapping(address uint256) public released; // 支付给每个受益人的金额 address[] public payees; // 受益人数组注意payees是公开数组Solc 会自动为其生成按索引取值的 gettershares、released是公开映射自动生成以地址为参数的 getter。totalShares与totalReleased都是public任何账户都可以直接链上查询分账进度。五、函数详解Functions分账合约中共有 6 个函数承担从初始化受益人到受益人领取款项的完整闭环。1. 构造函数constructor构造函数初始化受益人数组_payees和分账份额数组_shares并带有一系列严谨的校验两个数组长度必须相等payees and shares length mismatch数组长度不能为 0no payees_shares中的每个元素必须大于 0_payees中的地址不能是 0 地址、且不能有重复地址这些校验在_addPayee()中完成。constructor(address[] memory _payees, uint256[] memory _shares) payable { // 检查_payees和_shares数组长度相同且不为0 require(_payees.length _shares.length, PaymentSplitter: payees and shares length mismatch); require(_payees.length 0, PaymentSplitter: no payees); // 调用_addPayee更新受益人地址payees、受益人份额shares和总份额totalShares for (uint256 i 0; i _payees.length; i) { _addPayee(_payees[i], _shares[i]); } }构造函数被标记为payable意味着部署合约的同时可以一次性打入首笔待分账的资金例如后续 Remix 演示中部署时即转入1 ETH。2. receive() 回调函数receive()是 Solidity 的收款回调分账合约收到普通ETH转账时自动执行并释放PaymentReceived事件记录付款方与金额。receive() external payable virtual { emit PaymentReceived(msg.sender, msg.value); }函数被声明为virtual便于子合约在继承时覆写例如加白名单、加限额等自定义逻辑。3. release() 分账函数release()为有效受益人地址_account分配相应的ETH。任何人都可以触发这个函数但ETH只会转给受益人地址_account——这大大降低了单点依赖即使受益人本人不上链第三方如服务商也可以代劳触发。function release(address payable _account) public virtual { // account必须是有效受益人 require(shares[_account] 0, PaymentSplitter: account has no shares); // 计算account应得的eth uint256 payment releasable(_account); // 应得的eth不能为0 require(payment ! 0, PaymentSplitter: account is not due payment); // 更新总支付totalReleased和支付给每个受益人的金额released totalReleased payment; released[_account] payment; // 转账 _account.transfer(payment); emit PaymentReleased(_account, payment); }执行流程为先校验_account是有效受益人份额大于 0→ 调用releasable()计算本次应得金额 → 校验金额不为 0 → 更新totalReleased与released先记账后转账防止重入→ 使用transfer()转账 → 释放PaymentReleased事件。4. releasable() 计算可领取金额releasable()计算一个受益人地址当前可领取的ETHfunction releasable(address _account) public view returns (uint256) { // 计算分账合约总收入totalReceived uint256 totalReceived address(this).balance totalReleased; // 调用_pendingPayment计算account应得的ETH return pendingPayment(_account, totalReceived, released[_account]); }关键技巧在于总收入的计算totalReceived address(this).balance totalReleased。因为已经发放出去的totalReleased不再在合约余额里要把它们加回来才能还原分账合约的累计总收入从而保证新转入的每一笔ETH都能按份额重新摊分。5. pendingPayment() 计算应分金额pendingPayment()是分账的核心数学函数根据受益人地址_account、分账合约总收入_totalReceived和该地址已领取的金额_alreadyReleased计算该受益人当前应分的ETHfunction pendingPayment( address _account, uint256 _totalReceived, uint256 _alreadyReleased ) public view returns (uint256) { // account应得的ETH 总应得ETH - 已领到的ETH return (_totalReceived * shares[_account]) / totalShares - _alreadyReleased; }其公式为应分金额 (累计总收入 × 该受益人份额) / 总份额 − 该受益人已领取金额其中(_totalReceived * shares[_account]) / totalShares是该受益人按份额应得的累计总金额减去已领取的_alreadyReleased即为本次可提取的增量。注意这里使用整数除法向下取整因此少量 dust余数会保留在合约中这正是分账合约的常见行为。6. _addPayee() 新增受益人_addPayee()是新增受益人及其份额的私有函数只在构造函数初始化时被调用之后不可修改从机制上保证分账规则不可篡改function _addPayee(address _account, uint256 _accountShares) private { // 检查_account不为0地址 require(_account ! address(0), PaymentSplitter: account is the zero address); // 检查_accountShares不为0 require(_accountShares 0, PaymentSplitter: shares are 0); // 检查_account不重复 require(shares[_account] 0, PaymentSplitter: account already has shares); // 更新payeesshares和totalShares payees.push(_account); shares[_account] _accountShares; totalShares _accountShares; // 释放增加受益人事件 emit PayeeAdded(_account, _accountShares); }三重校验非 0 地址、份额大于 0、地址不重复确保初始化数据的合法性写入顺序为先更新存储、后释放事件。六、Remix 演示部署、入账与领款1. 部署 PaymentSplit 合约并转入 1 ETH在 Remix 中编译部署PaymentSplit构造函数输入两个受益人地址份额分别为1和3即分成比例为 1:3totalShares 4同时在Value处填入1 ETH随部署一并转入合约2. 查看受益人地址、份额、应分到的 ETH部署完成后依次查看状态变量payees(0)/payees(1)查看两个受益人地址shares(受益人A)1shares(受益人B)3totalShares4调用releasable(受益人A)、releasable(受益人B)可查看各自当前应分的ETH合约内共1 ETHA 应得0.25 ETHB 应得0.75 ETH。3. 调用 release 函数领取 ETH调用release(受益人地址)触发实际分账合约按份额计算应得金额并transfer到受益人账户。注意该函数不要求必须是受益人本人调用任何地址都可以代为触发但钱只会打给参数中指定的受益人地址。4. 查看总支出、受益人余额与应分金额的变化领取后再次查询totalReleased增加为已发放总额released(受益人)记录该受益人累计已领金额再次调用releasable()时由于已领取金额被扣除返回值归零或相应减少受益人账户余额增加相应ETH。七、安全性与设计要点总结最后梳理一下PaymentSplit在安全与工程实践上的几个关键设计这也是面试与审计中常见的考察点Pull Payment 模式资金先沉淀在合约中受益人主动release()领取避免向不可信地址主动 push 转账带来的重入与 gas 风险。先记账后转账release()中先更新totalReleased/released再执行transfer()天然防止重入攻击transfer()仅发送 2300 gas进一步限制恶意接收者的可执行逻辑。份额固化受益人与份额只在构造函数中通过_addPayee()设定private可见性 无更新函数保证规则一经部署即不可变体现Code is Law。累计摊分设计releasable()用合约当前余额 已发放总额还原累计总收入使后期新转入的ETH也能按原份额持续、公平地摊分给所有受益人。无权限触发release()无onlyOwner限制任何人均可代为触发避免受益人离线导致资金永久锁定。通过本讲的分账合约我们可以看到区块链上的分账本质是把信任从人际协商转移到可验证的代码规则上——事先定好份额收入入池各受益人按比例自行领取从根源上规避分赃不均的争议。若需深入可对照英文版教程 Languages/en/42_PaymentSplit_en/readme.md 与完整源码 42_PaymentSplit/PaymentSplit.sol 继续学习。【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表