如何在 OpenZeppelin Contracts ERC-4626 金库中给存取款添加费用并保持 preview 函数合规? 📅 发布时间:2026/9/13 19:07:01 👁 浏览次数: 如何在 OpenZeppelin Contracts ERC-4626 金库中给存取款添加费用并保持 preview 函数合规【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts如果你的 ERC-4626 金库要对 deposit/mint 收取入场费、对 withdraw/redeem 收取出场费直接的困难不在收费逻辑本身而在于 EIP-4626 对四个 preview 函数有明确的 MUST 约束改了存取款路径却不同步改 preview就会出现预览值与实际成交值不一致金库既不合规也会误导集成方。OpenZeppelin Contracts 的官方 ERC-4626 指南erc4626.adoc专门有一节 Custom behavior: Adding fees to the vault给出了合规要求和一个可直接参考的费用金库示例ERC4626Fees.sol本文按这条路径说明怎么实现、怎么配置费率、以及仓库里官方测试是如何核对结果的。先弄清 preview 函数必须满足的合规约束IERC4626 的接口注释对四个 preview 函数都写死了费用语义previewDeposit/previewMintMUST be inclusive of deposit fees返回值必须已包含入金费用previewWithdraw/previewRedeemMUST be inclusive of withdrawal fees返回值必须已包含出金费用。指南正文把这两条约束落到具体调用语义上调用deposit(100, receiver)时调用方必须恰好支付 100 个底层资产含费receiver 得到的 shares 数必须与previewDeposit(100)的返回值一致previewMint必须把用户要在 shares 成本之外额外支付的费用算进去取款方向用户给的数值应对应他实际收到的资产费用要加进previewWithdraw报出的 shares相应地Deposit事件应包含用户支付的资产数含费Withdraw事件应包含用户烧毁的 shares 数含费与用户实际收到的资产数扣费后。这个设计的后果是Deposit和Withdraw事件各自描述了两个汇率Buy-in 与 Exit 价差就是金库收取的费用。官方示例金库ERC4626Fees指南给出的示例是一个继承 ERC4626 的抽象合约源码费率用基点basis point表示_BASIS_POINT_SCALE 1e4。它的关键设计说明写在合约注释里费用以资产assets而非 shares 计——费用按存入/取出的资产额计算而不是按铸造/赎回的 shares 额计算。注释明确说这是一个 opinionated 的设计决定集成时要留意合约标注未经审计not been audited不应视为 production ready使用需谨慎。需要覆盖的函数分三组。四个 preview override把费用算进预览值/// dev Preview taking an entry fee on deposit. See {IERC4626-previewDeposit}. function previewDeposit(uint256 assets) public view virtual override returns (uint256) { uint256 fee _feeOnTotal(assets, _entryFeeBasisPoints()); return super.previewDeposit(assets - fee); } /// dev Preview adding an entry fee on mint. See {IERC4626-previewMint}. function previewMint(uint256 shares) public view virtual override returns (uint256) { uint256 assets super.previewMint(shares); return assets _feeOnRaw(assets, _entryFeeBasisPoints()); } /// dev Preview adding an exit fee on withdrawal. See {IERC4626-previewWithdraw}. function previewWithdraw(uint256 assets) public view virtual override returns (uint256) { uint256 fee _feeOnRaw(assets, _exitFeeBasisPoints()); return super.previewWithdraw(assets fee); } /// dev Preview taking an exit fee on redeem. See {IERC4626-previewRedeem}. function previewRedeem(uint256 shares) public view virtual override returns (uint256) { uint256 assets super.previewRedeem(shares); return assets - _feeOnTotal(assets, _exitFeeBasisPoints()); }两个内部 hook在真正动资产的位置把费用转走。这里覆盖的是内部函数而不是公共函数这与 ERC4626 的注释一致修改存取款行为应覆盖_deposit/_withdraw覆盖公共函数可能导致 deposit 与 mint、withdraw 与 redeem 之间行为不一致。/// dev Send entry fee to {_entryFeeRecipient}. See {ERC4626-_deposit}. function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override { uint256 fee _feeOnTotal(assets, _entryFeeBasisPoints()); address recipient _entryFeeRecipient(); super._deposit(caller, receiver, assets, shares); if (fee 0 recipient ! address(this)) { SafeERC20.safeTransfer(IERC20(asset()), recipient, fee); } } /// dev Send exit fee to {_exitFeeRecipient}. See {ERC4626-_withdraw}. function _withdraw( address caller, address receiver, address owner, uint256 assets, uint256 shares ) internal virtual override { uint256 fee _feeOnRaw(assets, _exitFeeBasisPoints()); address recipient _exitFeeRecipient(); super._withdraw(caller, receiver, owner, assets, shares); if (fee 0 recipient ! address(this)) { SafeERC20.safeTransfer(IERC20(asset()), recipient, fee); } }两个细节决定了 preview 和实际转账能对得上deposit的assets参数本来就含费所以取费用_feeOnTotal——从一个已含费的总额里抽出费用部分mint的目标是得到确定数量的 shares费用要额外加在上面所以用_feeOnRaw——在尚不含费的金额上计算要加收的费用。withdraw / redeem 同理对应_feeOnRaw/_feeOnTotal。源码注释说明了各自的适用操作_feeOnRaw用于 mint / withdraw_feeOnTotal用于 deposit / redeem当recipient address(this)费用接收方就是金库自己时不做 transfer费用留在金库内只有fee 0且接收方不是金库时才转出去。四个配置函数默认值都是不收费按你的产品替换即可下面的注释为源码原样保留function _entryFeeBasisPoints() internal view virtual returns (uint256) { return 0; // replace with e.g. 100 for 1% } function _exitFeeBasisPoints() internal view virtual returns (uint256) { return 0; // replace with e.g. 100 for 1% } function _entryFeeRecipient() internal view virtual returns (address) { return address(0); // replace with e.g. a treasury address } function _exitFeeRecipient() internal view virtual returns (address) { return address(0); // replace with e.g. a treasury address }如果你希望费率在部署时确定而不是写死在代码里仓库里已有现成的做法ERC4646FeesMock.sol 通过构造函数接收四个参数entry/exit 的基点与接收方存成immutable状态并 override 上述四个函数返回这些值。注意这个文件放在 mocks 目录下且文件名与内容不一致文件叫 ERC4646FeesMock合约叫ERC4626FeesMock把它当作部署形态的参考而非直接引用的生产合约。用官方测试核对 preview 与事件是否符合预期仓库自带的测试 ERC4626.test.js 中有专门的ERC4626Fees章节部署ERC4626FeesMock即上面的 mock 形态并按 5% 费率feeBasisPoints 500n验证。测试用的示例数值文档示例const feeBasisPoints 500n; // 5% const valueWithoutFees 10_000n; const fees (valueWithoutFees * feeBasisPoints) / 10_000n; const valueWithFees valueWithoutFees fees;入金侧entry fee 500 基点exit fee 0的断言previewDeposit(valueWithFees)等于valueWithoutFees——付 10,500 资产含 500 费用换 10,000 份 sharespreviewMint(valueWithoutFees)等于valueWithFees——想要 10,000 份 shares要付 10,500 资产交易后资产 token 余额变化用户-valueWithFees金库valueWithoutFeesfee recipientfeesshares 余额recipientvalueWithoutFees事件参数Deposit(holder, recipient, valueWithFees, valueWithoutFees)——事件里资产数是含费的 10,500shares 数是 10,000与指南中 Deposit 事件描述含费资产数 的要求一致。出金侧exit fee 500 基点entry fee 0的断言previewRedeem(valueWithFees)等于valueWithoutFees——烧 10,500 份 shares实际收到 10,000 资产previewWithdraw(valueWithoutFees)等于valueWithFees——想拿 10,000 资产需要烧 10,500 份 shares余额变化金库-valueWithFeesrecipientvalueWithoutFeesfee recipientfees。这组断言就是preview 合规的可核对标准preview 返回值与同一交易实际发生的转账、铸造、烧毁金额以及Deposit/Withdraw事件参数一一对应四个方向deposit、mint、withdraw、redeem全部成立。边界与限制示例合约注释明确写着This contract has not been audited and shouldnt be considered production ready。直接把它当生产合约用之前需要自行审计费用以资产计的语义是示例合约 opinionated 的选择如果你的费用应按 shares 计需要按同样的 preview / 内部 hook 结构自行重写不能只改配置函数_feeOnRaw/_feeOnTotal都使用向上取整Math.Rounding.Ceil即费用永远至少按整数单位向上收方向对用户略不利集成时要意识到这一点费用接收方默认为address(0)且未做校验替换成真实 treasury 地址是接入时的必改项空金库的 inflation attack通胀攻击风险与费用无关_decimalsOffset的默认虚拟偏移机制仍在生效相关分析见指南的 Inflation attack 一节。下一步如果只看了本文还不够完整背景金库汇率模型、通胀攻击的数学推导、费用与事件语义的完整论述都在 ERC-4626 指南实现细节可以对照 ERC4626.sol 中deposit/mint/withdraw/redeem如何调用 preview 与内部函数来读覆盖点与调用链在源码注释里有说明。【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考