diff --git a/config/default.toml b/config/default.toml index 71eb08a..c0e821c 100644 --- a/config/default.toml +++ b/config/default.toml @@ -75,7 +75,12 @@ chain = "bnb" pool = "0x6807dc923806fe8fd134338eabca509979a7e0cb" # Aave V3 PoolDataProvider on BSC — resolves aTokens and reserve # configuration bitmaps (paused / frozen flags) for the adapter. -data_provider = "0x41393e5e337606dc3821075Af65AeE84D7688CBD" +# Sourced from IPoolAddressesProvider.getPoolDataProvider() — the +# previous baked-in 0x41393e5e337606dc3821075Af65AeE84D7688CBD has no +# code on BSC mainnet, which made every Aave reserve probe revert with +# `buffer overrun` and silently disqualified the only flash-loan +# source. See #419. +data_provider = "0xc90Df74A7c16245c5F5C5870327Ceb38Fe5d5328" # Aave V3 IPoolAddressesProvider on BSC — `getPool()` is called at # startup and the result is compared against `pool` above. Mismatch # fails the boot before the bot can burn RPC budget on a stale pool. diff --git a/config/fork.toml b/config/fork.toml index a056d75..f95752b 100644 --- a/config/fork.toml +++ b/config/fork.toml @@ -72,9 +72,13 @@ comptroller = "0xfd36e2c2a6789db23113685031d7f16329158384" [flashloan.aave_v3_bsc] chain = "bnb" pool = "0x6807dc923806fe8fd134338eabca509979a7e0cb" -# Aave V3 PoolDataProvider on BSC — matches default.toml. Resolves -# aTokens and reserve configuration bitmaps for the adapter. -data_provider = "0x41393e5e337606dc3821075Af65AeE84D7688CBD" +# Aave V3 PoolDataProvider on BSC — resolves aTokens and reserve +# configuration bitmaps for the adapter. The previous value +# 0x41393e5e337606dc3821075Af65AeE84D7688CBD has no code on BSC +# mainnet (cast code returns 0x); the live address comes from +# IPoolAddressesProvider.getPoolDataProvider() and is the one below. +# See #419. +data_provider = "0xc90Df74A7c16245c5F5C5870327Ceb38Fe5d5328" # Aave V3 IPoolAddressesProvider — startup check is bypassed under # profile_tag = "fork" but kept here so the fork profile mirrors the # mainnet TOML structure 1:1. diff --git a/crates/charon-flashloan/src/aave.rs b/crates/charon-flashloan/src/aave.rs index ffec363..852b042 100644 --- a/crates/charon-flashloan/src/aave.rs +++ b/crates/charon-flashloan/src/aave.rs @@ -283,18 +283,59 @@ impl AaveFlashLoan { } // Paused is not exposed via the typed accessor, so read the // packed bitmap and check bit 60 ourselves. - let data = dp - .getReserveData(asset) - .call() - .await - .map_err(|e| FlashLoanError::rpc(format!("getReserveData: {e}")))?; - if bitmap_says_paused(data.configuration) || bitmap_says_frozen(data.configuration) { + // + // Aave V3 BSC's PoolDataProvider returns 12 fields here, not + // the 15 our `sol!` ABI declares (newer-Aave fields like + // `accruedToTreasury`, `unbacked`, `isolationModeTotalDebt` + // are absent on BSC's deploy). The strict typed decoder + // reports `buffer overrun while deserializing` on every + // quote, which silently disqualifies the only flash-loan + // source. We only need the leading `configuration` word for + // the paused/frozen bitmap check anyway, so issue the call + // ourselves and lift the first 32 bytes. See #419. + let configuration = read_uint256_first_word(self.provider.as_ref(), self.data_provider, { + IAaveV3DataProvider::getReserveDataCall { asset } + .abi_encode() + .into() + }) + .await + .map_err(|e| FlashLoanError::rpc(format!("getReserveData: {e}")))?; + if bitmap_says_paused(configuration) || bitmap_says_frozen(configuration) { return Err(FlashLoanError::ReservePaused { asset }); } Ok(()) } } +/// Issue an `eth_call` to `target` with `calldata` and decode the +/// first 32 bytes of the response as a `U256`. Used to read the +/// leading word of a tuple return when the on-chain ABI has more or +/// fewer fields than the workspace's `sol!` declaration would tolerate +/// — alloy's strict decoder rejects any mismatch with `buffer overrun` +/// or `ReserveMismatch`, but for paused/frozen-bitmap checks we only +/// need the first word and can safely drop the tail. See #419. +async fn read_uint256_first_word( + provider: &RootProvider, + target: Address, + calldata: alloy::primitives::Bytes, +) -> Result { + use alloy::rpc::types::TransactionRequest; + let tx = TransactionRequest::default() + .to(target) + .input(calldata.into()); + let bytes = provider.call(&tx).await.context("eth_call failed")?; + if bytes.len() < 32 { + anyhow::bail!( + "expected at least 32 bytes from tuple return, got {} (target={})", + bytes.len(), + target + ); + } + let mut buf = [0u8; 32]; + buf.copy_from_slice(&bytes[..32]); + Ok(U256::from_be_bytes(buf)) +} + /// Return true when bit `index` is set in the Aave packed /// configuration `U256`. fn bit_is_set(bitmap: U256, index: u32) -> bool {