跳到主要内容
Web3 前端面试题库
返回题库
中级合约交互代码审查

ERC-4626 的预估份额和实际到账,为什么对不上?

区分金库换算、操作预览、限额与链上滑点保护,并处理资产和份额的精度差异。

题目

存款页用 convertToShares 显示预计份额,确认时直接调用 deposit(assets, receiver)。赎回页用相同换算逻辑显示到账,用户却发现数量有偏差,有额度时也可能回滚。请审查这套流程,包括报价、额度、精度和最低到账保护。

考察目标

  • 区分参考换算与具体操作的预览。
  • 识别预览没有覆盖的余额、授权和限额。
  • 说明前端报价与链上保护的职责。
查看参考答案包含简要回答、深入分析、常见错误与评分标准

30 秒回答

convert 系列提供不含费用的理想换算,preview 系列对应具体存取操作;max 系列另管协议限额。页面按用户输入方向选择函数,再检查余额、授权和精度。preview 只反映调用时状态,不保证提交后的结果;标准 deposit 没有最低份额参数,需要支持该校验的 router 或扩展合约才能在同一交易里约束最低到账。确认后从事件核对实际份额。

深入回答

选择函数与数量单位

份额本身是 ERC-20,份额 decimals 可能与底层资产不同。输入资产数量用 previewDeposit,目标份额用 previewMint;取出指定资产用 previewWithdraw,销毁指定份额用 previewRedeem。四者依次预估获得份额、支付资产、销毁份额、获得资产,包含相应操作费用。预览的保守方向分别是不高估获得量、不低估支付量;convert 两个方向向下取整,不能替代具体操作报价。

报价之外的检查

preview 不替用户检查余额、allowance 或 max 限额;一次预览成功不代表可以提交。协议上限使用 max 系列,余额与授权另外读取。为他人赎回时还需检查 owner、receiver、实际 caller 与 share allowance。将相关读数固定到同一区块,确认时刷新并模拟调用;超时或回滚应保留输入,让用户能重新报价。

滑点与空金库

新金库的 donation/inflation 风险取决于实际实现;OpenZeppelin 的虚拟份额等防护不能推定任意金库都已采用。警惕零份额、小额存款和异常份额价格。刷新报价只能缩短窗口,不能阻止报价后状态变化。标准 deposit 不接收 minShares;前端计算下限后还需要受信任的 router/扩展在交易内部检查。没有这项能力就不能承诺“滑点已保护”。

确认结果

从对应金库、receiver 的 Deposit/Withdraw 事件及状态变化核算,不能把交易回执当作 Solidity 返回值。确认前的模拟结果仍是预估;重组后撤回确认状态并重新对账。净值展示和年化计算是另一条数据链路,不把可操纵的 preview 当成安全价格预言机。

示例代码

以下函数只读取同一区块的存款报价与协议上限,不执行存款,也不提供滑点保护;RPC 和真实金库地址由调用方传入。

import { createPublicClient, http, parseAbi, type Address } from "viem";
import { mainnet } from "viem/chains";

const abi = parseAbi([
  "function previewDeposit(uint256 assets) view returns (uint256)",
  "function maxDeposit(address receiver) view returns (uint256)",
]);

export async function quoteDeposit(
  rpcUrl: string,
  vault: Address,
  receiver: Address,
  assets: bigint,
) {
  if (assets <= 0n) throw new Error("存款数量应大于零");
  const client = createPublicClient({
    chain: mainnet,
    transport: http(rpcUrl),
  });
  const blockNumber = await client.getBlockNumber();
  const [shares, maxAssets] = await Promise.all([
    client.readContract({
      address: vault,
      abi,
      functionName: "previewDeposit",
      args: [assets],
      blockNumber,
    }),
    client.readContract({
      address: vault,
      abi,
      functionName: "maxDeposit",
      args: [receiver],
      blockNumber,
    }),
  ]);
  return { blockNumber, shares, maxAssets, withinLimit: assets <= maxAssets };
}

常见错误

  • 把 convert 的不含费参考值作为成交承诺。
  • 用 previewRedeem 回答铸造相同份额要支付多少资产。
  • 只调用 preview,忽略余额、授权和 max。
  • 把前端 minShares 变量当作标准 deposit 的链上保护。
  • 按底层资产 decimals 格式化份额,或对所有操作都向下舍入。
  • 将模拟返回值直接当作交易实际到账。

面试官追问

  1. previewDeposit 非零、maxDeposit 为零,页面怎样处理?
  2. 已模拟成功,打包时份额仍变少,哪些措施能真正限制损失?
  3. 第三方替 owner 赎回时需要哪一层授权?
  4. 如何识别一次 Deposit 事件属于当前操作,并处理重组?

评分标准

初级回答

  • 区分资产和份额,知道预估不是承诺。
  • 能为存入和赎回选择对应 preview。

中级回答

  • 解释费用、舍入、max、余额与 allowance 的差别。
  • 正确处理两种 decimals、同区块报价与事件对账。

高级回答

  • 说明 inflation 风险及同交易最低份额保护的边界。
  • 设计刷新、模拟、拒绝、回滚与重组后的完整用户流程。

参考资料

发现这道题有问题? 反馈此题