EulerSwap 中 Vault 提款如何运作:完整流程详解

本文深入探讨了 EulerSwap 中 vault 的提款流程,EulerSwap 是一个先进的去中心化交易所,它将 Euler Finance 的借贷基础设施与 Uniswap v4 的 hook 架构相结合,为流动性提供者提供先进的资本效率、收益生成和风险管理工具。

EulerSwap 是一个前沿的去中心化交易所(DEX),它将 Euler Finance 的借贷基础设施Uniswap v4 的 hook 架构相结合,为流动性提供者(LP)提供高级的资本效率、收益生成和风险管理工具。

EulerSwap 的一个关键组件是它的 vault(金库)系统,该系统使用 Euler Vault Kit (EVK) 构建,并符合 ERC-4626 代币化金库标准。该系统允许 LP 将资产存入 vault,赚取交易费用和借贷收益,并将这些存款用作 just-in-time (JIT) 流动性供应的抵押品。

理解 vault 提款流程对于 LP、开发者和 DeFi 爱好者来说至关重要,这样才能有效地与这一创新协议互动。

目录

  1. 介绍
  2. Ethereum Vault Connector (EVC)
  3. Euler Vault 执行
  4. 执行后状态检查
  5. 结论

1. 介绍

EulerSwap 的 vault 提款流程是一个复杂而安全的机制,它确保 LP 可以赎回他们的资产,同时保持协议的流动性和稳定性。该流程涉及两个主要组件:Ethereum Vault Connector (EVC),它处理身份验证、授权和延迟状态检查;以及 Euler Vault Kit (EVK),它在 vault 内执行提款逻辑。这种职责分离使得 EulerSwap 可以将验证职责委托给 EVC,从而确保所有系统状态在执行后保持有效。

当 LP 通过 EulerSwap 发起提款请求时,提款流程开始,这会触发一系列智能合约交互。这些交互包括通过 EVC 进行身份验证、在 vault 中执行,以及最终状态检查以验证帐户和 vault 的健康状况。该流程旨在优先考虑安全性,利用广泛的审计和一个 50 万美元的夺旗竞赛来对系统进行压力测试。

以下是理解 EulerSwap 中 vault 提款为何重要的原因:

  • 资本效率:LP 可以提取在多个池中使用的或作为抵押品的资产,从而确保灵活性,而不会中断流动性。
  • 安全性:EVC 强大的身份验证和延迟检查可防止未经授权的访问并维护系统完整性。
  • 收益优化:LP 可以战略性地安排提款时间,以最大程度地提高来自交易费用和借贷收益的回报。
  • 开发者集成:理解该流程使开发者能够构建与 EulerSwap 的 vault 无缝交互的应用程序。

下面,我们将深入研究每个步骤的技术细节,并提供代码片段和实践见解。

2. Ethereum Vault Connector (EVC)

EVC 是 EulerSwap 架构中的中央控制器,负责对调用者进行身份验证、管理执行上下文以及将状态检查推迟到操作完成后。当 LP 通过 EulerSwap 发起提款时,该流程首先调用 EVC 的 call() 函数,该函数由第 151 行的 FundsLib::withdrawAssets 函数触发:

IEVC(evc).call(vault, p.eulerAccount, 0, abi.encodeCall(IERC4626.withdraw, (avail, to, p.eulerAccount)));

2.1 EVC call() 函数

执行从 EthereumVaultConnector::call 开始,它设置了一个延迟检查框架,以确保操作在没有立即验证的情况下进行,从而在保持安全性的同时提供灵活性:

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() 调用将帐户健康状况检查(例如,抵押率)和 vault 状态检查(例如,供需上限)推迟到执行之后,从而降低 gas 成本并启用复杂的操作。
  • 上下文缓存contextCache 存储调用前的状态,允许 EVC 在执行后恢复它。
  • 不可重入性nonReentrantChecksAndControlCollateral 修饰符可防止重入攻击,这是一项关键的安全功能。

2.2 通过 callWithAuthenticationInternal 进行身份验证

