diff --git a/contracts/src/CharonLiquidator.sol b/contracts/src/CharonLiquidator.sol index 35e054d..4730306 100644 --- a/contracts/src/CharonLiquidator.sol +++ b/contracts/src/CharonLiquidator.sol @@ -2,7 +2,11 @@ pragma solidity 0.8.24; import { IFlashLoanSimpleReceiver } from "./interfaces/IFlashLoanSimpleReceiver.sol"; +import { IAaveV3Pool } from "./interfaces/IAaveV3Pool.sol"; +import { IVToken } from "./interfaces/IVToken.sol"; +import { ISwapRouter } from "./interfaces/ISwapRouter.sol"; import { IERC20 } from "./interfaces/IERC20.sol"; +import { IWETH } from "./interfaces/IWETH.sol"; // ───────────────────────────────────────────────────────────────────────────── // CharonLiquidator — multi-chain flash-loan liquidation engine, v0.1 @@ -13,20 +17,27 @@ import { IERC20 } from "./interfaces/IERC20.sol"; // 3. Aave calls back executeOperation(); inside we: // a. Approve Venus vToken to spend the debt asset. // b. Call vToken.liquidateBorrow() — repay debt, seize collateral vTokens. -// c. Call vToken.redeemUnderlying() — convert seized vTokens to underlying. -// d. Swap collateral → debt asset via PancakeSwap V3. -// e. Repay Aave (amount + premium). -// f. Transfer profit to owner. -// Steps (a–f) are NOT implemented in this skeleton — bodies revert loudly. +// c. Call vToken.redeem() — convert ALL seized vTokens to underlying. +// Special case: vBNB returns native BNB, which we wrap into WBNB. +// d. Swap collateral → debt asset via PancakeSwap V3 at the caller-supplied +// fee tier (500 / 3000 / 10000 depending on the pool). +// e. Sweep profit to the COLD wallet — hot wallet (owner) holds gas only. +// f. Approve Aave for repayment (amount + premium); Aave pulls it after return. // -// Security invariants (enforced even in skeleton): +// Security invariants: // - Only owner may trigger liquidations or rescue funds. // - executeOperation is only callable by the Aave Pool. // - initiator must equal address(this) — prevents a malicious pool from // invoking our callback with forged parameters. +// - Reentrancy guard on executeLiquidation (nonReentrant). +// - executeOperation NOT guarded with nonReentrant: it is called by Aave mid- +// flash-loan, re-entering executeLiquidation's guard frame. The msg.sender +// == AAVE_POOL gate is the equivalent protection for the callback. +// - Lingering approvals zeroed after each consume point (vToken, SwapRouter). +// - Profit is swept to the immutable COLD_WALLET, never to the hot wallet. +// This enforces the CLAUDE.md safety invariant: "hot wallet holds gas only". // - No tx.origin usage. No delegatecall. No assembly. No upgradeability. -// - No external imports — all interfaces are inline/local for zero-dependency -// forge build in the skeleton phase. +// - No external library imports — all interfaces are inline/local. // ───────────────────────────────────────────────────────────────────────────── /// @title CharonLiquidator @@ -34,7 +45,7 @@ import { IERC20 } from "./interfaces/IERC20.sol"; /// v0.1 supports Venus Protocol on BNB Chain. /// @dev Implements IFlashLoanSimpleReceiver for the Aave V3 flash-loan callback. /// The bot (hot wallet = owner) is the sole authorized caller of executeLiquidation. -/// All liquidation and swap logic is stubbed — see issue #12. +/// All profit is routed to the immutable cold wallet set at construction. contract CharonLiquidator is IFlashLoanSimpleReceiver { // ───────────────────────────────────────────────────────────────────────── // Protocol ID constants — must mirror the Rust `ProtocolId` enum order. @@ -43,11 +54,37 @@ contract CharonLiquidator is IFlashLoanSimpleReceiver { /// @dev ProtocolId::Venus = 3 in the Rust enum (0-indexed: Aave=0, Compound=1, ...). uint8 internal constant PROTOCOL_VENUS = 3; + // ───────────────────────────────────────────────────────────────────────── + // BNB Chain canonical addresses — hard-coded for the v0.1 BSC-only scope. + // ───────────────────────────────────────────────────────────────────────── + + /// @notice Venus vBNB market — the only vToken whose underlying is native BNB. + /// Venus `redeem()` on this market transfers native BNB to msg.sender + /// rather than calling `IERC20.transfer`, so the standard ERC-20 + /// balance read used for every other vToken returns zero here. + /// Mainnet: https://bscscan.com/address/0xA07c5b74C9B40447a954e1466938b865b6BBea36 + address internal constant VBNB = 0xA07c5b74C9B40447a954e1466938b865b6BBea36; + + /// @notice Canonical Wrapped BNB (WBNB). PancakeSwap V3 pools are quoted in WBNB, + /// so any vBNB-seized position must be wrapped before the swap leg. + /// Mainnet: https://bscscan.com/address/0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c + address internal constant WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c; + + // ───────────────────────────────────────────────────────────────────────── + // Reentrancy guard — simple two-state lock. + // Stored as uint256 rather than bool to match the Solidity optimizer's + // preferred SSTORE encoding and avoid zero→non-zero cold-write gas cost + // on the first call (storage slot is initialized to 1 at deploy time). + // ───────────────────────────────────────────────────────────────────────── + + uint256 private _entered = 1; + // ───────────────────────────────────────────────────────────────────────── // Immutable configuration — set once at construction, never changed. // ───────────────────────────────────────────────────────────────────────── - /// @notice The bot's hot wallet. Only address authorised to call executeLiquidation and rescue. + /// @notice The bot's hot wallet. Only address authorised to call executeLiquidation + /// and rescue. By policy it holds gas only — profit is never routed here. address public immutable owner; /// @notice Aave V3 Pool proxy on BNB Chain. @@ -58,6 +95,12 @@ contract CharonLiquidator is IFlashLoanSimpleReceiver { /// Mainnet: 0x13f4EA83D0bd40E75C8222255bc855a974568Dd4 address public immutable SWAP_ROUTER; + /// @notice Cold wallet — sole recipient of liquidation profit. + /// @dev Profit is transferred here inside executeOperation, never to the hot + /// wallet. Enforces the CLAUDE.md safety invariant that the bot wallet + /// holds gas only. Set once at construction and immutable thereafter. + address public immutable COLD_WALLET; + // ───────────────────────────────────────────────────────────────────────── // Structs // ───────────────────────────────────────────────────────────────────────── @@ -66,6 +109,9 @@ contract CharonLiquidator is IFlashLoanSimpleReceiver { /// @dev Packed into `bytes` and forwarded through Aave's flashLoanSimple `params` /// argument so executeOperation can decode them without extra storage. /// Field layout must remain stable — the Rust side abi-encodes this struct. + /// NOTE: the companion Rust `LiquidationParams` builder lives in the + /// charon-executor crate (landing in a later PR). When that crate is added + /// it must mirror this layout exactly, including `swapPoolFee`. struct LiquidationParams { /// @dev Protocol identifier. Must equal PROTOCOL_VENUS (3) for v0.1. uint8 protocolId; @@ -73,7 +119,9 @@ contract CharonLiquidator is IFlashLoanSimpleReceiver { address borrower; /// @dev Underlying ERC-20 token that the borrower owes (e.g., USDT). address debtToken; - /// @dev Underlying ERC-20 token posted as collateral (e.g., BNB/WBNB). + /// @dev Underlying ERC-20 token posted as collateral (e.g., WBNB for vBNB). + /// For the vBNB path this MUST be WBNB — the contract wraps the + /// native BNB returned by Venus into WBNB before the swap. address collateralToken; /// @dev Venus vToken representing the debt side (e.g., vUSDT). address debtVToken; @@ -84,6 +132,13 @@ contract CharonLiquidator is IFlashLoanSimpleReceiver { /// @dev Minimum amount of debtToken to receive from the collateral swap. /// Acts as a slippage floor — revert if swap output falls below this. uint256 minSwapOut; + /// @dev PancakeSwap V3 pool fee tier (hundredths of a bip) for the + /// collateral → debt swap. Live tiers on PCS V3: 100 / 500 / 2500 / + /// 10000; Uniswap-equivalent 3000 is also deployed. BTCB, ETH and XVS + /// deep pools are at 500 or 10000, not 3000 — hardcoding would route + /// through an empty pool and revert. Supplied per-opportunity by the + /// off-chain router. Must be non-zero. + uint24 swapPoolFee; } // ───────────────────────────────────────────────────────────────────────── @@ -94,9 +149,15 @@ contract CharonLiquidator is IFlashLoanSimpleReceiver { /// @param borrower The liquidated account. /// @param debtToken The underlying asset that was repaid. /// @param repayAmount The amount of debtToken that was repaid. - /// @param profit Net profit in debtToken units retained by this contract. + /// @param profit Net profit in debtToken units swept to the cold wallet. + /// @param recipient The cold wallet address that received `profit` (indexed + /// so off-chain monitors can filter by destination). event LiquidationExecuted( - address indexed borrower, address indexed debtToken, uint256 repayAmount, uint256 profit + address indexed borrower, + address indexed debtToken, + uint256 repayAmount, + uint256 profit, + address indexed recipient ); /// @notice Emitted when the owner recovers tokens or native BNB via rescue(). @@ -110,29 +171,44 @@ contract CharonLiquidator is IFlashLoanSimpleReceiver { // ───────────────────────────────────────────────────────────────────────── /// @dev Restricts a function to the deploying hot wallet (owner). - /// Uses a string revert for maximum compatibility with off-chain tooling - /// that parses revert reasons at this stage of the skeleton. modifier onlyOwner() { require(msg.sender == owner, "!owner"); _; } + /// @dev Prevents reentrant calls into executeLiquidation. + /// Uses 1/2 rather than 0/1 to avoid cold-write SSTORE costs on every call. + /// NOT applied to executeOperation — that function is called by Aave mid- + /// flash-loan and is already protected by the msg.sender == AAVE_POOL gate. + /// Applying nonReentrant to executeOperation would deadlock the flash loan. + modifier nonReentrant() { + require(_entered == 1, "reentrant"); + _entered = 2; + _; + _entered = 1; + } + // ───────────────────────────────────────────────────────────────────────── // Constructor // ───────────────────────────────────────────────────────────────────────── - /// @notice Deploys CharonLiquidator and permanently binds it to one Aave Pool - /// and one PancakeSwap V3 router. + /// @notice Deploys CharonLiquidator and permanently binds it to one Aave Pool, + /// one PancakeSwap V3 router, and one cold-wallet profit recipient. /// @dev msg.sender becomes the immutable owner (the bot's hot wallet). - /// Both addresses are validated non-zero at construction. + /// All three addresses are validated non-zero at construction. + /// The cold wallet is required: the CLAUDE.md safety invariant forbids + /// parking profit in the hot wallet. /// @param _aavePool Aave V3 Pool proxy address on BNB Chain. /// @param _swapRouter PancakeSwap V3 SwapRouter address on BNB Chain. - constructor(address _aavePool, address _swapRouter) { + /// @param _coldWallet Cold-wallet address that receives all liquidation profit. + constructor(address _aavePool, address _swapRouter, address _coldWallet) { require(_aavePool != address(0), "!aavePool"); require(_swapRouter != address(0), "!swapRouter"); + require(_coldWallet != address(0), "!coldWallet"); owner = msg.sender; AAVE_POOL = _aavePool; SWAP_ROUTER = _swapRouter; + COLD_WALLET = _coldWallet; } // ───────────────────────────────────────────────────────────────────────── @@ -140,20 +216,22 @@ contract CharonLiquidator is IFlashLoanSimpleReceiver { // ───────────────────────────────────────────────────────────────────────── /// @notice Initiates a flash-loan-backed liquidation of a Venus borrower. - /// @dev Called exclusively by the off-chain bot (owner). The function encodes - /// `params` and requests a flash loan from Aave; the actual liquidation - /// logic executes inside executeOperation(). + /// @dev Called exclusively by the off-chain bot (owner). Encodes `params` and + /// requests a flash loan from Aave V3; the actual liquidation logic executes + /// atomically inside the executeOperation() callback. /// - /// Checks performed here (skeleton phase): - /// - Caller is owner (onlyOwner modifier). - /// - protocolId == PROTOCOL_VENUS. - /// - Key addresses are non-zero. - /// - repayAmount > 0. + /// Flow: + /// 1. Validate inputs. + /// 2. ABI-encode params to bytes. + /// 3. Call IAaveV3Pool.flashLoanSimple — Aave transfers debtToken to this + /// contract then immediately calls executeOperation(). + /// 4. After executeOperation returns true, Aave pulls amount + premium + /// using the allowance set inside the callback. No further state work + /// is required here. /// - /// BODY NOT IMPLEMENTED — see issue #12. /// @param params All parameters describing the Venus liquidation opportunity. - function executeLiquidation(LiquidationParams calldata params) external onlyOwner { - // Input validation — performed even in skeleton so the deployed shape is correct. + function executeLiquidation(LiquidationParams calldata params) external onlyOwner nonReentrant { + // ── Input validation ────────────────────────────────────────────────── require(params.protocolId == PROTOCOL_VENUS, "!protocolId"); require(params.borrower != address(0), "!borrower"); require(params.debtToken != address(0), "!debtToken"); @@ -161,8 +239,30 @@ contract CharonLiquidator is IFlashLoanSimpleReceiver { require(params.debtVToken != address(0), "!debtVToken"); require(params.collateralVToken != address(0), "!collateralVToken"); require(params.repayAmount > 0, "!repayAmount"); + require(params.swapPoolFee > 0, "!swapPoolFee"); + // On the vBNB path the underlying returned by Venus is native BNB, which + // the contract wraps into WBNB before swapping. Enforce that the caller + // declared WBNB as collateralToken so the swap leg routes through a real + // pool and post-swap balance checks read the correct token. + if (params.collateralVToken == VBNB) { + require(params.collateralToken == WBNB, "vBNB requires WBNB"); + } - revert("CharonLiquidator: executeLiquidation not yet implemented"); + // ── Encode params and request the flash loan ────────────────────────── + // Aave forwards `encoded` verbatim to executeOperation as the `data` + // argument; we decode it there to recover the liquidation parameters. + bytes memory encoded = abi.encode(params); + + IAaveV3Pool(AAVE_POOL) + .flashLoanSimple( + address(this), // receiver — this contract implements the callback + params.debtToken, // asset — the token we need to repay Venus with + params.repayAmount, // amount — exact principal to borrow + encoded, // params — forwarded to executeOperation + 0 // referralCode — no referral + ); + // Aave has pulled amount + premium via the allowance set in executeOperation. + // Nothing further to do in this frame. } // ───────────────────────────────────────────────────────────────────────── @@ -171,37 +271,158 @@ contract CharonLiquidator is IFlashLoanSimpleReceiver { /// @notice Aave V3 flash-loan callback. Called by the Pool immediately after /// transferring `amount` of `asset` to this contract. - /// @dev Two security gates are checked before any logic runs: + /// @dev Security gates (preserved from skeleton): /// 1. msg.sender == AAVE_POOL — only the genuine Aave Pool may call this. /// 2. initiator == address(this) — only flash loans we ourselves initiated. - /// Both checks together prevent any external actor from using our callback - /// as a weapon (e.g., to drain approved allowances). /// - /// Full implementation (decode params, liquidate Venus, swap, repay): - /// see issue #12. + /// Full liquidation flow: + /// a. Decode LiquidationParams from `data`. + /// b. Sanity-check asset/amount match decoded params. + /// c. Approve debtVToken and call liquidateBorrow on Venus. + /// d. Zero out debtVToken approval (consumed). + /// e. Redeem all seized collateral vTokens for underlying. + /// If the seized vToken is vBNB, wrap the returned native BNB into WBNB. + /// f. Swap collateral underlying → debt token via PancakeSwap V3 at the + /// caller-supplied pool fee tier. + /// g. Zero out SwapRouter approval (consumed). + /// h. Verify post-swap balance covers totalOwed. + /// i. Sweep profit to COLD_WALLET (NEVER to the hot wallet / owner). + /// j. Emit LiquidationExecuted. + /// k. Approve Aave Pool for totalOwed (Aave pulls this after we return). + /// l. Return true. + /// + /// NOTE: nonReentrant is intentionally NOT applied here. Applying it would + /// deadlock the flash loan because executeLiquidation already holds the lock + /// (_entered == 2) when Aave re-enters this callback within the same tx. + /// The msg.sender == AAVE_POOL gate is the equivalent protection. /// - /// @dev Parameters: (asset, amount, premium, initiator, data) — see IFlashLoanSimpleReceiver. - /// `asset`, `amount`, `premium`, and `data` are unnamed in this skeleton to suppress - /// unused-variable compiler warnings; they will be named and consumed in issue #12. - /// `initiator` is named because the security gate reads it. - /// @return True on success (unreachable in skeleton — revert fires first). + /// @param asset The flash-loaned ERC-20 token (must equal p.debtToken). + /// @param amount The flash-loan principal (must equal p.repayAmount). + /// @param premium The Aave fee owed on top of `amount`. + /// @param initiator The address that initiated the flash loan (must be address(this)). + /// @param data ABI-encoded LiquidationParams forwarded from executeLiquidation. + /// @return True on success; Aave reverts the entire tx if false is returned. function executeOperation( - address, /* asset — flash-loaned ERC-20; used in issue #12 */ - uint256, /* amount — flash-loan principal; used in issue #12 */ - uint256, /* premium — Aave fee; used in issue #12 */ + address asset, + uint256 amount, + uint256 premium, address initiator, - bytes calldata /* data — ABI-encoded LiquidationParams; used in issue #12 */ - ) - external - override - returns (bool) - { - // Security gate 1: only the real Aave Pool can invoke this callback. + bytes calldata data + ) external override returns (bool) { + // ── Security gates (from skeleton — do not remove) ──────────────────── + // Gate 1: only the real Aave Pool can invoke this callback. require(msg.sender == AAVE_POOL, "!pool"); - // Security gate 2: we only process flash loans we ourselves requested. + // Gate 2: we only process flash loans we ourselves requested. require(initiator == address(this), "!initiator"); - revert("CharonLiquidator: executeOperation not yet implemented"); + // ── Step 1: decode parameters ───────────────────────────────────────── + LiquidationParams memory p = abi.decode(data, (LiquidationParams)); + + // ── Step 2: sanity — confirm Aave gave us exactly what we asked for ─── + // These checks catch any pool-side discrepancy and validate that the + // encoded params are consistent with the actual flash-loan terms. + require(asset == p.debtToken, "asset/debt mismatch"); + require(amount == p.repayAmount, "amount/repay mismatch"); + + // ── Step 3: liquidate on Venus ──────────────────────────────────────── + // Approve the debt vToken to spend exactly repayAmount of the debt asset. + // Venus pulls this during liquidateBorrow; approval is zeroed immediately + // after to eliminate lingering allowances. + IERC20(p.debtToken).approve(p.debtVToken, p.repayAmount); + + uint256 liqErr = IVToken(p.debtVToken) + .liquidateBorrow( + p.borrower, + p.repayAmount, + p.collateralVToken // seized vTokens land in address(this) + ); + require(liqErr == 0, "venus liquidate failed"); + + // Zero out vToken approval — liquidateBorrow has consumed it. + // Protects against a malicious or re-upgraded vToken contract + // attempting a second pull in future calls. + IERC20(p.debtToken).approve(p.debtVToken, 0); + + // ── Step 4: redeem seized collateral vTokens for underlying ─────────── + // balanceOf gives us the exact vToken units seized by liquidateBorrow. + // We use redeem(vTokenAmount) rather than redeemUnderlying(underlyingAmount) + // to drain the full balance in one call without rounding loss. + uint256 vBal = IVToken(p.collateralVToken).balanceOf(address(this)); + require(vBal > 0, "no collateral seized"); + + uint256 redeemErr = IVToken(p.collateralVToken).redeem(vBal); + require(redeemErr == 0, "venus redeem failed"); + + // vBNB returns NATIVE BNB, not an ERC-20. Wrap the full native balance + // into WBNB so the swap leg can treat it uniformly with every other + // vToken underlying. Reading IERC20(vBNB-underlying).balanceOf would + // return zero and the swap would revert. Wrapping before the balance + // read ensures `collateralBal` picks up the full seized amount. + if (p.collateralVToken == VBNB) { + uint256 nativeBal = address(this).balance; + require(nativeBal > 0, "no native BNB redeemed"); + IWETH(WBNB).deposit{ value: nativeBal }(); + } + + // ── Step 5: swap collateral underlying → debt token via PancakeSwap V3 ─ + // Read the full collateral balance just redeemed (or wrapped, for vBNB); + // use it as exact amountIn. + uint256 collateralBal = IERC20(p.collateralToken).balanceOf(address(this)); + + // Approve the router for the exact amount we are about to swap. + IERC20(p.collateralToken).approve(SWAP_ROUTER, collateralBal); + + ISwapRouter(SWAP_ROUTER) + .exactInputSingle( + ISwapRouter.ExactInputSingleParams({ + tokenIn: p.collateralToken, + tokenOut: p.debtToken, + fee: p.swapPoolFee, // caller-supplied — 500 / 2500 / 3000 / 10000 depending on pool + recipient: address(this), + deadline: block.timestamp, + amountIn: collateralBal, + amountOutMinimum: p.minSwapOut, // router reverts if output < this + sqrtPriceLimitX96: 0 // no price limit — slippage floor above is enough + }) + ); + + // Zero out router approval — exactInputSingle has consumed it. + IERC20(p.collateralToken).approve(SWAP_ROUTER, 0); + + // ── Step 6: verify post-swap balance covers repayment ───────────────── + uint256 totalOwed = amount + premium; + uint256 finalBal = IERC20(p.debtToken).balanceOf(address(this)); + + // Defensive check on top of the router's amountOutMinimum guard: + // ensures the contract cannot accidentally under-repay Aave even if + // minSwapOut was set below totalOwed by the caller. + require(finalBal >= totalOwed, "swap output below repayment"); + + // ── Step 7: sweep profit to COLD WALLET ─────────────────────────────── + // Profit must leave this contract to the cold wallet (NOT the hot-wallet + // owner) before we approve Aave. This enforces the CLAUDE.md safety + // invariant: hot wallet holds gas only. Sweeping before approval also + // prevents Aave from pulling more than totalOwed if the debt token has + // quirks (fee-on-transfer, rebasing, etc.). + uint256 profit = finalBal - totalOwed; + if (profit > 0) { + // transfer return value not checked: COLD_WALLET is a trusted address + // set at construction; a failure here reverts the whole tx (excess + // funds stay in the contract until rescued). Standard ERC-20s revert + // on failure. + IERC20(p.debtToken).transfer(COLD_WALLET, profit); + } + + // ── Step 8: emit before the final approval so logs reflect the full state ─ + emit LiquidationExecuted(p.borrower, p.debtToken, p.repayAmount, profit, COLD_WALLET); + + // ── Step 9: approve Aave to pull totalOwed ──────────────────────────── + // Aave pulls amount + premium from this contract after executeOperation + // returns true. We set approval here; Aave consumes it entirely, so + // there is no practical way to zero it out post-return in this call frame. + IERC20(p.debtToken).approve(AAVE_POOL, totalOwed); + + return true; } // ───────────────────────────────────────────────────────────────────────── @@ -217,8 +438,8 @@ contract CharonLiquidator is IFlashLoanSimpleReceiver { /// - onlyOwner: only the hot wallet can pull funds. /// - `to` is validated non-zero to prevent burning. /// - Uses IERC20.transfer directly (no SafeERC20) because this is a - /// skeleton with no OZ dependency; full impl (#12) should assess - /// whether fee-on-transfer tokens need special handling here. + /// no-external-dependency build; fee-on-transfer tokens may transfer + /// less than `amount` — that edge case is acceptable in rescue context. /// - Native transfer uses `call` rather than `transfer` or `send`. /// Solidity's `transfer` forwards a hard-coded 2300-gas stipend which /// reverts against any recipient whose fallback does non-trivial work diff --git a/contracts/src/interfaces/ISwapRouter.sol b/contracts/src/interfaces/ISwapRouter.sol new file mode 100644 index 0000000..d7439ad --- /dev/null +++ b/contracts/src/interfaces/ISwapRouter.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +/// @title ISwapRouter +/// @notice Minimal interface for the PancakeSwap V3 SwapRouter on BNB Chain. +/// @dev PancakeSwap V3 is a fork of Uniswap V3; the SwapRouter ABI is identical. +/// Only exactInputSingle is needed by CharonLiquidator v0.1 — the single-hop +/// swap from seized collateral back into the debt token. +/// BNB Chain mainnet SwapRouter: 0x13f4EA83D0bd40E75C8222255bc855a974568Dd4 +interface ISwapRouter { + /// @notice Parameters for a single-pool exact-input swap. + /// @dev Mirrors IV3SwapRouter.ExactInputSingleParams from Uniswap V3 / PCS V3. + struct ExactInputSingleParams { + /// @dev Token being sold (the collateral underlying recovered after redemption). + address tokenIn; + /// @dev Token being bought (the debt token needed to repay Aave). + address tokenOut; + /// @dev Pool fee tier in hundredths of a bip (e.g. 3000 = 0.30 %). + uint24 fee; + /// @dev Recipient of the output tokens. + address recipient; + /// @dev Unix timestamp after which the transaction reverts. + uint256 deadline; + /// @dev Exact amount of tokenIn to swap. + uint256 amountIn; + /// @dev Minimum acceptable amount of tokenOut; router reverts if not met. + uint256 amountOutMinimum; + /// @dev Square-root price limit in Q64.96 format. Pass 0 for no limit. + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another token. + /// @dev The router pulls `amountIn` from msg.sender (caller must pre-approve). + /// Reverts if the resulting output is below `params.amountOutMinimum`. + /// @param params The parameters for the swap, encoded as `ExactInputSingleParams`. + /// @return amountOut The amount of `tokenOut` received. + function exactInputSingle(ExactInputSingleParams calldata params) + external + payable + returns (uint256 amountOut); +} diff --git a/contracts/src/interfaces/IVToken.sol b/contracts/src/interfaces/IVToken.sol index 651b36d..874ed8c 100644 --- a/contracts/src/interfaces/IVToken.sol +++ b/contracts/src/interfaces/IVToken.sol @@ -29,6 +29,16 @@ interface IVToken { /// @return 0 on success, non-zero error code on failure. function redeemUnderlying(uint256 redeemAmount) external returns (uint256); + /// @notice Redeems exactly `redeemTokens` vTokens for the corresponding underlying asset. + /// @dev Compound V2 / Venus API. Transfers the caller's vTokens back to the protocol and + /// returns the proportional underlying. Returns an error code (0 = success). + /// CharonLiquidator uses this variant to drain the full seized vToken balance in one + /// call after liquidateBorrow(), avoiding any rounding that redeemUnderlying would + /// introduce when converting an inexact underlying amount to vTokens. + /// @param redeemTokens The number of vTokens to burn. + /// @return 0 on success, non-zero error code on failure. + function redeem(uint256 redeemTokens) external returns (uint256); + /// @notice Returns the vToken balance of `account`. /// @dev Used by rescue() to validate amounts before pulling vTokens out of the contract. /// @param account The address to query. diff --git a/contracts/src/interfaces/IWETH.sol b/contracts/src/interfaces/IWETH.sol new file mode 100644 index 0000000..60464be --- /dev/null +++ b/contracts/src/interfaces/IWETH.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +/// @title IWETH +/// @notice Minimal interface for the Wrapped BNB (WBNB) contract on BNB Chain. +/// @dev Canonical WBNB address on BSC mainnet: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c. +/// Identical ABI to canonical WETH9 — `deposit()` wraps native BNB into the ERC-20 +/// representation that DEX pools expect; `withdraw()` unwraps it back to native. +/// CharonLiquidator uses this exclusively on the vBNB redemption path: Venus +/// `vBNB.redeem()` returns native BNB, so the contract wraps the balance into +/// WBNB before forwarding to the PancakeSwap V3 router. +interface IWETH { + /// @notice Wraps the attached native BNB into an equivalent WBNB balance for msg.sender. + /// @dev MUST be called with non-zero `msg.value` — the contract mints 1:1 to the sender. + function deposit() external payable; + + /// @notice Unwraps `amount` WBNB of the caller back to native BNB. + /// @dev Reverts if the caller's WBNB balance is below `amount`. + /// @param amount The number of WBNB to unwrap. + function withdraw(uint256 amount) external; +}