Skip to content

feat(contracts): full Aave + Venus + PancakeSwap liquidation flow#37

Merged
obchain merged 7 commits intomainfrom
feat/12-charon-liquidator-full
Apr 24, 2026
Merged

feat(contracts): full Aave + Venus + PancakeSwap liquidation flow#37
obchain merged 7 commits intomainfrom
feat/12-charon-liquidator-full

Conversation

@obchain
Copy link
Copy Markdown
Owner

@obchain obchain commented Apr 21, 2026

Closes #13

Fills the two stub bodies in CharonLiquidator.sol so the contract performs a complete atomic liquidation. Skeleton signatures, storage layout, and event surface are unchanged.

  • executeLiquidation body: nonReentrant guard; ABI-encodes LiquidationParams and calls IAaveV3Pool.flashLoanSimple(self, debtToken, repayAmount, encoded, 0)
  • executeOperation body (runs inside Aave callback):
    1. Decode params, cross-check asset == debtToken and amount == repayAmount
    2. Approve debtVToken for repayAmount
    3. IVToken.liquidateBorrow(borrower, repayAmount, collateralVToken) — error code 0 required
    4. IVToken(collateralVToken).redeem(vBal) — pulls underlying collateral
    5. Approve PancakeSwap V3 router; exactInputSingle swap collateral → debtToken with amountOutMinimum = minSwapOut
    6. Final balance check: finalBal >= amount + premium (backstop)
    7. Sweep profit = finalBal - totalOwed to owner before returning
    8. Approve Aave for totalOwed, emit LiquidationExecuted, return true
  • Approvals zeroed after consumption for debtVToken and the swap router
  • New interfaces/ISwapRouter.sol (PancakeSwap V3 = Uniswap V3 ABI); IVToken extended with redeem(uint256)
  • Reentrancy guard on executeLiquidation only — the callback can't deadlock

No external libs (no OZ / aave-core). forge build + forge fmt --check both clean.

Depends on #12 (feat/11-foundry-skeleton).

…oses #12)

Fills the two stub bodies in `CharonLiquidator.sol` so the contract
performs a complete atomic liquidation. The skeleton's signatures,
storage layout, and event surface are unchanged — Rust callers and
deploy scripts written against #11 keep working.

- `executeLiquidation` body:
    - `nonReentrant` modifier added (1/2 storage flag, gas-cheap)
    - ABI-encodes `LiquidationParams` and calls
      `IAaveV3Pool.flashLoanSimple(self, debtToken, repayAmount,
      encoded, 0)`. The actual liquidation runs inside Aave's
      callback into `executeOperation`.

- `executeOperation` body, in order:
    1. Decode params, cross-check `asset == debtToken` and
       `amount == repayAmount`
    2. Approve `debtVToken` for `repayAmount`
    3. `IVToken.liquidateBorrow(borrower, repayAmount, collateralVToken)`
       — error code 0 required
    4. `IVToken(collateralVToken).redeem(vBal)` — pulls underlying
       collateral; non-zero error → revert
    5. Approve PancakeSwap V3 router for the seized underlying
    6. `ISwapRouter.exactInputSingle(...)` swap
       collateral → debtToken with `amountOutMinimum = minSwapOut`
    7. Final balance check: `finalBal >= amount + premium` — backstop
       in case the router doesn't catch it
    8. Sweep `profit = finalBal - totalOwed` to `owner` BEFORE
       returning so Aave only pulls exactly what it's owed
    9. Approve Aave for `totalOwed`, emit `LiquidationExecuted`,
       return `true`

- Approvals are zeroed after consumption for `debtVToken` and the
  swap router; the Aave allowance is fully consumed by the same-tx
  `transferFrom` so it can't leak.

- New `interfaces/ISwapRouter.sol` mirrors the Uniswap V3 surface
  PancakeSwap V3 implements — minimal `ExactInputSingleParams`
  struct + `exactInputSingle`. `interfaces/IVToken.sol` extended
  with the Compound-V2 `redeem(uint256)` variant required by step 4.

- Reentrancy guard sits only on `executeLiquidation`, not
  `executeOperation` — the callback runs inside the same call frame
  so adding it there would deadlock; the `msg.sender == AAVE_POOL`
  gate already blocks external entry.

- No external libs (still no OZ / aave-core). All imports stay
  inline. `forge build` + `forge fmt --check` both clean.

Tests + fork tests + deploy script land in #22 and a follow-up.
obchain added 5 commits April 23, 2026 13:07
BNB Chain has not adopted the Shanghai hard fork and rejects the
PUSH0 opcode. solc >= 0.8.20 emits PUSH0 by default, so contracts
compiled with the prior config would revert on first touch when
deployed to BSC. pinning the target to paris keeps our bytecode
compatible with the live chain until BSC enables Shanghai.

closes #118
drop the floating `^0.8.24` caret across CharonLiquidator.sol and the
five inline interfaces. pins deployed-bytecode producing solc to a
single known version and removes risk of a future 0.8.x patch release
subtly changing codegen, optimizer behaviour or metadata hash.

closes #119
hot wallet (owner) holds gas only — profit must exit the bot's
operational key perimeter inside the same atomic flash-loan frame.
the prior sweep targeted `owner`, violating the CLAUDE.md safety
invariant and exposing accumulated profit to any compromise of the
scanner/executor hot key.

add an immutable `COLD_WALLET` address set at construction, validate
it non-zero, and transfer `profit` to it inside executeOperation
before the Aave repayment approval. extend LiquidationExecuted with
an indexed `recipient` so off-chain monitors can filter by cold
wallet. constructor signature gains a third arg `_coldWallet`.

closes #120
vBNB is the only Venus vToken whose underlying is native BNB. its
`redeem()` transfers native BNB to msg.sender via a low-level call
rather than emitting an IERC20.transfer, so the existing
`IERC20(collateralToken).balanceOf(address(this))` read returned
zero after redemption and the PancakeSwap V3 swap leg reverted with
zero amountIn, stranding every seized vBNB position.

add an IWETH interface and hard-code the BSC mainnet vBNB and WBNB
addresses as internal constants. after redeem(), if the seized
vToken is vBNB, wrap address(this).balance into WBNB via
IWETH.deposit before the balance-of read. require callers that seize
vBNB to declare WBNB as collateralToken so the swap routes through a
real pool.

closes #121
…able

the swap leg hard-coded `fee: 3000` (0.30 %). that tier does not
exist or has near-zero liquidity for several Venus-collateral pairs
on PCS V3: BTCB/USDT sits in the 0.05 % (500) pool, ETH/USDT in the
0.01 % (100) pool, XVS/WBNB in the 1 % (10000) pool. routing those
through the 0.30 % pool would revert on `SPL` or eat unbounded
slippage.

add `uint24 swapPoolFee` to LiquidationParams, validate non-zero in
executeLiquidation, and pass it to ExactInputSingleParams.fee. the
off-chain opportunity router now selects the deepest pool per pair.

abi layout note: this extends LiquidationParams with a new tail
field. the companion Rust `LiquidationParams` builder in the
charon-executor crate (not yet present on this branch — lands with
PR #41) must mirror the added field.

closes #122
@obchain obchain changed the base branch from feat/11-foundry-skeleton to main April 24, 2026 10:42
…ator-full

# Conflicts:
#	contracts/src/CharonLiquidator.sol
@obchain obchain merged commit a7bfcbe into main Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[contracts] CharonLiquidator: Aave flashLoanSimple + Venus liquidate + PancakeSwap V3 swap

1 participant