然后,EVC 调用 callWithAuthenticationInternal 以验证调用者的身份并路由请求:

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();
        (success, result) = address(this).delegatecall(data);
    } else {
        if (targetContract != msg.sender) {
            authenticateCaller({account: onBehalfOfAccount, allowOperator: true, checkLockdownMode: true});
        }
        (success, result) = callWithContextInternal(targetContract, onBehalfOfAccount, value, data);
    }
}
  • 参数:
    • targetContract: vault 地址。
    • onBehalfOfAccount: LP 的帐户 ( p.eulerAccount)。
    • value: 设置为 0(无 ETH 转账)。
    • data: 编码调用 IERC4626.withdraw(avail, to, p.eulerAccount)
  • 逻辑: 由于 targetContract (vault) 不是 EVC 或 msg.sender (EulerSwap),因此该函数继续调用 authenticateCaller 以验证权限。

2.3 调用者身份验证

authenticateCaller 函数确保调用者被授权代表该帐户行事:

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 = 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;
}
  • 地址前缀p.eulerAccount 的前 19 个字节标识帐户的所有者,每个所有者最多支持 256 个子帐户。
  • 所有权检查:该函数检查 msgSender (EulerSwap) 是否与 account (p.eulerAccount) 共享一个共同的所有者。由于 EulerSwap 是一个单独的合约,因此此检查失败。
  • 操作员授权:EulerSwap 通常在部署期间设置为 LP 帐户的授权操作员,因此 isAccountOperatorAuthorizedInternal 返回 true,并将 authenticated 设置为 true
  • 智能合约限制:如果帐户不是所有者并且是智能合约,则身份验证失败,以防止未经授权的合约交互。
  • 锁定模式:如果帐户处于锁定模式(用于禁用操作的紧急功能),则调用将恢复。

2.4 上下文设置

身份验证后,callWithContextInternal 设置执行上下文并调用 vault:

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();
    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;
}
  • 上下文更新onBehalfOfAccount 设置为 p.eulerAccount,并且由于 if 语句中的任何条件(共同所有者、相同发送者等)都不为真,因此操作员被标记为已通过身份验证。
  • Vault 调用: EVC 执行 vault.withdraw(avail, to, p.eulerAccount)

3. Euler Vault 执行

提款逻辑在遵循 ERC-4626 标准的 Euler vault 中执行。入口点是 EVault::withdraw 函数,该函数通过修饰符委托给模块:

function withdraw(uint256 amount, address receiver, address owner)
    public
    virtual
    override
    callThroughEVC
    use(MODULE_VAULT)
    returns (uint256) {}

3.1 callThroughEVC 修饰符

callThroughEVC 修饰符确保所有调用都通过 EVC 路由,以实现一致的状态管理:

modifier callThroughEVC() {
    if (msg.sender == address(evc)) {
        _;
    } else {
        callThroughEVCInternal();
    }
}

由于 msg.sender 是 EVC(来自之前的调用),因此该修饰符允许执行继续执行 vault 的逻辑。

3.2 use(MODULE_VAULT) 修饰符

use 修饰符将调用委托给包含核心提款逻辑的 MODULE_VAULT 模块:

modifier use(address module) {
    _;
    delegateToModule(module);
}
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()) }
    }
}

这会将执行路由到 Vault::withdraw:

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 函数通过更新 vault 的状态和排队状态检查来设置提款。
  • 资产转换:请求的 amount 将转换为 Assets(一种用于精度的包装器类型)。
  • 份额计算toSharesUp 函数计算要销毁的份额数,向上舍入以确保偿付能力。
  • 完成finalizeWithdraw 函数销毁份额并将资产转移到接收者。

3.3 initOperation 细节

initOperation 函数执行关键设置:

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);
    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 更新updateVault 累积利息并更新总供应量和借款余额。
  • 身份验证EVCAuthenticateDeferred 确认调用来自 EVC 并跳过提款的控制器检查(因为 OP_WITHDRAWCONTROLLER_NEUTRAL_OPS 中)。
  • 状态检查EVCRequireStatusChecks 会将帐户和 vault 状态检查排队以进行执行后验证。
  • 快照:存储 vault 的现金和借款的快照以供以后验证。

3.4 闲置资产利用和提款队列

EulerSwap 优先使用闲置资产(vault 中未分配的资金)进行提款,以最大程度地减少中断:

uint256 vaultBalance = token.balanceOf(address(this));
if (withdrawAmount <= vaultBalance) {
    processWithdrawal(withdrawAmount);
}

如果闲置资产不足,该协议会处理提款队列,即策略的有序列表(外部收益生成合约):

for (uint256 i = 0; i < withdrawalQueue.length; i++) {
    address strategy = withdrawalQueue[i];
    uint256 available = strategy.availableFunds();
    uint256 toWithdraw = Math.min(available, remainingAmount);
    strategy.withdraw(toWithdraw);
    remainingAmount -= toWithdraw;
    if (remainingAmount == 0) break;
}

如果策略缺乏流动性,可能会强制启动提款,可能会造成损失:

uint256 loss = strategy.forceWithdraw(amountNeeded);
if (loss > 0) {
    totalLoss += loss;
    emit LossIncurred(strategy, loss);
}
require(totalLoss <= maxLoss, "Loss exceeds acceptable limit");

3.5 完成

finalizeWithdraw 函数会销毁计算出的份额并将资产转移给接收者:

burnShares(owner, shares);
token.safeTransfer(receiver, withdrawAmount);

4. 执行后状态检查

在 vault 执行提款后,控制权返回到 EVC 的 call 函数,该函数调用 restoreExecutionContext 以执行延迟状态检查:

function restoreExecutionContext(EC contextCache) internal virtual {
    if (!contextCache.areChecksDeferred()) {
        executionContext = contextCache.setChecksInProgress().setOnBehalfOfAccount(address(0));
        checkStatusAll(SetType.Account);
        checkStatusAll(SetType.Vault);
    }
    executionContext = contextCache;
}

4.1 帐户状态检查

checkStatusAll(SetType.Account) 调用验证帐户的健康状况:

function checkAccountStatusInternal(address account)
    internal
    virtual
    returns (bool isValid, bytes memory result) {
    SetStorage storage accountControllersStorage = accountControllers[account];
    uint256 numOfControllers = accountControllersStorage.numElements;
    address controller = accountControllersStorage.firstElement;

if (numOfControllers == 0) return (true, "");
    else if (numOfControllers > 1) return (false, abi.encodeWithSelector(EVC_ControllerViolation.selector));
    bool success;
    (success, result) = controller.staticcall(
        abi.encodeCall(IVault.checkAccountStatus, (account, accountCollaterals[account].get()))
    );
    isValid = success && result.length == 32
        && abi.decode(result, (bytes32)) == bytes32(IVault.checkAccountStatus.selector);
}
  • 控制器检查:如果未启用任何控制器,则帐户有效。如果存在多个控制器,则调用将恢复。
  • 流动性检查checkAccountStatus 函数验证帐户的抵押品价值是否超过其负债,从而防止清算风险。

4.2 Vault 状态检查

checkStatusAll(SetType.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);
}

这会调用 RiskManager::checkVaultStatus

function checkVaultStatus()
    public
    virtual
    reentrantOK
    onlyEVCChecks
    returns (bytes4 magicValue) {
    VaultCache memory vaultCache = updateVault();
    uint256 newInterestRate = computeInterestRate(vaultCache);

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;
        uint256 supply = vaultCache.cash.toUint() + vaultCache.totalBorrows.toAssetsDown().toUint();
        if (supply > vaultCache.supplyCap && supply > prevSupply)
            revert E_SupplyCapExceeded();
        snapshot.reset();
    }
    magicValue = IEVCVault.checkVaultStatus.selector;
}
  • 快照验证:来自 initOperation 的快照用于确保不超过供需上限,除非它们已被违反(在这种情况下,它们必须减少)。
  • 利率更新:vault 会根据利用率更新其利率,从而影响未来的计算。

5. 结论

EulerSwap 中的 vault 提款流程是一个经过精心设计的序列,它平衡了效率、安全性和灵活性。EVC 验证和路由请求,EVK 执行提款同时管理流动性,执行后检查确保系统保持稳定。主要功能包括:

  • 安全性:广泛的审计、不可重入性保护和 EVC 的延迟检查模型可降低风险。
  • 流动性管理:该协议优先考虑闲置资产,处理提款队列,并处理强制提款以满足请求。
  • 对开发者友好:ERC-4626 标准和模块化架构简化了集成。
  • 用户灵活性:LP 可以提取跨池使用的或作为抵押品的资产,从而优化资本效率。
  • 原文链接: medium.com/@ankitacode11...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
ankitacode11
ankitacode11
江湖只有他的大名,没有他的介绍。