Dapp-Learning 实战:基于 Hardhat 组合 Aave V3 与 Uniswap V3 构建一键做多/做空杠杆合约
工试云启 考证服务中心整理

示例工程区块链【免费下载链接】Dapp-LearningDapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.项目地址https://gitcode.com/gh_mirrors/da/Dapp-Learning点击查看免费下载导读本文以 Dapp-Learning 仓库中的 21-aave-uni-loan 样例项目为主线讲解如何用 Hardhat 测试框架驱动一个名为AaveApe的聚合合约它在同一笔交易内完成AAVE 质押 → AAVE 借贷 → Uniswap V3 兑换 → 再次存入 AAVE的闭环从而对两种资产同时实现做多与做空并通过闪电贷一键赎回平仓。读完本文你将掌握 AAVE V3 的 Pool Addresses Provider 入口、Credit Delegation 授权机制、Uniswap V3 SwapRouter 的 exactInputSingle/exactOutputSingle 调用以及基于主网 fork 的 Hardhat 测试与利息查询脚本的完整实战方案。一、项目背景为什么需要 AaveApeDeFi 中借贷 兑换是两个独立协议能力普通用户需要分多笔交易才能完成杠杆操作既费 Gas 又暴露中间状态风险。AaveApe 的思路是把它们组合进一个合约AAVE负责资产的存入supply与借出borrow提供杠杆的资金来源Uniswap负责资产兑换把借来的资产换成想要做多的资产Hardhat负责开发、部署、主网 fork 测试与脚本运行。从仓库目录结构可以清晰看到项目的完整组成contracts合约与接口、ignition/modules部署模块、scripts借贷/查询脚本、test主网 fork 测试。四个核心合约文件为 AaveApe.sol、AaveApePlus.sol、AaveApePlusV2.sol 与公共基类 AaveUniswapBase.sol分别对应基础版Uniswap 闪电兑换版Balancer ParaSwap 聚合版三档实现。二、合约交互入口两个协议的总机2.1 AAVE V3Pool Addresses ProviderAAVE V3 的交互入口是Pool Addresses Provider地址供应者。之所以不直接写死 Pool 地址是因为 AAVE 会通过升级 Pool / PoolDataProvider / PriceOracle 合约来修复 Bug而 Addresses Provider 可以动态返回这三个合约的最新地址集成方无需改动代码。三个核心合约职责如下合约职责Pool负责 存入supply/ 借贷borrow/ 闪电贷flashLoanPoolDataProvider提供 用户 / 池子 中相关的借贷信息余额、利率、配置PriceOracle价格预言机返回资产的 USD 报价基类 AaveUniswapBase.sol 在构造函数中固化ADDRESSES_PROVIDER并提供三个封装函数按需取最新地址LENDING_POOL()调用ADDRESSES_PROVIDER.getPool()拿到当前 Pool 代理地址并转成IPool接口getPriceOracle()getPriceOracle()返回价格预言机getProtocolDataProvider()getPoolDataProvider()返回数据提供者。对应接口定义见 IPoolAddressesProvider.sol 与 IPool.sol。2.2 Uniswap V3SwapRouterUniswap V3 的交互入口是SwapRouter通过它可以完成与 Uniswap V3 的所有交互单跳/多跳兑换。本仓库使用的接口为 ISwapRouter.sol其中两个关键方法exactInputSingle(ExactInputSingleParams)精确输入、按路径滑点输出最多数量用于做多时把借出的 B 全部换成 AexactOutputSingle(ExactOutputSingleParams)精确输出、输入尽量少的数量用于赎回时用 A 精确凑够偿还 B 的数额。两个结构体的字段包括tokenIn / tokenOut / fee / recipient / deadline / amountIn / amountOutMinimum或amountOut / amountInMaximum以及sqrtPriceLimitX96这些参数在 AaveApe 的ape与closePosition中均有完整体现。三、功能要点四个基础能力组合出杠杆策略AaveApe 合约主要有四个功能质押资产到 AAVEsupply从 AAVE 进行借贷borrow含 Credit Delegation到 Uniswap 进行资产置换swap从 AAVE 进行闪电贷flashLoan通过组合这四个功能可以实现自动做多、做空以及赎回平仓。3.1 做多同时做空另一种资产假设你有两种资产其中资产 A 想要做多资产 B 想要做空。Ape 合约在一笔交易中同时完成根据用户当前在 AAVE 质押的资产 A 的价值计算可借的最大数额的资产 B调用getAvailableBorrowInAsset从 AAVE 借贷资产 B_lendingPool.borrow(...)onBehalfOf为用户地址通过 Uniswap V3 把借出的资产 B 全部兑换为资产 A把兑换出的资产 A 继续存入 AAVE_lendingPool.supply(...)抵押物增加循环后可继续借更多 B。3.2 做空从 AAVE 借出资产 B 后Ape 通过 Uniswap V3 将 B 全部兑换为 A再把 A 存入 AAVE 作为抵押。若 B 相对 A 贬值赎回平仓时归还的 B 变少剩余 A 即为做空 B 的收益。3.3 赎回unwindApe通过 AAVE V3 的闪电贷借入偿还债务所需的 B在回调executeOperation内完成用抵押的 aToken 从 AAVE 取回 A → Uniswap 将 A 换成 B → 归还 AAVE 债务与闪电贷本金 手续费若有多余的 A 则再次为用户存入 AAVE。详细实现见下文源码剖析。3.4 前置条件Credit Delegation 授权需要注意在从 AAVE 中借贷资产 B时需要用户预先授权 Ape 合约使 Ape 能以用户名义借贷这就是 AAVE 的Credit Delegation信用委托机制。用户需要对目标资产的债务代币StableDebtToken / VariableDebtToken调用approveDelegation(aaveApeAddress, amount)。在 scripts/loan.js 与测试脚本中delegateCreditToTheApe正是通过assetDebtTokenapproveDelegation完成无限额委托。四、源码级实现剖析AaveApe 的完整调用链4.1 基类AaveUniswapBaseAaveUniswapBase.sol 是所有版本共用的基类持有三个 immutable 状态IPoolAddressesProvider public immutable ADDRESSES_PROVIDER; ISwapRouter public immutable UNISWAP_ROUTER; address public immutable UNISWAP_ROUTER_ADDRESS;构造函数接收lendingPoolAddressesProviderAddress与uniswapRouterAddress两个地址AaveApe的构造函数原样透传见 AaveApe.sol。4.2 可借额度计算getAvailableBorrowInAssetAaveApe.sol 中function getAvailableBorrowInAsset(address borrowAsset, address ape) public view returns (uint256) { // availableBorrowsBase V3 USD based (, , uint256 availableBorrowsBase, , , ) LENDING_POOL().getUserAccountData(ape); return getAssetAmount(borrowAsset, availableBorrowsBase); }getUserAccountData返回的第三项availableBorrowsBase是 AAVE V3 以 USD8 位精度计的可借额度getAssetAmount则通过预言机价格与资产 decimals 换算成目标资产数量uint256 assetAmount amountIn * 10**decimals / assetPrice;4.3 开仓主流程ape 函数AaveApe.sol 的ape(apeAsset, borrowAsset, interestRateMode)完整展示了四个步骤的调用链getAvailableBorrowInAsset计算最大可借量无额度时require(borrowAmount 0, Requires credit on Aave!)直接回滚_lendingPool.borrow(borrowAsset, borrowAmount, interestRateMode, 0, msg.sender)以用户名义借出 BIERC20(borrowAsset).approve(UNISWAP_ROUTER_ADDRESS, borrowAmount)授权 Router构造ISwapRouter.ExactInputSingleParams调用exactInputSinglefee由getBestPool(borrowAsset, apeAsset).fee()动态选择amountOutMinimum: 0、sqrtPriceLimitX96: 0不做价格保护示例演示用途授权 Pool 后_lendingPool.supply(apeAsset, outputAmount, msg.sender, 0)将兑换出的 A 存入 AAVE抵押物增长发出Ape事件记录开仓明细。superApe则在for循环中重复调用ape实现多次杠杆循环looping。4.4 池子选择getBestPoolUniswap V3 同一交易对存在多个不同费率池子。getBestPoolAaveApe.sol遍历v3Fees [100, 500, 3000, 10000]四档费率通过IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984)所有链通用常量地址的getPool(token0, token1, fee)查询池子并挑选liquidity()最大者作为最优流动性池。测试用例get max liquidity pool of WEH/DAI断言 WETH/DAI 一定存在非零地址池子见 test_aave_ape.js。4.5 闪电贷平仓unwindApe executeOperation平仓链路分两步第一步unwindApeAaveApe.sol通过getProtocolDataProvider().getUserReserveData(borrowAsset, msg.sender)读取用户的 stableDebt / variableDebt按interestRateMode1稳定2浮动确定repayAmount无债务时require(repayAmount 0, Requires debt on Aave!)构造flashLoan(receiverAddress, assets, amounts, modes[0], onBehalfOfaddress(this), paramsabi.encode(msg.sender, apeAsset, interestRateMode), referralCode0)发起闪电贷params中编码了真正需要平仓的用户地址。第二步回调executeOperationAaveApe.sol双重校验msg.sender ADDRESSES_PROVIDER.getPool()且initiator address(this)防止他人伪造回调计算amountOwing repayAmount premiums[0]本金 闪电贷手续费abi.decode(params, (address, address, uint256))还原用户与资产信息后调用内部函数closePosition。内部函数closePositionAaveApe.sol顺序执行授权 Pool 并用_lendingPool.repay(borrowAsset, repayAmount, rateMode, ape)归还 AAVE 债务计算可提取抵押取可借额度换算值与用户 aToken 余额的较小者_aToken.transferFrom(ape, address(this), maxCollateralAmount)将用户的 aToken 转入合约再_lendingPool.withdraw(apeAsset, maxCollateralAmount, address(this))取回 A授权 Router 后调用exactOutputSingleamountOut amountOwing、amountInMaximum maxCollateralAmount精确兑换出足够的 BleftoverAmount maxCollateralAmount - amountIn若有剩余 A 则_lendingPool.supply(apeAsset, leftoverAmount, ape, 0)为用户存回 AAVE授权 Pool 以归还闪电贷本金 手续费发出Ape事件标记平仓完成。测试 test_aave_ape.js 完整覆盖了这些分支无抵押回滚、未授权回滚、未给 aToken 授权回滚、抵押不足回滚以及存入 5 ETH → 委托信用 → ape → approve aToken → unwindApe后aBalanceAfter aBalanceBefore且debtBalanceAfter 0的成功路径。五、进阶版本AaveApePlus 与 AaveApePlusV25.1 AaveApePlusUniswap V3 Flash Swap 免手续费杠杆AaveApePlus.sol 把兑换从 Router 调用升级为Uniswap V3 池内 flash swap先借资产后结算省去对 Router 的授权步骤并引入滑点保护与健康因子检查flashApe(apeAsset, borrowAsset, borrowAmount, interestRateMode)L30-L60根据pool.token0() borrowAsset决定zeroForOne方向用TickMath.MIN_SQRT_RATIO 1 / MAX_SQRT_RATIO - 1作为价格极限调用pool.swap(...)完成借贷 兑换 抵押三步最后require(newHealthFactor 1e18, health factor 1!)校验用户健康因子uniswapV3SwapCallbackL149-L198是 Uniswap V3 的回调入口根据data中解码出的isFlashUnwind标志区分开仓/平仓开仓时把换得的 Asupply进 AAVE 并borrow出 B 还给池子平仓时先repayAAVE 债务、transferFrom用户的 aToken 并withdraw再用 A 归还 flash swapflashUnwindL62-L126中apeAssetToDebtAmount apeAssetToDebtAmount - apeAssetToDebtAmount.percentMul(pool.fee() / 100 50)显式预留了 0.3% 池子手续费 0.5% 滑点缓冲。测试见 test_aave_ape_plus.js其中还包含两种 ape 资产、赎回金额超过单一抵押物的场景先 WETH 换 WBTC 再质押 WBTC 做多再用 WBTC 抵押物平仓 DAI 债务。5.2 AaveApePlusV2Balancer V2 闪电贷 ParaSwap 聚合兑换AaveApePlusV2.sol 是更贴近生产环境的实现闪电贷来源可切换_checkVaultLiquidity先检查 Balancer V2 Vault常量地址0xBA12222222228d8Ba445958a75a0704d566BF2C8所有链通用流动性是否足够够则走vault.flashLoan不够则回退 AAVEflashLoanL135-L161因此实现了receiveFlashLoan与executeOperation双回调兑换走ParaSwap Augustus_paraSwap校验AUGUSTUS_REGISTRY.isValidAugustus后对getTokenTransferProxy授权再address(augustus).call(psp.swapCallData)执行离线聚合报价的 calldata失败时用 assembly 原样回滚 revert 原因L164-L183构造函数接收augustusRegistry并通过require(!augustusRegistry.isValidAugustus(address(0)), ...)校验注册表有效性getMaxCollateralAmount在可换得抵押物基础上再上浮 3% 作为缓冲assetAmount * 103 / 100。测试 test_aave_ape_plus_v2.js 预先硬编码了两段 ParaSwap calldataDAI→WETH 与 WETH→DAI并专门用一个crvUSD场景验证Balancer Vault 流动性不足时回退 AAVE 闪电贷的分支。六、操作步骤从部署到查询利息6.1 准备环境变量项目通过dotenv读取.env。原文档给出的初始化命令为cp .env.example .env需要配置的环境变量及用途与 hardhat.config.js 中process.env的引用一一对应环境变量用途ETHERSCAN_API_KEY用于合约验证etherscan.verifyINFURA_ID用于连接各以太坊网络ropsten/kovan/main/optimism 等ALCHEMY_IDPolygon 网络 RPCpolygon-mainnet.g.alchemy.com原文档说明在 Polygon 网络借贷时发现 Infura 网络延迟导致交易失败因此改用 Alchemy 验证PRIVATE_KEY用于发送交易各网络accounts数组TARGET_ADDRESS目标用户地址query.js用它查询该用户在 AAVE 上借贷所支付的所有利息6.2 部署合约// deploy aaveape npx hardhat ignition deploy ./ignition/modules/AaveApe.js --network maticIgnition 部署模块 AaveApe.js 会先检查当前网络是否存在于hardhat.config.js的networkAddressMapping中不存在则抛错提示添加配置然后从映射中取出lendingPoolAddressesProviderAddress与uniswapRouterAddress作为AaveApe构造函数参数完成部署。AaveApePlus.js、AaveApePlusV2.js部署模块结构相同其中 V2 传入的是augustusRegistryAddress。6.3 验证合约// contract verify npx hardhat verify --network matic 0x4699f609F4FD97A3cf74CB63EFf5cd1200Dfe3dA 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb 0xE592427A0AEce92De3Edee1F18E0157C05861564验证命令需要在构造函数参数中提供两个地址AAVE V3 的 Pool Addresses Provider 与 Uniswap V3 SwapRouter。各网络的这两个地址均配置在 hardhat.config.js 的networkAddressMapping中例如 Polygonmatic上为0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb与0xE592427A0AEce92De3Edee1F18E0157C05861564主网与 localhost 的 Provider 地址为0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9eOptimism 与 matic 相同。6.4 进行借贷// open max position on aave npx hardhat run scripts/loan.js --network maticscripts/loan.js 的完整流程为通过 WETH GatewayIWrappedTokenGatewayV3depositETH存入 0.1 ETH 作为初始抵押对 DAI 的浮动债务代币执行approveDelegation委托信用给 AaveApe调用aaveApeflashApe, 2)借贷 1000 DAI注意脚本实际调用的是升级版AaveApePlus的 flashApe注释中保留了ape与superApe的调用示例打印开仓前后 WETH 的 aToken 余额与 DAI 债务余额授权 aToken 后调用flashUnwind平仓验证债务清零最后通过withdrawETH取回 ETH。6.5 查询用户支付利息总额npx hardhat run scripts/query.js --network matic需要注意原文档强调应在 matic 上运行本脚本因为 matic 上所有可借贷币种的 APY 均使用浮动 APY如果使用其他网络需要修改代码中计算 interest部分传入getDebtToken接口的参数利率模式。scripts/query.js 的数据来源有两路链上通过getUserAccountData打印健康因子healthFactor、总抵押totalCollateralBase、清算阈值、LTV通过getReserveData打印 DAI 的浮动借贷利率、存款利率与借贷指数通过getAvailableBorrowInAsset打印用户可借 DAI 的 USD 价值TheGraph 子图脚本内置 GraphQL 查询querySql按用户地址拉取borrowHistory与repayHistory并按url映射matic / main / optimism 三个子图端点汇总。利息的计算公式为利息 累计已还(repay) 当前债务(currentDebt) - 累计已借(borrow)其中currentDebt通过对应债务代币的balanceOf实链读取最终按币种 decimals 归一化输出current total interest。6.6 主网 fork 测试hardhat.config.js 的hardhat网络配置了主网 forkingRPC 使用 Alchemy 主网端点三个测试文件 test_aave_ape.js、test_aave_ape_plus.js、test_aave_ape_plus_v2.js 均在 fork 出的主网状态上运行合约直接部署、networkAddressMapping中的地址真实可用、WETH/DAI/USDC/WBTC 池子真实存在因此可以端到端验证借贷、兑换、闪电贷全流程而不消耗真实资金。此外 package.json 依赖nomicfoundation/hardhat-toolbox、hardhat ^2.22.2、uniswap/v3-core、axios与graphql其中uniswap/v3-core为 ISwapRouter 接口提供IUniswapV3SwapCallback等基础类型。七、安全注意事项与扩展方向从源码与测试中可以提炼出几条对实际开发有参考价值的经验闪电贷回调鉴权executeOperation/receiveFlashLoan必须同时校验msg.sender与initiator防止外部合约冒充发起方见 AaveApe.sol 与 AaveApePlusV2.sol滑点与手续费预留示例代码中amountOutMinimum: 0仅为教学简化生产环境应结合预言机报价设置最小输出AaveApePlus 中显式扣减池子手续费与 0.5% 缓冲、V2 中 3% 抵押缓冲都是更稳健的实践授权面最小化用户只需对 AaveApe 合约委托债务代币授权与 aToken 授权脚本中以ethers.MaxUint256一次性授权是演示简化实际可收敛到单次所需额度利率模式一致性interestRateMode1稳定、2浮动贯穿 borrow / repay / getDebtToken 全链路平仓与查询脚本必须保持一致否则债务无法正确匹配多链适配networkAddressMapping已内置 matic / optimism / main / localhost 四套地址hardhat.config.js部署新网络时只需补充 Provider、Router、WETH Gateway 与资产地址即可无需修改合约代码。结语本样例以不到两百行核心合约代码串起了 AAVE V3 借贷、Uniswap V3 兑换、闪电贷与信用委托四大 DeFi 原语并通过 Hardhat 主网 fork 测试完成了全链路验证。无论是想理解杠杆聚合器类似杠杆 farming / 循环借贷的底层逻辑还是想学习如何编写集成多个 DeFi 协议的合约21-aave-uni-loan 目录下的合约、脚本与测试都是可直接对照学习的完整范例。赞分享示例工程区块链【免费下载链接】Dapp-LearningDapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.项目地址https://gitcode.com/gh_mirrors/da/Dapp-Learning点击查看免费下载相关推荐Aave V3 闪电贷实战基于 Dapp-Learning 的合约开发与 Sepolia 测试网部署全解析Aave V3 闪电贷实战基于 Dapp Learning 的合约开发与 Sepolia 测试网部署全解析 闪电贷Flash Loan是无抵押借贷的经典示例工程区块链WTF Solidity 第 57 讲闪电贷Flashloan实战——Uniswap V2 / V3 与 AAVE V3 合约实现WTF Solidity 第 57 讲闪电贷Flashloan实战——Uniswap V2 / V3 与 AAVE V3 合约实现 本讲是 WTF Sol示例工程区块链教程Dapp-Learning 实战基于 Aave V3 闪电贷的无抵押借贷合约开发与 Sepolia 部署全流程Dapp Learning 实战基于 Aave V3 闪电贷的无抵押借贷合约开发与 Sepolia 部署全流程 本文是 Dapp Learning 仓库 ba示例工程区块链上一篇ctf-wiki 格式化字符串漏洞实战64 位偏移计算、GOT/返回地址劫持、栈迁移与无源码盲打下一篇Lesson overview创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考