wp-evm-aave-v3 0.1.14

Aave v3 lending facade — per-chain market config + thin read wrappers
Documentation
//! Aave v3 protocol facade.
//!
//! Thin wrapper that bakes Aave v3's per-chain market addresses into the
//! `wp-evm-aave-v3` family read functions. Adding an Aave-v3 fork (Spark,
//! etc.) = copying this file and changing the addresses.

use alloy_primitives::{address, Address};
use alloy_provider::{network::Ethereum, Provider};
use anyhow::Result;
use wp_evm_aave_v3_provider as aave;
use wp_evm_base::chain::Chain;

// Re-export family types (through the provider's `data` re-export) so
// consumers only need `wp-evm-aave-v3`. A single `pub use` both brings the
// names into local scope (used by `CONFIG` and the fn signatures below) and
// re-exports them — no separate `use` line, no duplicate-import nit.
pub use wp_evm_aave_v3_provider::data::{
    AaveV3MarketConfig, ReserveState, SupplyParams, UserAccountData, WithdrawParams,
};

const MULTICALL3: Address = address!("cA11bde05977b3631167028862bE2a173976CA11");

/// Aave v3 Ethereum mainnet market configuration.
///
/// Addresses verified 2026-05-31 against bgd-labs/aave-address-book
/// (`AaveV3Ethereum`).
pub const CONFIG: AaveV3MarketConfig = AaveV3MarketConfig {
    addresses_provider: address!("2f39d218133AFaB8F2B819B1066c7E434Ad94E9e"),
    pool: address!("87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"),
    protocol_data_provider: address!("0a16f2FCC0D44FaE41cc54e079281D84A363bECD"),
    multicall: MULTICALL3,
};

/// Aave v3 market config shared by Arbitrum, Optimism, Polygon, and
/// Avalanche — these four were deployed deterministically to one address
/// set. Addresses verified 2026-05-31 against bgd-labs/aave-address-book.
const SHARED_L2: AaveV3MarketConfig = AaveV3MarketConfig {
    addresses_provider: address!("a97684ead0e402dC232d5A977953DF7ECBaB3CDb"),
    pool: address!("794a61358D6845594F94dc1DB02A252b5b4814aD"),
    protocol_data_provider: address!("243Aa95cAC2a25651eda86e80bEe66114413c43b"),
    multicall: MULTICALL3,
};

/// Aave v3 Arbitrum One market config.
pub const CONFIG_ARBITRUM: AaveV3MarketConfig = SHARED_L2;
/// Aave v3 Optimism market config.
pub const CONFIG_OPTIMISM: AaveV3MarketConfig = SHARED_L2;
/// Aave v3 Polygon PoS market config.
pub const CONFIG_POLYGON: AaveV3MarketConfig = SHARED_L2;
/// Aave v3 Avalanche C-Chain market config.
pub const CONFIG_AVALANCHE: AaveV3MarketConfig = SHARED_L2;

/// Aave v3 Base market config (its own deployment — distinct addresses).
/// Addresses verified 2026-05-31 against bgd-labs/aave-address-book.
pub const CONFIG_BASE: AaveV3MarketConfig = AaveV3MarketConfig {
    addresses_provider: address!("e20fCBdBfFC4Dd138cE8b2E6FBb6CB49777ad64D"),
    pool: address!("A238Dd80C259a72e81d7e4664a9801593F98d1c5"),
    protocol_data_provider: address!("0F43731EB8d45A581f4a36DD74F5f358bc90C73A"),
    multicall: MULTICALL3,
};

/// Pick the Aave v3 market config for a curated chain.
///
/// Returns `None` for chains without a curated Aave v3 deployment
/// (BSC/Celo not yet added; Sonic/HyperEVM have no Aave v3).
pub fn config_for_chain(chain: Chain) -> Option<&'static AaveV3MarketConfig> {
    match chain {
        Chain::Ethereum => Some(&CONFIG),
        Chain::Arbitrum => Some(&CONFIG_ARBITRUM),
        Chain::Optimism => Some(&CONFIG_OPTIMISM),
        Chain::Polygon => Some(&CONFIG_POLYGON),
        Chain::Avalanche => Some(&CONFIG_AVALANCHE),
        Chain::Base => Some(&CONFIG_BASE),
        Chain::Bsc | Chain::Sonic | Chain::Celo | Chain::HyperEvm => None,
    }
}

/// Hydrate a reserve's state on the given chain. Returns an error if the
/// chain has no curated Aave v3 config yet.
pub async fn reserve_state<P: Provider<Ethereum>>(
    provider: &P,
    chain: Chain,
    asset: Address,
) -> Result<ReserveState> {
    let cfg = config_for_chain(chain)
        .ok_or_else(|| anyhow::anyhow!("no Aave v3 config for chain {}", chain.name()))?;
    aave::hydrate::reserve_state(provider, cfg.multicall, cfg.pool, asset).await
}

