Aave — Part IX: Execution Contract
Getting AI To Do The Heavy Lifting
With user data ready, we can turn our concentration to executing the liquidations.
I have built many execution contracts before for various projects (search the archives if you’d like a refresher on them).
They all have a similar structure:
Validate that the caller is authorized, then process their inputs
Record the starting balance
Perform a sequence of validated actions across a set of smart contracts
Record the final balance, reverting if it has decreased
This is a well-understood pattern with plenty of examples, which is an ideal setup for agentic AI. My last entry on agentic coding was well-received, so I expect this will be useful too.
Agent Setup
For this effort, I created a new project in directory ~/code/aave_v3_liquidator and created a minimal Python 3.12 virtual environment using uv with these packages:
eth-apewith therecommended-pluginsextradegenbot0.6.0a2vyper0.4.3
I used Pi version 0.66.1, calling Kimi K2.5 through Synthetic.
Session Transcript
You can review the complete transcript if you’re curious.
Let’s break down the session with some context to help read between the lines:
The Task
I started the session with a task definition:
Help me prepare a plan for this project: create a Vyper smart contract that can execute an atomic Aave V3 liquidation against a given user. It will use the Uniswap V4 PoolManager as its "bank" and "market", i.e. it should perform an
unlock, then within the V4 callback, execute aliquidationCallat the Aave V3 Pool and aswapat the V4 PoolManager to handle these operations: a withdrawal of the debt token (sent to the Aave V3 Pool), a deposit of the collateral token (received from the Aave liquidation), and optionally another swap to consolidate profit in a given token. There are some subtleties regarding execution order that we will have to study, but I can retrieve contract source code as needed.
Then it thought for a bit, proposed a design, and gave a summary:
Next steps:
I can retrieve the Aave V3 Pool source code to verify
liquidationCallreturn valuesI can retrieve Uniswap V4 PoolManager interface to confirm
unlockCallbacksignatureDo you have a specific network in mind (Ethereum mainnet, testnet)?
Would you like me to start implementing the contract, or should we fetch and analyze the dependency contracts first to nail down the exact interfaces?
So I gave it some more context:
We are working on Ethereum mainnet. Retrieve the Aave V3 Pool source code by using
cast. First retrieve the implementation address from the proxy contract at 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2, then get its source. The Uniswap V4 PoolManager contract is not a proxy, so retrieve its source directly at 0x000000000004444c5dc75cB358380D2e3dE08A90
It read the cast skill to learn how to use Foundry correctly, then downloaded the contract source code for the Aave V3 Pool implementation contract at 0x8147b99df7672a21809c9093e6f6ce1a60f119bd and the Uniswap V4 PoolManager. It read through them to discover the execution flow for the key functions, and then summarized the operations:
I’ve retrieved both contracts. Here are the key findings:
Summary of Retrieved Contracts
Aave V3 Pool (Proxy:
0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2)
Implementation:
0x8147b99df7672a21809c9093e6f6ce1a60f119bdKey Function:
function liquidationCall( address collateralAsset, address debtAsset, address borrower, uint256 debtToCover, bool receiveAToken // false = receive underlying collateral ) external;
No return value - collateral is transferred directly to
msg.senderUniswap V4 PoolManager (
0x000000000004444c5dc75cB358380D2e3dE08A90)
Key Interface:
function unlock(bytes calldata data) external returns (bytes memory); // Callback that MUST be implemented interface IUnlockCallback { function unlockCallback(bytes calldata data) external returns (bytes memory); } function take(Currency currency, address to, uint256 amount) external; function settle() external payable returns (uint256 paid); function settleFor(address recipient) external payable returns (uint256 paid); function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData) external returns (BalanceDelta swapDelta); struct SwapParams { bool zeroForOne; int256 amountSpecified; // negative = exact input, positive = exact output uint160 sqrtPriceLimitX96; }Updated Execution Plan
Based on the source code, here’s the refined execution flow:
┌──────────────────┐ │ unlock(data) │ │ (caller = us) │ └────────┬─────────┘ ↓ ┌──────────────────┐ take(debtAsset, amount) ┌─────────────┐ │ unlockCallback() │ ───────────────────────────────→ │ PoolManager│ │ (us via PM) │ │ │ └────────┬─────────┘ └─────────────┘ ↓ ┌──────────────────┐ │approve(debtAsset)│ → liquidationCall(...) → receive collateral │ (to Aave Pool) │ └────────┬─────────┘ ↓ ┌──────────────────┐ Optional: swap collateral→profit │ Optional swap │ ───────────────────────────────────────────────→ │ (if consolidate) │ └────────┬─────────┘ ↓ ┌──────────────────┐ swap/debt operations to acquire debtAsset │ Acquire debt │ ───────────────────────────────────────────────→ │ for repayment │ └────────┬─────────┘ ↓ ┌──────────────────┐ settle() - repay flash borrow │ settle() │ ───────────────────────────────────────────────→ │ (debtAsset) │ └──────────────────┘ ↓ Validation: All deltas must be zero ↓ Return profit to callerKey Implementation Details
1. Execution Order (Critical)
# Inside unlockCallback: 1. take(debtAsset, self, debt_amount) # Create liability 2. approve(debtAsset, aave_pool, debt_amount) 3. liquidationCall(...) # Receive collateral 4. [Optional] swap(collateral→profit) # Consolidate 5. swap(profit→debtAsset) or take # Acquire repayment funds 6. settle(debtAsset) # Clear liability2. Vyper Interface Definitions
# Aave V3 Pool interface IPool: def liquidationCall( collateralAsset: address, debtAsset: address, borrower: address, debtToCover: uint256, receiveAToken: bool ): nonpayable # Uniswap V4 PoolManager interface IPoolManager: def unlock(data: Bytes[1024]) -> Bytes[1024]: nonpayable def take(currency: address, to: address, amount: uint256): nonpayable def settle() -> uint256: payable def swap( key: PoolKey, params: SwapParams, hookData: Bytes[256] ) -> BalanceDelta: nonpayable # Must implement interface IUnlockCallback: def unlockCallback(data: Bytes[1024]) -> Bytes[1024]: nonpayable3. Critical Subtleties
Callback validation: Check
msg.sender == pool_managerinsideunlockCallbackPoolKey ordering: V4 requires
currency0 < currency1; need sorting helperBalanceDelta: Track swap returns - negative means received, positive means owed
Token approvals: Infinite approve Aave Pool and PoolManager in constructor
Native ETH: Use WETH for ETH positions; PoolManager handles via Currency type
4. Profit Flow
Debt tokens are
takenfrom PoolManager (creates liability)Liquidation repays debt, receives collateral (+ liquidation bonus)
Optional: Swap collateral for preferred profit token
Acquire debt tokens to
settlethe liabilityRemaining tokens = profit → transfer to caller
Would you like me to proceed with implementing the Vyper contract now, or do you need any clarification on the execution flow?
Most models are trained to be helpful and maintain velocity, so you’ll notice that it “wants” to guide us down the route of progress: either begin implementing the contract immediately, or offer explanation of its proposed design.
Here is the point where things could go off the rails. There are a lot of assumptions baked into the proposed design, and I like to bring them to the surface.
So I choose a third route:


