Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/euler-v2-sdk/src/services/executionService/simulate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Address>();

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<
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions packages/euler-v2-sdk/test/simulate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down