diff --git a/packages/euler-v2-sdk/src/services/executionService/simulate.ts b/packages/euler-v2-sdk/src/services/executionService/simulate.ts index 635d744..23e37ca 100644 --- a/packages/euler-v2-sdk/src/services/executionService/simulate.ts +++ b/packages/euler-v2-sdk/src/services/executionService/simulate.ts @@ -89,6 +89,13 @@ const REWARD_STREAM_CLAIM_SELECTOR = toFunctionSelector( const REUL_UNLOCK_SELECTOR = toFunctionSelector( "function withdrawToByLockTimestamp(address,uint256,bool)", ); +// Swap verifier call used by wallet-receiving swaps (withdraw/redeem/wallet +// swaps): the swapped-in `asset` is transferred to the wallet. Its balance must +// be snapshotted so the swap output is visible to the wallet-balance display and +// to later wallet-sourced operations. +const SWAP_VERIFY_TRANSFER_SELECTOR = toFunctionSelector( + "function verifyAmountMinAndTransfer(address,address,uint256,uint256)", +); import { Account, type IAccount, @@ -149,6 +156,7 @@ import type { } from "../vaults/vaultMetaService/index.js"; import type { IWalletService } from "../walletService/index.js"; import { ethereumVaultConnectorAbi } from "./abis/ethereumVaultConnectorAbi.js"; +import { swapVerifierAbi } from "./abis/swapVerifierAbi.js"; import type { BatchItemDescription, EVCBatchItem, @@ -703,6 +711,31 @@ function collectClaimWalletBalanceTokens(batch: EVCBatchItem[]): Address[] { return [...tokens]; } +// Tokens a swap deposits straight into the wallet. A wallet-receiving swap +// (withdraw/redeem/wallet swap) ends with `verifyAmountMinAndTransfer(asset, …)`, +// transferring the swapped-in `asset` to the owner. That token is otherwise +// invisible to balance discovery — it is neither a touched vault's underlying nor +// a debited (requiredApproval) token — so its balance never gets snapshotted. +function collectSwapWalletBalanceTokens(batch: EVCBatchItem[]): Address[] { + const tokens = new Set
(); + + for (const item of batch) { + if (getSelector(item.data) !== SWAP_VERIFY_TRANSFER_SELECTOR) continue; + try { + const decoded = decodeFunctionData({ + abi: swapVerifierAbi, + data: item.data, + }); + if (decoded.functionName !== "verifyAmountMinAndTransfer") continue; + addWalletToken(tokens, decoded.args[0] as Address); + } catch { + // Unknown or malformed verifier calldata should not block simulation. + } + } + + return [...tokens]; +} + // Decode the lens-read block for a single layer into a populated Account plus // the EVault/EulerEarn entities observed at that point in the batch. async function decodeAccountSnapshot< @@ -1232,6 +1265,9 @@ async function buildSimulationBatch( for (const token of collectClaimWalletBalanceTokens(batch)) { addWalletToken(assetTokens, token); } + for (const token of collectSwapWalletBalanceTokens(batch)) { + addWalletToken(assetTokens, token); + } const unlockItems = batch.filter( (item) => getSelector(item.data) === REUL_UNLOCK_SELECTOR, diff --git a/packages/euler-v2-sdk/test/simulate.test.ts b/packages/euler-v2-sdk/test/simulate.test.ts index 3050b98..38b4ea0 100644 --- a/packages/euler-v2-sdk/test/simulate.test.ts +++ b/packages/euler-v2-sdk/test/simulate.test.ts @@ -608,6 +608,36 @@ test("simulateTransactionPlan tracks Merkl claim reward tokens", async () => { assert.ok(reads.has(getAddress(TOKEN))); }); +test("simulateTransactionPlan tracks swap-verifier wallet output tokens", async () => { + const plan: TransactionPlan = [ + { + type: "evcBatch", + items: [ + { + type: "operation", + name: "withdrawAndSwap", + items: [ + { + targetContract: TARGET, + onBehalfOfAccount: ACCOUNT, + value: 0n, + data: encodeFunctionData({ + abi: swapVerifierAbi, + functionName: "verifyAmountMinAndTransfer", + args: [TOKEN, ACCOUNT, 1n, 9999999999n], + }), + }, + ], + }, + ], + }, + ]; + + const reads = await simulateAndCollectWalletBalanceReads(plan); + + assert.ok(reads.has(getAddress(TOKEN))); +}); + test("simulateTransactionPlan tracks EUL balance for rEUL unlocks", async () => { const eulToken = getAddress("0x0000000000000000000000000000000000000017"); const plan: TransactionPlan = [