本文深入探讨了 EulerSwap 中金库提款的工作原理,从 EVC 身份验证到执行后状态检查。详细解释了在 EulerSwap 中提款的整个流程,包括通过 Euler Vault Connector (EVC) 进行身份验证,Euler Vault Kit (EVK) 执行验证后的操作,以及如何确保提款操作后系统的状态保持有效和安全。
深入了解 EulerSwap 中 vault 提款的工作原理,从 EVC 身份验证到执行后状态检查。 清晰、循序渐进的技术分解。
作为我在 Cyfrin EulerSwap 审计 之前的研究的一部分,我探索了 Ethereum Vault Connector(EVC)(一种连接借贷 vault 的合约)与 Euler Vault Kit(EVK)(Euler 借贷市场 vault)之间交互的复杂性。
在非常高的层面上,EVC 是负责身份验证、授权和执行检查的控制器,而 EVK 执行经过验证的操作。 这种职责分离允许 Euler Vault 将验证职责委派给 EVC。 这确保了所有受影响的系统状态在执行结束时都是正确的。
由于 EulerSwap
中的 vault 提款会同时通过 EVC 和 EVK,因此了解流程的工作原理至关重要:
调用 EVC 的行是 FundsLib::withdrawAssets#L151
:
IEVC(evc).call(vault, p.eulerAccount, 0, abi.encodeCall(IERC4626.withdraw, (avail, to, p.eulerAccount)));
我们进入:
执行从 EthereumVaultConnector::call
开始:
/// @inheritdoc IEVC
function call(
address targetContract,
address onBehalfOfAccount,
uint256 value,
bytes calldata data
) public payable virtual nonReentrantChecksAndControlCollateral returns (bytes memory result) {
EC contextCache = executionContext;
executionContext = contextCache.setChecksDeferred();
bool success;
(success, result) = callWithAuthenticationInternal(targetContract, onBehalfOfAccount, value, data);
if (!success) revertBytes(result);
restoreExecutionContext(contextCache);
}
contextCache.setChecksDeferred()
意味着账户检查(如账户健康检查和供应/借款上限)将推迟到执行 restoreExecutionContext()
时执行。 这会将验证推迟到执行之后,只要最终状态通过所有检查,允许操作自由进行。
contextCache
状态也在此处缓存,允许 EVC 在所有检查完成后 revert 到其调用前状态。
执行进入 EthereumVaultConnector::callWithAuthenticationInternal
:
/// @notice 调用目标合约的内部函数,具有必要的身份验证。
/// @dev 此函数决定是使用 delegatecall 还是常规调用,具体取决于目标合约。
/// 如果目标合约是此合约,它使用 delegatecall 来保留 msg.sender 以进行身份验证。
/// 否则,它会在需要时对调用者进行身份验证,然后继续进行常规调用。
/// @param targetContract 要调用的合约地址。
/// @param onBehalfOfAccount 代表其进行调用的账户地址。
/// @param value 与调用一起发送的 value 金额。
/// @param data 与调用一起发送的 calldata。
/// @return success 一个布尔值,指示调用是否成功。
/// @return result 从调用返回的字节。
function callWithAuthenticationInternal(
address targetContract,
address onBehalfOfAccount,
uint256 value,
bytes calldata data
) internal virtual returns (bool success, bytes memory result) {
if (targetContract == address(this)) {
if (onBehalfOfAccount != address(0)) {
revert EVC_InvalidAddress();
}
if (value != 0) {
revert EVC_InvalidValue();
}
// delegatecall 在此处用于保留 msg.sender,以便能够执行身份验证
(success, result) = address(this).delegatecall(data);
} else {
// 当目标合约等于 call() 和 batch() 中的 msg.sender 时,不需要身份验证
if (targetContract != msg.sender) {
authenticateCaller({account: onBehalfOfAccount, allowOperator: true, checkLockdownMode: true});
}
(success, result) = callWithContextInternal(targetContract, onBehalfOfAccount, value, data);
}
}
传入的参数是:
function callWithAuthenticationInternal(
address targetContract, // vault
address onBehalfOfAccount, // p.eulerAccount
uint256 value, // 0
bytes calldata data // abi.encodeCall(IERC4626.withdraw, (avail, to, p.eulerAccount))
)
由于 targetContract != address(this)
,即 EVC 合约,执行进入 else
流程。 由于 targetContract
(vault) 不等于 msg.sender
(EulerSwap
),因此调用进入 EthereumVaultConnector::authenticateCaller
,其中 checkLockDownMode == true
。Lockdown mode (封锁模式)是账户所有者可以在紧急情况下用来禁用其账户的功能:
/// @notice 验证函数的调用者。
/// @dev 此函数检查调用者是否是账户的所有者或授权的操作员,以及账户是否处于锁定模式。
/// @param account 要验证调用者的账户地址。
/// @param allowOperator 一个布尔值,指示是否允许操作员以调用者的身份进行验证。
/// @param checkLockdownMode 一个布尔值,指示函数是否应检查账户的锁定模式。
/// @return 经过身份验证的调用者的地址。
function authenticateCaller(
address account,
bool allowOperator,
bool checkLockdownMode
) internal virtual returns (address) {
bytes19 addressPrefix = getAddressPrefixInternal(account);
address owner = ownerLookup[addressPrefix].owner;
bool lockdownMode = ownerLookup[addressPrefix].isLockdownMode;
address msgSender = _msgSender();
bool authenticated = false;
// 检查调用者是否是账户的所有者
if (haveCommonOwnerInternal(account, msgSender)) {
// 如果所有者尚未注册,则进行注册
if (owner == address(0)) {
ownerLookup[addressPrefix].owner = owner = msgSender;
emit OwnerRegistered(addressPrefix, msgSender);
authenticated = true;
} else if (owner == msgSender) {
authenticated = true;
}
}
// 如果调用者不是所有者,则检查它是否是操作员(如果允许操作员)
if (!authenticated && allowOperator && isAccountOperatorAuthorizedInternal(account, msgSender)) {
authenticated = true;
}
// 如果经过身份验证的账户是非所有者,则阻止其账户成为智能合约
if (authenticated && owner != account && account.code.length != 0) {
authenticated = false;
}
// 如果所有者或操作员均未通过身份验证,则必须还原
if (!authenticated) {
revert EVC_NotAuthorized();
}
// 如果账户处于锁定模式,则还原,除非未检查锁定模式
if (checkLockdownMode && lockdownMode) {
revert EVC_LockdownMode();
}
return msgSender;
}
让我们逐部分地分解一下:
bytes19 addressPrefix = getAddressPrefixInternal(account); // `p.eulerAccount` 的前 19 个字节
address owner = ownerLookup[addressPrefix].owner; // euler 账户的所有者
bool lockdownMode = ownerLookup[addressPrefix].isLockdownMode; // 大概是 false
address msgSender = _msgSender(); // EulerSwap
bool authenticated = false;
条件 if (haveCommonOwnerInternal(account, msgSender))
未执行,因为 msgSender
是 EulerSwap
合约,并且它与账户 (p.eulerAccount)
没有共同所有者。 这是因为 EulerSwap
和 p.eulerAccount
在结构上不相关,并且不能是 subaccounts(子账户)。正是 EVC 抽象允许账户所有者创建单独的(子)账户。
子账户将与所有者 address 具有相同的前 19 个字节。 因此,只能有 256 个此类账户。 由于 EulerSwap
是一个单独部署的账户,因此它极不可能与所有者地址共享前 19 个字节,因此不能是子账户。
下一个:
// 如果调用者不是所有者,则检查它是否是操作员(如果允许操作员)
if (!authenticated && allowOperator && isAccountOperatorAuthorizedInternal(account, msgSender)) {
authenticated = true;
}
此时,authenticated
为 false
,并且传递了 allowOperator: true
,因此执行进入 EthereumVaultConnector::isAccountOperatorAuthorizedInternal
:
/// @notice 检查是否为特定账户授权了操作员。
/// @dev 通过检查是否在账户地址前缀的操作员位字段中设置了操作员的位来确定操作员授权。 如果所有者尚未注册 (address(0)),则意味着操作员无法获得授权,因此返回 false。 bitMask 是通过将 1 左移所有者地址和账户地址的 XOR 来计算的,从而有效地检查操作员是否对特定账户具有授权。
/// @param account 要检查操作员授权的账户地址。
/// @param operator 要检查授权状态的操作员地址。
/// @return isAuthorized 如果为账户授权了操作员,则为 True,否则为 False。
function isAccountOperatorAuthorizedInternal(
address account,
address operator
) internal view returns (bool isAuthorized) {
bytes19 addressPrefix = getAddressPrefixInternal(account);
address owner = ownerLookup[addressPrefix].owner;
// 如果所有者尚未注册,则意味着操作员无法获得授权
if (owner == address(0)) return false;
// bitMask 定义了操作员已授权的账户。 bitMask 是从账户号创建的,该账户号是一个二进制数字,最大为 2^8 或 256。 1 << (uint160(owner) ^ uint160(account)) 将该数字转换为一个 256 位位置的二进制数组,如 0...010...0,在 uint256 中以位置方式对账户进行标记。
uint256 bitMask = 1 << (uint160(owner) ^ uint160(account));
return operatorLookup[addressPrefix][operator] & bitMask != 0;
}
此检查返回 true
,因为 EulerSwap
的一项要求是所有者将其 EulerSwap
实例设置为授权 operator(操作员),并且在 verified EulerSwap
实例的部署期间也会进行验证。
你可以参考 EthereumVaultConnector::setAccountOperator
,它本质上是将相应的授权位翻转为 1。
因此,authenticated
变为 true
。
下一个 if
有点棘手:
// 如果经过身份验证的账户是非所有者,则阻止其账户成为智能合约
if (authenticated && owner != account && account.code.length != 0) {
authenticated = false;
}
authenticated
的值从上面可知为 true
,为了简单起见,让我们假设 owner
与 account
相同。 正如注释中所述,如果 p.eulerAccount
不是 所有者,则它不能是 smart contract(Euler 团队强制执行的一项规则)。
最终检查非常简单:如果身份验证失败或账户处于 lockDownMode
,则还原:
// 如果所有者或操作员均未通过身份验证,则必须还原
if (!authenticated) {
revert EVC_NotAuthorized();
}
// 如果账户处于锁定模式,则还原,除非未检查锁定模式
if (checkLockdownMode && lockdownMode) {
revert EVC_LockdownMode();
}
return msgSender;
身份验证现已完成,并且调用通过 EthereumVaultConnector::callWithContextInternal
继续到 vault:
/// @notice 使用特定上下文调用目标合约的内部函数。
/// @dev 此函数在调用期间设置执行上下文。
/// @param targetContract 要调用的合约地址。
/// @param onBehalfOfAccount 代表其进行调用的账户地址。
/// @param value 与调用一起发送的 value 金额。
/// @param data 与调用一起发送的 calldata。
function callWithContextInternal(
address targetContract,
address onBehalfOfAccount,
uint256 value,
bytes calldata data
) internal virtual returns (bool success, bytes memory result) {
if (value == type(uint256).max) {
value = address(this).balance;
} else if (value > address(this).balance) {
revert EVC_InvalidValue();
}
EC contextCache = executionContext;
address msgSender = _msgSender();
// 在外部调用期间,在执行上下文中设置 onBehalfOfAccount。
// 考虑到 operatorAuthenticated 仅供外部
// 合约观察,因此在此处设置它而不是在身份验证函数中设置它就足够了。
// 除了通常的情况(当所有者代表其账户进行操作时),
// operatorAuthenticated 应该在即将执行许可自调用时清除,当
// 的目标合约等于 call() 和 batch() 中的 msg.sender,或者当 controlCollateral 正在
// 进行中(在这种情况下,operatorAuthenticated 不相关)
if (
haveCommonOwnerInternal(onBehalfOfAccount, msgSender) || targetContract == msg.sender
|| targetContract == address(this) || contextCache.isControlCollateralInProgress()
) {
executionContext = contextCache.setOnBehalfOfAccount(onBehalfOfAccount).clearOperatorAuthenticated();
} else {
executionContext = contextCache.setOnBehalfOfAccount(onBehalfOfAccount).setOperatorAuthenticated();
}
emit CallWithContext(
msgSender, getAddressPrefixInternal(onBehalfOfAccount), onBehalfOfAccount, targetContract, bytes4(data)
);
(success, result) = targetContract.call{value: value}(data);
executionContext = contextCache;
}
没有传递本地值,因此跳过第一个 if。
下一部分:
// 在外部调用期间,在执行上下文中设置 onBehalfOfAccount。
// 考虑到 operatorAuthenticated 仅供外部
// 合约观察,因此在此处设置它而不是在身份验证函数中设置它就足够了。
// 除了通常的情况(当所有者代表其账户进行操作时),
// operatorAuthenticated 应该在即将执行许可自调用时清除,当
// 的目标合约等于 call() 和 batch() 中的 msg.sender,或者当 controlCollateral 正在
// 进行中(在这种情况下,operatorAuthenticated 不相关)
if (
haveCommonOwnerInternal(onBehalfOfAccount, msgSender) || targetContract == msg.sender
|| targetContract == address(this) || contextCache.isControlCollateralInProgress()
) {
executionContext = contextCache.setOnBehalfOfAccount(onBehalfOfAccount).clearOperatorAuthenticated();
} else {
executionContext = contextCache.setOnBehalfOfAccount(onBehalfOfAccount).setOperatorAuthenticated();
}
整个 if
语句解析为 false
:
haveCommonOwnerInternal(onBehalfOfAccount, msgSender): onBehalfOfAccount
(p. eulerAccount
)和 msg.sender
(EulerSwap
)没有共同所有者
targetContract == msg.sender: targetContract
(vault) 不是 msg.sender
(EulerSwap
)
targetContract == address(this): targetContract
(vault) 不是 address(this)
(EVC 合约)
contextCache.isControlCollateralInProgress()
:没有控制抵押品(在清算头寸时使用)处于活动状态
因此,executionContext
更新为包括 OnBehalfOfAccount
并将操作员标记为已通过身份验证:
executionContext = contextCache.setOnBehalfOfAccount(onBehalfOfAccount).setOperatorAuthenticated();
接下来,执行实际调用:
(success, result) = targetContract.call{value: value}(data);
在这种情况下,它是:
vault.withdraw(avail, to, p.eulerAccount);
现在,对实际 vault
的调用发生在 EVault::withdraw
中:
function withdraw(uint256 amount, address receiver, address owner) public virtual override callThroughEVC use(MODULE_VAULT) returns (uint256) {}
这是一个有趣的模式:EVault::withdraw
是一个空包装器 function(函数)。 实际逻辑包含在修饰符中:callThroughEVC
和 use(MODULE_VAULT)
。
首先,调用修饰符 Dispatch::callThroughEVC
:
// 修饰符确保函数的正文始终从 EVC 调用执行。
// 这是通过拦截直接传入 vault 的调用并将它们传递
// 给 EVC.call 函数来实现的。 EVC 使用原始 calldata 重新调用 vault。 因此,账户
// 和 vault 状态检查始终在检查延迟帧中执行,在调用的最后,
// 在 vault 的重入保护之外。
// 修饰符应用于所有计划账户或 vault 状态检查的函数。
modifier callThroughEVC() {
if (msg.sender == address(evc)) {
_;
} else {
callThroughEVCInternal();
}
}
在这种情况下,msg.sender
是 address(evc)
,因此修饰符允许立即继续执行。 这确保了对 Euler vault 的调用始终通过 EVC 进行,因为 vault 已将其许多会计检查委托给 EVC。
接下来是 Dispatch::use
,其中执行实际的提款逻辑:
// 修饰符将函数调用代理到一个模块并低级别返回结果
modifier use(address module) {
_; // 当使用修饰符时,假定函数体为空。
delegateToModule(module);
}
它首先执行上面的空函数体(即 EVault::withdraw
),然后进入 Dispatch::delegateToModule
:
function delegateToModule(address module) private {
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), module, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
此处使用的模块是 use(MODULE_VAULT)
,这意味着 delegatecall
将执行路由到 Vault::withdraw
:
/// @inheritdoc IERC4626
function withdraw(uint256 amount, address receiver, address owner) public virtual nonReentrant returns (uint256) {
(VaultCache memory vaultCache, address account) = initOperation(OP_WITHDRAW, owner);
Assets assets = amount.toAssets();
if (assets.isZero()) return 0;
Shares shares = assets.toSharesUp(vaultCache);
finalizeWithdraw(vaultCache, assets, shares, account, receiver, owner);
return shares.toUint();
}
initOperation(OP_WITHDRAW, owner)
将通过 Base::initOperation
完成验证和设置:
// 生成 vault 快照并存储它。
// 在 EVC 中将 vault 和可能的账户检查排队(调用者、当前账户、onBehalfOfAccount 或无)。
// 如果需要,如果此合约不是经过身份验证的账户的控制器,则还原。
// 返回 VaultCache 和活动账户。
function initOperation(uint32 operation, address accountToCheck)
internal
virtual
returns (VaultCache memory vaultCache, address account)
{
vaultCache = updateVault();
account = EVCAuthenticateDeferred(CONTROLLER_NEUTRAL_OPS & operation == 0);
callHook(vaultCache.hookedOps, operation, account);
EVCRequireStatusChecks(accountToCheck == CHECKACCOUNT_CALLER ? account : accountToCheck);
// 快照仅用于验证在检查供应上限时供应是否增加,以及在检查借款上限时借款是否增加。 当上限变量
// 减少(变得更安全)时,不会检查上限。 因此,如果两个上限均已禁用,则禁用快照。
// 快照在 vault 状态检查期间清除,因此不能忽略 vault 状态检查。
if (
!vaultCache.snapshotInitialized
&& !(vaultCache.supplyCap == type(uint256).max && vaultCache.borrowCap == type(uint256).max)
) {
vaultStorage.snapshotInitialized = vaultCache.snapshotInitialized = true;
snapshot.set(vaultCache.cash, vaultCache.totalBorrows.toAssetsUp());
}
}
Cache::updateVault
处理利息累积、费用以及总供应量和借款的跟踪。
callHook
允许运行特定于 vault 的逻辑。 为了简洁起见,让我们假设 vault 没有Hook。
然后执行继续:
account = EVCAuthenticateDeferred(CONTROLLER_NEUTRAL_OPS & operation == 0);
vault 将验证是否必须为此操作启用 controller。 控制器是负责验证头寸是否健康的 vault。 由于我们只是提款,因此不需要它。 CONTROLLER_NEUTRAL_OPS
定义为:
uint32 constant CONTROLLER_NEUTRAL_OPS = OP_DEPOSIT | OP_MINT | OP_WITHDRAW | OP_REDEEM | OP_TRANSFER | OP_SKIM
| OP_REPAY | OP_REPAY_WITH_SHARES | OP_CONVERT_FEES | OP_FLASHLOAN | OP_TOUCH | OP_VAULT_STATUS_CHECK;
由于 OP_WITHDRAW
是 CONTROLLER_NEUTRAL_OPS
的一部分,因此语句 CONTROLLER_NEUTRAL_OPS & operation == 0
将为 false
此结果导致调用 EVCClient::EVCAuthenticateDeferred
,其中控制器检查已禁用:
// 对账户和控制器进行身份验证,确保通过 EVC 进行调用,并且状态检查被推迟
function EVCAuthenticateDeferred(bool checkController) internal view virtual returns (address) {
assert(msg.sender == address(evc)); // 这确保了 callThroughEVC 修饰符被利用
(address onBehalfOfAccount, bool controllerEnabled) =
evc.getCurrentOnBehalfOfAccount(checkController ? address(this) : address(0));
if (checkController && !controllerEnabled) revert E_ControllerDisabled();
return onBehalfOfAccount;
}
由于 checkController == false
,因此调用 getCurrentOnBehalfOfAccount
时使用 address(0)
,从而跳过控制器验证。
EthereumVaultConnector::getCurrentOnBehalfOfAccount
返回 onBehalfOfAccount
和(如果启用了控制器):
/// @inheritdoc IEVC
function getCurrentOnBehalfOfAccount(address controllerToCheck)
external
view
returns (address onBehalfOfAccount, bool controllerEnabled)
{
onBehalfOfAccount = executionContext.getOnBehalfOfAccount();
// 为了安全起见,如果没有经过身份验证的账户,则还原
if (onBehalfOfAccount == address(0)) {
revert EVC_OnBehalfOfAccountNotAuthenticated();
}
controllerEnabled =
controllerToCheck == address(0) ? false : accountControllers[onBehalfOfAccount].contains(controllerToCheck);
}
正如在调用 EVault
之前设置的那样,onBehalfOfAccount
为 p.eulerAccount
。
initOperation
中的下一行是 EVCClient::EVCRequireStatusChecks
:
function EVCRequireStatusChecks(address account) internal virtual {
assert(account != CHECKACCOUNT_CALLER); // 特殊值现在应该已解析
if (account == CHECKACCOUNT_NONE) {
evc.requireVaultStatusCheck();
} else {
evc.requireAccountAndVaultStatusCheck(account);
}
}
由于 account
(p.eulerAccount
) 不是 CHECKACCOUNT_NONE
,因此 EVault
告诉 EVC 在操作完成后需要进行状态检查,EthereumVaultConnector::requireAccountAndVaultStatusCheck
:
/// @inheritdoc IEVC
function requireAccountAndVaultStatusCheck(address account) public payable virtual {
if (executionContext.areChecksDeferred()) {
accountStatusChecks.insert(account);
vaultStatusChecks.insert(msg.sender);
} else {
requireAccountStatusCheckInternalNonReentrantChecks(account);
requireVaultStatusCheckInternalNonReentrantChecks(msg.sender);
}
}
自从进入 EthereumVaultConnector::call
以来,检查已被延迟。 因此,EVault
和账户 (p.eulerAccount
) 已输入到状态检查列表中。
这确保状态检查在执行后而不是在执行期间运行,从而保持延迟检查模型。
最后,initOperation
存储 vault 的可用资产和借款的传入快照,这些快照稍后将用于 vault 状态验证:
// 快照仅用于验证在检查供应上限时供应是否增加,以及在检查借款上限时借款是否增加。 当上限变量
// 减少(变得更安全)时,不会检查上限。 因此,如果两个上限均已禁用,则禁用快照。
// 快照在 vault 状态检查期间清除,因此不能忽略 vault 状态检查。
if (
!vaultCache.snapshotInitialized
&& !(vaultCache.supplyCap == type(uint256).max && vaultCache.borrowCap == type(uint256).max)
) {
vaultStorage.snapshotInitialized = vaultCache.snapshotInitialized = true;
snapshot.set(vaultCache.cash, vaultCache.totalBorrows.toAssetsUp());
}
Vault::withdraw
中的剩余步骤只是标准 ERC-4626 提款:
Assets assets = amount.toAssets();
if (assets.isZero()) return 0;
Shares shares = assets.toSharesUp(vaultCache);
finalizeWithdraw(vaultCache, assets, shares, account, receiver, owner);
return shares.toUint();
这里没有什么意外:转换、会计和最终确定。
完成此操作后,执行返回到 EthereumVaultConnector
:
执行进入 EthereumVaultConnector::call
的最后几行:
bool success;
(success, result) = callWithAuthenticationInternal(targetContract, onBehalfOfAccount, value, data);
if (!success) revertBytes(result);
restoreExecutionContext(contextCache);
第一行处理故障传播。 假设我们的调用成功并且没有还原,则执行继续到 EthereumVaultConnector::restoreExecutionContext
:
/// @notice 从缓存状态还原执行上下文。
/// @dev 此函数将执行上下文还原到先前缓存的状态,如果不再延迟,则执行必要的状态
/// 检查。 如果不再延迟检查,它会将执行上下文设置为
/// 指示检查正在进行中,并清除“代表”账户。 然后,它在将执行上下文还原到缓存状态之前,为 both
/// 账户和 vault 执行状态检查。
/// @param contextCache 要从中还原的缓存执行上下文。
function restoreExecutionContext(EC contextCache) internal virtual {
if (!contextCache.areChecksDeferred()) {
executionContext = contextCache.setChecksInProgress().setOnBehalfOfAccount(address(0));
checkStatusAll(SetType.Account);
checkStatusAll(SetType.Vault);
}
executionContext = contextCache;
}
使用来自 vault 操作之前的缓存状态调用 restoreExecutionContext
。 因为 contextCache.areChecksDeferred()
返回 false
(假设在我们的交换之前没有与 EVC 交互)并且检查已完成,所以系统继续通过 EthereumVaultConnector::checkStatusAll
执行操作后状态验证:
/// @notice 检查集合中所有实体的状态(账户或 vault),并清除检查。
/// @dev 根据 setType 迭代 accountStatusChecks 或 vaultStatusChecks 并执行状态
/// 检查。
/// 在执行检查时清除检查。
/// @param setType 要对其执行状态检查的集合类型(账户或 vault)。
function checkStatusAll(SetType setType) internal virtual {
setType == SetType.Account
? accountStatusChecks.forEachAndClear(requireAccountStatusCheckInternal)
: vaultStatusChecks.forEachAndClear(requireVaultStatusCheckInternal);
}
为 Account
和 Vault
调用 checkStatusAll
。 这对账户健康状况执行验证,并确保未超过 Vault 借款和供应上限。
它委托给 forEachAndClear
,后者仅为每个项目调用提供的函数,然后将其删除。
EthereumVaultConnector::requireAccountStatusCheckInternal
调用状态检查并处理还原。
function requireAccountStatusCheckInternal(address account) internal virtual {
(bool isValid, bytes memory result) = checkAccountStatusInternal(account);
if (!isValid) {
revertBytes(result);
}
}
这只是委托给 EthereumVaultConnector::checkAccountStatusInternal
,后者执行实际评估:
/// @notice 在内部检查账户的状态。
/// @dev 此函数首先检查账户的控制器数量。 如果未启用任何控制器,
/// 它会立即返回 true,指示账户状态有效,而无需进一步检查。 如果有多个
/// 控制器,则会返回 EVC_ControllerViolation 错误。 对于单个控制器,它会继续
/// 调用控制器以检查账户状态。
/// @param account 要检查其状态的账户地址。
/// @return isValid 一个布尔值,指示账户状态是否有效。
/// @return result 来自控制器调用的字节,指示账户状态。
function checkAccountStatusInternal(address account) internal virtual returns (bool isValid, bytes memory result) {
SetStorage storage accountControllersStorage = accountControllers[account];
uint256 numOfControllers = accountControllersStorage.流动性检查由 [`LiquidityUtils::checkAccountStatus`](https://github.com/euler-xyz/euler-vault-kit/blob/master/src/EVault/shared/LiquidityUtils.sol#L33-L55) 处理:
```javascript
// 检查是否存在负债,或者抵押品的价值(根据借款 LTV 调整后)是否大于负债价值。由于此函数使用买入/卖出价,因此应仅在账户状态检查中使用,
// 而不用于确定账户是否可以被清算(使用中间价)。
function checkLiquidity(VaultCache memory vaultCache, address account, address[] memory collaterals)
internal
view
virtual
{
validateOracle(vaultCache);
Owed owed = vaultStorage.users[account].getOwed();
if (owed.isZero()) return;
uint256 liabilityValue = getLiabilityValue(vaultCache, account, owed, false);
uint256 collateralValue;
for (uint256 i; i < collaterals.length; ++i) {
collateralValue += getCollateralValue(vaultCache, account, collaterals[i], false);
if (collateralValue > liabilityValue) return;
}
revert E_AccountLiquidity();
}
在不详细介绍每个细节的情况下,代码确认账户(例如,p.eulerAccount
)保持健康,并且提款不会使其面临清算的风险。
最后的验证步骤是由 EthereumVaultConnector::requireVaultStatusCheckInternal
处理的 vault 状态检查:
function requireVaultStatusCheckInternal(address vault) internal virtual {
(bool isValid, bytes memory result) = checkVaultStatusInternal(vault);
if (!isValid) {
revertBytes(result);
}
}
它会定向到 EthereumVaultConnector::checkVaultStatusInternal
:
/// @notice 在内部检查 **vault** 的状态。
/// @dev 此函数对 **vault** 进行外部调用以检查其状态。
/// @param vault 要检查状态的 **vault** 的地址。
/// @return isValid 一个布尔值,指示 **vault** 状态是否有效。
/// @return result 从 **vault** 调用返回的字节,指示 **vault** 状态。
function checkVaultStatusInternal(address vault) internal virtual returns (bool isValid, bytes memory result) {
bool success;
(success, result) = vault.call(abi.encodeCall(IVault.checkVaultStatus, ()));
isValid =
success && result.length == 32 && abi.decode(result, (bytes32)) == bytes32(IVault.checkVaultStatus.selector);
emit VaultStatusCheck(vault);
}
这将对 vault.checkVaultStatus()
进行外部调用,在本例中,它解析为 RiskManager::checkVaultStatus
:
/// @inheritdoc IRiskManager
/// @dev 有关 `checkAccountStatus` 的重入注释
function checkVaultStatus() public virtual reentrantOK onlyEVCChecks returns (bytes4 magicValue) {
// 使用更新变体以确保利息在利率更新之前在存储中累积。
// 由于在 **vault** 状态检查期间进行利率重定向,因此不得免除 **vault** 状态检查。
VaultCache memory vaultCache = updateVault();
uint256 newInterestRate = computeInterestRate(vaultCache);
logVaultStatus(vaultCache, newInterestRate);
// 我们使用快照来检查借款或供应是否增加,如果增加,则检查借款和供应上限。如果快照已初始化,则表示已配置上限。如果在批处理中间设置上限,则
// 快照表示当时 **vault** 的状态。
if (vaultCache.snapshotInitialized) {
vaultStorage.snapshotInitialized = vaultCache.snapshotInitialized = false;
Assets snapshotCash = snapshot.cash;
Assets snapshotBorrows = snapshot.borrows;
uint256 prevBorrows = snapshotBorrows.toUint();
uint256 borrows = vaultCache.totalBorrows.toAssetsUp().toUint();
if (borrows > vaultCache.borrowCap && borrows > prevBorrows) revert E_BorrowCapExceeded();
uint256 prevSupply = snapshotCash.toUint() + prevBorrows;
// 借款向下舍入,因为总资产可能会在还款期间增加。
// 当偿还的用户债务四舍五入为资产并用于增加现金时,可能会发生这种情况,
// 而 totalBorrows 将仅通过实际债务进行调整,小于现金的增加。
// 如果多个账户需要在超过供应上限时偿还,他们应该在
// 单独的批次。
uint256 supply = vaultCache.cash.toUint() + vaultCache.totalBorrows.toAssetsDown().toUint();
if (supply > vaultCache.supplyCap && supply > prevSupply) revert E_SupplyCapExceeded();
snapshot.reset();
}
callHookWithLock(vaultCache.hookedOps, OP_VAULT_STATUS_CHECK, address(evc));
magicValue = IEVCVault.checkVaultStatus.selector;
}
之前保存的快照(在 initOperation
期间)用于验证是否超过了供应上限。注意:如果供应或借款上限之前已经突破,则突破是可以的;但是,总供应或借款之后必须减少。否则,调用将恢复。
最后,可以选择调用 hook。如果满足所有这些条件,则提款完成,并且 EulerSwap
可以继续。
总而言之,通过 EulerSwap 发起的提款会触发一系列严格控制的步骤:通过 EVC 进行身份验证、上下文执行、延迟验证以及最后,对账户和 vault 的状态检查。每个组件协同工作,以确保提款使系统处于有效、安全的状态。只有这样,执行才会恢复到 EulerSwap 中。
- 原文链接: cyfrin.io/blog/how-vault...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!