/// Hydrate an account's aggregate health on the given chain.
pub async fn user_account_data<P: Provider<Ethereum>>(
    provider: &P,
    chain: Chain,
    user: Address,
) -> Result<UserAccountData> {
    let cfg = config_for_chain(chain)
        .ok_or_else(|| anyhow::anyhow!("no Aave v3 config for chain {}", chain.name()))?;
    aave::hydrate::user_account_data(provider, cfg.multicall, cfg.pool, user).await
}

/// Build a `PlanFragment` for an Aave v3 `supply` on the given chain.
pub fn plan_supply(
    chain: Chain,
    params: &SupplyParams,
) -> Result<wp_evm_aave_v3_provider::data::PlanFragment> {
    let cfg = config_for_chain(chain)
        .ok_or_else(|| anyhow::anyhow!("no Aave v3 config for chain {}", chain.name()))?;
    Ok(wp_evm_aave_v3_provider::plan::plan_supply(params, cfg.pool))
}

/// Build a `PlanFragment` for an Aave v3 `withdraw` on the given chain.
pub fn plan_withdraw(
    chain: Chain,
    params: &WithdrawParams,
) -> Result<wp_evm_aave_v3_provider::data::PlanFragment> {
    let cfg = config_for_chain(chain)
        .ok_or_else(|| anyhow::anyhow!("no Aave v3 config for chain {}", chain.name()))?;
    Ok(wp_evm_aave_v3_provider::plan::plan_withdraw(params, cfg.pool))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ethereum_config_addresses_are_set() {
        let cfg = config_for_chain(Chain::Ethereum).expect("ethereum config");
        assert_eq!(cfg.pool, address!("87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"));
        assert_eq!(cfg.multicall, MULTICALL3);
    }

    #[test]
    fn shared_l2_chains_use_canonical_pool() {
        // Arbitrum / Optimism / Polygon / Avalanche share one deterministic
        // Aave v3 deployment.
        for chain in [Chain::Arbitrum, Chain::Optimism, Chain::Polygon, Chain::Avalanche] {
            let cfg = config_for_chain(chain).unwrap_or_else(|| panic!("config for {chain}"));
            assert_eq!(
                cfg.pool,
                address!("794a61358D6845594F94dc1DB02A252b5b4814aD"),
                "wrong pool for {chain}"
            );
            assert_eq!(
                cfg.addresses_provider,
                address!("a97684ead0e402dC232d5A977953DF7ECBaB3CDb"),
                "wrong addresses_provider for {chain}"
            );
            assert_eq!(cfg.multicall, MULTICALL3, "wrong multicall for {chain}");
        }
    }

    #[test]
    fn base_has_its_own_deployment() {
        let cfg = config_for_chain(Chain::Base).expect("base config");
        assert_eq!(cfg.pool, address!("A238Dd80C259a72e81d7e4664a9801593F98d1c5"));
        assert_eq!(cfg.addresses_provider, address!("e20fCBdBfFC4Dd138cE8b2E6FBb6CB49777ad64D"));
        assert_eq!(cfg.multicall, MULTICALL3);
    }

    #[test]
    fn chains_without_aave_v3_return_none() {
        // No Aave v3 deployment / not yet curated.
        for chain in [Chain::Bsc, Chain::Sonic, Chain::Celo, Chain::HyperEvm] {
            assert!(config_for_chain(chain).is_none(), "{chain} must be None");
        }
    }

    #[test]
    fn supply_plan_uses_ethereum_pool() {
        use alloy_primitives::{address, U256};
        let f = plan_supply(
            Chain::Ethereum,
            &SupplyParams {
                asset: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
                amount: U256::from(100u64),
                on_behalf_of: address!("000000000000000000000000000000000000dEaD"),
            },
        )
        .expect("ethereum supply plan");
        assert_eq!(f.calls[0].target, CONFIG.pool);
        assert_eq!(f.approvals[0].spender, CONFIG.pool);
    }

    #[test]
    fn supply_plan_unsupported_chain_errors() {
        use alloy_primitives::{address, U256};
        let err = plan_supply(
            Chain::Bsc,
            &SupplyParams {
                asset: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
                amount: U256::from(1u64),
                on_behalf_of: address!("000000000000000000000000000000000000dEaD"),
            },
        )
        .unwrap_err();
        assert!(format!("{err:#}").contains("no Aave v3 config for chain bsc"));
    }
}