From d8ca5b594e872732aff5189875da8adb85073718 Mon Sep 17 00:00:00 2001 From: obchain Date: Sun, 3 May 2026 22:16:56 +0530 Subject: [PATCH 1/2] fix(flashloan): correct Aave V3 BSC PoolDataProvider + tolerate truncated getReserveData (closes #419) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related defects on the Aave V3 BSC flash-loan path that together silenced the only flash-loan source on every fork demo and live run: 1. `config/default.toml` and `config/fork.toml` baked the PoolDataProvider as 0x41393e5e337606dc3821075Af65AeE84D7688CBD, which has no code on BSC mainnet — `cast code` returns 0x at any block. Calls returned an empty buffer and surfaced as "buffer overrun" decode errors at every quote step. Sourced the correct address from IPoolAddressesProvider.getPoolDataProvider() on BSC: 0xc90Df74A7c16245c5F5C5870327Ceb38Fe5d5328. Updated both profiles with a comment pointing at the on-chain source-of-truth so future drift is easier to spot. 2. With the right data provider in place, `getReserveData` returns 12 fields on BSC, not the 15 our `sol!` ABI declares — newer-Aave fields (`accruedToTreasury`, `unbacked`, `isolationModeTotalDebt`) are absent on BSC's deploy. The strict typed decoder still rejects the truncated tuple, breaking every paused/frozen-bitmap probe. We only consume the leading `configuration` word for that check, so issue the call ourselves and lift the first 32 bytes via a new `read_uint256_first_word` helper. `getReserveConfigurationData` stays on the typed decode (its 10-tuple matches BSC). Local validation: replay against block 91323624 with the four documented seed borrowers now reaches the flash-loan quote stage without buffer-overrun warnings (build pending in companion test branch that also carries #418's vToken decoder relaxation). --- config/default.toml | 7 +++- config/fork.toml | 10 ++++-- crates/charon-flashloan/src/aave.rs | 53 ++++++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 8 deletions(-) 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..ec7c4fb 100644 --- a/crates/charon-flashloan/src/aave.rs +++ b/crates/charon-flashloan/src/aave.rs @@ -283,18 +283,63 @@ 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() + // + // 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(data.configuration) || bitmap_says_frozen(data.configuration) { + 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 { From e12f4bc9636482711b0bf6a65297a7fe59ced74b Mon Sep 17 00:00:00 2001 From: obchain Date: Sun, 3 May 2026 22:22:29 +0530 Subject: [PATCH 2/2] style(flashloan): cargo fmt aave.rs --- crates/charon-flashloan/src/aave.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/crates/charon-flashloan/src/aave.rs b/crates/charon-flashloan/src/aave.rs index ec7c4fb..852b042 100644 --- a/crates/charon-flashloan/src/aave.rs +++ b/crates/charon-flashloan/src/aave.rs @@ -293,14 +293,13 @@ impl AaveFlashLoan { // 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}")))?; + 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 }); } @@ -324,10 +323,7 @@ async fn read_uint256_first_word( let tx = TransactionRequest::default() .to(target) .input(calldata.into()); - let bytes = provider - .call(&tx) - .await - .context("eth_call failed")?; + 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={})",