From 0e5dbafb45e54710f18a88df062151390b3cdb49 Mon Sep 17 00:00:00 2001 From: Rubilmax Date: Fri, 27 Sep 2024 16:55:51 +0200 Subject: [PATCH] feat(blue-sdk-viem): add bundler --- .github/workflows/test.yml | 3 + packages/blue-sdk-ethers/src/errors.ts | 13 + .../blue-sdk-ethers/src/signatures/utils.ts | 3 +- packages/blue-sdk-viem-bundler/.env.example | 2 + .../blue-sdk-viem-bundler/hardhat.config.ts | 69 + packages/blue-sdk-viem-bundler/package.json | 82 + .../src/BundlerAction.ts | 1157 +++++ packages/blue-sdk-viem-bundler/src/abis.ts | 855 ++++ packages/blue-sdk-viem-bundler/src/errors.ts | 24 + .../src/helpers/actions.ts | 838 ++++ .../src/helpers/index.ts | 2 + .../src/helpers/operations.ts | 919 ++++ packages/blue-sdk-viem-bundler/src/index.ts | 7 + .../src/types/actions.ts | 285 ++ .../blue-sdk-viem-bundler/src/types/index.ts | 2 + .../src/types/operations.ts | 168 + .../tests/e2e/fixtures.ts | 60 + .../tests/e2e/helpers.ts | 185 + .../e2e/services/BundlerService.base.test.ts | 176 + .../services/BundlerService.ethereum.test.ts | 3995 +++++++++++++++++ .../tsconfig.build.json | 0 .../tsconfig.json | 0 .../CHANGELOG.md | 4 +- .../package.json | 2 +- .../src/SimulationState.ts | 0 .../src/errors.ts | 0 .../src/handlers/blue/accrueInterest.ts | 0 .../src/handlers/blue/borrow.ts | 0 .../src/handlers/blue/index.ts | 0 .../src/handlers/blue/repay.ts | 0 .../src/handlers/blue/setAuthorization.ts | 0 .../src/handlers/blue/supply.ts | 0 .../src/handlers/blue/supplyCollateral.ts | 0 .../src/handlers/blue/withdraw.ts | 0 .../src/handlers/blue/withdrawCollateral.ts | 0 .../src/handlers/dispatchers.ts | 0 .../src/handlers/erc20/approve.ts | 0 .../src/handlers/erc20/index.ts | 0 .../src/handlers/erc20/permit.ts | 0 .../src/handlers/erc20/permit2.ts | 0 .../src/handlers/erc20/transfer.ts | 0 .../src/handlers/erc20/transfer2.ts | 0 .../src/handlers/erc20/unwrap.ts | 0 .../src/handlers/erc20/wrap.ts | 0 .../src/handlers/index.ts | 0 .../src/handlers/metamorpho/accrueInterest.ts | 0 .../src/handlers/metamorpho/deposit.ts | 0 .../src/handlers/metamorpho/index.ts | 0 .../handlers/metamorpho/publicReallocate.ts | 0 .../src/handlers/metamorpho/reallocate.ts | 0 .../src/handlers/metamorpho/withdraw.ts | 0 .../src/handlers/types.ts | 0 .../src/hooks/index.ts | 0 .../src/hooks/useSimulationState.ts | 0 .../src/index.ts | 0 .../src/operations.ts | 0 .../tsconfig.build.json | 8 + .../blue-sdk-viem-simulation/tsconfig.json | 8 + packages/blue-sdk-viem/src/abis.ts | 31 - packages/blue-sdk-viem/src/fetch/Token.ts | 15 +- packages/blue-sdk-viem/src/index.ts | 2 + .../blue-sdk-viem/src/signatures/index.ts | 2 - .../blue-sdk-viem/src/signatures/manager.ts | 74 +- .../blue-sdk-viem/src/signatures/permit.ts | 167 +- .../blue-sdk-viem/src/signatures/permit2.ts | 177 +- .../blue-sdk-viem/src/signatures/types.ts | 20 - .../blue-sdk-viem/src/signatures/utils.ts | 14 - packages/blue-sdk/src/errors.ts | 12 - packages/morpho-ts/src/utils.ts | 22 + yarn.lock | 90 +- 70 files changed, 9164 insertions(+), 329 deletions(-) create mode 100644 packages/blue-sdk-ethers/src/errors.ts create mode 100644 packages/blue-sdk-viem-bundler/.env.example create mode 100644 packages/blue-sdk-viem-bundler/hardhat.config.ts create mode 100644 packages/blue-sdk-viem-bundler/package.json create mode 100644 packages/blue-sdk-viem-bundler/src/BundlerAction.ts create mode 100644 packages/blue-sdk-viem-bundler/src/abis.ts create mode 100644 packages/blue-sdk-viem-bundler/src/errors.ts create mode 100644 packages/blue-sdk-viem-bundler/src/helpers/actions.ts create mode 100644 packages/blue-sdk-viem-bundler/src/helpers/index.ts create mode 100644 packages/blue-sdk-viem-bundler/src/helpers/operations.ts create mode 100644 packages/blue-sdk-viem-bundler/src/index.ts create mode 100644 packages/blue-sdk-viem-bundler/src/types/actions.ts create mode 100644 packages/blue-sdk-viem-bundler/src/types/index.ts create mode 100644 packages/blue-sdk-viem-bundler/src/types/operations.ts create mode 100644 packages/blue-sdk-viem-bundler/tests/e2e/fixtures.ts create mode 100644 packages/blue-sdk-viem-bundler/tests/e2e/helpers.ts create mode 100644 packages/blue-sdk-viem-bundler/tests/e2e/services/BundlerService.base.test.ts create mode 100644 packages/blue-sdk-viem-bundler/tests/e2e/services/BundlerService.ethereum.test.ts rename packages/{blue-sdk-simulation => blue-sdk-viem-bundler}/tsconfig.build.json (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-bundler}/tsconfig.json (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/CHANGELOG.md (72%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/package.json (96%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/SimulationState.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/errors.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/blue/accrueInterest.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/blue/borrow.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/blue/index.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/blue/repay.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/blue/setAuthorization.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/blue/supply.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/blue/supplyCollateral.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/blue/withdraw.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/blue/withdrawCollateral.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/dispatchers.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/erc20/approve.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/erc20/index.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/erc20/permit.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/erc20/permit2.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/erc20/transfer.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/erc20/transfer2.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/erc20/unwrap.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/erc20/wrap.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/index.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/metamorpho/accrueInterest.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/metamorpho/deposit.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/metamorpho/index.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/metamorpho/publicReallocate.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/metamorpho/reallocate.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/metamorpho/withdraw.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/handlers/types.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/hooks/index.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/hooks/useSimulationState.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/index.ts (100%) rename packages/{blue-sdk-simulation => blue-sdk-viem-simulation}/src/operations.ts (100%) create mode 100644 packages/blue-sdk-viem-simulation/tsconfig.build.json create mode 100644 packages/blue-sdk-viem-simulation/tsconfig.json delete mode 100644 packages/blue-sdk-viem/src/signatures/types.ts delete mode 100644 packages/blue-sdk-viem/src/signatures/utils.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 51fbfc50..84f46437 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,9 @@ jobs: - name: blue-sdk-viem jest: false hardhat: true + - name: blue-sdk-viem-bundler + jest: false + hardhat: true - name: blue-sdk-ethers-liquidation jest: false hardhat: true diff --git a/packages/blue-sdk-ethers/src/errors.ts b/packages/blue-sdk-ethers/src/errors.ts new file mode 100644 index 00000000..a5d607dc --- /dev/null +++ b/packages/blue-sdk-ethers/src/errors.ts @@ -0,0 +1,13 @@ +import { Address } from "@morpho-org/blue-sdk"; + +export class InvalidSignatureError extends Error { + constructor( + public readonly hash: string, + public readonly signer: Address, + public readonly recovered: Address, + ) { + super( + `invalid signature for hash ${hash}: expected ${signer}, recovered ${recovered}`, + ); + } +} diff --git a/packages/blue-sdk-ethers/src/signatures/utils.ts b/packages/blue-sdk-ethers/src/signatures/utils.ts index 19e246fd..77454b18 100644 --- a/packages/blue-sdk-ethers/src/signatures/utils.ts +++ b/packages/blue-sdk-ethers/src/signatures/utils.ts @@ -8,8 +8,9 @@ import { recoverAddress, } from "ethers"; -import { Address, InvalidSignatureError } from "@morpho-org/blue-sdk"; +import { Address } from "@morpho-org/blue-sdk"; +import { InvalidSignatureError } from "../errors"; import { SignatureMessage } from "./types"; export async function safeSignTypedData( diff --git a/packages/blue-sdk-viem-bundler/.env.example b/packages/blue-sdk-viem-bundler/.env.example new file mode 100644 index 00000000..1e314b5a --- /dev/null +++ b/packages/blue-sdk-viem-bundler/.env.example @@ -0,0 +1,2 @@ +MAINNET_RPC_URL= +BASE_RPC_URL= diff --git a/packages/blue-sdk-viem-bundler/hardhat.config.ts b/packages/blue-sdk-viem-bundler/hardhat.config.ts new file mode 100644 index 00000000..95a7a558 --- /dev/null +++ b/packages/blue-sdk-viem-bundler/hardhat.config.ts @@ -0,0 +1,69 @@ +import "dotenv/config"; +import "hardhat-deal"; +import { HardhatUserConfig } from "hardhat/config"; +import { HardhatNetworkUserConfig } from "hardhat/types"; + +import "@nomicfoundation/hardhat-chai-matchers"; +import "@nomicfoundation/hardhat-ethers"; +import "@nomicfoundation/hardhat-network-helpers"; + +import { ChainId, addresses } from "@morpho-org/blue-sdk"; + +const hardhatNetworkConfigs: Record = { + ethereum: { + chainId: 1, + forking: { + url: process.env.MAINNET_RPC_URL!, + blockNumber: 19_750_000, + }, + }, + base: { + chainId: 8453, + forking: { + url: process.env.BASE_RPC_URL!, + blockNumber: 16_260_000, + }, + }, +}; + +const network = process.env.NETWORK || "ethereum"; +const hardhatNetworkConfig = hardhatNetworkConfigs[network]; +if (!hardhatNetworkConfig) throw Error(`invalid network: ${network}`); +if (!hardhatNetworkConfig.forking?.url) + throw Error(`no RPC url provided for network: ${network}`); + +const config: HardhatUserConfig = { + networks: { + hardhat: { + gasPrice: 0, + initialBaseFeePerGas: 0, + allowUnlimitedContractSize: true, + allowBlocksWithSameTimestamp: true, + mining: { + mempool: { + order: "fifo", + }, + }, + ...hardhatNetworkConfig, + }, + }, + paths: { + tests: "./tests/e2e/", + cache: "./cache", + }, + mocha: { + timeout: 300000, + reporterOptions: { + maxDiffSize: 2 ** 16, + }, + fgrep: network, + }, + tracer: { + nameTags: { + [addresses[ChainId.EthMainnet].bundler]: "EthereumBundler", + [addresses[ChainId.BaseMainnet].bundler]: "BaseBundler", + }, + }, +}; + +export default config; diff --git a/packages/blue-sdk-viem-bundler/package.json b/packages/blue-sdk-viem-bundler/package.json new file mode 100644 index 00000000..3a1cbe1e --- /dev/null +++ b/packages/blue-sdk-viem-bundler/package.json @@ -0,0 +1,82 @@ +{ + "name": "@morpho-org/blue-sdk-viem-bundler", + "version": "1.0.0", + "author": "Morpho Association ", + "license": "MIT", + "type": "module", + "main": "src/index.ts", + "files": [ + "lib" + ], + "scripts": { + "prepublish": "yarn build", + "build": "tsc --build tsconfig.build.json", + "test": "hardhat test", + "test-ethereum": "NETWORK=ethereum yarn test", + "test-base": "NETWORK=base yarn test" + }, + "peerDependencies": { + "@morpho-org/blue-sdk": "workspace:^", + "@morpho-org/blue-sdk-viem": "workspace:^", + "@morpho-org/blue-sdk-viem-simulation": "workspace:^", + "@morpho-org/morpho-ts": "workspace:^", + "mutative": "^1.0.8", + "viem": "^2.0.0" + }, + "devDependencies": { + "@morpho-org/blue-sdk": "workspace:^", + "@morpho-org/blue-sdk-viem": "workspace:^", + "@morpho-org/blue-sdk-viem-simulation": "workspace:^", + "@morpho-org/morpho-test": "workspace:^", + "@morpho-org/morpho-ts": "workspace:^", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", + "@nomicfoundation/hardhat-ethers": "^3.0.6", + "@nomicfoundation/hardhat-network-helpers": "^1.0.9", + "@types/chai": "^4.3.17", + "@types/jest": "^29.5.12", + "@types/lodash": "^4.17.7", + "@types/mocha": "^10.0.6", + "@types/node": "^22.2.0", + "@types/sinon": "^17.0.3", + "@types/sinon-chai": "^3.2.12", + "chai": "^4.5.0", + "dotenv": "^16.3.1", + "ethers": "^6.12.1", + "ethers-types": "^3.17.1", + "hardhat": "^2.22.10", + "hardhat-deal": "^3.1.0", + "jest": "^29.6.2", + "lodash": "^4.17.21", + "mocha": "^10.4.0", + "sinon": "^19.0.2", + "sinon-chai": "^3.7.0", + "ts-jest": "^29.2.4", + "ts-node": "^10.9.2", + "typescript": "^5.6.2", + "viem": "^2.21.15" + }, + "publishConfig": { + "access": "public", + "main": "lib/index.js" + }, + "jest": { + "verbose": true, + "testTimeout": 15000, + "maxWorkers": 1, + "transform": { + "^.+\\.tsx?$": [ + "ts-jest", + { + "tsconfig": "tsconfig.json" + } + ] + }, + "testMatch": [ + "**/*.test.ts" + ], + "testPathIgnorePatterns": [ + "node_modules/", + "tests/e2e/" + ] + } +} diff --git a/packages/blue-sdk-viem-bundler/src/BundlerAction.ts b/packages/blue-sdk-viem-bundler/src/BundlerAction.ts new file mode 100644 index 00000000..dbda4c78 --- /dev/null +++ b/packages/blue-sdk-viem-bundler/src/BundlerAction.ts @@ -0,0 +1,1157 @@ +import { + aaveV2MigrationBundlerAbi, + aaveV3MigrationBundlerAbi, + aaveV3OptimizerMigrationBundlerAbi, + compoundV2MigrationBundlerAbi, + compoundV3MigrationBundlerAbi, + erc20WrapperBundlerAbi, + erc4626BundlerAbi, + ethereumPermitBundlerAbi, + morphoBundlerAbi, + permit2BundlerAbi, + permitBundlerAbi, + stEthBundlerAbi, + transferBundlerAbi, + urdBundlerAbi, + wNativeBundlerAbi, +} from "./abis.js"; + +import { + Address, + Hex, + encodeAbiParameters, + encodeFunctionData, + parseSignature, +} from "viem"; +import { BundlerErrors } from "./errors.js"; +import { + Action, + Authorization, + MarketParams, + Permit2PermitSingle, + ReallocationWithdrawal, +} from "./types/actions.js"; + +export type BundlerCall = Hex; + +/** + * Namespace to easily encode calls to the Bundler contract, using ethers. + */ +export namespace BundlerAction { + export function encode({ type, args }: Action): BundlerCall { + switch (type) { + /* ERC20 */ + case "nativeTransfer": { + return BundlerAction.nativeTransfer(...args); + } + case "erc20Transfer": { + return BundlerAction.erc20Transfer(...args); + } + case "erc20TransferFrom": { + return BundlerAction.erc20TransferFrom(...args); + } + + /* ERC20Wrapper */ + case "erc20WrapperDepositFor": { + return BundlerAction.erc20WrapperDepositFor(...args); + } + case "erc20WrapperWithdrawTo": { + return BundlerAction.erc20WrapperWithdrawTo(...args); + } + + /* Permit */ + case "permit": { + const [asset, amount, deadline, signature, skipRevert = true] = args; + if (signature === null) throw new BundlerErrors.MissingSignature(); + + return BundlerAction.permit( + asset, + amount, + deadline, + signature, + skipRevert, + ); + } + case "permitDai": { + const [nonce, expiry, allowed, signature, skipRevert = true] = args; + if (signature === null) throw new BundlerErrors.MissingSignature(); + + return BundlerAction.permitDai( + nonce, + expiry, + allowed, + signature, + skipRevert, + ); + } + + /* Permit2 */ + case "approve2": { + const [permitSingle, signature, skipRevert = true] = args; + if (signature === null) throw new BundlerErrors.MissingSignature(); + + return BundlerAction.approve2(permitSingle, signature, skipRevert); + } + case "transferFrom2": { + return BundlerAction.transferFrom2(...args); + } + + /* ERC4626 */ + case "erc4626Mint": { + return BundlerAction.erc4626Mint(...args); + } + case "erc4626Deposit": { + return BundlerAction.erc4626Deposit(...args); + } + case "erc4626Withdraw": { + return BundlerAction.erc4626Withdraw(...args); + } + case "erc4626Redeem": { + return BundlerAction.erc4626Redeem(...args); + } + + /* Morpho */ + case "morphoSetAuthorizationWithSig": { + const [authorization, signature, skipRevert = true] = args; + if (signature === null) throw new BundlerErrors.MissingSignature(); + + return BundlerAction.morphoSetAuthorizationWithSig( + authorization, + signature, + skipRevert, + ); + } + case "morphoSupply": { + const [ + market, + assets, + shares, + slippageAmount, + onBehalf, + onMorphoSupply, + ] = args; + + return BundlerAction.morphoSupply( + market, + assets, + shares, + slippageAmount, + onBehalf, + onMorphoSupply.map(BundlerAction.encode), + ); + } + case "morphoSupplyCollateral": { + const [market, amount, onBehalf, onMorphoSupplyCollateral] = args; + + return BundlerAction.morphoSupplyCollateral( + market, + amount, + onBehalf, + onMorphoSupplyCollateral.map(BundlerAction.encode), + ); + } + case "morphoBorrow": { + const [market, assets, shares, slippageAmount, receiver] = args; + + return BundlerAction.morphoBorrow( + market, + assets, + shares, + slippageAmount, + receiver, + ); + } + case "morphoRepay": { + const [ + market, + assets, + shares, + slippageAmount, + onBehalf, + onMorphoRepay, + ] = args; + + return BundlerAction.morphoRepay( + market, + assets, + shares, + slippageAmount, + onBehalf, + onMorphoRepay.map(BundlerAction.encode), + ); + } + case "morphoWithdraw": { + const [market, assets, shares, slippageAmount, receiver] = args; + + return BundlerAction.morphoWithdraw( + market, + assets, + shares, + slippageAmount, + receiver, + ); + } + case "morphoWithdrawCollateral": { + const [market, amount, receiver] = args; + + return BundlerAction.morphoWithdrawCollateral(market, amount, receiver); + } + + /* MetaMorpho */ + case "reallocateTo": { + const [publicAllocator, vault, value, withdrawals, supplyMarket] = args; + + return BundlerAction.metaMorphoReallocateTo( + publicAllocator, + vault, + value, + withdrawals, + supplyMarket, + ); + } + + /* Universal Rewards Distributor */ + case "urdClaim": { + const [distributor, account, reward, amount, proof, skipRevert = true] = + args; + return BundlerAction.urdClaim( + distributor, + account, + reward, + amount, + proof, + skipRevert, + ); + } + + /* Wrapped Native */ + case "wrapNative": { + return BundlerAction.wrapNative(...args); + } + case "unwrapNative": { + return BundlerAction.unwrapNative(...args); + } + + /* stETH */ + case "stakeEth": { + return BundlerAction.stakeEth(...args); + } + + /* Wrapped stETH */ + case "wrapStEth": { + return BundlerAction.wrapStEth(...args); + } + case "unwrapStEth": { + return BundlerAction.unwrapStEth(...args); + } + + /* AaveV2 */ + case "aaveV2Repay": { + const [asset, amount, rateMode = 1n] = args; + return BundlerAction.aaveV2Repay(asset, amount, rateMode); + } + case "aaveV2Withdraw": { + return BundlerAction.aaveV2Withdraw(...args); + } + + /* AaveV3 */ + case "aaveV3Repay": { + const [asset, amount, rateMode = 1n] = args; + return BundlerAction.aaveV3Repay(asset, amount, rateMode); + } + case "aaveV3Withdraw": { + return BundlerAction.aaveV3Withdraw(...args); + } + + /* AaveV3 Optimizer */ + case "aaveV3OptimizerRepay": { + return BundlerAction.aaveV3OptimizerRepay(...args); + } + case "aaveV3OptimizerWithdraw": { + const [underlying, amount, maxIterations = 4n] = args; + return BundlerAction.aaveV3OptimizerWithdraw( + underlying, + amount, + maxIterations, + ); + } + case "aaveV3OptimizerWithdrawCollateral": { + return BundlerAction.aaveV3OptimizerWithdrawCollateral(...args); + } + case "aaveV3OptimizerApproveManagerWithSig": { + const [isApproved, nonce, deadline, signature, skipRevert = true] = + args; + + if (signature === null) throw new BundlerErrors.MissingSignature(); + + return BundlerAction.aaveV3OptimizerApproveManagerWithSig( + isApproved, + nonce, + deadline, + signature, + skipRevert, + ); + } + + /* CompoundV2 */ + case "compoundV2Repay": { + return BundlerAction.compoundV2Repay(...args); + } + case "compoundV2Redeem": { + return BundlerAction.compoundV2Redeem(...args); + } + + /* CompoundV3 */ + case "compoundV3Repay": { + return BundlerAction.compoundV3Repay(...args); + } + case "compoundV3WithdrawFrom": { + return BundlerAction.compoundV3WithdrawFrom(...args); + } + case "compoundV3AllowBySig": { + const [ + instance, + isAllowed, + nonce, + expiry, + signature, + skipRevert = true, + ] = args; + + if (signature === null) throw new BundlerErrors.MissingSignature(); + + return BundlerAction.compoundV3AllowBySig( + instance, + isAllowed, + nonce, + expiry, + signature, + skipRevert, + ); + } + } + + throw Error(`unhandled action encoding: ${type}`); + } + + /* ERC20 */ + + /** + * Encodes a call to the Bundler to transfer native tokens (ETH on ethereum, MATIC on polygon, etc). + * @param recipient The address to send native tokens to. + * @param amount The amount of native tokens to send (in wei). + */ + export function nativeTransfer( + recipient: Address, + amount: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: transferBundlerAbi, + functionName: "nativeTransfer", + args: [recipient, amount], + }); + } + + /** + * Encodes a call to the Bundler to transfer ERC20 tokens. + * @param asset The address of the ERC20 token to transfer. + * @param recipient The address to send tokens to. + * @param amount The amount of tokens to send. + */ + export function erc20Transfer( + asset: Address, + recipient: Address, + amount: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: transferBundlerAbi, + functionName: "erc20Transfer", + args: [asset, recipient, amount], + }); + } + + /** + * Encodes a call to the Bundler to transfer ERC20 tokens from the sender to the Bundler. + * @param asset The address of the ERC20 token to transfer. + * @param amount The amount of tokens to send. + */ + export function erc20TransferFrom( + asset: Address, + amount: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: transferBundlerAbi, + functionName: "erc20TransferFrom", + args: [asset, amount], + }); + } + + /* Permit */ + + /** + * Encodes a call to the Bundler to permit an ERC20 token. + * @param asset The address of the ERC20 token to permit. + * @param amount The amount of tokens to permit. + * @param deadline The timestamp until which the signature is valid. + * @param signature The Ethers signature to permit the tokens. + * @param skipRevert Whether to allow the permit to revert without making the whole multicall revert. + */ + export function permit( + asset: Address, + amount: bigint, + deadline: bigint, + signature: Hex, + skipRevert: boolean, + ): BundlerCall { + const { r, s, yParity } = parseSignature(signature); + + return encodeFunctionData({ + abi: permitBundlerAbi, + functionName: "permit", + args: [asset, amount, deadline, yParity, r, s, skipRevert], + }); + } + + /** + * Encodes a call to the Bundler to permit DAI. + * @param nonce The permit nonce used. + * @param expiry The timestamp until which the signature is valid. + * @param allowed The amount of DAI to permit. + * @param signature The Ethers signature to permit the tokens. + * @param skipRevert Whether to allow the permit to revert without making the whole multicall revert. + */ + export function permitDai( + nonce: bigint, + expiry: bigint, + allowed: boolean, + signature: Hex, + skipRevert: boolean, + ): BundlerCall { + const { r, s, yParity } = parseSignature(signature); + + return encodeFunctionData({ + abi: ethereumPermitBundlerAbi, + functionName: "permitDai", + args: [nonce, expiry, allowed, yParity, r, s, skipRevert], + }); + } + + /* Permit2 */ + + /** + * Encodes a call to the Bundler to permit ERC20 tokens via Permit2. + * @param permitSingle The permit details to submit to Permit2. + * @param signature The Ethers signature to permit the tokens. + * @param skipRevert Whether to allow the permit to revert without making the whole multicall revert. + */ + export function approve2( + permitSingle: Permit2PermitSingle, + signature: Hex, + skipRevert: boolean, + ): BundlerCall { + return encodeFunctionData({ + abi: permit2BundlerAbi, + functionName: "approve2", + args: [permitSingle, signature, skipRevert], + }); + } + + /** + * Encodes a call to the Bundler to transfer ERC20 tokens via Permit2 from the sender to the Bundler. + * @param asset The address of the ERC20 token to transfer. + * @param amount The amount of tokens to send. + */ + export function transferFrom2(asset: Address, amount: bigint): BundlerCall { + return encodeFunctionData({ + abi: permit2BundlerAbi, + functionName: "transferFrom2", + args: [asset, amount], + }); + } + + /* ERC20 Wrapper */ + + /** + * Encodes a call to the Bundler to wrap ERC20 tokens via the provided ERC20Wrapper. + * @param wrapper The address of the ERC20 wrapper token. + * @param amount The amount of tokens to send. + */ + export function erc20WrapperDepositFor( + wrapper: Address, + amount: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: erc20WrapperBundlerAbi, + functionName: "erc20WrapperDepositFor", + args: [wrapper, amount], + }); + } + + /** + * Encodes a call to the Bundler to unwrap ERC20 tokens from the provided ERC20Wrapper. + * @param wrapper The address of the ERC20 wrapper token. + * @param account The address to send the underlying ERC20 tokens. + * @param amount The amount of tokens to send. + */ + export function erc20WrapperWithdrawTo( + wrapper: Address, + account: Address, + amount: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: erc20WrapperBundlerAbi, + functionName: "erc20WrapperWithdrawTo", + args: [wrapper, account, amount], + }); + } + + /* ERC4626 */ + + /** + * Encodes a call to the Bundler to mint shares of the provided ERC4626 vault. + * @param erc4626 The address of the ERC4626 vault. + * @param shares The amount of shares to mint. + * @param maxAssets The maximum amount of assets to deposit (protects the sender from unexpected slippage). + * @param receiver The address to send the shares to. + */ + export function erc4626Mint( + erc4626: Address, + shares: bigint, + maxAssets: bigint, + receiver: Address, + ): BundlerCall { + return encodeFunctionData({ + abi: erc4626BundlerAbi, + functionName: "erc4626Mint", + args: [erc4626, shares, maxAssets, receiver], + }); + } + + /** + * Encodes a call to the Bundler to deposit assets into the provided ERC4626 vault. + * @param erc4626 The address of the ERC4626 vault. + * @param assets The amount of assets to deposit. + * @param minShares The minimum amount of shares to mint (protects the sender from unexpected slippage). + * @param receiver The address to send the shares to. + */ + export function erc4626Deposit( + erc4626: Address, + assets: bigint, + minShares: bigint, + receiver: Address, + ): BundlerCall { + return encodeFunctionData({ + abi: erc4626BundlerAbi, + functionName: "erc4626Deposit", + args: [erc4626, assets, minShares, receiver], + }); + } + + /** + * Encodes a call to the Bundler to withdraw assets from the provided ERC4626 vault. + * @param erc4626 The address of the ERC4626 vault. + * @param assets The amount of assets to withdraw. + * @param maxShares The maximum amount of shares to redeem (protects the sender from unexpected slippage). + * @param receiver The address to send the assets to. + */ + export function erc4626Withdraw( + erc4626: Address, + assets: bigint, + maxShares: bigint, + receiver: Address, + owner: Address, + ): BundlerCall { + return encodeFunctionData({ + abi: erc4626BundlerAbi, + functionName: "erc4626Withdraw", + args: [erc4626, assets, maxShares, receiver, owner], + }); + } + + /** + * Encodes a call to the Bundler to redeem shares from the provided ERC4626 vault. + * @param erc4626 The address of the ERC4626 vault. + * @param shares The amount of shares to redeem. + * @param minAssets The minimum amount of assets to withdraw (protects the sender from unexpected slippage). + * @param receiver The address to send the assets to. + */ + export function erc4626Redeem( + erc4626: Address, + shares: bigint, + minAssets: bigint, + receiver: Address, + owner: Address, + ): BundlerCall { + return encodeFunctionData({ + abi: erc4626BundlerAbi, + functionName: "erc4626Redeem", + args: [erc4626, shares, minAssets, receiver, owner], + }); + } + + /* Morpho */ + + /** + * Encodes a call to the Bundler to authorize an account on Morpho Blue. + * @param authorization The authorization details to submit to Morpho Blue. + * @param signature The Ethers signature to authorize the account. + * @param skipRevert Whether to allow the authorization call to revert without making the whole multicall revert. + */ + export function morphoSetAuthorizationWithSig( + authorization: Authorization, + signature: Hex, + skipRevert: boolean, + ): BundlerCall { + const { r, s, yParity } = parseSignature(signature); + + return encodeFunctionData({ + abi: morphoBundlerAbi, + functionName: "morphoSetAuthorizationWithSig", + args: [authorization, { v: yParity, r, s }, skipRevert], + }); + } + + /** + * Encodes a call to the Bundler to supply to a Morpho Blue market. + * @param market The market params to supply to. + * @param assets The amount of assets to supply. + * @param shares The amount of supply shares to mint. + * @param slippageAmount The maximum (resp. minimum) amount of assets (resp. supply shares) to supply (resp. mint) (protects the sender from unexpected slippage). + * @param onBehalf The address to supply on behalf of. + * @param callbackCalls The array of calls to execute inside Morpho Blue's `onMorphoSupply` callback. + */ + export function morphoSupply( + market: MarketParams, + assets: bigint, + shares: bigint, + slippageAmount: bigint, + onBehalf: Address, + callbackCalls: BundlerCall[], + ): BundlerCall { + return encodeFunctionData({ + abi: morphoBundlerAbi, + functionName: "morphoSupply", + args: [ + market, + assets, + shares, + slippageAmount, + onBehalf, + encodeAbiParameters([{ type: "bytes[]" }], [callbackCalls]), + ], + }); + } + + /** + * Encodes a call to the Bundler to supply collateral to a Morpho Blue market. + * @param market The market params to supply to. + * @param assets The amount of assets to supply. + * @param onBehalf The address to supply on behalf of. + * @param callbackCalls The array of calls to execute inside Morpho Blue's `onMorphoSupplyCollateral` callback. + */ + export function morphoSupplyCollateral( + market: MarketParams, + assets: bigint, + onBehalf: Address, + callbackCalls: BundlerCall[], + ): BundlerCall { + return encodeFunctionData({ + abi: morphoBundlerAbi, + functionName: "morphoSupplyCollateral", + args: [ + market, + assets, + onBehalf, + encodeAbiParameters([{ type: "bytes[]" }], [callbackCalls]), + ], + }); + } + + /** + * Encodes a call to the Bundler to borrow from a Morpho Blue market. + * @param market The market params to borrow from. + * @param assets The amount of assets to borrow. + * @param shares The amount of borrow shares to mint. + * @param slippageAmount The minimum (resp. maximum) amount of assets (resp. borrow shares) to borrow (resp. mint) (protects the sender from unexpected slippage). + * @param receiver The address to send borrowed tokens to. + */ + export function morphoBorrow( + market: MarketParams, + assets: bigint, + shares: bigint, + slippageAmount: bigint, + receiver: Address, + ): BundlerCall { + return encodeFunctionData({ + abi: morphoBundlerAbi, + functionName: "morphoBorrow", + args: [market, assets, shares, slippageAmount, receiver], + }); + } + + /** + * Encodes a call to the Bundler to repay to a Morpho Blue market. + * @param market The market params to repay to. + * @param assets The amount of assets to repay. + * @param shares The amount of borrow shares to redeem. + * @param slippageAmount The maximum (resp. minimum) amount of assets (resp. borrow shares) to repay (resp. redeem) (protects the sender from unexpected slippage). + * @param onBehalf The address to repay on behalf of. + * @param callbackCalls The array of calls to execute inside Morpho Blue's `onMorphoSupply` callback. + */ + export function morphoRepay( + market: MarketParams, + assets: bigint, + shares: bigint, + slippageAmount: bigint, + onBehalf: Address, + callbackCalls: BundlerCall[], + ): BundlerCall { + return encodeFunctionData({ + abi: morphoBundlerAbi, + functionName: "morphoRepay", + args: [ + market, + assets, + shares, + slippageAmount, + onBehalf, + encodeAbiParameters([{ type: "bytes[]" }], [callbackCalls]), + ], + }); + } + + /** + * Encodes a call to the Bundler to withdraw from a Morpho Blue market. + * @param market The market params to withdraw from. + * @param assets The amount of assets to withdraw. + * @param shares The amount of supply shares to redeem. + * @param slippageAmount The minimum (resp. maximum) amount of assets (resp. supply shares) to withdraw (resp. redeem) (protects the sender from unexpected slippage). + * @param receiver The address to send withdrawn tokens to. + */ + export function morphoWithdraw( + market: MarketParams, + assets: bigint, + shares: bigint, + slippageAmount: bigint, + receiver: Address, + ): BundlerCall { + return encodeFunctionData({ + abi: morphoBundlerAbi, + functionName: "morphoWithdraw", + args: [market, assets, shares, slippageAmount, receiver], + }); + } + + /** + * Encodes a call to the Bundler to withdraw collateral from a Morpho Blue market. + * @param market The market params to withdraw from. + * @param assets The amount of assets to withdraw. + * @param receiver The address to send withdrawn tokens to. + */ + export function morphoWithdrawCollateral( + market: MarketParams, + assets: bigint, + receiver: Address, + ): BundlerCall { + return encodeFunctionData({ + abi: morphoBundlerAbi, + functionName: "morphoWithdrawCollateral", + args: [market, assets, receiver], + }); + } + + /** + * Encodes a call to the Bundler to flash loan from Morpho Blue. + * @param asset The address of the ERC20 token to flash loan. + * @param amount The amount of tokens to flash loan. + * @param callbackCalls The array of calls to execute inside Morpho Blue's `onMorphoFlashLoan` callback. + */ + export function morphoFlashLoan( + asset: Address, + amount: bigint, + callbackCalls: BundlerCall[], + ): BundlerCall { + return encodeFunctionData({ + abi: morphoBundlerAbi, + functionName: "morphoFlashLoan", + args: [ + asset, + amount, + encodeAbiParameters([{ type: "bytes[]" }], [callbackCalls]), + ], + }); + } + + /** + * Encodes a call to the Bundler to trigger a public reallocation on the PublicAllocator. + * @param publicAllocator The address of the PublicAllocator to use. + * @param vault The vault to reallocate. + * @param value The value of the call. Can be used to pay the vault reallocation fees. + * @param withdrawals The array of withdrawals to perform, before supplying everything to the supply market. + * @param supplyMarketParams The market params to reallocate to. + */ + export function metaMorphoReallocateTo( + publicAllocator: Address, + vault: Address, + value: bigint, + withdrawals: ReallocationWithdrawal[], + supplyMarketParams: MarketParams, + ): BundlerCall { + return encodeFunctionData({ + abi: morphoBundlerAbi, + functionName: "reallocateTo", + args: [publicAllocator, vault, value, withdrawals, supplyMarketParams], + }); + } + + /* Universal Rewards Distributor */ + + /** + * Encodes a call to the Bundler to claim rewards from the Universal Rewards Distributor. + * @param distributor The address of the distributor to claim rewards from. + * @param account The address to claim rewards for. + * @param reward The address of the reward token to claim. + * @param amount The amount of rewards to claim. + * @param proof The Merkle proof to claim the rewards. + * @param skipRevert Whether to allow the claim to revert without making the whole multicall revert. + */ + export function urdClaim( + distributor: Address, + account: Address, + reward: Address, + amount: bigint, + proof: Hex[], + skipRevert: boolean, + ): BundlerCall { + return encodeFunctionData({ + abi: urdBundlerAbi, + functionName: "urdClaim", + args: [distributor, account, reward, amount, proof, skipRevert], + }); + } + + /* Wrapped Native */ + + /** + * Encodes a call to the Bundler to wrap native tokens (ETH to WETH on ethereum, MATIC to WMATIC on polygon, etc). + * @param amount The amount of native tokens to wrap (in wei). + */ + export function wrapNative(amount: bigint): BundlerCall { + return encodeFunctionData({ + abi: wNativeBundlerAbi, + functionName: "wrapNative", + args: [amount], + }); + } + + /** + * Encodes a call to the Bundler to unwrap native tokens (WETH to ETH on ethereum, WMATIC to MATIC on polygon, etc). + * @param amount The amount of native tokens to unwrap (in wei). + */ + export function unwrapNative(amount: bigint): BundlerCall { + return encodeFunctionData({ + abi: wNativeBundlerAbi, + functionName: "unwrapNative", + args: [amount], + }); + } + + /* stETH */ + + /** + * Encodes a call to the Bundler to stake native tokens using Lido (ETH to stETH on ethereum). + * @param amount The amount of native tokens to stake (in wei). + * @param minShares The minimum amount of shares to mint (protects the sender from unexpected slippage). + * @param referral The referral address to use. + */ + export function stakeEth( + amount: bigint, + minShares: bigint, + referral: Address, + ): BundlerCall { + return encodeFunctionData({ + abi: stEthBundlerAbi, + functionName: "stakeEth", + args: [amount, minShares, referral], + }); + } + + /* Wrapped stETH */ + + /** + * Encodes a call to the Bundler to wrap stETH (stETH to wstETH on ethereum). + * @param amount The amount of stETH to wrap (in wei). + */ + export function wrapStEth(amount: bigint): BundlerCall { + return encodeFunctionData({ + abi: stEthBundlerAbi, + functionName: "wrapStEth", + args: [amount], + }); + } + + /** + * Encodes a call to the Bundler to unwrap wstETH (wstETH to stETH on ethereum). + * @param amount The amount of wstETH to unwrap (in wei). + */ + export function unwrapStEth(amount: bigint): BundlerCall { + return encodeFunctionData({ + abi: stEthBundlerAbi, + functionName: "unwrapStEth", + args: [amount], + }); + } + + /* AaveV2 */ + + /** + * ! Only available on AaveV2MigrationBundler instances (not the main Bundler contract!). + * Encodes a call to the Bundler to repay a debt on AaveV2. + * @param asset The debt asset to repay. + * @param amount The amount of debt to repay. + * @param rateMode The interest rate mode used by the debt to repay. + */ + export function aaveV2Repay( + asset: Address, + amount: bigint, + rateMode: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: aaveV2MigrationBundlerAbi, + functionName: "aaveV2Repay", + args: [asset, amount, rateMode], + }); + } + + /** + * ! Only available on AaveV2MigrationBundler instances (not the main Bundler contract!). + * Encodes a call to the Bundler to withdrawn from AaveV2. + * @param asset The asset to withdraw. + * @param amount The amount of asset to withdraw. + */ + export function aaveV2Withdraw(asset: Address, amount: bigint): BundlerCall { + return encodeFunctionData({ + abi: aaveV2MigrationBundlerAbi, + functionName: "aaveV2Withdraw", + args: [asset, amount], + }); + } + + /* AaveV3 */ + + /** + * ! Only available on AaveV3MigrationBundler instances (not the main Bundler contract!). + * Encodes a call to the Bundler to repay a debt on AaveV3. + * @param asset The debt asset to repay. + * @param amount The amount of debt to repay. + * @param rateMode The interest rate mode used by the debt to repay. + */ + export function aaveV3Repay( + asset: Address, + amount: bigint, + rateMode: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: aaveV3MigrationBundlerAbi, + functionName: "aaveV3Repay", + args: [asset, amount, rateMode], + }); + } + + /** + * ! Only available on AaveV3MigrationBundler instances (not the main Bundler contract!). + * Encodes a call to the Bundler to withdrawn from AaveV3. + * @param asset The asset to withdraw. + * @param amount The amount of asset to withdraw. + */ + export function aaveV3Withdraw(asset: Address, amount: bigint): BundlerCall { + return encodeFunctionData({ + abi: aaveV3MigrationBundlerAbi, + functionName: "aaveV3Withdraw", + args: [asset, amount], + }); + } + + /* AaveV3 Optimizer */ + + /** + * ! Only available on AaveV3OptimizerMigrationBundler instances (not the main Bundler contract!). + * Encodes a call to the Bundler to repay a debt on Morpho's AaveV3Optimizer. + * @param underlying The underlying debt asset to repay. + * @param amount The amount of debt to repay. + * @param maxIterations The maximum amount of iterations to use for the repayment. + */ + export function aaveV3OptimizerRepay( + underlying: Address, + amount: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: aaveV3OptimizerMigrationBundlerAbi, + functionName: "aaveV3OptimizerRepay", + args: [underlying, amount], + }); + } + + /** + * ! Only available on AaveV3OptimizerMigrationBundler instances (not the main Bundler contract!). + * Encodes a call to the Bundler to withdraw from Morpho's AaveV3Optimizer. + * @param underlying The underlying asset to withdraw. + * @param amount The amount to withdraw. + * @param maxIterations The maximum amount of iterations to use for the withdrawal. + */ + export function aaveV3OptimizerWithdraw( + underlying: Address, + amount: bigint, + maxIterations: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: aaveV3OptimizerMigrationBundlerAbi, + functionName: "aaveV3OptimizerWithdraw", + args: [underlying, amount, maxIterations], + }); + } + + /** + * ! Only available on AaveV3OptimizerMigrationBundler instances (not the main Bundler contract!). + * Encodes a call to the Bundler to withdraw collateral from Morpho's AaveV3Optimizer. + * @param underlying The underlying asset to withdraw. + * @param amount The amount to withdraw. + */ + export function aaveV3OptimizerWithdrawCollateral( + underlying: Address, + amount: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: aaveV3OptimizerMigrationBundlerAbi, + functionName: "aaveV3OptimizerWithdrawCollateral", + args: [underlying, amount], + }); + } + + /** + * ! Only available on AaveV3OptimizerMigrationBundler instances (not the main Bundler contract!). + * Encodes a call to the Bundler to approve the Bundler as the sender's manager on Morpho's AaveV3Optimizer. + * @param isApproved Whether the manager is approved. + * @param nonce The nonce used to sign. + * @param deadline The timestamp until which the signature is valid. + * @param signature The Ethers signature to submit. + * @param skipRevert Whether to allow the signature to revert without making the whole multicall revert. + */ + export function aaveV3OptimizerApproveManagerWithSig( + isApproved: boolean, + nonce: bigint, + deadline: bigint, + signature: Hex, + skipRevert: boolean, + ): BundlerCall { + const { r, s, yParity } = parseSignature(signature); + + return encodeFunctionData({ + abi: aaveV3OptimizerMigrationBundlerAbi, + functionName: "aaveV3OptimizerApproveManagerWithSig", + args: [isApproved, nonce, deadline, { v: yParity, r, s }, skipRevert], + }); + } + + /* CompoundV2 */ + + /** + * ! Only available on CompoundV2MigrationBundler instances (not the main Bundler contract!). + * Encodes a call to the Bundler to repay a debt on CompoundV2. + * @param cToken The cToken on which to repay the debt. + * @param amount The amount of debt to repay. + */ + export function compoundV2Repay( + cToken: Address, + amount: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: compoundV2MigrationBundlerAbi, + functionName: "compoundV2Repay", + args: [cToken, amount], + }); + } + + /** + * ! Only available on CompoundV2MigrationBundler instances (not the main Bundler contract!). + * Encodes a call to the Bundler to withdraw collateral from CompoundV2. + * @param cToken The cToken on which to withdraw. + * @param amount The amount to withdraw. + */ + export function compoundV2Redeem( + cToken: Address, + amount: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: compoundV2MigrationBundlerAbi, + functionName: "compoundV2Redeem", + args: [cToken, amount], + }); + } + + /* CompoundV3 */ + + /** + * ! Only available on CompoundV3MigrationBundler instances (not the main Bundler contract!). + * Encodes a call to the Bundler to repay a debt on CompoundV3. + * @param instance The CompoundV3 instance on which to repay the debt. + * @param amount The amount of debt to repay. + */ + export function compoundV3Repay( + instance: Address, + amount: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: compoundV3MigrationBundlerAbi, + functionName: "compoundV3Repay", + args: [instance, amount], + }); + } + + /** + * ! Only available on CompoundV3MigrationBundler instances (not the main Bundler contract!). + * Encodes a call to the Bundler to withdraw collateral from CompoundV3. + * @param instance The CompoundV3 instance on which to withdraw. + * @param amount The amount to withdraw. + */ + export function compoundV3WithdrawFrom( + instance: Address, + asset: Address, + amount: bigint, + ): BundlerCall { + return encodeFunctionData({ + abi: compoundV3MigrationBundlerAbi, + functionName: "compoundV3WithdrawFrom", + args: [instance, asset, amount], + }); + } + + /** + * ! Only available on CompoundV3MigrationBundler instances (not the main Bundler contract!). + * Encodes a call to the Bundler to allow the Bundler to act on the sender's position on CompoundV3. + * @param instance The CompoundV3 instance on which to submit the signature. + * @param isAllowed Whether the manager is allowed. + * @param nonce The nonce used to sign. + * @param expiry The timestamp until which the signature is valid. + * @param signature The Ethers signature to submit. + * @param skipRevert Whether to allow the signature to revert without making the whole multicall revert. + */ + export function compoundV3AllowBySig( + instance: Address, + isAllowed: boolean, + nonce: bigint, + expiry: bigint, + signature: Hex, + skipRevert: boolean, + ): BundlerCall { + const { r, s, yParity } = parseSignature(signature); + + return encodeFunctionData({ + abi: compoundV3MigrationBundlerAbi, + functionName: "compoundV3AllowBySig", + args: [instance, isAllowed, nonce, expiry, yParity, r, s, skipRevert], + }); + } +} + +export default BundlerAction; diff --git a/packages/blue-sdk-viem-bundler/src/abis.ts b/packages/blue-sdk-viem-bundler/src/abis.ts new file mode 100644 index 00000000..c56a5840 --- /dev/null +++ b/packages/blue-sdk-viem-bundler/src/abis.ts @@ -0,0 +1,855 @@ +export const baseBundlerAbi = [ + { + type: "function", + name: "initiator", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "multicall", + inputs: [{ name: "data", type: "bytes[]", internalType: "bytes[]" }], + outputs: [], + stateMutability: "payable", + }, +] as const; + +export const transferBundlerAbi = [ + ...baseBundlerAbi, + { + type: "function", + name: "erc20Transfer", + inputs: [ + { name: "asset", type: "address", internalType: "address" }, + { name: "recipient", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "erc20TransferFrom", + inputs: [ + { name: "asset", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "nativeTransfer", + inputs: [ + { name: "recipient", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, +] as const; + +export const permitBundlerAbi = [ + ...baseBundlerAbi, + { + type: "function", + name: "permit", + inputs: [ + { name: "asset", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + { name: "deadline", type: "uint256", internalType: "uint256" }, + { name: "v", type: "uint8", internalType: "uint8" }, + { name: "r", type: "bytes32", internalType: "bytes32" }, + { name: "s", type: "bytes32", internalType: "bytes32" }, + { name: "skipRevert", type: "bool", internalType: "bool" }, + ], + outputs: [], + stateMutability: "payable", + }, +] as const; + +export const ethereumPermitBundlerAbi = [ + ...permitBundlerAbi, + { + type: "function", + name: "permitDai", + inputs: [ + { name: "nonce", type: "uint256", internalType: "uint256" }, + { name: "expiry", type: "uint256", internalType: "uint256" }, + { name: "allowed", type: "bool", internalType: "bool" }, + { name: "v", type: "uint8", internalType: "uint8" }, + { name: "r", type: "bytes32", internalType: "bytes32" }, + { name: "s", type: "bytes32", internalType: "bytes32" }, + { name: "skipRevert", type: "bool", internalType: "bool" }, + ], + outputs: [], + stateMutability: "payable", + }, +] as const; + +export const permit2BundlerAbi = [ + ...baseBundlerAbi, + { + type: "function", + name: "approve2", + inputs: [ + { + name: "permitSingle", + type: "tuple", + internalType: "struct IAllowanceTransfer.PermitSingle", + components: [ + { + name: "details", + type: "tuple", + internalType: "struct IAllowanceTransfer.PermitDetails", + components: [ + { name: "token", type: "address", internalType: "address" }, + { name: "amount", type: "uint160", internalType: "uint160" }, + { name: "expiration", type: "uint48", internalType: "uint48" }, + { name: "nonce", type: "uint48", internalType: "uint48" }, + ], + }, + { name: "spender", type: "address", internalType: "address" }, + { name: "sigDeadline", type: "uint256", internalType: "uint256" }, + ], + }, + { name: "signature", type: "bytes", internalType: "bytes" }, + { name: "skipRevert", type: "bool", internalType: "bool" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "transferFrom2", + inputs: [ + { name: "asset", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { type: "error", name: "UnsafeCast", inputs: [] }, +] as const; + +export const erc20WrapperBundlerAbi = [ + ...baseBundlerAbi, + { + type: "function", + name: "erc20WrapperDepositFor", + inputs: [ + { name: "wrapper", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "erc20WrapperWithdrawTo", + inputs: [ + { name: "wrapper", type: "address", internalType: "address" }, + { name: "account", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, +] as const; + +export const erc4626BundlerAbi = [ + ...baseBundlerAbi, + { + type: "function", + name: "erc4626Deposit", + inputs: [ + { name: "vault", type: "address", internalType: "address" }, + { name: "assets", type: "uint256", internalType: "uint256" }, + { name: "minShares", type: "uint256", internalType: "uint256" }, + { name: "receiver", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "erc4626Mint", + inputs: [ + { name: "vault", type: "address", internalType: "address" }, + { name: "shares", type: "uint256", internalType: "uint256" }, + { name: "maxAssets", type: "uint256", internalType: "uint256" }, + { name: "receiver", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "erc4626Redeem", + inputs: [ + { name: "vault", type: "address", internalType: "address" }, + { name: "shares", type: "uint256", internalType: "uint256" }, + { name: "minAssets", type: "uint256", internalType: "uint256" }, + { name: "receiver", type: "address", internalType: "address" }, + { name: "owner", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "erc4626Withdraw", + inputs: [ + { name: "vault", type: "address", internalType: "address" }, + { name: "assets", type: "uint256", internalType: "uint256" }, + { name: "maxShares", type: "uint256", internalType: "uint256" }, + { name: "receiver", type: "address", internalType: "address" }, + { name: "owner", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "payable", + }, +] as const; + +export const morphoBundlerAbi = [ + ...baseBundlerAbi, + { + type: "function", + name: "MORPHO", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "contract IMorpho" }], + stateMutability: "view", + }, + { + type: "function", + name: "morphoBorrow", + inputs: [ + { + name: "marketParams", + type: "tuple", + internalType: "struct MarketParams", + components: [ + { name: "loanToken", type: "address", internalType: "address" }, + { name: "collateralToken", type: "address", internalType: "address" }, + { name: "oracle", type: "address", internalType: "address" }, + { name: "irm", type: "address", internalType: "address" }, + { name: "lltv", type: "uint256", internalType: "uint256" }, + ], + }, + { name: "assets", type: "uint256", internalType: "uint256" }, + { name: "shares", type: "uint256", internalType: "uint256" }, + { name: "slippageAmount", type: "uint256", internalType: "uint256" }, + { name: "receiver", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "morphoFlashLoan", + inputs: [ + { name: "token", type: "address", internalType: "address" }, + { name: "assets", type: "uint256", internalType: "uint256" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "morphoRepay", + inputs: [ + { + name: "marketParams", + type: "tuple", + internalType: "struct MarketParams", + components: [ + { name: "loanToken", type: "address", internalType: "address" }, + { name: "collateralToken", type: "address", internalType: "address" }, + { name: "oracle", type: "address", internalType: "address" }, + { name: "irm", type: "address", internalType: "address" }, + { name: "lltv", type: "uint256", internalType: "uint256" }, + ], + }, + { name: "assets", type: "uint256", internalType: "uint256" }, + { name: "shares", type: "uint256", internalType: "uint256" }, + { name: "slippageAmount", type: "uint256", internalType: "uint256" }, + { name: "onBehalf", type: "address", internalType: "address" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "morphoSetAuthorizationWithSig", + inputs: [ + { + name: "authorization", + type: "tuple", + internalType: "struct Authorization", + components: [ + { name: "authorizer", type: "address", internalType: "address" }, + { name: "authorized", type: "address", internalType: "address" }, + { name: "isAuthorized", type: "bool", internalType: "bool" }, + { name: "nonce", type: "uint256", internalType: "uint256" }, + { name: "deadline", type: "uint256", internalType: "uint256" }, + ], + }, + { + name: "signature", + type: "tuple", + internalType: "struct Signature", + components: [ + { name: "v", type: "uint8", internalType: "uint8" }, + { name: "r", type: "bytes32", internalType: "bytes32" }, + { name: "s", type: "bytes32", internalType: "bytes32" }, + ], + }, + { name: "skipRevert", type: "bool", internalType: "bool" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "morphoSupply", + inputs: [ + { + name: "marketParams", + type: "tuple", + internalType: "struct MarketParams", + components: [ + { name: "loanToken", type: "address", internalType: "address" }, + { name: "collateralToken", type: "address", internalType: "address" }, + { name: "oracle", type: "address", internalType: "address" }, + { name: "irm", type: "address", internalType: "address" }, + { name: "lltv", type: "uint256", internalType: "uint256" }, + ], + }, + { name: "assets", type: "uint256", internalType: "uint256" }, + { name: "shares", type: "uint256", internalType: "uint256" }, + { name: "slippageAmount", type: "uint256", internalType: "uint256" }, + { name: "onBehalf", type: "address", internalType: "address" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "morphoSupplyCollateral", + inputs: [ + { + name: "marketParams", + type: "tuple", + internalType: "struct MarketParams", + components: [ + { name: "loanToken", type: "address", internalType: "address" }, + { name: "collateralToken", type: "address", internalType: "address" }, + { name: "oracle", type: "address", internalType: "address" }, + { name: "irm", type: "address", internalType: "address" }, + { name: "lltv", type: "uint256", internalType: "uint256" }, + ], + }, + { name: "assets", type: "uint256", internalType: "uint256" }, + { name: "onBehalf", type: "address", internalType: "address" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "morphoWithdraw", + inputs: [ + { + name: "marketParams", + type: "tuple", + internalType: "struct MarketParams", + components: [ + { name: "loanToken", type: "address", internalType: "address" }, + { name: "collateralToken", type: "address", internalType: "address" }, + { name: "oracle", type: "address", internalType: "address" }, + { name: "irm", type: "address", internalType: "address" }, + { name: "lltv", type: "uint256", internalType: "uint256" }, + ], + }, + { name: "assets", type: "uint256", internalType: "uint256" }, + { name: "shares", type: "uint256", internalType: "uint256" }, + { name: "slippageAmount", type: "uint256", internalType: "uint256" }, + { name: "receiver", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "morphoWithdrawCollateral", + inputs: [ + { + name: "marketParams", + type: "tuple", + internalType: "struct MarketParams", + components: [ + { name: "loanToken", type: "address", internalType: "address" }, + { name: "collateralToken", type: "address", internalType: "address" }, + { name: "oracle", type: "address", internalType: "address" }, + { name: "irm", type: "address", internalType: "address" }, + { name: "lltv", type: "uint256", internalType: "uint256" }, + ], + }, + { name: "assets", type: "uint256", internalType: "uint256" }, + { name: "receiver", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "onMorphoFlashLoan", + inputs: [ + { name: "", type: "uint256", internalType: "uint256" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "onMorphoRepay", + inputs: [ + { name: "", type: "uint256", internalType: "uint256" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "onMorphoSupply", + inputs: [ + { name: "", type: "uint256", internalType: "uint256" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "onMorphoSupplyCollateral", + inputs: [ + { name: "", type: "uint256", internalType: "uint256" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "reallocateTo", + inputs: [ + { name: "publicAllocator", type: "address", internalType: "address" }, + { name: "vault", type: "address", internalType: "address" }, + { name: "value", type: "uint256", internalType: "uint256" }, + { + name: "withdrawals", + type: "tuple[]", + internalType: "struct Withdrawal[]", + components: [ + { + name: "marketParams", + type: "tuple", + internalType: "struct MarketParams", + components: [ + { name: "loanToken", type: "address", internalType: "address" }, + { + name: "collateralToken", + type: "address", + internalType: "address", + }, + { name: "oracle", type: "address", internalType: "address" }, + { name: "irm", type: "address", internalType: "address" }, + { name: "lltv", type: "uint256", internalType: "uint256" }, + ], + }, + { name: "amount", type: "uint128", internalType: "uint128" }, + ], + }, + { + name: "supplyMarketParams", + type: "tuple", + internalType: "struct MarketParams", + components: [ + { name: "loanToken", type: "address", internalType: "address" }, + { name: "collateralToken", type: "address", internalType: "address" }, + { name: "oracle", type: "address", internalType: "address" }, + { name: "irm", type: "address", internalType: "address" }, + { name: "lltv", type: "uint256", internalType: "uint256" }, + ], + }, + ], + outputs: [], + stateMutability: "payable", + }, +] as const; + +export const urdBundlerAbi = [ + ...baseBundlerAbi, + { + type: "function", + name: "urdClaim", + inputs: [ + { name: "distributor", type: "address", internalType: "address" }, + { name: "account", type: "address", internalType: "address" }, + { name: "reward", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + { name: "proof", type: "bytes32[]", internalType: "bytes32[]" }, + { name: "skipRevert", type: "bool", internalType: "bool" }, + ], + outputs: [], + stateMutability: "payable", + }, +] as const; + +export const wNativeBundlerAbi = [ + ...baseBundlerAbi, + { type: "receive", stateMutability: "payable" }, + { + type: "function", + name: "WRAPPED_NATIVE", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "unwrapNative", + inputs: [{ name: "amount", type: "uint256", internalType: "uint256" }], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "wrapNative", + inputs: [{ name: "amount", type: "uint256", internalType: "uint256" }], + outputs: [], + stateMutability: "payable", + }, +] as const; + +export const stEthBundlerAbi = [ + ...baseBundlerAbi, + { + type: "function", + name: "ST_ETH", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "WST_ETH", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "stakeEth", + inputs: [ + { name: "amount", type: "uint256", internalType: "uint256" }, + { name: "minShares", type: "uint256", internalType: "uint256" }, + { name: "referral", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "unwrapStEth", + inputs: [{ name: "amount", type: "uint256", internalType: "uint256" }], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "wrapStEth", + inputs: [{ name: "amount", type: "uint256", internalType: "uint256" }], + outputs: [], + stateMutability: "payable", + }, +] as const; + +export const aaveV2MigrationBundlerAbi = [ + ...transferBundlerAbi, + ...permitBundlerAbi, + ...permit2BundlerAbi, + ...stEthBundlerAbi, + ...erc4626BundlerAbi, + ...morphoBundlerAbi, + { + type: "constructor", + inputs: [ + { name: "morpho", type: "address", internalType: "address" }, + { name: "aaveV2Pool", type: "address", internalType: "address" }, + { name: "wstEth", type: "address", internalType: "address" }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "AAVE_V2_POOL", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "contract IAaveV2" }], + stateMutability: "view", + }, + { + type: "function", + name: "aaveV2Repay", + inputs: [ + { name: "asset", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + { name: "interestRateMode", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "aaveV2Withdraw", + inputs: [ + { name: "asset", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { type: "error", name: "UnsafeCast", inputs: [] }, +] as const; + +export const aaveV3MigrationBundlerAbi = [ + ...transferBundlerAbi, + ...permitBundlerAbi, + ...permit2BundlerAbi, + ...stEthBundlerAbi, + ...erc4626BundlerAbi, + ...morphoBundlerAbi, + { + type: "constructor", + inputs: [ + { name: "morpho", type: "address", internalType: "address" }, + { name: "aaveV3Pool", type: "address", internalType: "address" }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "AAVE_V3_POOL", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "contract IAaveV3" }], + stateMutability: "view", + }, + { + type: "function", + name: "aaveV3Repay", + inputs: [ + { name: "asset", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + { name: "interestRateMode", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "aaveV3Withdraw", + inputs: [ + { name: "asset", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { type: "error", name: "UnsafeCast", inputs: [] }, +] as const; + +export const aaveV3OptimizerMigrationBundlerAbi = [ + ...transferBundlerAbi, + ...permitBundlerAbi, + ...permit2BundlerAbi, + ...stEthBundlerAbi, + ...erc4626BundlerAbi, + ...morphoBundlerAbi, + { + type: "constructor", + inputs: [ + { name: "morpho", type: "address", internalType: "address" }, + { name: "aaveV3Optimizer", type: "address", internalType: "address" }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "AAVE_V3_OPTIMIZER", + inputs: [], + outputs: [ + { name: "", type: "address", internalType: "contract IAaveV3Optimizer" }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "aaveV3OptimizerApproveManagerWithSig", + inputs: [ + { name: "isApproved", type: "bool", internalType: "bool" }, + { name: "nonce", type: "uint256", internalType: "uint256" }, + { name: "deadline", type: "uint256", internalType: "uint256" }, + { + name: "signature", + type: "tuple", + internalType: "struct Signature", + components: [ + { name: "v", type: "uint8", internalType: "uint8" }, + { name: "r", type: "bytes32", internalType: "bytes32" }, + { name: "s", type: "bytes32", internalType: "bytes32" }, + ], + }, + { name: "skipRevert", type: "bool", internalType: "bool" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "aaveV3OptimizerRepay", + inputs: [ + { name: "underlying", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "aaveV3OptimizerWithdraw", + inputs: [ + { name: "underlying", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + { name: "maxIterations", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "aaveV3OptimizerWithdrawCollateral", + inputs: [ + { name: "underlying", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { type: "error", name: "UnsafeCast", inputs: [] }, +] as const; + +export const compoundV2MigrationBundlerAbi = [ + ...transferBundlerAbi, + ...permitBundlerAbi, + ...permit2BundlerAbi, + ...stEthBundlerAbi, + ...erc4626BundlerAbi, + ...morphoBundlerAbi, + { + type: "constructor", + inputs: [ + { name: "morpho", type: "address", internalType: "address" }, + { name: "wNative", type: "address", internalType: "address" }, + { name: "cEth", type: "address", internalType: "address" }, + ], + stateMutability: "nonpayable", + }, + { type: "receive", stateMutability: "payable" }, + { + type: "function", + name: "C_ETH", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "compoundV2Redeem", + inputs: [ + { name: "cToken", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "compoundV2Repay", + inputs: [ + { name: "cToken", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { type: "error", name: "UnsafeCast", inputs: [] }, +] as const; + +export const compoundV3MigrationBundlerAbi = [ + ...transferBundlerAbi, + ...permitBundlerAbi, + ...permit2BundlerAbi, + ...stEthBundlerAbi, + ...erc4626BundlerAbi, + ...morphoBundlerAbi, + { + type: "constructor", + inputs: [{ name: "morpho", type: "address", internalType: "address" }], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "compoundV3AllowBySig", + inputs: [ + { name: "instance", type: "address", internalType: "address" }, + { name: "isAllowed", type: "bool", internalType: "bool" }, + { name: "nonce", type: "uint256", internalType: "uint256" }, + { name: "expiry", type: "uint256", internalType: "uint256" }, + { name: "v", type: "uint8", internalType: "uint8" }, + { name: "r", type: "bytes32", internalType: "bytes32" }, + { name: "s", type: "bytes32", internalType: "bytes32" }, + { name: "skipRevert", type: "bool", internalType: "bool" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "compoundV3Repay", + inputs: [ + { name: "instance", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "compoundV3WithdrawFrom", + inputs: [ + { name: "instance", type: "address", internalType: "address" }, + { name: "asset", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + { type: "error", name: "UnsafeCast", inputs: [] }, +] as const; diff --git a/packages/blue-sdk-viem-bundler/src/errors.ts b/packages/blue-sdk-viem-bundler/src/errors.ts new file mode 100644 index 00000000..4625c049 --- /dev/null +++ b/packages/blue-sdk-viem-bundler/src/errors.ts @@ -0,0 +1,24 @@ +import { SimulationResult } from "@morpho-org/blue-sdk-viem-simulation"; + +import { InputBundlerOperation } from "./types/index.js"; + +export namespace BundlerErrors { + export class Bundle extends Error { + constructor( + public readonly error: Error, + public readonly index: number, + public readonly inputOperation: InputBundlerOperation, + public readonly steps: SimulationResult, + ) { + super(error.message); + + this.stack = error.stack; + } + } + + export class MissingSignature extends Error { + constructor() { + super(`missing signature`); + } + } +} diff --git a/packages/blue-sdk-viem-bundler/src/helpers/actions.ts b/packages/blue-sdk-viem-bundler/src/helpers/actions.ts new file mode 100644 index 00000000..b3bf6d63 --- /dev/null +++ b/packages/blue-sdk-viem-bundler/src/helpers/actions.ts @@ -0,0 +1,838 @@ +import { + Address, + encodeFunctionData, + erc20Abi, + maxUint256, + zeroAddress, +} from "viem"; + +import { + ChainId, + MathLib, + NATIVE_ADDRESS, + convexWrapperTokens, + erc20WrapperTokens, + getChainAddresses, +} from "@morpho-org/blue-sdk"; +import { + MaybeDraft, + Operation, + SimulationResult, + SimulationState, + simulateOperation, +} from "@morpho-org/blue-sdk-viem-simulation"; +import { Time, getValue } from "@morpho-org/morpho-ts"; + +import { + blueAbi, + getAuthorizationTypedData, + getDaiPermitTypedData, + getPermit2PermitTypedData, + getPermitTypedData, +} from "@morpho-org/blue-sdk-viem"; +import { sendTransaction, signTypedData } from "viem/actions"; +import BundlerAction from "../BundlerAction.js"; +import { baseBundlerAbi } from "../abis.js"; +import { + Action, + ActionBundle, + BundlerOperation, + TransactionRequirement, +} from "../types/index.js"; + +export const APPROVE_ONLY_ONCE_TOKENS: Partial> = { + [ChainId.EthMainnet]: [ + "0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT + "0xD533a949740bb3306d119CC777fa900bA034cd52", // CRV + ], +}; + +const MAX_TOKEN_APPROVALS: Partial>> = { + [ChainId.EthMainnet]: { + "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984": MathLib.maxUint(96), // UNI --> see https://github.com/Uniswap/governance/blob/eabd8c71ad01f61fb54ed6945162021ee419998e/contracts/Uni.sol#L154 + }, +}; + +const encodeErc20Approval = ( + token: Address, + sender: Address, + spender: Address, + amount: bigint, + data: MaybeDraft, +) => { + const { chainId } = data; + const { morpho, bundler, permit2 } = getChainAddresses(chainId); + + amount = MathLib.min( + amount, + MAX_TOKEN_APPROVALS[chainId]?.[token] ?? maxUint256, + ); + + const txRequirements: TransactionRequirement[] = []; + + if (APPROVE_ONLY_ONCE_TOKENS[chainId]?.includes(token)) { + const contract = + spender === morpho + ? "morpho" + : spender === bundler + ? "bundler" + : spender === permit2 + ? "permit2" + : undefined; + + const currentAllowance = + contract != null + ? data.getHolding(sender, token).erc20Allowances[contract] + : data.vaults[spender]?.asset === token + ? data.getVaultUser(spender, sender).allowance + : 0n; + + if (currentAllowance !== 0n) + txRequirements.push({ + type: "erc20Approve", + args: [token, spender, 0n], + tx: { + to: token, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [spender, 0n], + }), + }, + }); + } + + txRequirements.push({ + type: "erc20Approve", + args: [token, spender, amount], + tx: { + to: token, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [spender, amount], + }), + }, + }); + + return txRequirements; +}; + +export const encodeOperation = ( + operation: BundlerOperation, + dataBefore: MaybeDraft, + supportsSignature = true, + index = 0, +) => { + const { chainId } = dataBefore; + const deadline = Time.timestamp() + Time.s.from.h(24n); + const { + morpho, + bundler, + publicAllocator, + permit2, + wNative, + dai, + wstEth, + stEth, + } = getChainAddresses(chainId); + + let value = 0n; + const actions: Action[] = []; + const requirements: ActionBundle["requirements"] = { + signatures: [], + txs: [], + }; + + let callbackBundle: ActionBundle | undefined; + + const callback = getValue(operation.args, "callback"); + + const simulatedOperation = { + ...operation, + args: { + ...operation.args, + ...(callback && { + callback: (dataBefore) => { + callbackBundle = encodeBundle( + callback, + dataBefore, + supportsSignature, + ); + + return callback; + }, + }), + }, + } as Operation; + + // Operations with callbacks are encoded recursively as a side-effect of the simulation, within the callback itself. + const dataAfter = simulateOperation(simulatedOperation, dataBefore, index); + + if (callbackBundle) { + requirements.txs.push(...callbackBundle.requirements.txs); + requirements.signatures.push(...callbackBundle.requirements.signatures); + } + + const { sender, address } = operation; + + switch (operation.type) { + case "Blue_SetAuthorization": { + const { owner } = operation.args; + + if (supportsSignature) { + const ownerData = dataBefore.getUser(owner); + + const authorization = { + authorizer: owner, + authorized: bundler, + isAuthorized: true, + deadline, + nonce: ownerData.morphoNonce, + }; + + const action: Action = { + type: "morphoSetAuthorizationWithSig", + args: [authorization, null], + }; + + actions.push(action); + + requirements.signatures.push({ + action, + async sign(client) { + if (action.args[1] != null) return action.args[1]; + + return (action.args[1] = await signTypedData(client, { + account: sender, + ...getAuthorizationTypedData(authorization, chainId), + })); + }, + }); + + break; + } + + // Signatures are not supported, fallback to standard approval. + + requirements.txs.push({ + type: "morphoSetAuthorization", + args: [bundler, true], + tx: { + to: morpho, + data: encodeFunctionData({ + abi: blueAbi, + functionName: "setAuthorization", + args: [bundler, true], + }), + }, + }); + + break; + } + case "Erc20_Approve": { + // Native token cannot be approved. + if (address === NATIVE_ADDRESS) break; + + const { amount, spender } = operation.args; + + // Signatures are not supported, skip Permit2 approval. + if (!supportsSignature && spender === permit2) break; + + requirements.txs.push( + ...encodeErc20Approval(address, sender, spender, amount, dataBefore), + ); + + break; + } + case "Erc20_Permit": { + // Native token cannot be permitted. + if (address === NATIVE_ADDRESS) break; + + const { amount, spender, nonce } = operation.args; + + if (supportsSignature) { + const action: Action = + address === dai + ? { + type: "permitDai", + args: [nonce, deadline, true, null], + } + : { + type: "permit", + args: [address, amount, deadline, null], + }; + + actions.push(action); + + const tokenData = dataBefore.getToken(address); + + requirements.signatures.push({ + action, + async sign(client) { + if (action.args[3] != null) return action.args[3]; // action is already signed + + return (action.args[3] = + address === dai + ? await signTypedData(client, { + account: sender, + ...getDaiPermitTypedData( + { + owner: sender, + spender, + allowance: amount, + nonce, + deadline, + }, + chainId, + ), + }) + : await signTypedData(client, { + account: sender, + ...getPermitTypedData( + { + name: tokenData.name, + address: tokenData.address, + owner: sender, + spender, + allowance: amount, + nonce, + deadline, + }, + chainId, + ), + })); + }, + }); + + break; + } + + // Simple permit is not supported, fallback to standard approval. + + requirements.txs.push( + ...encodeErc20Approval(address, sender, spender, amount, dataBefore), + ); + + break; + } + case "Erc20_Permit2": { + // Native token cannot be permitted. + if (address === NATIVE_ADDRESS) break; + + const { amount, spender, expiration, nonce } = operation.args; + + if (supportsSignature) { + const action: Action = { + type: "approve2", + args: [ + { + details: { + token: address, + amount, + nonce: Number(nonce), + expiration: Number(expiration), + }, + spender, + sigDeadline: deadline, + }, + null, + ], + }; + + actions.push(action); + + requirements.signatures.push({ + action, + async sign(client) { + const { details, spender, sigDeadline } = action.args[0]; + + if (action.args[1] != null) return action.args[1]; // action is already signed + + return (action.args[1] = await signTypedData(client, { + account: sender, + ...getPermit2PermitTypedData( + { + spender, + allowance: details.amount, + erc20: details.token, + nonce: details.nonce, + deadline: sigDeadline, + expiration: details.expiration, + }, + chainId, + ), + })); + }, + }); + + break; + } + + // Signatures are not supported, fallback to standard approval. + + requirements.txs.push( + ...encodeErc20Approval(address, sender, spender, amount, dataBefore), + ); + + break; + } + case "Erc20_Transfer": { + const { amount, from, to } = operation.args; + + // Output transfer from the bundler. + if (from === bundler) { + if (address === NATIVE_ADDRESS) { + actions.push({ + type: "nativeTransfer", + args: [to, amount], + }); + + break; + } + + actions.push({ + type: "erc20Transfer", + args: [address, to, amount], + }); + + break; + } + + // Input transfer to the bundler. + if (to === bundler) { + // Native token transfer is added to the call value (thus batched at the start of the bundle). + if (address === NATIVE_ADDRESS) { + value += amount; + + break; + } + + actions.push({ + type: "erc20TransferFrom", + args: [address, amount], + }); + + break; + } + + // Any other transfer is ignored. + + break; + } + case "Erc20_Transfer2": { + const { amount, from, to } = operation.args; + + // Output transfer2 from the bundler is treated like a standard output transfer. + if (from === bundler) { + if (address === NATIVE_ADDRESS) { + actions.push({ + type: "nativeTransfer", + args: [to, amount], + }); + + break; + } + + actions.push({ + type: "erc20Transfer", + args: [address, to, amount], + }); + + break; + } + + // Input transfer2 to the bundler. + if (to === bundler) { + // Native token transfer is added to the call value (thus batched at the start of the bundle). + if (address === NATIVE_ADDRESS) { + value += amount; + + break; + } + + if (supportsSignature) { + actions.push({ + type: "transferFrom2", + args: [address, amount], + }); + + break; + } + + // Signatures are not supported, fallback to standard transfer. + + actions.push({ + type: "erc20TransferFrom", + args: [address, amount], + }); + + break; + } + + // Any other transfer is ignored. + + break; + } + case "Erc20_Wrap": { + const { amount } = operation.args; + + switch (address) { + case wNative: { + actions.push({ + type: "wrapNative", + args: [amount], + }); + + break; + } + case wstEth: { + actions.push({ + type: "wrapStEth", + args: [amount], + }); + + break; + } + case stEth: { + actions.push({ + type: "stakeEth", + args: [amount, 0n, zeroAddress], + }); + + break; + } + default: { + if (erc20WrapperTokens[chainId].has(address)) { + actions.push({ + type: "erc20WrapperDepositFor", + args: [address, amount], + }); + + break; + } + + // Convex token wrapping is executed onchain along with supplyCollateral, via depositFor. + if (!convexWrapperTokens[chainId].has(address)) + throw Error(`unexpected token wrap: ${address}`); + } + } + + break; + } + case "Erc20_Unwrap": { + const { amount, receiver } = operation.args; + + switch (address) { + case wNative: { + actions.push({ + type: "unwrapNative", + args: [amount], + }); + + break; + } + case wstEth: { + actions.push({ + type: "unwrapStEth", + args: [amount], + }); + + break; + } + default: { + if (!erc20WrapperTokens[chainId].has(address)) + throw Error(`unexpected token unwrap: ${address}`); + + actions.push({ + type: "erc20WrapperWithdrawTo", + args: [address, receiver, amount], + }); + } + } + + break; + } + case "Blue_Supply": { + const { id, assets = 0n, shares = 0n, onBehalf } = operation.args; + + const { config } = dataBefore.getMarket(id); + + // Already takes slippage into account. + const slippageAmount = + shares === 0n + ? dataAfter.getPosition(onBehalf, id).supplyShares - + dataBefore.getPosition(onBehalf, id).supplyShares + : dataAfter.getHolding(sender, config.loanToken).balance - + dataBefore.getHolding(sender, config.loanToken).balance; + + actions.push({ + type: "morphoSupply", + args: [ + config, + assets, + shares, + slippageAmount, + onBehalf, + callbackBundle?.actions ?? [], + ], + }); + + break; + } + case "Blue_Withdraw": { + const { + id, + assets = 0n, + shares = 0n, + onBehalf, + receiver, + } = operation.args; + + const { config } = dataBefore.getMarket(id); + + // Already takes slippage into account. + const slippageAmount = + shares === 0n + ? dataBefore.getPosition(onBehalf, id).supplyShares - + dataAfter.getPosition(onBehalf, id).supplyShares + : dataBefore.getHolding(sender, config.loanToken).balance - + dataAfter.getHolding(sender, config.loanToken).balance; + + actions.push({ + type: "morphoWithdraw", + args: [config, assets, shares, slippageAmount, receiver], + }); + + break; + } + case "Blue_Borrow": { + const { + id, + assets = 0n, + shares = 0n, + onBehalf, + receiver, + } = operation.args; + + const { config } = dataBefore.getMarket(id); + + // Already takes slippage into account. + const slippageAmount = + shares === 0n + ? dataAfter.getPosition(onBehalf, id).borrowShares - + dataBefore.getPosition(onBehalf, id).borrowShares + : dataAfter.getHolding(sender, config.loanToken).balance - + dataBefore.getHolding(sender, config.loanToken).balance; + + actions.push({ + type: "morphoBorrow", + args: [config, assets, shares, slippageAmount, receiver], + }); + + break; + } + case "Blue_Repay": { + const { id, assets = 0n, shares = 0n, onBehalf } = operation.args; + + const { config } = dataBefore.getMarket(id); + + // Already takes slippage into account. + const slippageAmount = + shares === 0n + ? dataBefore.getPosition(onBehalf, id).borrowShares - + dataAfter.getPosition(onBehalf, id).borrowShares + : dataBefore.getHolding(sender, config.loanToken).balance - + dataAfter.getHolding(sender, config.loanToken).balance; + + actions.push({ + type: "morphoRepay", + args: [ + config, + assets, + shares, + slippageAmount, + onBehalf, + callbackBundle?.actions ?? [], + ], + }); + + break; + } + case "Blue_SupplyCollateral": { + const { id, assets, onBehalf } = operation.args; + + const { config } = dataBefore.getMarket(id); + + if (convexWrapperTokens[chainId].has(config.collateralToken)) { + actions.push({ + type: "erc20WrapperDepositFor", + args: [config.collateralToken, assets], + }); + + break; + } + + actions.push({ + type: "morphoSupplyCollateral", + args: [config, assets, onBehalf, callbackBundle?.actions ?? []], + }); + + break; + } + case "Blue_WithdrawCollateral": { + const { id, assets, receiver } = operation.args; + + const { config } = dataBefore.getMarket(id); + + actions.push({ + type: "morphoWithdrawCollateral", + args: [config, assets, receiver], + }); + + break; + } + case "MetaMorpho_Deposit": { + const { assets = 0n, shares = 0n, owner } = operation.args; + + if (shares === 0n) { + // Already takes slippage into account. + const expectedShares = + dataAfter.getHolding(owner, address).balance - + dataBefore.getHolding(owner, address).balance; + + actions.push({ + type: "erc4626Deposit", + args: [address, assets, expectedShares, owner], + }); + } else { + const vaultConfig = dataBefore.getVault(address); + + // Already takes slippage into account. + const expectedAssets = + dataBefore.getHolding(sender, vaultConfig.asset).balance - + dataAfter.getHolding(sender, vaultConfig.asset).balance; + + actions.push({ + type: "erc4626Mint", + args: [address, shares, expectedAssets, owner], + }); + } + + break; + } + case "MetaMorpho_Withdraw": { + const { assets = 0n, shares = 0n, owner, receiver } = operation.args; + + if (assets > 0n) { + // Already takes slippage into account. + const expectedShares = + dataBefore.getHolding(owner, address).balance - + dataAfter.getHolding(owner, address).balance; + + actions.push({ + type: "erc4626Withdraw", + args: [address, assets, expectedShares, receiver, owner], + }); + } else { + const vaultConfig = dataBefore.getVault(address); + + // Already takes slippage into account. + const expectedAssets = + dataAfter.getHolding(receiver, vaultConfig.asset).balance - + dataBefore.getHolding(receiver, vaultConfig.asset).balance; + + actions.push({ + type: "erc4626Redeem", + args: [address, shares, expectedAssets, receiver, owner], + }); + } + + break; + } + case "MetaMorpho_PublicReallocate": { + const { withdrawals, supplyMarketId } = operation.args; + + if (publicAllocator == null) + throw Error(`unknown public allocator on chain ${chainId}`); + + const { fee } = dataBefore.getVault(address).publicAllocatorConfig!; + + // Value is already accrued via another native input transfer. + + actions.push({ + type: "reallocateTo", + args: [ + publicAllocator, + address, + fee, + withdrawals.map(({ id, assets }) => ({ + marketParams: dataBefore.getMarket(id).config, + amount: assets, + })), + dataBefore.getMarket(supplyMarketId).config, + ], + }); + + break; + } + } + + return { + dataAfter, + value, + actions, + requirements, + }; +}; + +export function encodeBundle( + operations: BundlerOperation[], + startData: MaybeDraft, + supportsSignature = true, +): ActionBundle { + const { chainId } = startData; + const { bundler } = getChainAddresses(chainId); + + let value = 0n; + const actions: Action[] = []; + const requirements: ActionBundle["requirements"] = { + signatures: [], + txs: [], + }; + + const steps: SimulationResult = [startData]; + + for (let index = 0; index < operations.length; ++index) { + const bundle = encodeOperation( + operations[index]!, + steps[index]!, + supportsSignature, + index, + ); + + steps.push(bundle.dataAfter); + + value += bundle.value; + actions.push(...bundle.actions); + requirements.signatures.push(...bundle.requirements.signatures); + requirements.txs.push(...bundle.requirements.txs); + } + + sendTransaction; + + return { + steps, + actions, + requirements, + tx: () => ({ + to: bundler, + value, + data: encodeFunctionData({ + abi: baseBundlerAbi, + functionName: "multicall", + args: [actions.map(BundlerAction.encode)], + }), + }), + }; +} diff --git a/packages/blue-sdk-viem-bundler/src/helpers/index.ts b/packages/blue-sdk-viem-bundler/src/helpers/index.ts new file mode 100644 index 00000000..8d1a9e7f --- /dev/null +++ b/packages/blue-sdk-viem-bundler/src/helpers/index.ts @@ -0,0 +1,2 @@ +export * from "./actions.js"; +export * from "./operations.js"; diff --git a/packages/blue-sdk-viem-bundler/src/helpers/operations.ts b/packages/blue-sdk-viem-bundler/src/helpers/operations.ts new file mode 100644 index 00000000..72b13c18 --- /dev/null +++ b/packages/blue-sdk-viem-bundler/src/helpers/operations.ts @@ -0,0 +1,919 @@ +import { + Address, + DEFAULT_SLIPPAGE_TOLERANCE, + DEFAULT_SUPPLY_TARGET_UTILIZATION, + MarketId, + MarketUtils, + MathLib, + NATIVE_ADDRESS, + erc20WrapperTokens, + getChainAddresses, + getUnwrappedToken, + permissionedBackedTokens, + permissionedWrapperTokens, +} from "@morpho-org/blue-sdk"; +import { + Erc20Operations, + MaybeDraft, + Operation, + Operations, + SimulationResult, + SimulationState, + handleOperation, + handleOperations, + produceImmutable, + simulateOperation, +} from "@morpho-org/blue-sdk-viem-simulation"; +import { entries, getLast, getValue, keys } from "@morpho-org/morpho-ts"; + +import { maxUint256 } from "viem"; +import { BundlerErrors } from "../errors.js"; +import { + BundlerOperation, + CallbackBundlerOperation, + InputBundlerOperation, +} from "../types/index.js"; + +export interface BundlingOptions { + withSimplePermit?: Set
; + publicAllocatorOptions?: PublicAllocatorOptions & { + supplyTargetUtilization?: Record; + }; + getRequirementOperations?: ( + requiredTokenAmounts: { + token: string; + required: bigint; + }[], + ) => BundlerOperation[]; +} + +export const populateInputTransfer = ( + { address, args: { amount, from } }: Operations["Erc20_Transfer"], + data: MaybeDraft, + { hasSimplePermit = false }: { hasSimplePermit?: boolean } = {}, +): Exclude[] => { + const { bundler, permit2 } = getChainAddresses(data.chainId); + + // If native token, it is expected to be sent along as call value. + if (address === NATIVE_ADDRESS) + return [ + { + type: "Erc20_Transfer", + sender: from, + address, + args: { + amount, + from, + to: bundler, + }, + }, + ]; + + const { erc20Allowances, permit2Allowances, erc2612Nonce } = data.getHolding( + from, + address, + ); + + // ERC20 allowance to the bundler is enough, consume it. + if (erc20Allowances.bundler >= amount) + return [ + { + type: "Erc20_Transfer", + sender: bundler, + address, + args: { + amount, + from, + to: bundler, + }, + }, + ]; + + const operations: Exclude[] = []; + + // Try using simple permit. + const useSimplePermit = + erc2612Nonce != null && + (data.tryGetVault(address) != null || // MetaMorpho vaults implement EIP-2612. + hasSimplePermit); + const isPermissioned = + permissionedWrapperTokens[data.chainId].has(address) || + permissionedBackedTokens[data.chainId].has(address); + + if (useSimplePermit) + operations.push({ + type: "Erc20_Permit", + sender: from, + address, + args: { + amount, + spender: bundler, + nonce: erc2612Nonce, + }, + }); + // Token is permissioned and Permit2 may not be authorized so Permit2 cannot be used. + else if (isPermissioned) + operations.push({ + type: "Erc20_Approve", + sender: from, + address, + args: { + amount, + spender: bundler, + }, + }); + + if (useSimplePermit || isPermissioned) + operations.push({ + type: "Erc20_Transfer", + sender: bundler, + address, + args: { + amount, + from, + to: bundler, + }, + }); + // Simple permit is not supported and token is not permissioned: fallback to Permit2. + else { + if (erc20Allowances.permit2 < amount) + operations.push({ + type: "Erc20_Approve", + sender: from, + address, + args: { + amount: MathLib.MAX_UINT_160, // Always approve infinite. + spender: permit2, + }, + }); + + if ( + permit2Allowances.bundler.amount < amount || + permit2Allowances.bundler.expiration < data.block.timestamp + ) + operations.push({ + type: "Erc20_Permit2", + sender: from, + address, + args: { + amount, + spender: bundler, + expiration: MathLib.MAX_UINT_48, // Always approve indefinitely. + nonce: permit2Allowances.bundler.nonce, + }, + }); + + operations.push({ + type: "Erc20_Transfer2", + sender: bundler, + address, + args: { + amount, + from, + to: bundler, + }, + }); + } + + return operations; +}; + +/** + * Simulates the input operation on the given simulation data with args tweaked so the bundler operates on behalf of the sender. + * Then, populates a bundle of operations made of: + * - required approvals to the bundler + * - required input transfers to the bundler + * - required token wrapping + * - the given operation + * @param inputOperation The input operation to populate a bundle for. + * @param data The simulation data to determine the required steps of the bundle to populate. If the provided simulation data is the result of a simulation + * of an already populated bundle, the `Transfer` and `Wrap` operation are only populated if required. + * @param wrapSlippage The slippage simulated during wraps. Should never be 0. + * @return The bundle of operations to optimize and skim before being encoded. + */ +export const populateSubBundle = ( + inputOperation: InputBundlerOperation, + data: MaybeDraft, + options: BundlingOptions = {}, +) => { + const { sender } = inputOperation; + const { morpho, bundler } = getChainAddresses(data.chainId); + const { + withSimplePermit = new Set(), + publicAllocatorOptions, + getRequirementOperations, + } = options; + + const operations: Exclude[] = []; + + const wrappedToken = + inputOperation.type === "Erc20_Wrap" + ? data.getWrappedToken(inputOperation.address) + : undefined; + + const isErc20Wrapper = + !!wrappedToken && + erc20WrapperTokens[data.chainId].has(wrappedToken.address); + + // Transform input operation to act on behalf of the sender, via the bundler. + const mainOperation = produceImmutable(inputOperation, (draft) => { + draft.sender = bundler; + + // Redirect MetaMorpho operation owner. + switch (draft.type) { + case "Erc20_Wrap": { + // ERC20Wrapper are skipped because tokens are sent to the caller, not the bundler. + if (isErc20Wrapper) { + draft.args.owner = sender; + break; + } + } + case "MetaMorpho_Deposit": + case "MetaMorpho_Withdraw": + // Only if sender is owner otherwise the owner would be lost. + if (draft.args.owner === sender) draft.args.owner = bundler; + } + + // Redirect operation targets. + switch (draft.type) { + case "Blue_Borrow": + case "Blue_Withdraw": + case "Blue_WithdrawCollateral": + draft.args.onBehalf = sender; + case "MetaMorpho_Withdraw": + // Only if sender is receiver otherwise the receiver would be lost. + if (draft.args.receiver === sender) draft.args.receiver = bundler; + } + }); + + const needsBundlerAuthorization = + mainOperation.type === "Blue_Borrow" || + mainOperation.type === "Blue_Withdraw" || + mainOperation.type === "Blue_WithdrawCollateral"; + if (needsBundlerAuthorization && !data.getUser(sender).isBundlerAuthorized) + operations.push({ + type: "Blue_SetAuthorization", + sender: bundler, + address: morpho, + args: { + owner: sender, + isBundlerAuthorized: true, + }, + }); + + // Reallocate liquidity if necessary. + if ( + mainOperation.type === "Blue_Borrow" || + mainOperation.type === "Blue_Withdraw" + ) { + const market = data + .getMarket(mainOperation.args.id) + .accrueInterest(data.block.timestamp); + + const borrowedAssets = + mainOperation.type === "Blue_Borrow" + ? mainOperation.args.assets ?? + market.toBorrowAssets(mainOperation.args.shares) + : 0n; + const withdrawnAssets = + mainOperation.type === "Blue_Withdraw" + ? mainOperation.args.assets ?? + market.toSupplyAssets(mainOperation.args.shares) + : 0n; + + const newTotalSupplyAssets = market.totalSupplyAssets - withdrawnAssets; + const newTotalBorrowAssets = market.totalBorrowAssets + borrowedAssets; + + const reallocations: { + [vault: Address]: { + id: MarketId; + assets: bigint; + }[]; + } = {}; + + const supplyTargetUtilization = + publicAllocatorOptions?.supplyTargetUtilization?.[market.config.id] ?? + DEFAULT_SUPPLY_TARGET_UTILIZATION; + + if ( + MarketUtils.getUtilization({ + totalSupplyAssets: newTotalSupplyAssets, + totalBorrowAssets: newTotalBorrowAssets, + }) > supplyTargetUtilization + ) { + // Liquidity is insufficient: trigger a public reallocation and try to have a resulting utilization as low as possible, above the target. + // Solve: newTotalBorrowAssets / (newTotalSupplyAssets + reallocatedAssets) = supplyTargetUtilization + // Worst case is: there is not enough withdrawals available to fill reallocatedAssets, so utilization is above supplyTargetUtilization. + let requiredAssets = + supplyTargetUtilization === 0n + ? MathLib.MAX_UINT_160 + : MathLib.wDivDown(newTotalBorrowAssets, supplyTargetUtilization) - + newTotalSupplyAssets; + + const { withdrawals } = data.getMarketPublicReallocations( + market.id, + publicAllocatorOptions, + ); + + for (const { vault, ...withdrawal } of withdrawals) { + const vaultReallocations = (reallocations[vault] ??= []); + + if (withdrawal.assets > requiredAssets) { + vaultReallocations.push({ + ...withdrawal, + assets: requiredAssets, + }); + + break; + } + + requiredAssets -= withdrawal.assets; + vaultReallocations.push(withdrawal); + } + + const fees = keys(reallocations).reduce( + (total, vault) => + total + data.getVault(vault).publicAllocatorConfig!.fee, + 0n, + ); + + // Native input transfer of all fees. + if (fees > 0n) + operations.push({ + type: "Erc20_Transfer", + sender, + address: NATIVE_ADDRESS, + args: { + amount: fees, + from: sender, + to: bundler, + }, + }); + } + + // Reallocate each vault. + operations.push( + ...Object.entries(reallocations).map( + ([vault, vaultWithdrawals]) => + ({ + type: "MetaMorpho_PublicReallocate", + sender: bundler, + address: vault, + args: { + // Reallocation withdrawals must be sorted by market id in ascending alphabetical order. + withdrawals: vaultWithdrawals.sort(({ id: idA }, { id: idB }) => + idA > idB ? 1 : -1, + ), + supplyMarketId: market.id, + }, + }) as Operations["MetaMorpho_PublicReallocate"], + ), + ); + } + + const callback = getValue(mainOperation.args, "callback"); + + const simulatedOperation = { + ...mainOperation, + args: { + ...mainOperation.args, + ...(callback && { + callback: (data) => { + const operations = callback.flatMap((inputOperation) => { + const subBundleOperations = populateSubBundle( + inputOperation, + data, + options, + ); + + // Handle to mutate data (not simulate). + handleBundlerOperations(subBundleOperations, data); + + return subBundleOperations; + }); + + (mainOperation as CallbackBundlerOperation).args.callback = + operations; + + return []; + }, + }), + }, + } as Operation; + + // Operations with callbacks are populated recursively as a side-effect of the simulation, within the callback itself. + let requiredTokenAmounts = data.simulateRequiredTokenAmounts( + (operations as Operation[]).concat([simulatedOperation]), + ); + + const allOperations = (operations as BundlerOperation[]).concat([ + mainOperation, + ]); + + // Skip approvals/transfers if operation only uses available balances (via maxUint256). + if ( + ("amount" in mainOperation.args && + mainOperation.args.amount === maxUint256) || + ("assets" in mainOperation.args && + mainOperation.args.assets === maxUint256) || + ("shares" in mainOperation.args && mainOperation.args.shares === maxUint256) + ) { + if (mainOperation.type === "MetaMorpho_Withdraw") + mainOperation.args.owner = bundler; + + return allOperations; + } + + const requirementOperations = + getRequirementOperations?.(requiredTokenAmounts) ?? []; + + requiredTokenAmounts = data.simulateRequiredTokenAmounts( + requirementOperations + .concat(allOperations) + .map((operation) => getSimulatedBundlerOperation(operation)), + ); + + // Append required input transfers. + requiredTokenAmounts.forEach(({ token, required }) => { + requirementOperations.push( + ...populateInputTransfer( + { + type: "Erc20_Transfer", + sender: bundler, + address: token, + args: { + amount: required, + from: sender, + to: bundler, + }, + }, + data, + { hasSimplePermit: withSimplePermit.has(token) }, + ), + ); + }); + + return requirementOperations.concat(allOperations); +}; + +/** + * Merges unnecessary duplicate `Erc20_Approve`, `Erc20_Transfer` and `Erc20_Wrap`. + * Also redirects `Blue_Borrow|Withdraw|WithdrawCollateral` & `MetaMorpho_Withdraw` operations from the bundler to the receiver, + * as long as the tokens received (possibly ERC4626 shares) are not used afterwards in the bundle. + * For all the other remaining tokens, appends `Erc20_Transfer` operations to the bundle, from the bundler to the receiver. + * @param operations The bundle to optimize. + * @param startData The start data from which to simulate th bundle. + * @param receiver The receiver of skimmed tokens. + * @param unwrapTokens The set of tokens to unwrap before transferring to the receiver. + * @param unwrapSlippage The slippage simulated during unwraps. Should never be 0. + * @return The optimized bundle. + */ +export const finalizeBundle = ( + operations: BundlerOperation[], + startData: SimulationState, + receiver: Address, + unwrapTokens = new Set
(), + unwrapSlippage = DEFAULT_SLIPPAGE_TOLERANCE, +) => { + const nbOperations = operations.length; + if (nbOperations === 0) return operations; + + const { bundler } = getChainAddresses(startData.chainId); + + if (receiver === bundler) throw Error(`receiver is bundler`); + + const approvals = [] as Operations["Erc20_Approve"][]; + const permits = [] as Operations["Erc20_Permit"][]; + const permit2s = [] as Operations["Erc20_Permit2"][]; + const inputTransfers = [] as Operations["Erc20_Transfer"][]; + const inputTransfer2s = [] as Operations["Erc20_Transfer2"][]; + const others = [] as BundlerOperation[]; + + // TODO input transfers can be merged to the right-most position where transferred assets are still not used + // Merge together approvals, permits, permit2s & input transfers. + operations.forEach((operation) => { + switch (operation.type) { + case "Erc20_Approve": { + const duplicateApproval = approvals.find( + (approval) => + approval.address === operation.address && + approval.sender === operation.sender && + approval.args.spender === operation.args.spender, + ); + + if (duplicateApproval == null) return approvals.push(operation); + + duplicateApproval.args.amount += operation.args.amount; + + break; + } + case "Erc20_Permit": { + const duplicatePermit = permits.find( + (permit) => + permit.address === operation.address && + permit.sender === operation.sender && + permit.args.spender === operation.args.spender, + ); + + if (duplicatePermit == null) { + const lastPermit = permits.findLast( + (permit) => + permit.address === operation.address && + permit.sender === operation.sender, + ); + + if (lastPermit) operation.args.nonce = lastPermit.args.nonce + 1n; + + permits.push(operation); + } else duplicatePermit.args.amount += operation.args.amount; + + break; + } + case "Erc20_Permit2": { + const duplicatePermit2 = permit2s.find( + (permit2) => + permit2.address === operation.address && + permit2.sender === operation.sender && + permit2.args.spender === operation.args.spender, + ); + + if (duplicatePermit2 == null) { + const lastPermit2 = permit2s.findLast( + (permit2) => + permit2.address === operation.address && + permit2.sender === operation.sender, + ); + + if (lastPermit2) operation.args.nonce = lastPermit2.args.nonce + 1n; + + permit2s.push(operation); + } else duplicatePermit2.args.amount += operation.args.amount; + + break; + } + case "Erc20_Transfer": { + const { + address, + sender, + args: { amount, from, to }, + } = operation; + + if ( + from !== bundler && + to === bundler && + !erc20WrapperTokens[startData.chainId].has(address) + ) { + const duplicateTransfer = inputTransfers.find( + (transfer) => + transfer.address === address && + transfer.sender === sender && + transfer.args.from === from, + ); + + if ( + duplicateTransfer == null || + // Don't merge the input transfer if from didn't have enough balance at the start. + startData.getHolding(from, address).balance < amount + ) + return inputTransfers.push(operation); + + duplicateTransfer.args.amount += amount; + + return; + } + + others.push(operation); + + break; + } + case "Erc20_Transfer2": { + const { + address, + sender, + args: { amount, from, to }, + } = operation; + + if (from !== bundler && to === bundler) { + const duplicateTransfer2 = inputTransfer2s.find( + (transfer) => + transfer.address === address && + transfer.sender === sender && + transfer.args.from === from, + ); + + if ( + duplicateTransfer2 == null || + // Don't merge the input transfer if from didn't have enough balance at the start. + startData.getHolding(from, address).balance < amount + ) + return inputTransfer2s.push(operation); + + duplicateTransfer2.args.amount += amount; + + return; + } + + others.push(operation); + + break; + } + // Cannot factorize public reallocations because the liquidity may not always be available before other operations. + default: + others.push(operation); + } + }); + + operations = [ + approvals, + permits, + permit2s, + inputTransfers, + inputTransfer2s, + others, + ].flat(1); + + let steps = simulateBundlerOperations(operations, startData); + + // Redirect MetaMorpho deposits. + operations.forEach((operation, index) => { + if ( + operation.type !== "MetaMorpho_Deposit" || + operation.args.owner !== bundler + ) + return; + + const token = operation.address; + + // shares are not defined when depositing assets, so we rely on simulation steps. + const shares = + steps[index + 1]!.getHolding(bundler, token).balance - + steps[index]!.getHolding(bundler, token).balance; + + if ( + steps + .slice(index + 2) + .some((step) => step.getHolding(bundler, token).balance < shares) + ) + // If the bundler's balance is at least once lower than assets, the bundler does need these assets. + return; + + operation.args.owner = receiver; + }); + + // Redirect borrows, withdrawals & MetaMorpho withdrawals. + operations.forEach((operation, index) => { + let token: Address; + switch (operation.type) { + case "Blue_Borrow": + case "Blue_Withdraw": + token = startData.getMarket(operation.args.id).config.loanToken; + break; + case "Blue_WithdrawCollateral": + token = startData.getMarket(operation.args.id).config.collateralToken; + break; + case "MetaMorpho_Withdraw": + token = startData.getVault(operation.address).config.asset; + break; + default: + return; + } + + if (operation.args.receiver !== bundler || unwrapTokens.has(token)) return; + + // assets are not defined when using shares, so we rely on simulation steps. + const assets = + steps[index + 1]!.getHolding(bundler, token).balance - + steps[index]!.getHolding(bundler, token).balance; + + if ( + steps + .slice(index + 2) + .some((step) => step.getHolding(bundler, token).balance < assets) + ) + // If the bundler's balance is at least once lower than assets, the bundler does need these assets. + return; + + operation.args.receiver = receiver; + }); + + // Simplify Erc20_Transfer(sender = bundler, to = bundler) + MetaMorpho_Withdraw(owner = bundler) = MetaMorpho_Withdraw(owner = from). + operations.forEach((operation, index) => { + if ( + operation.type !== "MetaMorpho_Withdraw" || + operation.args.owner !== bundler + ) + return; + + // shares are not defined when using assets, so we rely on simulation steps. + const shares = + steps[index]!.getHolding(bundler, operation.address).balance - + steps[index + 1]!.getHolding(bundler, operation.address).balance; + + const inputTransferIndex = operations.findIndex( + (candidate) => + candidate.type === "Erc20_Transfer" && + candidate.address === operation.address && + candidate.sender === bundler && + candidate.args.to === bundler && + candidate.args.amount >= shares, + ); + if (inputTransferIndex <= 0) return; + + const inputTransfer = operations[ + inputTransferIndex + ] as Operations["Erc20_Transfer"]; + + inputTransfer.args.amount -= shares; + + operation.args.owner = inputTransfer.args.from; + }); + + // Filter out useless input transfers. + operations = operations.filter((operation, index) => { + if (operation.type !== "Erc20_Transfer") return true; + + const { amount, from, to } = operation.args; + + if (from === bundler || to !== bundler) return true; + + const token = operation.address; + + if ( + steps + .slice(index + 2) + .some((step) => step.getHolding(bundler, token).balance < amount) + ) + // If the bundler's balance is at least once less than amount, the bundler does need these assets. + // Do not only keep the amount actually used in this case because some input transfers + // are expected to be larger to account for slippage. + return true; + + return false; + }); + + // Simulate without slippage to skim the bundler of all possible surplus of shares & assets. + steps = simulateBundlerOperations(operations, startData, { slippage: 0n }); + + // Unwrap requested remaining wrapped tokens. + const unwraps = [] as Erc20Operations["Erc20_Unwrap"][]; + + const endBundlerTokenData = getLast(steps).holdings[bundler] ?? {}; + + unwrapTokens.forEach((wrappedToken) => { + const remaining = endBundlerTokenData[wrappedToken]?.balance ?? 0n; + if (remaining <= 5n) return; + + const unwrappedToken = getUnwrappedToken(wrappedToken, startData.chainId); + if (unwrappedToken == null) return; + + unwraps.push({ + type: "Erc20_Unwrap", + address: wrappedToken, + sender: bundler, + args: { + amount: maxUint256, + receiver, + slippage: unwrapSlippage, + }, + }); + }); + + if (unwraps.length > 0) + steps = simulateBundlerOperations(operations.concat(unwraps), startData, { + slippage: 0n, + }); + + // Skim any token expected to be left on the bundler. + const skims = [] as Erc20Operations["Erc20_Transfer"][]; + { + const startBundlerTokenData = steps[0].holdings[bundler] ?? {}; + const endBundlerTokenData = getLast(steps).holdings[bundler] ?? {}; + + skims.push( + ...entries(endBundlerTokenData) + .filter( + ([token, { balance }]) => + balance - (startBundlerTokenData[token]?.balance ?? 0n) > 5n, + ) + .map( + ([address]) => + ({ + type: "Erc20_Transfer", + address, + sender: bundler, + args: { + amount: maxUint256, + from: bundler, + to: receiver, + }, + }) as Erc20Operations["Erc20_Transfer"], + ), + ); + } + + return operations.concat(unwraps, skims); +}; + +export const populateBundle = ( + inputOperations: InputBundlerOperation[], + data: MaybeDraft, + options?: BundlingOptions, +) => { + const steps: SimulationResult = [data]; + + let end = data; + const operations = inputOperations.flatMap((inputOperation, index) => { + try { + const subBundleOperations = populateSubBundle( + inputOperation, + end, + options, + ); + + steps.push( + (end = getLast(simulateBundlerOperations(subBundleOperations, end))), + ); + + return subBundleOperations; + } catch (error: any) { + throw new BundlerErrors.Bundle(error, index, inputOperation, steps); + } + }); + + return { operations, steps }; +}; + +export const getSimulatedBundlerOperation = ( + operation: BundlerOperation, + { slippage }: { slippage?: bigint } = {}, +) => { + const callback = getValue(operation.args, "callback"); + + const simulatedOperation = { + ...operation, + args: { + ...operation.args, + ...(callback && { + callback: () => + callback.map((operation) => + getSimulatedBundlerOperation(operation, { slippage }), + ), + }), + }, + } as Operation; + + if (slippage != null) { + switch (simulatedOperation.type) { + case "Erc20_Wrap": + case "Erc20_Unwrap": + case "Blue_Supply": + case "Blue_Withdraw": + case "Blue_Borrow": + case "Blue_Repay": + case "MetaMorpho_Deposit": + case "MetaMorpho_Withdraw": + simulatedOperation.args.slippage = slippage; + break; + } + } + + return simulatedOperation; +}; + +export const handleBundlerOperation = + (options?: { slippage?: bigint }) => + ( + operation: BundlerOperation, + startData: MaybeDraft, + index?: number, + ) => + handleOperation( + getSimulatedBundlerOperation(operation, options), + startData, + index, + ); + +export const handleBundlerOperations = ( + operations: BundlerOperation[], + startData: MaybeDraft, + options?: { slippage?: bigint }, +) => handleOperations(operations, startData, handleBundlerOperation(options)); + +export const simulateBundlerOperation = + (options?: { slippage?: bigint }) => + ( + operation: BundlerOperation, + startData: MaybeDraft, + index?: number, + ) => + simulateOperation( + getSimulatedBundlerOperation(operation, options), + startData, + index, + ); + +export const simulateBundlerOperations = ( + operations: BundlerOperation[], + startData: MaybeDraft, + options?: { slippage?: bigint }, +) => handleOperations(operations, startData, simulateBundlerOperation(options)); diff --git a/packages/blue-sdk-viem-bundler/src/index.ts b/packages/blue-sdk-viem-bundler/src/index.ts new file mode 100644 index 00000000..a0cbeacb --- /dev/null +++ b/packages/blue-sdk-viem-bundler/src/index.ts @@ -0,0 +1,7 @@ +export * from "./helpers/index.js"; +export * from "./errors.js"; +export * from "./types/index.js"; + +export * as helpers from "./helpers/index.js"; +export * as errors from "./errors.js"; +export * as types from "./types/index.js"; diff --git a/packages/blue-sdk-viem-bundler/src/types/actions.ts b/packages/blue-sdk-viem-bundler/src/types/actions.ts new file mode 100644 index 00000000..2b4eaf3c --- /dev/null +++ b/packages/blue-sdk-viem-bundler/src/types/actions.ts @@ -0,0 +1,285 @@ +import { + Account, + Chain, + Client, + Hex, + RpcSchema, + SendTransactionRequest, + Transport, +} from "viem"; + +import { Address, MarketConfig } from "@morpho-org/blue-sdk"; +import { SimulationResult } from "@morpho-org/blue-sdk-viem-simulation"; + +export type MarketParams = Pick< + MarketConfig, + "loanToken" | "collateralToken" | "oracle" | "irm" | "lltv" +>; + +export interface Authorization { + authorizer: Address; + authorized: Address; + isAuthorized: boolean; + nonce: bigint; + deadline: bigint; +} + +export interface ReallocationWithdrawal { + marketParams: MarketParams; + amount: bigint; +} + +export interface Permit2PermitSingleDetails { + token: Address; + amount: bigint; + expiration: number; + nonce: number; +} + +export interface Permit2PermitSingle { + details: Permit2PermitSingleDetails; + spender: Address; + sigDeadline: bigint; +} + +export interface ActionArgs { + /* ERC20 */ + nativeTransfer: [recipient: Address, amount: bigint]; + erc20Transfer: [asset: Address, recipient: Address, amount: bigint]; + erc20TransferFrom: [asset: Address, amount: bigint]; + + /* ERC20Wrapper */ + erc20WrapperDepositFor: [wrapper: Address, amount: bigint]; + erc20WrapperWithdrawTo: [wrapper: Address, account: Address, amount: bigint]; + + /* Permit */ + permit: [ + asset: Address, + amount: bigint, + deadline: bigint, + signature: Hex | null, + skipRevert?: boolean, + ]; + permitDai: [ + nonce: bigint, + expiry: bigint, + allowed: boolean, + signature: Hex | null, + skipRevert?: boolean, + ]; + + /* Permit2 */ + approve2: [ + permitSingle: Permit2PermitSingle, + signature: Hex | null, + skipRevert?: boolean, + ]; + transferFrom2: [asset: Address, amount: bigint]; + + /* ERC4626 */ + erc4626Mint: [ + erc4626: Address, + shares: bigint, + maxAssets: bigint, + receiver: Address, + ]; + erc4626Deposit: [ + erc4626: Address, + assets: bigint, + minShares: bigint, + receiver: Address, + ]; + erc4626Withdraw: [ + erc4626: Address, + assets: bigint, + maxShares: bigint, + receiver: Address, + owner: Address, + ]; + erc4626Redeem: [ + erc4626: Address, + shares: bigint, + minAssets: bigint, + receiver: Address, + owner: Address, + ]; + + /* Morpho */ + morphoSetAuthorizationWithSig: [ + authorization: { + authorizer: Address; + authorized: Address; + isAuthorized: boolean; + nonce: bigint; + deadline: bigint; + }, + signature: Hex | null, + skipRevert?: boolean, + ]; + morphoSupply: [ + market: MarketParams, + assets: bigint, + shares: bigint, + slippageAmount: bigint, + onBehalf: Address, + onMorphoSupply: Action[], + ]; + morphoSupplyCollateral: [ + market: MarketParams, + assets: bigint, + onBehalf: Address, + onMorphoSupplyCollateral: Action[], + ]; + morphoBorrow: [ + market: MarketParams, + assets: bigint, + shares: bigint, + slippageAmount: bigint, + receiver: Address, + ]; + morphoRepay: [ + market: MarketParams, + assets: bigint, + shares: bigint, + slippageAmount: bigint, + onBehalf: Address, + onMorphoRepay: Action[], + ]; + morphoWithdraw: [ + market: MarketParams, + assets: bigint, + shares: bigint, + slippageAmount: bigint, + receiver: Address, + ]; + morphoWithdrawCollateral: [ + market: MarketParams, + assets: bigint, + receiver: Address, + ]; + + /* MetaMorpho */ + + reallocateTo: [ + publicAllocator: Address, + vault: Address, + value: bigint, + withdrawals: ReallocationWithdrawal[], + supplyMarket: MarketParams, + ]; + + /* Universal Rewards Distributor */ + + urdClaim: [ + distributor: Address, + account: Address, + reward: Address, + amount: bigint, + proof: Hex[], + skipRevert?: boolean, + ]; + + /* Wrapped Native */ + wrapNative: [amount: bigint]; + unwrapNative: [amount: bigint]; + + /* stETH */ + stakeEth: [amount: bigint, minShares: bigint, referral: Address]; + + /* Wrapped stETH */ + wrapStEth: [amount: bigint]; + unwrapStEth: [amount: bigint]; + + /* AaveV2 */ + aaveV2Repay: [asset: Address, amount: bigint, rateMode?: bigint]; + aaveV2Withdraw: [asset: Address, amount: bigint]; + + /* AaveV3 */ + aaveV3Repay: [asset: Address, amount: bigint, rateMode?: bigint]; + aaveV3Withdraw: [asset: Address, amount: bigint]; + + /* AaveV3 Optimizer */ + aaveV3OptimizerRepay: [underlying: Address, amount: bigint]; + aaveV3OptimizerWithdraw: [ + underlying: Address, + amount: bigint, + maxIterations: bigint, + ]; + aaveV3OptimizerWithdrawCollateral: [underlying: Address, amount: bigint]; + aaveV3OptimizerApproveManagerWithSig: [ + isApproved: boolean, + nonce: bigint, + deadline: bigint, + signature: Hex | null, + skipRevert?: boolean, + ]; + + /* CompoundV2 */ + compoundV2Repay: [cToken: Address, amount: bigint]; + compoundV2Redeem: [cToken: Address, amount: bigint]; + + /* CompoundV3 */ + compoundV3Repay: [instance: Address, amount: bigint]; + compoundV3WithdrawFrom: [instance: Address, asset: Address, amount: bigint]; + compoundV3AllowBySig: [ + instance: Address, + isAllowed: boolean, + nonce: bigint, + expiry: bigint, + signature: Hex | null, + skipRevert?: boolean, + ]; +} + +export type ActionType = keyof ActionArgs; + +export type Actions = { + [T in ActionType]: { + type: T; + args: ActionArgs[T]; + }; +}; + +export type Action = Actions[ActionType]; + +export interface TransactionRequirementArgs { + /* ERC20 */ + erc20Approve: [asset: Address, recipient: Address, amount: bigint]; + + /* Morpho */ + morphoSetAuthorization: [authorized: Address, isAuthorized: boolean]; +} + +export type TransactionRequirementType = keyof TransactionRequirementArgs; + +export type Requirements = { + [T in TransactionRequirementType]: { + type: T; + args: TransactionRequirementArgs[T]; + tx: SendTransactionRequest; + }; +}; + +export type TransactionRequirement = Requirements[TransactionRequirementType]; + +export interface SignatureRequirement { + action: Action; + sign: < + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, + rpcSchema extends RpcSchema | undefined = undefined, + >( + client: Client, + ) => Promise; +} + +export interface ActionBundle { + steps: SimulationResult; + actions: Action[]; + requirements: { + signatures: SignatureRequirement[]; + txs: TransactionRequirement[]; + }; + tx: () => SendTransactionRequest; +} diff --git a/packages/blue-sdk-viem-bundler/src/types/index.ts b/packages/blue-sdk-viem-bundler/src/types/index.ts new file mode 100644 index 00000000..8d1a9e7f --- /dev/null +++ b/packages/blue-sdk-viem-bundler/src/types/index.ts @@ -0,0 +1,2 @@ +export * from "./actions.js"; +export * from "./operations.js"; diff --git a/packages/blue-sdk-viem-bundler/src/types/operations.ts b/packages/blue-sdk-viem-bundler/src/types/operations.ts new file mode 100644 index 00000000..00a4d5f1 --- /dev/null +++ b/packages/blue-sdk-viem-bundler/src/types/operations.ts @@ -0,0 +1,168 @@ +import { + BlueOperationArgs, + BlueOperationType, + CALLBACK_OPERATIONS, + Erc20OperationArgs, + Erc20OperationType, + MetaMorphoOperationArgs, + MetaMorphoOperationType, + OperationArgs, + OperationType, + WithOperationArgs, +} from "@morpho-org/blue-sdk-viem-simulation"; + +export const BUNDLER_OPERATIONS = [ + "Blue_SetAuthorization", + "Blue_Borrow", + "Blue_Repay", + "Blue_Supply", + "Blue_SupplyCollateral", + "Blue_Withdraw", + "Blue_WithdrawCollateral", + "MetaMorpho_Deposit", + "MetaMorpho_Withdraw", + "MetaMorpho_PublicReallocate", + "Erc20_Approve", + "Erc20_Permit", + "Erc20_Permit2", + "Erc20_Transfer", + "Erc20_Transfer2", + "Erc20_Wrap", + "Erc20_Unwrap", +] as const satisfies readonly OperationType[]; + +export type BundlerOperationType = (typeof BUNDLER_OPERATIONS)[number]; + +export interface BundlerOperationArgs + extends Omit { + Blue_SupplyCollateral: Omit< + BlueOperationArgs["Blue_SupplyCollateral"], + "callback" + > & { callback?: BundlerOperation[] }; + + Blue_Supply: Omit & { + callback?: BundlerOperation[]; + }; + Blue_Repay: Omit & { + callback?: BundlerOperation[]; + }; +} +export type BundlerOperations = { + [OperationType in BundlerOperationType]: WithOperationArgs< + OperationType, + BundlerOperationArgs + >; +}; +export type BundlerOperation = BundlerOperations[BundlerOperationType]; + +export type CallbackBundlerOperationType = (typeof CALLBACK_OPERATIONS)[number]; +export type CallbackBundlerOperations = { + [OperationType in CallbackBundlerOperationType]: WithOperationArgs< + OperationType, + BundlerOperationArgs + >; +}; +export type CallbackBundlerOperation = + CallbackBundlerOperations[CallbackBundlerOperationType]; + +export const BLUE_INPUT_OPERATIONS = [ + "Blue_Borrow", + "Blue_Repay", + "Blue_Supply", + "Blue_SupplyCollateral", + "Blue_Withdraw", + "Blue_WithdrawCollateral", +] as const satisfies readonly BlueOperationType[]; + +export type BlueInputBundlerOperationType = + (typeof BLUE_INPUT_OPERATIONS)[number]; + +export interface BlueInputBundlerOperationArgs + extends Omit { + Blue_SupplyCollateral: Omit< + BlueOperationArgs["Blue_SupplyCollateral"], + "callback" + > & { callback?: InputBundlerOperation[] }; + + Blue_Supply: Omit & { + callback?: InputBundlerOperation[]; + }; + Blue_Repay: Omit & { + callback?: InputBundlerOperation[]; + }; +} +export type BlueInputBundlerOperations = { + [OperationType in BlueInputBundlerOperationType]: WithOperationArgs< + OperationType, + BlueInputBundlerOperationArgs + >; +}; +export type BlueInputBundlerOperation = + BlueInputBundlerOperations[BlueInputBundlerOperationType]; + +export const METAMORPHO_INPUT_OPERATIONS = [ + "MetaMorpho_Deposit", + "MetaMorpho_Withdraw", +] as const satisfies readonly MetaMorphoOperationType[]; + +export type MetaMorphoInputBundlerOperationType = + (typeof METAMORPHO_INPUT_OPERATIONS)[number]; +export type MetaMorphoInputBundlerOperation = + BundlerOperations[MetaMorphoInputBundlerOperationType]; + +export const ERC20_INPUT_OPERATIONS = [ + "Erc20_Wrap", + "Erc20_Unwrap", +] as const satisfies readonly Erc20OperationType[]; + +export type Erc20InputBundlerOperationType = + (typeof ERC20_INPUT_OPERATIONS)[number]; +export type Erc20InputBundlerOperation = + BundlerOperations[Erc20InputBundlerOperationType]; + +export interface InputBundlerOperationArgs + extends BlueOperationArgs, + MetaMorphoOperationArgs, + Erc20OperationArgs {} + +export type InputBundlerOperationType = + | BlueInputBundlerOperationType + | MetaMorphoInputBundlerOperationType + | Erc20InputBundlerOperationType; + +export type InputBundlerOperation = + | BlueInputBundlerOperation + | MetaMorphoInputBundlerOperation + | Erc20InputBundlerOperation; + +// export const isBundlerOperation = ( +// operation: Operation +// ): operation is BundlerOperation => { +// return (BUNDLER_OPERATIONS as readonly OperationType[]).includes( +// operation.type +// ); +// }; + +export const isBlueInputBundlerOperation = (operation: { + type: OperationType; +}): operation is BlueInputBundlerOperation => { + return (BLUE_INPUT_OPERATIONS as readonly OperationType[]).includes( + operation.type, + ); +}; + +export const isMetaMorphoInputBundlerOperation = (operation: { + type: OperationType; +}): operation is MetaMorphoInputBundlerOperation => { + return (METAMORPHO_INPUT_OPERATIONS as readonly OperationType[]).includes( + operation.type, + ); +}; + +export const isErc20InputBundlerOperation = (operation: { + type: OperationType; +}): operation is Erc20InputBundlerOperation => { + return (ERC20_INPUT_OPERATIONS as readonly OperationType[]).includes( + operation.type, + ); +}; diff --git a/packages/blue-sdk-viem-bundler/tests/e2e/fixtures.ts b/packages/blue-sdk-viem-bundler/tests/e2e/fixtures.ts new file mode 100644 index 00000000..594b94a3 --- /dev/null +++ b/packages/blue-sdk-viem-bundler/tests/e2e/fixtures.ts @@ -0,0 +1,60 @@ +import { Address, ChainId, VaultConfig, addresses } from "@morpho-org/blue-sdk"; + +export const steakUsdc = new VaultConfig({ + address: "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB", + decimals: 18, + decimalsOffset: 12n, + symbol: "steakUSDC", + name: "Steakhouse USDC", + asset: addresses[ChainId.EthMainnet].usdc, +}); + +export const bbUsdc = new VaultConfig({ + address: "0x186514400e52270cef3D80e1c6F8d10A75d47344", + decimals: 18, + decimalsOffset: 12n, + symbol: "bbUSDC", + name: "BlockAnalytica USDC", + asset: addresses[ChainId.EthMainnet].wNative, +}); + +export const bbETH = new VaultConfig({ + address: "0x38989BBA00BDF8181F4082995b3DEAe96163aC5D", + decimals: 18, + decimalsOffset: 0n, + symbol: "bbETH", + name: "BlockAnalytica ETH", + asset: addresses[ChainId.EthMainnet].wNative, +}); + +export const bbUSDT = new VaultConfig({ + address: "0x2C25f6C25770fFEC5959D34B94Bf898865e5D6b1", + decimals: 18, + decimalsOffset: 12n, + symbol: "bbUSDT", + name: "BlockAnalytica USDT", + asset: "0xdAC17F958D2ee523a2206206994597C13D831ec7", +}); + +export const re7WETH = new VaultConfig({ + address: "0x78Fc2c2eD1A4cDb5402365934aE5648aDAd094d0", + decimals: 18, + decimalsOffset: 0n, + symbol: "re7WETH", + name: "Re7 WETH", + asset: addresses[ChainId.EthMainnet].wNative, +}); + +export const WITH_SIMPLE_PERMIT: Record> = { + [ChainId.EthMainnet]: new Set([ + addresses[ChainId.EthMainnet].wstEth, + addresses[ChainId.EthMainnet].sDai, + addresses[ChainId.EthMainnet].osEth, + addresses[ChainId.EthMainnet].usdc, + addresses[ChainId.EthMainnet].dai, + ]), + [ChainId.BaseMainnet]: new Set([ + addresses[ChainId.BaseMainnet].usdc, + addresses[ChainId.BaseMainnet].verUsdc, + ]), +}; diff --git a/packages/blue-sdk-viem-bundler/tests/e2e/helpers.ts b/packages/blue-sdk-viem-bundler/tests/e2e/helpers.ts new file mode 100644 index 00000000..828dc807 --- /dev/null +++ b/packages/blue-sdk-viem-bundler/tests/e2e/helpers.ts @@ -0,0 +1,185 @@ +import { ZeroAddress } from "ethers"; +import { ERC20__factory, MorphoBlue__factory } from "ethers-types"; +import { ethers } from "hardhat"; +import { deal } from "hardhat-deal"; + +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; + +import { + Address, + MarketConfig, + NATIVE_ADDRESS, + UnknownMarketConfigError, + VaultConfig, + getChainAddresses, + getUnwrappedToken, +} from "@morpho-org/blue-sdk"; +import { assertApproxEqAbs, mine } from "@morpho-org/morpho-test"; +import { keys } from "@morpho-org/morpho-ts"; + +import { + BundlingOptions, + InputBundlerOperation, + encodeBundle, + finalizeBundle, + populateBundle, +} from "../../src/index.js"; + +import { SimulationState } from "@morpho-org/blue-sdk-viem-simulation"; +import { WITH_SIMPLE_PERMIT } from "./fixtures.js"; + +export const donate = + ( + signer: SignerWithAddress, + erc20: Address, + donation: bigint, + vault: Address, + morpho: Address, + ) => + async (data: SimulationState) => { + await deal(erc20, signer.address, donation); + await ERC20__factory.connect(erc20, signer).approve(morpho, donation); + await MorphoBlue__factory.connect(morpho, signer).supply( + data.getMarket(data.getVault(vault).withdrawQueue[0]!).config, + donation, + 0n, + vault, + "0x", + ); + }; + +export const setupBundle = async ( + bundlerService: BundlerService, + signer: SignerWithAddress, + inputOperations: InputBundlerOperation[], + { + unwrapTokens, + unwrapSlippage, + onBundleTx, + ...options + }: BundlingOptions & { + unwrapTokens?: Set
; + unwrapSlippage?: bigint; + onBundleTx?: (data: SimulationState) => Promise | void; + } = {}, +) => { + const { value: startData } = await bundlerService.simulationService.data; + + let { operations } = populateBundle(inputOperations, startData, { + ...options, + withSimplePermit: new Set([ + ...WITH_SIMPLE_PERMIT[startData.chainId], + ...(options?.withSimplePermit ?? []), + ]), + }); + operations = finalizeBundle( + operations, + startData, + signer.address, + unwrapTokens, + unwrapSlippage, + ); + + const bundle = encodeBundle( + operations, + startData, + isSigner(bundlerService.chainService.runner), + ); + + const tokens = new Set
(); + + operations.forEach((operation) => { + const { address } = operation; + + if ( + isBlueOperation(operation) && + operation.type !== "Blue_SetAuthorization" + ) { + try { + const marketConfig = MarketConfig.get(operation.args.id); + + if (marketConfig.loanToken !== ZeroAddress) + tokens.add(marketConfig.loanToken); + + if (marketConfig.collateralToken !== ZeroAddress) + tokens.add(marketConfig.collateralToken); + } catch (error) { + if (!(error instanceof UnknownMarketConfigError)) throw error; + } + } + + if (isMetaMorphoOperation(operation)) { + tokens.add(address); + + const vaultConfig = VaultConfig.get(address, startData.chainId); + if (vaultConfig) tokens.add(vaultConfig.asset); + } + + if (isErc20Operation(operation)) { + tokens.add(address); + + const unwrapped = getUnwrappedToken(address, startData.chainId); + if (unwrapped != null) tokens.add(unwrapped); + } + }); + + if (onBundleTx != null) { + const balancesBefore = await Promise.all( + [...tokens, ...keys(startData.blue.tokensData)].map(async (token) => ({ + token, + balance: await (token === NATIVE_ADDRESS + ? ethers.provider.getBalance(signer.address) + : ERC20__factory.connect(token, signer).balanceOf(signer.address)), + })), + ); + + await onBundleTx(startData)?.then(() => mine(0)); + + await Promise.all( + balancesBefore.map(({ token, balance }) => + token === NATIVE_ADDRESS + ? setBalance(signer.address, balance) + : deal(token, signer.address, balance), + ), + ); + } + + await Promise.all( + bundle.requirements.signatures.map((requirement) => + requirement.sign(signer)!.wait(), + ), + ); + + const txs = bundle.requirements.txs.map(({ tx }) => tx).concat([bundle.tx()]); + + for (const tx of txs) { + await sendTransaction(signer, tx) + .wait() + .then(({ status, context }) => { + if (status !== NotificationStatus.error) return; + + throw context.error; // Bubble up revert reason. + }); + } + + const { bundler } = getChainAddresses(startData.chainId); + + await Promise.all( + [...tokens].map(async (token) => { + const balance = + token === NATIVE_ADDRESS + ? await ethers.provider.getBalance(bundler) + : await ERC20__factory.connect(token, signer).balanceOf(bundler); + + assertApproxEqAbs( + balance, + 0n, + 5n, + `non-zero bundler balance for token ${token}: ${balance}`, + ); + }), + ); + + return { operations, bundle }; +}; diff --git a/packages/blue-sdk-viem-bundler/tests/e2e/services/BundlerService.base.test.ts b/packages/blue-sdk-viem-bundler/tests/e2e/services/BundlerService.base.test.ts new file mode 100644 index 00000000..1fe531aa --- /dev/null +++ b/packages/blue-sdk-viem-bundler/tests/e2e/services/BundlerService.base.test.ts @@ -0,0 +1,176 @@ +import { expect } from "chai"; +import { parseEther, parseUnits } from "ethers"; +import { MorphoBlue__factory } from "ethers-types"; +import { ethers } from "hardhat"; +import { deal } from "hardhat-deal"; + +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; + +import { + ChainId, + DEFAULT_SLIPPAGE_TOLERANCE, + MarketConfig, + addresses, +} from "@morpho-org/blue-sdk"; +import { mine, reset } from "@morpho-org/morpho-test"; + +import { setupBundle } from "../helpers.js"; + +const { morpho, bundler, adaptiveCurveIrm, wNative, usdc, verUsdc } = + addresses[ChainId.BaseMainnet]; + +describe("BundlerService (base)", () => { + let signer: SignerWithAddress; + + before(async () => { + const signers = await ethers.getSigners(); + + signer = signers[0]!; + }); + + afterEach(async () => { + // Wait for all fetch promises to resolve before reset. + await bundlerService?.simulationService.data; + + bundlerService?.simulationService.chainService.close(); + bundlerService?.simulationService.metaMorphoService.blueService.close(); + bundlerService?.simulationService.metaMorphoService.close(); + bundlerService?.simulationService.close(); + + await reset(); + }); + + describe("with provider + address", () => { + beforeEach(() => { + bundlerService = new BundlerService( + new SimulationService( + new MetaMorphoService( + new BlueService(new ChainService(ethers.provider), { + users: [signer.address], + }), + ), + ), + ); + }); + + it("should wrap then supply aUSDC", async () => { + const blue = await MorphoBlue__factory.connect(morpho, signer); + const config = new MarketConfig({ + collateralToken: wNative, + loanToken: verUsdc, + lltv: parseEther("0.86"), + irm: adaptiveCurveIrm, + oracle: "0xFEa2D58cEfCb9fcb597723c6bAE66fFE4193aFE4", + }); + await blue.createMarket(config); + + bundlerService.simulationService.metaMorphoService.deleteUsers( + signer.address, + ); + signer = await ethers.getImpersonatedSigner( + "0x53753098E2660AbD4834A3eD713D11AC1123421A", + ); + bundlerService.simulationService.metaMorphoService.addUsers( + signer.address, + ); + + bundlerService.simulationService.metaMorphoService.addMarkets(config.id); + + const assets = parseUnits("500", 6); + await deal(usdc, signer.address, assets); + await mine(); + + const { operations } = await setupBundle(bundlerService, signer, [ + { + type: "Erc20_Wrap", + sender: signer.address, + address: verUsdc, + args: { + amount: assets, + owner: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_Supply", + sender: signer.address, + address: morpho, + args: { + id: config.id, + assets, + onBehalf: signer.address, + }, + }, + ]); + + expect(operations).to.eql([ + { + type: "Erc20_Permit", + sender: signer.address, + address: usdc, + args: { + amount: assets, + spender: bundler, + nonce: 0n, + }, + }, + { + type: "Erc20_Permit", + sender: signer.address, + address: verUsdc, + args: { + amount: assets, + spender: bundler, + nonce: 0n, + }, + }, + { + type: "Erc20_Transfer", + sender: bundler, + address: usdc, + args: { + amount: assets, + from: signer.address, + to: bundler, + }, + }, + { + type: "Erc20_Wrap", + sender: bundler, + address: verUsdc, + args: { + amount: assets, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Erc20_Transfer", + sender: bundler, + address: verUsdc, + args: { + amount: assets, + from: signer.address, + to: bundler, + }, + }, + { + type: "Blue_Supply", + sender: bundler, + address: morpho, + args: { + id: config.id, + assets, + onBehalf: signer.address, + }, + }, + ]); + + const position = await blue.position(config.id, signer.address); + + expect(position.collateral).to.equal(0n); + expect(position.supplyShares).to.equal(assets * 1_000000n); + expect(position.borrowShares).to.equal(0n); + }); + }); +}); diff --git a/packages/blue-sdk-viem-bundler/tests/e2e/services/BundlerService.ethereum.test.ts b/packages/blue-sdk-viem-bundler/tests/e2e/services/BundlerService.ethereum.test.ts new file mode 100644 index 00000000..4de0c4fd --- /dev/null +++ b/packages/blue-sdk-viem-bundler/tests/e2e/services/BundlerService.ethereum.test.ts @@ -0,0 +1,3995 @@ +import { expect } from "chai"; +import { MaxUint256, ZeroAddress, parseEther, parseUnits } from "ethers"; +import { MetaMorpho__factory, PublicAllocator__factory } from "ethers-types"; +import { ethers } from "hardhat"; +import { deal } from "hardhat-deal"; +import _omit from "lodash/omit"; + +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; + +import { BlueService, ChainService } from "@morpho-org/blue-core-sdk"; +import { MetaMorphoService } from "@morpho-org/blue-metamorpho-sdk"; +import { + ChainId, + DEFAULT_SLIPPAGE_TOLERANCE, + MarketConfig, + MarketUtils, + MathLib, + NATIVE_ADDRESS, + addresses, +} from "@morpho-org/blue-sdk"; +import { MAINNET_MARKETS } from "@morpho-org/blue-sdk/lib/tests/mocks/markets"; +import { + Erc20Errors, + SimulationService, +} from "@morpho-org/blue-simulation-sdk"; +import { + ERC20__factory, + Morpho__factory, +} from "@morpho-org/morpho-blue-bundlers/types"; +import { + assertApproxEqAbs, + assertApproxEqRel, + mine, + reset, +} from "@morpho-org/morpho-test"; + +import { BundlerService } from "../../../src/index.js"; +import { bbETH, bbUSDT, bbUsdc, re7WETH, steakUsdc } from "../fixtures.js"; +import { donate, setupBundle } from "../helpers.js"; + +const { + morpho, + bundler, + publicAllocator, + permit2, + usdc, + stEth, + wNative, + wstEth, +} = addresses[ChainId.EthMainnet]; +const usdt = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; + +describe("BundlerService (ethereum)", () => { + let signer: SignerWithAddress; + let donator: SignerWithAddress; + + let bundlerService: BundlerService; + + before(async () => { + const signers = await ethers.getSigners(); + + signer = signers[0]!; + donator = signers[1]!; + }); + + afterEach(async () => { + // Wait for all fetch promises to resolve before reset. + await bundlerService?.simulationService.data; + + bundlerService?.simulationService.chainService.close(); + bundlerService?.simulationService.metaMorphoService.blueService.close(); + bundlerService?.simulationService.metaMorphoService.close(); + bundlerService?.simulationService.close(); + + await reset(); + }); + + describe("with signer", () => { + beforeEach(() => { + bundlerService = new BundlerService( + new SimulationService( + new MetaMorphoService( + new BlueService(new ChainService(signer), { + users: [signer.address, donator.address], + }), + ), + ), + ); + }); + + it("should fail if balance exceeded", async () => { + const id = MAINNET_MARKETS.eth_wstEth.id; + bundlerService.simulationService.metaMorphoService.addMarkets(id); + + const wBalance = parseUnits("5000"); + const balance = await ethers.provider.getBalance(signer.address); + await deal(wNative, signer.address, wBalance); + await mine(); + + const assets = balance + wBalance + 1n; + + await expect( + setupBundle(bundlerService, signer, [ + { + type: "Blue_Supply", + sender: signer.address, + address: morpho, + args: { + id, + assets, + onBehalf: signer.address, + }, + }, + ]), + ).to.be.rejectedWith( + new Erc20Errors.InsufficientBalance(wNative, signer.address).message, + ); + }); + + it("should wrap + skim stETH if required with less wstETH than expected slippage", async () => { + const id = MAINNET_MARKETS.eth_wstEth.id; + bundlerService.simulationService.metaMorphoService.addMarkets(id); + + const blue = Morpho__factory.connect(morpho, signer); + const erc20 = ERC20__factory.connect(stEth, signer); + + const wBalance = parseUnits("0.0005"); + // Dealing stETH does not work. + await signer.sendTransaction({ + to: stEth, + value: (await ethers.provider.getBalance(signer.address)) / 2n, + }); + await deal(wstEth, signer.address, wBalance); + await mine(); + + const { value: data } = await bundlerService.simulationService.data; + + const { balance } = data.getHolding(signer.address, stEth); + const { balance: bundlerBalance } = data.getHolding(bundler, stEth); + + const wstEthToken = data.getWrappedToken(wstEth); + const assets = + wstEthToken.toWrappedExactAmountIn( + balance, + DEFAULT_SLIPPAGE_TOLERANCE, + ) + wBalance; + + const { operations, bundle } = await setupBundle(bundlerService, signer, [ + { + type: "Erc20_Wrap", + sender: signer.address, + address: wstEth, + args: { + amount: balance, + owner: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SupplyCollateral", + sender: signer.address, + address: morpho, + args: { + id, + assets, + onBehalf: signer.address, + }, + }, + ]); + + expect(operations.length).to.equal(8); + expect(bundle.requirements.txs.length).to.equal(1); + expect(bundle.requirements.signatures.length).to.equal(2); + + expect(operations).to.eql([ + { + type: "Erc20_Approve", + sender: signer.address, + address: stEth, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }, + { + type: "Erc20_Permit", + sender: signer.address, + address: wstEth, + args: { + amount: wBalance, + spender: bundler, + nonce: 0n, + }, + }, + { + type: "Erc20_Permit2", + sender: signer.address, + address: stEth, + args: { + amount: balance - bundlerBalance, + spender: bundler, + expiration: MathLib.MAX_UINT_48, + nonce: 0n, + }, + }, + { + type: "Erc20_Transfer", + sender: bundler, + address: wstEth, + args: { + amount: wBalance, + from: signer.address, + to: bundler, + }, + }, + { + type: "Erc20_Transfer2", + sender: bundler, + address: stEth, + args: { + amount: balance - bundlerBalance, + from: signer.address, + to: bundler, + }, + }, + { + type: "Erc20_Wrap", + sender: bundler, + address: wstEth, + args: { + amount: balance, + owner: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SupplyCollateral", + sender: bundler, + address: morpho, + args: { + id, + assets, + onBehalf: signer.address, + }, + }, + { + type: "Erc20_Transfer", + sender: bundler, + address: wstEth, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }, + ]); + + const position = await blue.position(id, signer.address); + + assertApproxEqAbs(await erc20.balanceOf(signer.address), 0n, 10n); + expect(position.collateral).to.equal(assets); + expect(position.supplyShares).to.equal(0); + expect(position.borrowShares).to.equal(0); + + expect(await erc20.allowance(signer.address, permit2)).to.equal( + MathLib.MAX_UINT_160 - (balance - bundlerBalance), + ); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, steakUsdc.address)).to.equal( + 0, + ); + }); + + it("should borrow with already enough collateral", async () => { + const id = MAINNET_MARKETS.usdc_wstEth.id; + bundlerService.simulationService.metaMorphoService.addMarkets(id); + + const blue = Morpho__factory.connect(morpho, signer); + const erc20 = ERC20__factory.connect(wstEth, signer); + + const collateral = parseUnits("50"); + const assets = parseUnits("13000", 6); + await deal(wstEth, signer.address, collateral); + await erc20.approve(morpho, MaxUint256); + await blue.supplyCollateral( + MAINNET_MARKETS.usdc_wstEth, + collateral, + signer.address, + "0x", + ); + await mine(); + + const { operations, bundle } = await setupBundle(bundlerService, signer, [ + { + type: "Blue_Borrow", + sender: signer.address, + address: morpho, + args: { + id, + assets, + onBehalf: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ]); + + expect(operations.length).to.equal(2); + expect(bundle.requirements.txs.length).to.equal(0); + expect(bundle.requirements.signatures.length).to.equal(1); + + expect(operations[0]).to.eql({ + type: "Blue_SetAuthorization", + sender: bundler, + address: morpho, + args: { + owner: signer.address, + isBundlerAuthorized: true, + }, + }); + expect(operations[1]).to.eql({ + type: "Blue_Borrow", + sender: bundler, + address: morpho, + args: { + id, + assets, + onBehalf: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + + const market = await blue.market(id); + const position = await blue.position(id, signer.address); + + expect(await erc20.balanceOf(signer.address)).to.equal(0); + expect(position.collateral).to.equal(collateral); + expect(position.supplyShares).to.equal(0); + expect( + MarketUtils.toBorrowAssets(position.borrowShares, market), + ).to.equal(assets + 1n); + + expect(await erc20.allowance(signer.address, permit2)).to.equal(0); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, steakUsdc.address)).to.equal( + 0, + ); + }); + + it("should deposit steakUSDC via permit", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + steakUsdc.address, + ); + + const erc20 = ERC20__factory.connect(usdc, signer); + const erc4626 = MetaMorpho__factory.connect(steakUsdc.address, signer); + + const amount = parseUnits("1000000", 6); + await deal(usdc, signer.address, amount); + await mine(); + + const { operations, bundle } = await setupBundle(bundlerService, signer, [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: steakUsdc.address, + args: { + assets: amount, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ]); + + expect(operations.length).to.equal(3); + expect(bundle.requirements.txs.length).to.equal(0); + expect(bundle.requirements.signatures.length).to.equal(1); + + expect(operations[0]).to.eql({ + type: "Erc20_Permit", + sender: signer.address, + address: usdc, + args: { + amount, + spender: bundler, + nonce: 1n, + }, + }); + expect(operations[1]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: usdc, + args: { + amount, + from: signer.address, + to: bundler, + }, + }); + expect(operations[2]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: steakUsdc.address, + args: { + assets: amount, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + + expect(await erc20.balanceOf(signer.address)).to.equal(0); + expect(await erc4626.maxWithdraw(signer.address)).to.equal(amount - 1n); + + expect(await erc20.allowance(signer.address, permit2)).to.equal(0); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, steakUsdc.address)).to.equal( + 0, + ); + }); + + it("should deposit bbUSDT via permit2", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + bbUSDT.address, + ); + + const erc20 = ERC20__factory.connect(usdt, signer); + const erc4626 = MetaMorpho__factory.connect(bbUSDT.address, signer); + + const amount = parseUnits("1000000", 6); + await deal(usdt, signer.address, amount); + await mine(); + + const { operations, bundle } = await setupBundle(bundlerService, signer, [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbUSDT.address, + args: { + assets: amount, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ]); + + expect(operations.length).to.equal(4); + expect(bundle.requirements.txs.length).to.equal(1); + expect(bundle.requirements.signatures.length).to.equal(1); + + expect(operations[0]).to.eql({ + type: "Erc20_Approve", + sender: signer.address, + address: usdt, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }); + expect(operations[1]).to.eql({ + type: "Erc20_Permit2", + sender: signer.address, + address: usdt, + args: { + amount, + spender: bundler, + expiration: MathLib.MAX_UINT_48, + nonce: 0n, + }, + }); + expect(operations[2]).to.eql({ + type: "Erc20_Transfer2", + sender: bundler, + address: usdt, + args: { + amount, + from: signer.address, + to: bundler, + }, + }); + expect(operations[3]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: bbUSDT.address, + args: { + assets: amount, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + + expect(await erc20.balanceOf(signer.address)).to.equal(0); + expect(await erc4626.maxWithdraw(signer.address)).to.equal(amount - 1n); + + expect(await erc20.allowance(signer.address, permit2)).to.equal( + MathLib.MAX_UINT_160 - amount, + ); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, bbUSDT.address)).to.equal(0); + }); + + it("should simulate bbUSDT deposit into supply max collateral without skim", async () => { + const blue = Morpho__factory.connect(morpho, signer); + const erc20 = ERC20__factory.connect(usdt, signer); + const erc4626 = MetaMorpho__factory.connect(bbUSDT.address, signer); + + const amount = parseUnits("1000000", 6); + const expectedShares = await erc4626.convertToShares(amount); + await deal(usdt, signer.address, amount); + + const marketConfig = new MarketConfig({ + loanToken: ZeroAddress, + collateralToken: bbUSDT.address, + lltv: 0n, + oracle: ZeroAddress, + irm: ZeroAddress, + }); + await blue.createMarket(marketConfig); + + bundlerService.simulationService.metaMorphoService.addMarkets( + marketConfig.id, + ); + bundlerService.simulationService.metaMorphoService.addVaults( + bbUSDT.address, + ); + + const { operations, bundle } = await setupBundle(bundlerService, signer, [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbUSDT.address, + args: { + assets: amount, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SupplyCollateral", + sender: signer.address, + address: morpho, + args: { + id: marketConfig.id, + assets: MaxUint256, + onBehalf: signer.address, + }, + }, + ]); + + expect(operations.length).to.equal(5); + expect(bundle.requirements.txs.length).to.equal(1); + expect(bundle.requirements.signatures.length).to.equal(1); + + expect(operations[0]).to.eql({ + type: "Erc20_Approve", + sender: signer.address, + address: usdt, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }); + expect(operations[1]).to.eql({ + type: "Erc20_Permit2", + sender: signer.address, + address: usdt, + args: { + amount, + spender: bundler, + expiration: MathLib.MAX_UINT_48, + nonce: 0n, + }, + }); + expect(operations[2]).to.eql({ + type: "Erc20_Transfer2", + sender: bundler, + address: usdt, + args: { + amount, + from: signer.address, + to: bundler, + }, + }); + expect(operations[3]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: bbUSDT.address, + args: { + assets: amount, + owner: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[4]).to.eql({ + type: "Blue_SupplyCollateral", + sender: bundler, + address: morpho, + args: { + id: marketConfig.id, + assets: MaxUint256, + onBehalf: signer.address, + }, + }); + + expect(await erc20.balanceOf(signer.address)).to.equal(0); + expect(await erc4626.balanceOf(signer.address)).to.equal(0); + + const { collateral } = await blue.position( + marketConfig.id, + signer.address, + ); + assertApproxEqAbs(collateral, expectedShares, parseUnits("0.1")); + + expect(await erc20.allowance(signer.address, permit2)).to.equal( + MathLib.MAX_UINT_160 - amount, + ); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, bbUSDT.address)).to.equal(0); + }); + + it("should simulate bbUSDT deposit into supply collateral with skim", async () => { + const blue = Morpho__factory.connect(morpho, signer); + const erc20 = ERC20__factory.connect(usdt, signer); + const erc4626 = MetaMorpho__factory.connect(bbUSDT.address, signer); + + const amount = parseUnits("1000000", 6); + const shares = parseEther("500000"); + const expectedShares = await erc4626.convertToShares(amount); + await deal(usdt, signer.address, amount); + + const marketConfig = new MarketConfig({ + loanToken: ZeroAddress, + collateralToken: bbUSDT.address, + lltv: 0n, + oracle: ZeroAddress, + irm: ZeroAddress, + }); + await blue.createMarket(marketConfig); + + bundlerService.simulationService.metaMorphoService.addMarkets( + marketConfig.id, + ); + bundlerService.simulationService.metaMorphoService.addVaults( + bbUSDT.address, + ); + + const { operations, bundle } = await setupBundle(bundlerService, signer, [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbUSDT.address, + args: { + assets: amount, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SupplyCollateral", + sender: signer.address, + address: morpho, + args: { + id: marketConfig.id, + assets: shares, + onBehalf: signer.address, + }, + }, + ]); + + expect(operations.length).to.equal(6); + expect(bundle.requirements.txs.length).to.equal(1); + expect(bundle.requirements.signatures.length).to.equal(1); + + expect(operations[0]).to.eql({ + type: "Erc20_Approve", + sender: signer.address, + address: usdt, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }); + expect(operations[1]).to.eql({ + type: "Erc20_Permit2", + sender: signer.address, + address: usdt, + args: { + amount, + spender: bundler, + expiration: MathLib.MAX_UINT_48, + nonce: 0n, + }, + }); + expect(operations[2]).to.eql({ + type: "Erc20_Transfer2", + sender: bundler, + address: usdt, + args: { + amount, + from: signer.address, + to: bundler, + }, + }); + expect(operations[3]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: bbUSDT.address, + args: { + assets: amount, + owner: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[4]).to.eql({ + type: "Blue_SupplyCollateral", + sender: bundler, + address: morpho, + args: { + id: marketConfig.id, + assets: shares, + onBehalf: signer.address, + }, + }); + expect(operations[5]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: bbUSDT.address, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }); + + expect(await erc20.balanceOf(signer.address)).to.equal(0); + assertApproxEqAbs( + await erc4626.balanceOf(signer.address), + expectedShares - shares, + parseUnits("0.1"), + ); + + const { collateral } = await blue.position( + marketConfig.id, + signer.address, + ); + expect(collateral).to.equal(shares); + + expect(await erc20.allowance(signer.address, permit2)).to.equal( + MathLib.MAX_UINT_160 - amount, + ); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, bbUSDT.address)).to.equal(0); + }); + + it("should simulate bbETH mint on behalf with slippage & unwrap remaining WETH", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + bbETH.address, + ); + + const erc20 = ERC20__factory.connect(wNative, signer); + const erc4626 = MetaMorpho__factory.connect(bbETH.address, signer); + + const shares = parseUnits("99"); + const assets = await erc4626.previewMint(shares); + await deal(wNative, signer.address, assets + parseUnits("10")); + + const { operations, bundle } = await setupBundle( + bundlerService, + signer, + [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbETH.address, + args: { + shares, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ], + { + onBundleTx: donate( + donator, + wNative, + parseUnits("1"), + bbETH.address, + morpho, + ), + }, + ); + + expect(bundle.requirements.txs.length).to.equal(1); + expect(bundle.requirements.signatures.length).to.equal(1); + + expect(operations).to.eql([ + { + type: "Erc20_Approve", + sender: signer.address, + address: wNative, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }, + { + type: "Erc20_Permit2", + sender: signer.address, + address: wNative, + args: { + amount: expect.bigint, + spender: bundler, + expiration: expect.bigint, + nonce: 0n, + }, + }, + { + type: "Erc20_Transfer2", + sender: bundler, + address: wNative, + args: { + amount: expect.bigint, + from: signer.address, + to: bundler, + }, + }, + { + type: "MetaMorpho_Deposit", + sender: bundler, + address: bbETH.address, + args: { + shares, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Erc20_Transfer", + address: wNative, + sender: bundler, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }, + ]); + + expect(await erc20.balanceOf(bundler)).to.equal(0); + expect(await erc20.balanceOf(donator.address)).to.equal(0); + expect(await erc4626.maxWithdraw(signer.address)).to.equal(0); + assertApproxEqRel( + await erc4626.maxWithdraw(donator.address), + assets - 1n, + DEFAULT_SLIPPAGE_TOLERANCE, + ); + assertApproxEqAbs( + await erc20.balanceOf(signer.address), + parseUnits("10"), + parseUnits("0.025"), + ); + + expect(await erc20.allowance(signer.address, permit2)).not.to.equal(0); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, bbETH.address)).to.equal(0); + }); + + it("should fail bbETH mint on behalf with slippage exceeded", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + bbETH.address, + ); + + const erc4626 = MetaMorpho__factory.connect(bbETH.address, signer); + + const shares = parseUnits("99"); + const assets = await erc4626.previewMint(shares); + await deal(wNative, signer.address, assets + parseUnits("10")); + + await expect( + setupBundle( + bundlerService, + signer, + [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbETH.address, + args: { + shares, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ], + { + onBundleTx: donate( + donator, + wNative, + parseUnits("10"), + bbETH.address, + morpho, + ), + }, + ), + ).to.be.reverted; + }); + + it("should borrow USDC against wstETH into steakUSDC half deposit on behalf with slippage & unwrap remaining wstETH", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + steakUsdc.address, + ); + + const { value: startData } = await bundlerService.simulationService.data; + + const collateral = ERC20__factory.connect(wstEth, signer); + const loan = ERC20__factory.connect(usdc, signer); + const erc4626 = MetaMorpho__factory.connect(steakUsdc.address, signer); + + const id = MAINNET_MARKETS.usdc_wstEth.id; + const market = startData.getMarket(id); + + const collateralAssets = parseUnits("100"); + const loanShares = parseUnits("50000", 12); + const loanAssets = market.toBorrowAssets(loanShares); + await deal(wstEth, signer.address, collateralAssets); + await mine(); + + const { operations, bundle } = await setupBundle( + bundlerService, + signer, + [ + { + type: "Blue_SupplyCollateral", + sender: signer.address, + address: morpho, + args: { + id, + assets: collateralAssets, + onBehalf: signer.address, + }, + }, + { + type: "Blue_Borrow", + sender: signer.address, + address: morpho, + args: { + id, + shares: loanShares, + onBehalf: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: steakUsdc.address, + args: { + assets: loanAssets / 2n, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ], + { + unwrapTokens: new Set([wstEth]), + onBundleTx: donate( + donator, + usdc, + parseUnits("1000", 6), + steakUsdc.address, + morpho, + ), + }, + ); + + expect(operations.length).to.equal(7); + expect(bundle.requirements.txs.length).to.equal(0); + expect(bundle.requirements.signatures.length).to.equal(2); + + expect(operations[0]).to.eql({ + type: "Erc20_Permit", + sender: signer.address, + address: wstEth, + args: { + amount: collateralAssets, + spender: bundler, + nonce: 0n, + }, + }); + expect(operations[1]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: wstEth, + args: { + amount: collateralAssets, + from: signer.address, + to: bundler, + }, + }); + expect(operations[2]).to.eql({ + type: "Blue_SupplyCollateral", + sender: bundler, + address: morpho, + args: { + id, + assets: collateralAssets, + onBehalf: signer.address, + }, + }); + expect(operations[3]).to.eql({ + type: "Blue_SetAuthorization", + sender: bundler, + address: morpho, + args: { + owner: signer.address, + isBundlerAuthorized: true, + }, + }); + expect(operations[4]).to.eql({ + type: "Blue_Borrow", + sender: bundler, + address: morpho, + args: { + id, + shares: loanShares, + onBehalf: signer.address, + receiver: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[5]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: steakUsdc.address, + args: { + assets: loanAssets / 2n, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[6]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: usdc, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }); + + expect(await collateral.balanceOf(signer.address)).to.equal(0); + assertApproxEqRel( + await loan.balanceOf(signer.address), + loanAssets / 2n, + DEFAULT_SLIPPAGE_TOLERANCE, + ); + expect(await collateral.balanceOf(donator.address)).to.equal(0); + expect(await loan.balanceOf(donator.address)).to.equal(0); + expect(await erc4626.maxWithdraw(signer.address)).to.equal(0); + assertApproxEqRel( + await erc4626.maxWithdraw(donator.address), + loanAssets / 2n, + DEFAULT_SLIPPAGE_TOLERANCE, + ); + + expect(await collateral.allowance(signer.address, permit2)).to.equal(0); + expect(await collateral.allowance(signer.address, bundler)).to.equal(0); + expect( + await collateral.allowance(signer.address, bbETH.address), + ).to.equal(0); + expect(await loan.allowance(signer.address, permit2)).to.equal(0); + expect(await loan.allowance(signer.address, bundler)).to.equal(0); + expect(await loan.allowance(signer.address, bbETH.address)).to.equal(0); + }); + + it("should redeem all bbETH with slippage + wstETH leverage into bbETH deposit & unwrap remaining WETH", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + bbETH.address, + ); + + const id = MAINNET_MARKETS.eth_wstEth.id; + const loan = ERC20__factory.connect(wNative, signer); + const collateral = ERC20__factory.connect(wstEth, signer); + const erc4626 = MetaMorpho__factory.connect(bbETH.address, signer); + + const collateralAssets = parseUnits("100"); + const loanAssets = parseUnits("95"); + + await deal(wstEth, signer.address, collateralAssets); + await deal(wNative, signer.address, loanAssets); + await collateral.approve(morpho, collateralAssets); + await loan.approve(bbETH.address, loanAssets); + await erc4626.deposit(loanAssets, signer.address); + + const { value: startData } = await bundlerService.simulationService.data; + + const shares = startData.getHolding( + signer.address, + bbETH.address, + ).balance; + + const { operations, bundle } = await setupBundle( + bundlerService, + signer, + [ + { + type: "MetaMorpho_Withdraw", + sender: signer.address, + address: bbETH.address, + args: { + shares, + owner: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SupplyCollateral", + sender: signer.address, + address: morpho, + args: { + id, + assets: collateralAssets, + onBehalf: signer.address, + }, + }, + { + type: "Blue_Borrow", + sender: signer.address, + address: morpho, + args: { + id, + assets: loanAssets, + onBehalf: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbETH.address, + args: { + assets: loanAssets, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ], + { + unwrapTokens: new Set([wstEth, wNative]), + onBundleTx: donate( + donator, + wNative, + parseUnits("1"), + bbETH.address, + morpho, + ), + }, + ); + + expect(operations.length).to.equal(10); + expect(bundle.requirements.txs.length).to.equal(0); + expect(bundle.requirements.signatures.length).to.equal(3); + + expect(operations[0]).to.eql({ + type: "Erc20_Permit", + sender: signer.address, + address: bbETH.address, + args: { + amount: shares, + spender: bundler, + nonce: 0n, + }, + }); + expect(operations[1]).to.eql({ + type: "Erc20_Permit", + sender: signer.address, + address: wstEth, + args: { + amount: collateralAssets, + spender: bundler, + nonce: 0n, + }, + }); + expect(operations[2]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: wstEth, + args: { + amount: collateralAssets, + from: signer.address, + to: bundler, + }, + }); + expect(operations[3]).to.eql({ + type: "MetaMorpho_Withdraw", + sender: bundler, + address: bbETH.address, + args: { + shares, + owner: signer.address, + receiver: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[4]).to.eql({ + type: "Blue_SupplyCollateral", + sender: bundler, + address: morpho, + args: { + id, + assets: collateralAssets, + onBehalf: signer.address, + }, + }); + expect(operations[5]).to.eql({ + type: "Blue_SetAuthorization", + sender: bundler, + address: morpho, + args: { + owner: signer.address, + isBundlerAuthorized: true, + }, + }); + expect(operations[6]).to.eql({ + type: "Blue_Borrow", + sender: bundler, + address: morpho, + args: { + id, + assets: loanAssets, + onBehalf: signer.address, + receiver: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[7]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: bbETH.address, + args: { + assets: loanAssets, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[8]).to.eql({ + type: "Erc20_Unwrap", + sender: bundler, + address: wNative, + args: { + amount: MaxUint256, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[9]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: NATIVE_ADDRESS, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }); + }); + + it("should deleverage wstETH into MetaMorpho bbETH -> re7WETH arbitrage with slippage", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + bbETH.address, + re7WETH.address, + ); + + const id = MAINNET_MARKETS.eth_wstEth.id; + const blue = Morpho__factory.connect(morpho, signer); + const loan = ERC20__factory.connect(wNative, signer); + const collateral = ERC20__factory.connect(wstEth, signer); + const erc4626 = MetaMorpho__factory.connect(bbETH.address, signer); + + const collateralAssets = parseUnits("100"); + const loanAssets = parseUnits("95"); + + await deal(wstEth, signer.address, collateralAssets); + await deal(wNative, signer.address, loanAssets); + await collateral.approve(morpho, collateralAssets); + await loan.approve(bbETH.address, loanAssets); + await erc4626.deposit(loanAssets, signer.address); + + await blue.supplyCollateral( + MAINNET_MARKETS.eth_wstEth, + collateralAssets, + signer.address, + "0x", + ); + await blue.borrow( + MAINNET_MARKETS.eth_wstEth, + loanAssets, + 0n, + signer.address, + signer.address, + ); + + const { operations, bundle } = await setupBundle( + bundlerService, + signer, + [ + { + type: "Blue_Repay", + sender: signer.address, + address: morpho, + args: { + id, + assets: loanAssets / 2n, + onBehalf: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_WithdrawCollateral", + sender: signer.address, + address: morpho, + args: { + id, + assets: collateralAssets / 2n, + onBehalf: signer.address, + receiver: signer.address, + }, + }, + { + type: "MetaMorpho_Withdraw", + sender: signer.address, + address: bbETH.address, + args: { + assets: loanAssets / 2n, + owner: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_Repay", + sender: signer.address, + address: morpho, + args: { + id, + assets: loanAssets / 4n, + onBehalf: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: re7WETH.address, + args: { + assets: loanAssets / 4n, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ], + { + unwrapTokens: new Set([wNative]), + onBundleTx: async (data) => { + await donate( + donator, + wNative, + parseUnits("0.5"), + bbETH.address, + morpho, + )(data); + await donate( + signer, + wNative, + parseUnits("0.5"), + re7WETH.address, + morpho, + )(data); + }, + }, + ); + + expect(operations.length).to.equal(10); + expect(bundle.requirements.txs.length).to.equal(1); + expect(bundle.requirements.signatures.length).to.equal(3); + + expect(operations[0]).to.eql({ + type: "Erc20_Approve", + sender: signer.address, + address: wNative, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }); + expect(_omit(operations[1], "args.amount")).to.eql({ + type: "Erc20_Permit", + sender: signer.address, + address: bbETH.address, + args: { + spender: bundler, + nonce: 0n, + }, + }); + expect(operations[2]).to.eql({ + type: "Erc20_Permit2", + sender: signer.address, + address: wNative, + args: { + amount: loanAssets / 2n, + spender: bundler, + expiration: MathLib.MAX_UINT_48, + nonce: 0n, + }, + }); + expect(operations[3]).to.eql({ + type: "Erc20_Transfer2", + sender: bundler, + address: wNative, + args: { + amount: loanAssets / 2n, + from: signer.address, + to: bundler, + }, + }); + expect(operations[4]).to.eql({ + type: "Blue_Repay", + sender: bundler, + address: morpho, + args: { + id, + assets: loanAssets / 2n, + onBehalf: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[5]).to.eql({ + type: "Blue_SetAuthorization", + sender: bundler, + address: morpho, + args: { + owner: signer.address, + isBundlerAuthorized: true, + }, + }); + expect(operations[6]).to.eql({ + type: "Blue_WithdrawCollateral", + sender: bundler, + address: morpho, + args: { + id, + assets: collateralAssets / 2n, + onBehalf: signer.address, + receiver: signer.address, + }, + }); + expect(operations[7]).to.eql({ + type: "MetaMorpho_Withdraw", + sender: bundler, + address: bbETH.address, + args: { + assets: loanAssets / 2n, + owner: signer.address, + receiver: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[8]).to.eql({ + type: "Blue_Repay", + sender: bundler, + address: morpho, + args: { + id, + assets: loanAssets / 4n, + onBehalf: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[9]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: re7WETH.address, + args: { + assets: loanAssets / 4n, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + }); + + it("should borrow USDC with shared liquidity and reallocation fee + unwrap remaining WETH", async () => { + const steakUsdcMm = MetaMorpho__factory.connect( + steakUsdc.address, + signer, + ); + const bbUsdcMm = MetaMorpho__factory.connect(bbUsdc.address, signer); + const bbEthMm = MetaMorpho__factory.connect(bbETH.address, signer); + + const steakUsdcOwner = await ethers.getImpersonatedSigner( + await steakUsdcMm.owner(), + ); + const bbUsdcOwner = await ethers.getImpersonatedSigner( + await bbUsdcMm.owner(), + ); + + const publicAllocatorContract = PublicAllocator__factory.connect( + publicAllocator, + signer, + ); + + await publicAllocatorContract + .connect(steakUsdcOwner) + .setFlowCaps(steakUsdc.address, [ + { + id: MAINNET_MARKETS.usdc_wstEth.id, + caps: { + maxIn: parseUnits("10000", 6), + maxOut: 0n, + }, + }, + { + id: MAINNET_MARKETS.usdc_wbtc.id, + caps: { + maxIn: 0n, + maxOut: parseUnits("20000", 6), // Less than bbUsdc but more than maxIn. + }, + }, + ]); + + const bbUsdcFee = parseEther("0.002"); + + await publicAllocatorContract + .connect(bbUsdcOwner) + .setFee(bbUsdc.address, bbUsdcFee); + await publicAllocatorContract + .connect(bbUsdcOwner) + .setFlowCaps(bbUsdc.address, [ + { + id: MAINNET_MARKETS.usdc_wstEth.id, + caps: { + maxIn: parseUnits("1000000", 6), + maxOut: 0n, + }, + }, + { + id: MAINNET_MARKETS.usdc_wbtc.id, + caps: { + maxIn: 0n, + maxOut: parseUnits("100000", 6), + }, + }, + ]); + + bundlerService.simulationService.metaMorphoService.addVaults( + steakUsdc.address, + bbUsdc.address, + bbETH.address, + ); + + const { value: startData } = await bundlerService.simulationService.data; + + const collateral = ERC20__factory.connect(wstEth, signer); + const loan = ERC20__factory.connect(usdc, signer); + + const id = MAINNET_MARKETS.usdc_wstEth.id; + + const collateralAssets = parseUnits("50000"); + const loanAssets = startData + .getMarketPublicReallocations(id) + .data.getMarket(id).liquidity; + const depositAssets = parseUnits("50"); + await deal(wstEth, signer.address, collateralAssets); + await deal(wNative, signer.address, depositAssets); + await mine(); + + const { operations, bundle } = await setupBundle( + bundlerService, + signer, + [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbETH.address, + args: { + assets: depositAssets, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SupplyCollateral", + sender: signer.address, + address: morpho, + args: { + id, + assets: collateralAssets, + onBehalf: signer.address, + }, + }, + { + type: "Blue_Borrow", + sender: signer.address, + address: morpho, + args: { + id, + assets: loanAssets, + onBehalf: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ], + + { + unwrapTokens: new Set([wNative]), + }, + ); + + expect(bundle.requirements.txs.length).to.equal(1); + expect(bundle.requirements.signatures.length).to.equal(3); + + expect(operations).to.eql([ + { + type: "Erc20_Approve", + sender: signer.address, + address: wNative, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }, + { + type: "Erc20_Permit", + sender: signer.address, + address: wstEth, + args: { + amount: collateralAssets, + spender: bundler, + nonce: 0n, + }, + }, + { + type: "Erc20_Permit2", + sender: signer.address, + address: wNative, + args: { + amount: depositAssets, + spender: bundler, + expiration: expect.bigint, + nonce: 0n, + }, + }, + { + type: "Erc20_Transfer", + sender: bundler, + address: wstEth, + args: { + amount: collateralAssets, + from: signer.address, + to: bundler, + }, + }, + { + type: "Erc20_Transfer", + sender: signer.address, + address: NATIVE_ADDRESS, + args: { + amount: bbUsdcFee, + from: signer.address, + to: bundler, + }, + }, + { + type: "Erc20_Transfer2", + sender: bundler, + address: wNative, + args: { + amount: depositAssets, + from: signer.address, + to: bundler, + }, + }, + { + type: "MetaMorpho_Deposit", + sender: bundler, + address: bbETH.address, + args: { + assets: depositAssets, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SupplyCollateral", + sender: bundler, + address: morpho, + args: { + id, + assets: collateralAssets, + onBehalf: signer.address, + }, + }, + { + type: "Blue_SetAuthorization", + sender: bundler, + address: morpho, + args: { + owner: signer.address, + isBundlerAuthorized: true, + }, + }, + { + type: "MetaMorpho_PublicReallocate", + sender: bundler, + address: bbUsdc.address, + args: { + withdrawals: [ + { + id: MAINNET_MARKETS.usdc_wbtc.id, + assets: parseUnits("100000", 6), + }, + ], + supplyMarketId: id, + }, + }, + { + type: "MetaMorpho_PublicReallocate", + sender: bundler, + address: steakUsdc.address, + args: { + withdrawals: [ + { + id: MAINNET_MARKETS.usdc_wbtc.id, + assets: parseUnits("10000", 6), + }, + ], + supplyMarketId: id, + }, + }, + { + type: "Blue_Borrow", + sender: bundler, + address: morpho, + args: { + id, + assets: loanAssets, + onBehalf: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ]); + + expect(await collateral.balanceOf(signer.address)).to.equal(0); + expect(await loan.balanceOf(signer.address)).to.equal(loanAssets); + expect(await collateral.balanceOf(donator.address)).to.equal(0); + expect(await loan.balanceOf(donator.address)).to.equal(0); + expect(await bbEthMm.maxWithdraw(signer.address)).to.equal(0); + expect(await bbEthMm.maxWithdraw(donator.address)).to.equal( + depositAssets - 1n, + ); + + expect(await collateral.allowance(signer.address, permit2)).to.equal(0); + expect(await collateral.allowance(signer.address, bundler)).to.equal(0); + expect( + await collateral.allowance(signer.address, bbETH.address), + ).to.equal(0); + expect(await loan.allowance(signer.address, permit2)).to.equal(0); + expect(await loan.allowance(signer.address, bundler)).to.equal(0); + expect(await loan.allowance(signer.address, bbETH.address)).to.equal(0); + }); + + it("should close a WETH/wstETH position + unwrap wstEth + skim WETH", async () => { + const market = MAINNET_MARKETS.eth_wstEth; + bundlerService.simulationService.metaMorphoService.addMarkets(market.id); + + const blue = Morpho__factory.connect(morpho, signer); + + const collateralAmount = parseUnits("1"); + const borrowAmount = parseUnits("0.5"); + + const wstEthContract = ERC20__factory.connect(wstEth, signer); + const stEthContract = ERC20__factory.connect(stEth, signer); + const wEthContract = ERC20__factory.connect(wNative, signer); + + await deal(wstEth, signer.address, collateralAmount); + await deal(stEth, signer.address, 0n); + + await wstEthContract.approve(blue, MaxUint256); + await blue.supplyCollateral( + market, + collateralAmount, + signer.address, + "0x", + ); + + await blue.borrow( + market, + borrowAmount, + 0n, + signer.address, + signer.address, + ); + + const extraWethAmount = parseEther("0.1"); + + await deal(wNative, signer.address, borrowAmount + extraWethAmount); + + const { value: data } = await bundlerService.simulationService.data; + + const position = data.getAccrualPosition(signer.address, market.id); + + const { operations, bundle } = await setupBundle( + bundlerService, + signer, + [ + { + type: "Blue_Repay", + sender: signer.address, + address: morpho, + args: { + id: market.id, + shares: position.borrowShares, + onBehalf: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_WithdrawCollateral", + sender: signer.address, + address: morpho, + args: { + id: market.id, + assets: position.collateral, + receiver: signer.address, + onBehalf: signer.address, + }, + }, + ], + { unwrapTokens: new Set([wstEth]) }, + ); + + const repayAmount = MathLib.wMulUp( + position.borrowAssets, + MathLib.WAD + DEFAULT_SLIPPAGE_TOLERANCE, + ); + + expect(operations.length).to.equal(9); + expect(bundle.requirements.txs.length).to.equal(1); + expect(bundle.requirements.signatures.length).to.equal(2); + expect(operations).eql([ + { + type: "Erc20_Approve", + sender: signer.address, + address: wNative, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }, + { + type: "Erc20_Permit2", + sender: signer.address, + address: wNative, + args: { + amount: repayAmount, + spender: bundler, + expiration: expect.bigint, + nonce: 0n, + }, + }, + { + type: "Erc20_Transfer2", + sender: bundler, + address: wNative, + args: { + amount: repayAmount, + from: signer.address, + to: bundler, + }, + }, + { + type: "Blue_Repay", + sender: bundler, + address: morpho, + args: { + id: market.id, + shares: position.borrowShares, + onBehalf: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SetAuthorization", + sender: bundler, + address: morpho, + args: { + owner: signer.address, + isBundlerAuthorized: true, + }, + }, + { + type: "Blue_WithdrawCollateral", + sender: bundler, + address: morpho, + args: { + id: market.id, + assets: position.collateral, + receiver: bundler, + onBehalf: signer.address, + }, + }, + { + type: "Erc20_Unwrap", + address: wstEth, + sender: bundler, + args: { + amount: MaxUint256, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Erc20_Transfer", + address: wNative, + sender: bundler, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }, + { + type: "Erc20_Transfer", + address: stEth, + sender: bundler, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }, + ]); + + const chainPosition = await blue.position(market.id, signer.address); + + const [ + bundlerWstEthBalance, + bundlerStEthBalance, + bundlerWEthBalance, + userStEthBalance, + userWstEthBalance, + userWEthBalance, + ] = await Promise.all([ + wstEthContract.balanceOf(bundler), + stEthContract.balanceOf(bundler), + wEthContract.balanceOf(bundler), + stEthContract.balanceOf(signer), + wstEthContract.balanceOf(signer), + wEthContract.balanceOf(signer), + ]); + + const wstEthToken = data.getWrappedToken(wstEth); + + const latestBlock = (await signer.provider.getBlock("latest"))!; + + const accruedInterests = + position.accrueInterest(BigInt(latestBlock.timestamp)).borrowAssets - + borrowAmount; + + expect(chainPosition.collateral).to.equal(0); + expect(chainPosition.supplyShares).to.equal(0); + expect(chainPosition.borrowShares).to.equal(0); + + expect(bundlerWstEthBalance).to.equal(0); + expect(bundlerStEthBalance).to.equal(1n); // 1 stETH is always remaining in the bundler + expect(bundlerWEthBalance).to.equal(0); + + expect(userStEthBalance).to.approximately( + wstEthToken.toUnwrappedExactAmountIn(collateralAmount, 0n), + 1n, + ); + expect(userWstEthBalance).to.equal(0); + expect(userWEthBalance).to.equal(extraWethAmount - accruedInterests); // we normally didn't experienced any slippage + }); + }); + + describe("with provider + address", () => { + beforeEach(() => { + bundlerService = new BundlerService( + new SimulationService( + new MetaMorphoService( + new BlueService(new ChainService(ethers.provider), { + users: [signer.address, donator.address], + }), + ), + ), + ); + }); + + it("should fail if balance exceeded", async () => { + const id = MAINNET_MARKETS.eth_wstEth.id; + bundlerService.simulationService.metaMorphoService.addMarkets(id); + + const wBalance = parseUnits("5000"); + const balance = await ethers.provider.getBalance(signer.address); + await deal(wNative, signer.address, wBalance); + await mine(); + + const assets = balance + wBalance + 1n; + + await expect( + setupBundle(bundlerService, signer, [ + { + type: "Blue_Supply", + sender: signer.address, + address: morpho, + args: { + id, + assets, + onBehalf: signer.address, + }, + }, + ]), + ).to.be.rejectedWith( + new Erc20Errors.InsufficientBalance(wNative, signer.address).message, + ); + }); + + it("should wrap + skim stETH if required with less wstETH than expected slippage", async () => { + const id = MAINNET_MARKETS.eth_wstEth.id; + bundlerService.simulationService.metaMorphoService.addMarkets(id); + + const blue = Morpho__factory.connect(morpho, signer); + const erc20 = ERC20__factory.connect(stEth, signer); + + const wBalance = parseUnits("0.0005"); + // Dealing stETH does not work. + await signer.sendTransaction({ + to: stEth, + value: (await ethers.provider.getBalance(signer.address)) / 2n, + }); + await deal(wstEth, signer.address, wBalance); + await mine(); + + const { value: data } = await bundlerService.simulationService.data; + + const { balance } = data.getHolding(signer.address, stEth); + const { balance: bundlerBalance } = data.getHolding(bundler, stEth); + + const wstEthToken = data.getWrappedToken(wstEth); + const assets = + wstEthToken.toWrappedExactAmountIn( + balance, + DEFAULT_SLIPPAGE_TOLERANCE, + ) + wBalance; + + const { operations, bundle } = await setupBundle(bundlerService, signer, [ + { + type: "Erc20_Wrap", + sender: signer.address, + address: wstEth, + args: { + amount: balance, + owner: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SupplyCollateral", + sender: signer.address, + address: morpho, + args: { + id, + assets, + onBehalf: signer.address, + }, + }, + ]); + + expect(operations.length).to.equal(8); + expect(bundle.requirements.signatures).to.eql([]); + + expect(bundle.requirements.txs).to.eql([ + { + type: "erc20Approve", + tx: { + to: wstEth, + data: expect.string, + }, + args: [wstEth, bundler, wBalance], + }, + { + type: "erc20Approve", + tx: { + to: stEth, + data: expect.string, + }, + args: [stEth, bundler, balance - bundlerBalance], + }, + ]); + + expect(operations[0]).to.eql({ + type: "Erc20_Approve", + sender: signer.address, + address: stEth, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }); + expect(operations[1]).to.eql({ + type: "Erc20_Permit", + sender: signer.address, + address: wstEth, + args: { + amount: wBalance, + spender: bundler, + nonce: 0n, + }, + }); + expect(operations[2]).to.eql({ + type: "Erc20_Permit2", + sender: signer.address, + address: stEth, + args: { + amount: balance - bundlerBalance, + spender: bundler, + expiration: MathLib.MAX_UINT_48, + nonce: 0n, + }, + }); + expect(operations[3]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: wstEth, + args: { + amount: wBalance, + from: signer.address, + to: bundler, + }, + }); + expect(operations[4]).to.eql({ + type: "Erc20_Transfer2", + sender: bundler, + address: stEth, + args: { + amount: balance - bundlerBalance, + from: signer.address, + to: bundler, + }, + }); + expect(operations[5]).to.eql({ + type: "Erc20_Wrap", + sender: bundler, + address: wstEth, + args: { + amount: balance, + owner: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[6]).to.eql({ + type: "Blue_SupplyCollateral", + sender: bundler, + address: morpho, + args: { + id, + assets, + onBehalf: signer.address, + }, + }); + expect(operations[7]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: wstEth, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }); + + const position = await blue.position(id, signer.address); + + assertApproxEqAbs(await erc20.balanceOf(signer.address), 0n, 10n); + expect(position.collateral).to.equal(assets); + expect(position.supplyShares).to.equal(0); + expect(position.borrowShares).to.equal(0); + + expect(await erc20.allowance(signer.address, permit2)).to.equal(0); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, steakUsdc.address)).to.equal( + 0, + ); + }); + + it("should borrow with already enough collateral", async () => { + const id = MAINNET_MARKETS.usdc_wstEth.id; + bundlerService.simulationService.metaMorphoService.addMarkets(id); + + const blue = Morpho__factory.connect(morpho, signer); + const erc20 = ERC20__factory.connect(wstEth, signer); + + const collateral = parseUnits("50"); + const assets = parseUnits("13000", 6); + await deal(wstEth, signer.address, collateral); + await erc20.approve(morpho, MaxUint256); + await blue.supplyCollateral( + MAINNET_MARKETS.usdc_wstEth, + collateral, + signer.address, + "0x", + ); + await mine(); + + const { operations, bundle } = await setupBundle(bundlerService, signer, [ + { + type: "Blue_Borrow", + sender: signer.address, + address: morpho, + args: { + id, + assets, + onBehalf: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ]); + + expect(operations.length).to.equal(2); + expect(bundle.requirements.txs.length).to.equal(1); + expect(bundle.requirements.signatures.length).to.equal(0); + + expect(bundle.requirements.txs[0]!.type).to.equal( + "morphoSetAuthorization", + ); + expect(bundle.requirements.txs[0]!.args).to.eql([bundler, true]); + + expect(operations[0]).to.eql({ + type: "Blue_SetAuthorization", + sender: bundler, + address: morpho, + args: { + owner: signer.address, + isBundlerAuthorized: true, + }, + }); + expect(operations[1]).to.eql({ + type: "Blue_Borrow", + sender: bundler, + address: morpho, + args: { + id, + assets, + onBehalf: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + + const market = await blue.market(id); + const position = await blue.position(id, signer.address); + + expect(await erc20.balanceOf(signer.address)).to.equal(0); + expect(position.collateral).to.equal(collateral); + expect(position.supplyShares).to.equal(0); + expect( + MarketUtils.toBorrowAssets(position.borrowShares, market), + ).to.equal(assets + 1n); + + expect(await erc20.allowance(signer.address, permit2)).to.equal(0); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, steakUsdc.address)).to.equal( + 0, + ); + }); + + it("should deposit steakUSDC via permit", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + steakUsdc.address, + ); + + const amount = parseUnits("1000000", 6); + await deal(usdc, signer.address, amount); + await mine(); + + const { operations, bundle } = await setupBundle(bundlerService, signer, [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: steakUsdc.address, + args: { + assets: amount, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ]); + + expect(operations.length).to.equal(3); + expect(bundle.requirements.txs.length).to.equal(1); + expect(bundle.requirements.signatures.length).to.equal(0); + + expect(bundle.requirements.txs[0]!.type).to.equal("erc20Approve"); + expect(bundle.requirements.txs[0]!.args).to.eql([usdc, bundler, amount]); + + expect(operations[0]).to.eql({ + type: "Erc20_Permit", + sender: signer.address, + address: usdc, + args: { + amount, + spender: bundler, + nonce: 1n, + }, + }); + expect(operations[1]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: usdc, + args: { + amount, + from: signer.address, + to: bundler, + }, + }); + expect(operations[2]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: steakUsdc.address, + args: { + assets: amount, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + + const erc20 = ERC20__factory.connect(usdc, signer); + const erc4626 = MetaMorpho__factory.connect(steakUsdc.address, signer); + + expect(await erc20.balanceOf(signer.address)).to.equal(0); + expect(await erc4626.maxWithdraw(signer.address)).to.equal(amount - 1n); + + expect(await erc20.allowance(signer.address, permit2)).to.equal(0); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, steakUsdc.address)).to.equal( + 0, + ); + }); + + it("should deposit bbUSDT via permit2", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + bbUSDT.address, + ); + + const erc20 = ERC20__factory.connect(usdt, signer); + const erc4626 = MetaMorpho__factory.connect(bbUSDT.address, signer); + + const amount = parseUnits("1000000", 6); + await deal(usdt, signer.address, amount); + await erc20.approve(bundler, 1n); + await mine(); + + const { operations, bundle } = await setupBundle(bundlerService, signer, [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbUSDT.address, + args: { + assets: amount, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ]); + + expect(operations.length).to.equal(4); + expect(bundle.requirements.txs.length).to.equal(2); + expect(bundle.requirements.signatures.length).to.equal(0); + + expect(bundle.requirements.txs[0]!.type).to.equal("erc20Approve"); + expect(bundle.requirements.txs[0]!.args).to.eql([usdt, bundler, 0n]); + expect(bundle.requirements.txs[1]!.type).to.equal("erc20Approve"); + expect(bundle.requirements.txs[1]!.args).to.eql([usdt, bundler, amount]); + + expect(operations[0]).to.eql({ + type: "Erc20_Approve", + sender: signer.address, + address: usdt, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }); + expect(operations[1]).to.eql({ + type: "Erc20_Permit2", + sender: signer.address, + address: usdt, + args: { + amount, + spender: bundler, + expiration: MathLib.MAX_UINT_48, + nonce: 0n, + }, + }); + expect(operations[2]).to.eql({ + type: "Erc20_Transfer2", + sender: bundler, + address: usdt, + args: { + amount, + from: signer.address, + to: bundler, + }, + }); + expect(operations[3]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: bbUSDT.address, + args: { + assets: amount, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + + expect(await erc20.balanceOf(signer.address)).to.equal(0); + expect(await erc4626.maxWithdraw(signer.address)).to.equal(amount - 1n); + + expect(await erc20.allowance(signer.address, permit2)).to.equal(0); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, bbUSDT.address)).to.equal(0); + }); + + it("should simulate bbUSDT deposit into supply max collateral without skim", async () => { + const blue = Morpho__factory.connect(morpho, signer); + const erc20 = ERC20__factory.connect(usdt, signer); + const erc4626 = MetaMorpho__factory.connect(bbUSDT.address, signer); + + const amount = parseUnits("1000000", 6); + const expectedShares = await erc4626.convertToShares(amount); + await deal(usdt, signer.address, amount); + + const marketConfig = new MarketConfig({ + loanToken: ZeroAddress, + collateralToken: bbUSDT.address, + lltv: 0n, + oracle: ZeroAddress, + irm: ZeroAddress, + }); + await blue.createMarket(marketConfig); + + bundlerService.simulationService.metaMorphoService.addMarkets( + marketConfig.id, + ); + bundlerService.simulationService.metaMorphoService.addVaults( + bbUSDT.address, + ); + + const { operations, bundle } = await setupBundle(bundlerService, signer, [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbUSDT.address, + args: { + assets: amount, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SupplyCollateral", + sender: signer.address, + address: morpho, + args: { + id: marketConfig.id, + assets: MaxUint256, + onBehalf: signer.address, + }, + }, + ]); + + expect(operations.length).to.equal(5); + expect(bundle.requirements.txs.length).to.equal(1); + expect(bundle.requirements.signatures.length).to.equal(0); + + expect(bundle.requirements.txs[0]!.type).to.equal("erc20Approve"); + expect(bundle.requirements.txs[0]!.args).to.eql([usdt, bundler, amount]); + + expect(operations[0]).to.eql({ + type: "Erc20_Approve", + sender: signer.address, + address: usdt, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }); + expect(operations[1]).to.eql({ + type: "Erc20_Permit2", + sender: signer.address, + address: usdt, + args: { + amount, + spender: bundler, + expiration: MathLib.MAX_UINT_48, + nonce: 0n, + }, + }); + expect(operations[2]).to.eql({ + type: "Erc20_Transfer2", + sender: bundler, + address: usdt, + args: { + amount, + from: signer.address, + to: bundler, + }, + }); + expect(operations[3]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: bbUSDT.address, + args: { + assets: amount, + owner: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[4]).to.eql({ + type: "Blue_SupplyCollateral", + sender: bundler, + address: morpho, + args: { + id: marketConfig.id, + assets: MaxUint256, + onBehalf: signer.address, + }, + }); + + expect(await erc20.balanceOf(signer.address)).to.equal(0); + expect(await erc4626.balanceOf(signer.address)).to.equal(0); + + const { collateral } = await blue.position( + marketConfig.id, + signer.address, + ); + assertApproxEqAbs(collateral, expectedShares, parseUnits("0.1")); + + expect(await erc20.allowance(signer.address, permit2)).to.equal(0); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, bbUSDT.address)).to.equal(0); + }); + + it("should simulate bbUSDT deposit into supply collateral with skim", async () => { + const blue = Morpho__factory.connect(morpho, signer); + const erc20 = ERC20__factory.connect(usdt, signer); + const erc4626 = MetaMorpho__factory.connect(bbUSDT.address, signer); + + const amount = parseUnits("1000000", 6); + const shares = parseEther("500000"); + const expectedShares = await erc4626.convertToShares(amount); + await deal(usdt, signer.address, amount); + + const marketConfig = new MarketConfig({ + loanToken: ZeroAddress, + collateralToken: bbUSDT.address, + lltv: 0n, + oracle: ZeroAddress, + irm: ZeroAddress, + }); + await blue.createMarket(marketConfig); + + bundlerService.simulationService.metaMorphoService.addMarkets( + marketConfig.id, + ); + bundlerService.simulationService.metaMorphoService.addVaults( + bbUSDT.address, + ); + + const { operations, bundle } = await setupBundle(bundlerService, signer, [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbUSDT.address, + args: { + assets: amount, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SupplyCollateral", + sender: signer.address, + address: morpho, + args: { + id: marketConfig.id, + assets: shares, + onBehalf: signer.address, + }, + }, + ]); + + expect(operations.length).to.equal(6); + expect(bundle.requirements.txs.length).to.equal(1); + expect(bundle.requirements.signatures.length).to.equal(0); + + expect(bundle.requirements.txs[0]!.type).to.equal("erc20Approve"); + expect(bundle.requirements.txs[0]!.args).to.eql([usdt, bundler, amount]); + + expect(operations[0]).to.eql({ + type: "Erc20_Approve", + sender: signer.address, + address: usdt, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }); + expect(operations[1]).to.eql({ + type: "Erc20_Permit2", + sender: signer.address, + address: usdt, + args: { + amount, + spender: bundler, + expiration: MathLib.MAX_UINT_48, + nonce: 0n, + }, + }); + expect(operations[2]).to.eql({ + type: "Erc20_Transfer2", + sender: bundler, + address: usdt, + args: { + amount, + from: signer.address, + to: bundler, + }, + }); + expect(operations[3]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: bbUSDT.address, + args: { + assets: amount, + owner: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[4]).to.eql({ + type: "Blue_SupplyCollateral", + sender: bundler, + address: morpho, + args: { + id: marketConfig.id, + assets: shares, + onBehalf: signer.address, + }, + }); + expect(operations[5]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: bbUSDT.address, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }); + + expect(await erc20.balanceOf(signer.address)).to.equal(0); + assertApproxEqAbs( + await erc4626.balanceOf(signer.address), + expectedShares - shares, + parseUnits("0.1"), + ); + + const { collateral } = await blue.position( + marketConfig.id, + signer.address, + ); + expect(collateral).to.equal(shares); + + expect(await erc20.allowance(signer.address, permit2)).to.equal(0); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, bbUSDT.address)).to.equal(0); + }); + + it("should simulate bbETH mint on behalf with slippage & unwrap remaining WETH", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + bbETH.address, + ); + + const erc20 = ERC20__factory.connect(wNative, signer); + const erc4626 = MetaMorpho__factory.connect(bbETH.address, signer); + + const shares = parseUnits("99"); + const assets = await erc4626.previewMint(shares); + await deal(wNative, signer.address, assets + parseUnits("10")); + + const { operations, bundle } = await setupBundle( + bundlerService, + signer, + [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbETH.address, + args: { + shares, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ], + + { + onBundleTx: donate( + donator, + wNative, + parseUnits("1"), + bbETH.address, + morpho, + ), + }, + ); + + expect(bundle.requirements.txs.length).to.equal(1); + expect(bundle.requirements.signatures.length).to.equal(0); + + expect(operations).to.eql([ + { + type: "Erc20_Approve", + sender: signer.address, + address: wNative, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }, + { + type: "Erc20_Permit2", + sender: signer.address, + address: wNative, + args: { + amount: expect.bigint, + spender: bundler, + expiration: expect.bigint, + nonce: 0n, + }, + }, + { + type: "Erc20_Transfer2", + sender: bundler, + address: wNative, + args: { + amount: expect.bigint, + from: signer.address, + to: bundler, + }, + }, + { + type: "MetaMorpho_Deposit", + sender: bundler, + address: bbETH.address, + args: { + shares, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Erc20_Transfer", + sender: bundler, + address: wNative, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }, + ]); + + expect(await erc20.balanceOf(donator.address)).to.equal(0); + expect(await erc20.balanceOf(bundler)).to.equal(0); + expect(await erc4626.maxWithdraw(signer.address)).to.equal(0); + assertApproxEqRel( + await erc4626.maxWithdraw(donator.address), + assets - 1n, + DEFAULT_SLIPPAGE_TOLERANCE, + ); + assertApproxEqAbs( + await erc20.balanceOf(signer.address), + parseUnits("10"), + parseUnits("0.025"), + ); + + expect(await erc20.allowance(signer.address, permit2)).to.equal(0); + expect(await erc20.allowance(signer.address, bundler)).to.equal(0); + expect(await erc20.allowance(signer.address, bbETH.address)).to.equal(0); + }); + + it("should fail bbETH mint on behalf with slippage exceeded", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + bbETH.address, + ); + + const erc4626 = MetaMorpho__factory.connect(bbETH.address, signer); + + const shares = parseUnits("99"); + const assets = await erc4626.previewMint(shares); + await deal(wNative, signer.address, assets + parseUnits("10")); + + await expect( + setupBundle( + bundlerService, + signer, + [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbETH.address, + args: { + shares, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ], + + { + unwrapTokens: new Set([wNative]), + onBundleTx: donate( + donator, + wNative, + parseUnits("10"), + bbETH.address, + morpho, + ), + }, + ), + ).to.be.reverted; + }); + + it("should borrow USDC against wstETH into steakUSDC half deposit on behalf with slippage & unwrap remaining wstETH", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + steakUsdc.address, + ); + + const { value: startData } = await bundlerService.simulationService.data; + + const collateral = ERC20__factory.connect(wstEth, signer); + const loan = ERC20__factory.connect(usdc, signer); + const erc4626 = MetaMorpho__factory.connect(steakUsdc.address, signer); + + const id = MAINNET_MARKETS.usdc_wstEth.id; + const market = startData.getMarket(id); + + const collateralAssets = parseUnits("100"); + const loanShares = parseUnits("50000", 12); + const loanAssets = market.toBorrowAssets(loanShares); + await deal(wstEth, signer.address, collateralAssets); + await mine(); + + const { operations, bundle } = await setupBundle( + bundlerService, + signer, + [ + { + type: "Blue_SupplyCollateral", + sender: signer.address, + address: morpho, + args: { + id, + assets: collateralAssets, + onBehalf: signer.address, + }, + }, + { + type: "Blue_Borrow", + sender: signer.address, + address: morpho, + args: { + id, + shares: loanShares, + onBehalf: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: steakUsdc.address, + args: { + assets: loanAssets / 2n, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ], + { + unwrapTokens: new Set([wstEth]), + onBundleTx: donate( + donator, + usdc, + parseUnits("1000", 6), + steakUsdc.address, + morpho, + ), + }, + ); + + expect(operations.length).to.equal(7); + expect(bundle.requirements.txs.length).to.equal(2); + expect(bundle.requirements.signatures.length).to.equal(0); + + expect(bundle.requirements.txs[0]!.type).to.equal("erc20Approve"); + expect(bundle.requirements.txs[0]!.args).to.eql([ + wstEth, + bundler, + collateralAssets, + ]); + expect(bundle.requirements.txs[1]!.type).to.equal( + "morphoSetAuthorization", + ); + expect(bundle.requirements.txs[1]!.args).to.eql([bundler, true]); + + expect(operations[0]).to.eql({ + type: "Erc20_Permit", + sender: signer.address, + address: wstEth, + args: { + amount: collateralAssets, + spender: bundler, + nonce: 0n, + }, + }); + expect(operations[1]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: wstEth, + args: { + amount: collateralAssets, + from: signer.address, + to: bundler, + }, + }); + expect(operations[2]).to.eql({ + type: "Blue_SupplyCollateral", + sender: bundler, + address: morpho, + args: { + id, + assets: collateralAssets, + onBehalf: signer.address, + }, + }); + expect(operations[3]).to.eql({ + type: "Blue_SetAuthorization", + sender: bundler, + address: morpho, + args: { + owner: signer.address, + isBundlerAuthorized: true, + }, + }); + expect(operations[4]).to.eql({ + type: "Blue_Borrow", + sender: bundler, + address: morpho, + args: { + id, + shares: loanShares, + onBehalf: signer.address, + receiver: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[5]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: steakUsdc.address, + args: { + assets: loanAssets / 2n, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[6]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: usdc, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }); + + expect(await collateral.balanceOf(signer.address)).to.equal(0); + assertApproxEqRel( + await loan.balanceOf(signer.address), + loanAssets / 2n, + DEFAULT_SLIPPAGE_TOLERANCE, + ); + expect(await collateral.balanceOf(donator.address)).to.equal(0); + expect(await loan.balanceOf(donator.address)).to.equal(0); + expect(await erc4626.maxWithdraw(signer.address)).to.equal(0); + assertApproxEqRel( + await erc4626.maxWithdraw(donator.address), + loanAssets / 2n, + DEFAULT_SLIPPAGE_TOLERANCE, + ); + + expect(await collateral.allowance(signer.address, permit2)).to.equal(0); + expect(await collateral.allowance(signer.address, bundler)).to.equal(0); + expect( + await collateral.allowance(signer.address, bbETH.address), + ).to.equal(0); + expect(await loan.allowance(signer.address, permit2)).to.equal(0); + expect(await loan.allowance(signer.address, bundler)).to.equal(0); + expect(await loan.allowance(signer.address, bbETH.address)).to.equal(0); + }); + + it("should redeem all bbETH with slippage + wstETH leverage into bbETH deposit & unwrap remaining WETH", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + bbETH.address, + ); + + const id = MAINNET_MARKETS.eth_wstEth.id; + const loan = ERC20__factory.connect(wNative, signer); + const collateral = ERC20__factory.connect(wstEth, signer); + const erc4626 = MetaMorpho__factory.connect(bbETH.address, signer); + + const collateralAssets = parseUnits("100"); + const loanAssets = parseUnits("95"); + + await deal(wstEth, signer.address, collateralAssets); + await deal(wNative, signer.address, loanAssets); + await collateral.approve(morpho, collateralAssets); + await loan.approve(bbETH.address, loanAssets); + await erc4626.deposit(loanAssets, signer.address); + + const { value: startData } = await bundlerService.simulationService.data; + + const shares = startData.getHolding( + signer.address, + bbETH.address, + ).balance; + + const { operations, bundle } = await setupBundle( + bundlerService, + signer, + [ + { + type: "MetaMorpho_Withdraw", + sender: signer.address, + address: bbETH.address, + args: { + shares, + owner: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SupplyCollateral", + sender: signer.address, + address: morpho, + args: { + id, + assets: collateralAssets, + onBehalf: signer.address, + }, + }, + { + type: "Blue_Borrow", + sender: signer.address, + address: morpho, + args: { + id, + assets: loanAssets, + onBehalf: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbETH.address, + args: { + assets: loanAssets, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ], + { + unwrapTokens: new Set([wstEth, wNative]), + onBundleTx: donate( + donator, + wNative, + parseUnits("1"), + bbETH.address, + morpho, + ), + }, + ); + + expect(operations.length).to.equal(10); + expect(bundle.requirements.txs.length).to.equal(3); + expect(bundle.requirements.signatures.length).to.equal(0); + + expect(bundle.requirements.txs[0]!.type).to.equal("erc20Approve"); + expect(bundle.requirements.txs[0]!.args).to.eql([ + bbETH.address, + bundler, + shares, + ]); + expect(bundle.requirements.txs[1]!.type).to.equal("erc20Approve"); + expect(bundle.requirements.txs[1]!.args).to.eql([ + wstEth, + bundler, + collateralAssets, + ]); + expect(bundle.requirements.txs[2]!.type).to.equal( + "morphoSetAuthorization", + ); + expect(bundle.requirements.txs[2]!.args).to.eql([bundler, true]); + + expect(operations[0]).to.eql({ + type: "Erc20_Permit", + sender: signer.address, + address: bbETH.address, + args: { + amount: shares, + spender: bundler, + nonce: 0n, + }, + }); + expect(operations[1]).to.eql({ + type: "Erc20_Permit", + sender: signer.address, + address: wstEth, + args: { + amount: collateralAssets, + spender: bundler, + nonce: 0n, + }, + }); + expect(operations[2]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: wstEth, + args: { + amount: collateralAssets, + from: signer.address, + to: bundler, + }, + }); + expect(operations[3]).to.eql({ + type: "MetaMorpho_Withdraw", + sender: bundler, + address: bbETH.address, + args: { + shares, + owner: signer.address, + receiver: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[4]).to.eql({ + type: "Blue_SupplyCollateral", + sender: bundler, + address: morpho, + args: { + id, + assets: collateralAssets, + onBehalf: signer.address, + }, + }); + expect(operations[5]).to.eql({ + type: "Blue_SetAuthorization", + sender: bundler, + address: morpho, + args: { + owner: signer.address, + isBundlerAuthorized: true, + }, + }); + expect(operations[6]).to.eql({ + type: "Blue_Borrow", + sender: bundler, + address: morpho, + args: { + id, + assets: loanAssets, + onBehalf: signer.address, + receiver: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[7]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: bbETH.address, + args: { + assets: loanAssets, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[8]).to.eql({ + type: "Erc20_Unwrap", + sender: bundler, + address: wNative, + args: { + amount: MaxUint256, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[9]).to.eql({ + type: "Erc20_Transfer", + sender: bundler, + address: NATIVE_ADDRESS, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }); + }); + + it("should deleverage wstETH into MetaMorpho bbETH -> re7WETH arbitrage with slippage", async () => { + bundlerService.simulationService.metaMorphoService.addVaults( + bbETH.address, + re7WETH.address, + ); + + const id = MAINNET_MARKETS.eth_wstEth.id; + const blue = Morpho__factory.connect(morpho, signer); + const loan = ERC20__factory.connect(wNative, signer); + const collateral = ERC20__factory.connect(wstEth, signer); + const erc4626 = MetaMorpho__factory.connect(bbETH.address, signer); + + const collateralAssets = parseUnits("100"); + const loanAssets = parseUnits("95"); + + await deal(wstEth, signer.address, collateralAssets); + await deal(wNative, signer.address, loanAssets); + await collateral.approve(morpho, collateralAssets); + await loan.approve(bbETH.address, loanAssets); + await erc4626.deposit(loanAssets, signer.address); + + await blue.supplyCollateral( + MAINNET_MARKETS.eth_wstEth, + collateralAssets, + signer.address, + "0x", + ); + await blue.borrow( + MAINNET_MARKETS.eth_wstEth, + loanAssets, + 0n, + signer.address, + signer.address, + ); + await mine(); + + const { operations, bundle } = await setupBundle( + bundlerService, + signer, + [ + { + type: "Blue_Repay", + sender: signer.address, + address: morpho, + args: { + id, + assets: loanAssets / 2n, + onBehalf: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_WithdrawCollateral", + sender: signer.address, + address: morpho, + args: { + id, + assets: collateralAssets / 2n, + onBehalf: signer.address, + receiver: signer.address, + }, + }, + { + type: "MetaMorpho_Withdraw", + sender: signer.address, + address: bbETH.address, + args: { + assets: loanAssets / 2n, + owner: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_Repay", + sender: signer.address, + address: morpho, + args: { + id, + assets: loanAssets / 4n, + onBehalf: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: re7WETH.address, + args: { + assets: loanAssets / 4n, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ], + { + unwrapTokens: new Set([wNative]), + onBundleTx: async (data) => { + await donate( + donator, + wNative, + parseUnits("0.5"), + bbETH.address, + morpho, + )(data); + await donate( + signer, + wNative, + parseUnits("0.5"), + re7WETH.address, + morpho, + )(data); + }, + }, + ); + + expect(operations.length).to.equal(10); + expect(bundle.requirements.txs.length).to.equal(3); + expect(bundle.requirements.signatures.length).to.equal(0); + + expect(bundle.requirements.txs[0]!.type).to.equal("erc20Approve"); + expect(bundle.requirements.txs[0]!.args).to.eql([ + bbETH.address, + bundler, + expect.bigint, + ]); + expect(bundle.requirements.txs[1]!.type).to.equal("erc20Approve"); + expect(bundle.requirements.txs[1]!.args).to.eql([ + wNative, + bundler, + loanAssets / 2n, + ]); + expect(bundle.requirements.txs[2]!.type).to.equal( + "morphoSetAuthorization", + ); + expect(bundle.requirements.txs[2]!.args).to.eql([bundler, true]); + + expect(operations[0]).to.eql({ + type: "Erc20_Approve", + sender: signer.address, + address: wNative, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }); + expect(_omit(operations[1], "args.amount")).to.eql({ + type: "Erc20_Permit", + sender: signer.address, + address: bbETH.address, + args: { + spender: bundler, + nonce: 0n, + }, + }); + expect(operations[2]).to.eql({ + type: "Erc20_Permit2", + sender: signer.address, + address: wNative, + args: { + amount: loanAssets / 2n, + spender: bundler, + expiration: MathLib.MAX_UINT_48, + nonce: 0n, + }, + }); + expect(operations[3]).to.eql({ + type: "Erc20_Transfer2", + sender: bundler, + address: wNative, + args: { + amount: loanAssets / 2n, + from: signer.address, + to: bundler, + }, + }); + expect(operations[4]).to.eql({ + type: "Blue_Repay", + sender: bundler, + address: morpho, + args: { + id, + assets: loanAssets / 2n, + onBehalf: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[5]).to.eql({ + type: "Blue_SetAuthorization", + sender: bundler, + address: morpho, + args: { + owner: signer.address, + isBundlerAuthorized: true, + }, + }); + expect(operations[6]).to.eql({ + type: "Blue_WithdrawCollateral", + sender: bundler, + address: morpho, + args: { + id, + assets: collateralAssets / 2n, + onBehalf: signer.address, + receiver: signer.address, + }, + }); + expect(operations[7]).to.eql({ + type: "MetaMorpho_Withdraw", + sender: bundler, + address: bbETH.address, + args: { + assets: loanAssets / 2n, + owner: signer.address, + receiver: bundler, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[8]).to.eql({ + type: "Blue_Repay", + sender: bundler, + address: morpho, + args: { + id, + assets: loanAssets / 4n, + onBehalf: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + expect(operations[9]).to.eql({ + type: "MetaMorpho_Deposit", + sender: bundler, + address: re7WETH.address, + args: { + assets: loanAssets / 4n, + owner: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }); + }); + + it("should borrow USDC with shared liquidity and reallocation fee + unwrap remaining WETH", async () => { + const steakUsdcMm = MetaMorpho__factory.connect( + steakUsdc.address, + signer, + ); + const bbUsdcMm = MetaMorpho__factory.connect(bbUsdc.address, signer); + const bbEthMm = MetaMorpho__factory.connect(bbETH.address, signer); + + const steakUsdcOwner = await ethers.getImpersonatedSigner( + await steakUsdcMm.owner(), + ); + const bbUsdcOwner = await ethers.getImpersonatedSigner( + await bbUsdcMm.owner(), + ); + + const publicAllocatorContract = PublicAllocator__factory.connect( + publicAllocator, + signer, + ); + + await publicAllocatorContract + .connect(steakUsdcOwner) + .setFlowCaps(steakUsdc.address, [ + { + id: MAINNET_MARKETS.usdc_wstEth.id, + caps: { + maxIn: parseUnits("10000", 6), + maxOut: 0n, + }, + }, + { + id: MAINNET_MARKETS.usdc_wbtc.id, + caps: { + maxIn: 0n, + maxOut: parseUnits("20000", 6), // Less than bbUsdc but more than maxIn. + }, + }, + ]); + + const bbUsdcFee = parseEther("0.002"); + + await publicAllocatorContract + .connect(bbUsdcOwner) + .setFee(bbUsdc.address, bbUsdcFee); + await publicAllocatorContract + .connect(bbUsdcOwner) + .setFlowCaps(bbUsdc.address, [ + { + id: MAINNET_MARKETS.usdc_wstEth.id, + caps: { + maxIn: parseUnits("1000000", 6), + maxOut: 0n, + }, + }, + { + id: MAINNET_MARKETS.usdc_wbtc.id, + caps: { + maxIn: 0n, + maxOut: parseUnits("100000", 6), + }, + }, + ]); + + bundlerService.simulationService.metaMorphoService.addVaults( + steakUsdc.address, + bbUsdc.address, + bbETH.address, + ); + + const { value: startData } = await bundlerService.simulationService.data; + + const collateral = ERC20__factory.connect(wstEth, signer); + const loan = ERC20__factory.connect(usdc, signer); + + const id = MAINNET_MARKETS.usdc_wstEth.id; + + const collateralAssets = parseUnits("50000"); + const loanAssets = startData + .getMarketPublicReallocations(id) + .data.getMarket(id).liquidity; + const depositAssets = parseUnits("50"); + await deal(wstEth, signer.address, collateralAssets); + await deal(wNative, signer.address, depositAssets); + await mine(); + + const { operations, bundle } = await setupBundle( + bundlerService, + signer, + [ + { + type: "MetaMorpho_Deposit", + sender: signer.address, + address: bbETH.address, + args: { + assets: depositAssets, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SupplyCollateral", + sender: signer.address, + address: morpho, + args: { + id, + assets: collateralAssets, + onBehalf: signer.address, + }, + }, + { + type: "Blue_Borrow", + sender: signer.address, + address: morpho, + args: { + id, + assets: loanAssets, + onBehalf: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ], + { unwrapTokens: new Set([wNative]) }, + ); + + expect(bundle.requirements).to.eql({ + txs: [ + { + type: "erc20Approve", + args: [wstEth, bundler, collateralAssets], + tx: expect.anything, + }, + { + type: "erc20Approve", + args: [wNative, bundler, depositAssets], + tx: expect.anything, + }, + { + type: "morphoSetAuthorization", + args: [bundler, true], + tx: expect.anything, + }, + ], + signatures: [], + }); + + expect(operations).to.eql([ + { + type: "Erc20_Approve", + sender: signer.address, + address: wNative, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }, + { + type: "Erc20_Permit", + sender: signer.address, + address: wstEth, + args: { + amount: collateralAssets, + spender: bundler, + nonce: 0n, + }, + }, + { + type: "Erc20_Permit2", + sender: signer.address, + address: wNative, + args: { + amount: depositAssets, + spender: bundler, + expiration: expect.bigint, + nonce: 0n, + }, + }, + { + type: "Erc20_Transfer", + sender: bundler, + address: wstEth, + args: { + amount: collateralAssets, + from: signer.address, + to: bundler, + }, + }, + { + type: "Erc20_Transfer", + sender: signer.address, + address: NATIVE_ADDRESS, + args: { + amount: bbUsdcFee, + from: signer.address, + to: bundler, + }, + }, + { + type: "Erc20_Transfer2", + sender: bundler, + address: wNative, + args: { + amount: depositAssets, + from: signer.address, + to: bundler, + }, + }, + { + type: "MetaMorpho_Deposit", + sender: bundler, + address: bbETH.address, + args: { + assets: depositAssets, + owner: donator.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SupplyCollateral", + sender: bundler, + address: morpho, + args: { + id, + assets: collateralAssets, + onBehalf: signer.address, + }, + }, + { + type: "Blue_SetAuthorization", + sender: bundler, + address: morpho, + args: { + owner: signer.address, + isBundlerAuthorized: true, + }, + }, + { + type: "MetaMorpho_PublicReallocate", + sender: bundler, + address: bbUsdc.address, + args: { + withdrawals: [ + { + id: MAINNET_MARKETS.usdc_wbtc.id, + assets: parseUnits("100000", 6), + }, + ], + supplyMarketId: id, + }, + }, + { + type: "MetaMorpho_PublicReallocate", + sender: bundler, + address: steakUsdc.address, + args: { + withdrawals: [ + { + id: MAINNET_MARKETS.usdc_wbtc.id, + assets: parseUnits("10000", 6), + }, + ], + supplyMarketId: id, + }, + }, + { + type: "Blue_Borrow", + sender: bundler, + address: morpho, + args: { + id, + assets: loanAssets, + onBehalf: signer.address, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + ]); + + expect(await collateral.balanceOf(signer.address)).to.equal(0); + expect(await loan.balanceOf(signer.address)).to.equal(loanAssets); + expect(await collateral.balanceOf(donator.address)).to.equal(0); + expect(await loan.balanceOf(donator.address)).to.equal(0); + expect(await bbEthMm.maxWithdraw(signer.address)).to.equal(0); + expect(await bbEthMm.maxWithdraw(donator.address)).to.equal( + depositAssets - 1n, + ); + + expect(await collateral.allowance(signer.address, permit2)).to.equal(0); + expect(await collateral.allowance(signer.address, bundler)).to.equal(0); + expect( + await collateral.allowance(signer.address, bbETH.address), + ).to.equal(0); + expect(await loan.allowance(signer.address, permit2)).to.equal(0); + expect(await loan.allowance(signer.address, bundler)).to.equal(0); + expect(await loan.allowance(signer.address, bbETH.address)).to.equal(0); + }); + + it("should close a WETH/wstETH position + unwrap wstEth + skim WETH", async () => { + const market = MAINNET_MARKETS.eth_wstEth; + bundlerService.simulationService.metaMorphoService.addMarkets(market.id); + + const blue = Morpho__factory.connect(morpho, signer); + + const collateralAmount = parseUnits("1"); + const borrowAmount = parseUnits("0.5"); + + const wstEthContract = ERC20__factory.connect(wstEth, signer); + const stEthContract = ERC20__factory.connect(stEth, signer); + const wEthContract = ERC20__factory.connect(wNative, signer); + + await deal(wstEth, signer.address, collateralAmount); + await deal(stEth, signer.address, 0n); + + await wstEthContract.approve(blue, MaxUint256); + await blue.supplyCollateral( + market, + collateralAmount, + signer.address, + "0x", + ); + + await blue.borrow( + market, + borrowAmount, + 0n, + signer.address, + signer.address, + ); + + const extraWethAmount = parseEther("0.1"); + + await deal(wNative, signer.address, borrowAmount + extraWethAmount); + + const { value: data } = await bundlerService.simulationService.data; + + const position = data.getAccrualPosition(signer.address, market.id); + + const { operations, bundle } = await setupBundle( + bundlerService, + signer, + [ + { + type: "Blue_Repay", + sender: signer.address, + address: morpho, + args: { + id: market.id, + shares: position.borrowShares, + onBehalf: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_WithdrawCollateral", + sender: signer.address, + address: morpho, + args: { + id: market.id, + assets: position.collateral, + receiver: signer.address, + onBehalf: signer.address, + }, + }, + ], + { unwrapTokens: new Set([wstEth]) }, + ); + + const repayAmount = MathLib.wMulUp( + position.borrowAssets, + MathLib.WAD + DEFAULT_SLIPPAGE_TOLERANCE, + ); + + expect(operations.length).to.equal(9); + expect(bundle.requirements.txs.length).to.equal(2); + expect(bundle.requirements.signatures.length).to.equal(0); + expect(operations).eql([ + { + type: "Erc20_Approve", + sender: signer.address, + address: wNative, + args: { + amount: MathLib.MAX_UINT_160, + spender: permit2, + }, + }, + { + type: "Erc20_Permit2", + sender: signer.address, + address: wNative, + args: { + amount: repayAmount, + spender: bundler, + expiration: expect.bigint, + nonce: 0n, + }, + }, + { + type: "Erc20_Transfer2", + sender: bundler, + address: wNative, + args: { + amount: repayAmount, + from: signer.address, + to: bundler, + }, + }, + { + type: "Blue_Repay", + sender: bundler, + address: morpho, + args: { + id: market.id, + shares: position.borrowShares, + onBehalf: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Blue_SetAuthorization", + sender: bundler, + address: morpho, + args: { + owner: signer.address, + isBundlerAuthorized: true, + }, + }, + { + type: "Blue_WithdrawCollateral", + sender: bundler, + address: morpho, + args: { + id: market.id, + assets: position.collateral, + receiver: bundler, + onBehalf: signer.address, + }, + }, + { + type: "Erc20_Unwrap", + address: wstEth, + sender: bundler, + args: { + amount: MaxUint256, + receiver: signer.address, + slippage: DEFAULT_SLIPPAGE_TOLERANCE, + }, + }, + { + type: "Erc20_Transfer", + address: wNative, + sender: bundler, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }, + { + type: "Erc20_Transfer", + address: stEth, + sender: bundler, + args: { + amount: MaxUint256, + from: bundler, + to: signer.address, + }, + }, + ]); + + const chainPosition = await blue.position(market.id, signer.address); + + const [ + bundlerWstEthBalance, + bundlerStEthBalance, + bundlerWEthBalance, + userStEthBalance, + userWstEthBalance, + userWEthBalance, + ] = await Promise.all([ + wstEthContract.balanceOf(bundler), + stEthContract.balanceOf(bundler), + wEthContract.balanceOf(bundler), + stEthContract.balanceOf(signer), + wstEthContract.balanceOf(signer), + wEthContract.balanceOf(signer), + ]); + + const wstEthToken = data.getWrappedToken(wstEth); + + const latestBlock = (await signer.provider.getBlock("latest"))!; + + const accruedInterests = + position.accrueInterest(BigInt(latestBlock.timestamp)).borrowAssets - + borrowAmount; + + expect(chainPosition.collateral).to.equal(0); + expect(chainPosition.supplyShares).to.equal(0); + expect(chainPosition.borrowShares).to.equal(0); + + expect(bundlerWstEthBalance).to.equal(0); + expect(bundlerStEthBalance).to.equal(1n); // 1 stETH is always remaining in the bundler + expect(bundlerWEthBalance).to.equal(0); + + expect(userStEthBalance).to.approximately( + wstEthToken.toUnwrappedExactAmountIn(collateralAmount, 0n), + 1n, + ); + expect(userWstEthBalance).to.equal(0); + expect(userWEthBalance).to.equal(extraWethAmount - accruedInterests); // we normally didn't experienced any slippage + }); + }); +}); diff --git a/packages/blue-sdk-simulation/tsconfig.build.json b/packages/blue-sdk-viem-bundler/tsconfig.build.json similarity index 100% rename from packages/blue-sdk-simulation/tsconfig.build.json rename to packages/blue-sdk-viem-bundler/tsconfig.build.json diff --git a/packages/blue-sdk-simulation/tsconfig.json b/packages/blue-sdk-viem-bundler/tsconfig.json similarity index 100% rename from packages/blue-sdk-simulation/tsconfig.json rename to packages/blue-sdk-viem-bundler/tsconfig.json diff --git a/packages/blue-sdk-simulation/CHANGELOG.md b/packages/blue-sdk-viem-simulation/CHANGELOG.md similarity index 72% rename from packages/blue-sdk-simulation/CHANGELOG.md rename to packages/blue-sdk-viem-simulation/CHANGELOG.md index 48a2cfa3..67126af5 100644 --- a/packages/blue-sdk-simulation/CHANGELOG.md +++ b/packages/blue-sdk-viem-simulation/CHANGELOG.md @@ -5,11 +5,11 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ## 1.10.1-alpha.1 (2024-09-20) -**Note:** Version bump only for package @morpho-org/blue-sdk-simulation +**Note:** Version bump only for package @morpho-org/blue-sdk-viem-simulation ## 1.10.1-alpha.0 (2024-09-20) -**Note:** Version bump only for package @morpho-org/blue-sdk-simulation +**Note:** Version bump only for package @morpho-org/blue-sdk-viem-simulation ## 1.7.6 (2024-09-18) diff --git a/packages/blue-sdk-simulation/package.json b/packages/blue-sdk-viem-simulation/package.json similarity index 96% rename from packages/blue-sdk-simulation/package.json rename to packages/blue-sdk-viem-simulation/package.json index f2fdb361..6ab20662 100644 --- a/packages/blue-sdk-simulation/package.json +++ b/packages/blue-sdk-viem-simulation/package.json @@ -1,5 +1,5 @@ { - "name": "@morpho-org/blue-sdk-simulation", + "name": "@morpho-org/blue-sdk-viem-simulation", "version": "1.10.5-alpha.8", "author": "Morpho Association ", "license": "MIT", diff --git a/packages/blue-sdk-simulation/src/SimulationState.ts b/packages/blue-sdk-viem-simulation/src/SimulationState.ts similarity index 100% rename from packages/blue-sdk-simulation/src/SimulationState.ts rename to packages/blue-sdk-viem-simulation/src/SimulationState.ts diff --git a/packages/blue-sdk-simulation/src/errors.ts b/packages/blue-sdk-viem-simulation/src/errors.ts similarity index 100% rename from packages/blue-sdk-simulation/src/errors.ts rename to packages/blue-sdk-viem-simulation/src/errors.ts diff --git a/packages/blue-sdk-simulation/src/handlers/blue/accrueInterest.ts b/packages/blue-sdk-viem-simulation/src/handlers/blue/accrueInterest.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/blue/accrueInterest.ts rename to packages/blue-sdk-viem-simulation/src/handlers/blue/accrueInterest.ts diff --git a/packages/blue-sdk-simulation/src/handlers/blue/borrow.ts b/packages/blue-sdk-viem-simulation/src/handlers/blue/borrow.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/blue/borrow.ts rename to packages/blue-sdk-viem-simulation/src/handlers/blue/borrow.ts diff --git a/packages/blue-sdk-simulation/src/handlers/blue/index.ts b/packages/blue-sdk-viem-simulation/src/handlers/blue/index.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/blue/index.ts rename to packages/blue-sdk-viem-simulation/src/handlers/blue/index.ts diff --git a/packages/blue-sdk-simulation/src/handlers/blue/repay.ts b/packages/blue-sdk-viem-simulation/src/handlers/blue/repay.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/blue/repay.ts rename to packages/blue-sdk-viem-simulation/src/handlers/blue/repay.ts diff --git a/packages/blue-sdk-simulation/src/handlers/blue/setAuthorization.ts b/packages/blue-sdk-viem-simulation/src/handlers/blue/setAuthorization.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/blue/setAuthorization.ts rename to packages/blue-sdk-viem-simulation/src/handlers/blue/setAuthorization.ts diff --git a/packages/blue-sdk-simulation/src/handlers/blue/supply.ts b/packages/blue-sdk-viem-simulation/src/handlers/blue/supply.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/blue/supply.ts rename to packages/blue-sdk-viem-simulation/src/handlers/blue/supply.ts diff --git a/packages/blue-sdk-simulation/src/handlers/blue/supplyCollateral.ts b/packages/blue-sdk-viem-simulation/src/handlers/blue/supplyCollateral.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/blue/supplyCollateral.ts rename to packages/blue-sdk-viem-simulation/src/handlers/blue/supplyCollateral.ts diff --git a/packages/blue-sdk-simulation/src/handlers/blue/withdraw.ts b/packages/blue-sdk-viem-simulation/src/handlers/blue/withdraw.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/blue/withdraw.ts rename to packages/blue-sdk-viem-simulation/src/handlers/blue/withdraw.ts diff --git a/packages/blue-sdk-simulation/src/handlers/blue/withdrawCollateral.ts b/packages/blue-sdk-viem-simulation/src/handlers/blue/withdrawCollateral.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/blue/withdrawCollateral.ts rename to packages/blue-sdk-viem-simulation/src/handlers/blue/withdrawCollateral.ts diff --git a/packages/blue-sdk-simulation/src/handlers/dispatchers.ts b/packages/blue-sdk-viem-simulation/src/handlers/dispatchers.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/dispatchers.ts rename to packages/blue-sdk-viem-simulation/src/handlers/dispatchers.ts diff --git a/packages/blue-sdk-simulation/src/handlers/erc20/approve.ts b/packages/blue-sdk-viem-simulation/src/handlers/erc20/approve.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/erc20/approve.ts rename to packages/blue-sdk-viem-simulation/src/handlers/erc20/approve.ts diff --git a/packages/blue-sdk-simulation/src/handlers/erc20/index.ts b/packages/blue-sdk-viem-simulation/src/handlers/erc20/index.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/erc20/index.ts rename to packages/blue-sdk-viem-simulation/src/handlers/erc20/index.ts diff --git a/packages/blue-sdk-simulation/src/handlers/erc20/permit.ts b/packages/blue-sdk-viem-simulation/src/handlers/erc20/permit.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/erc20/permit.ts rename to packages/blue-sdk-viem-simulation/src/handlers/erc20/permit.ts diff --git a/packages/blue-sdk-simulation/src/handlers/erc20/permit2.ts b/packages/blue-sdk-viem-simulation/src/handlers/erc20/permit2.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/erc20/permit2.ts rename to packages/blue-sdk-viem-simulation/src/handlers/erc20/permit2.ts diff --git a/packages/blue-sdk-simulation/src/handlers/erc20/transfer.ts b/packages/blue-sdk-viem-simulation/src/handlers/erc20/transfer.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/erc20/transfer.ts rename to packages/blue-sdk-viem-simulation/src/handlers/erc20/transfer.ts diff --git a/packages/blue-sdk-simulation/src/handlers/erc20/transfer2.ts b/packages/blue-sdk-viem-simulation/src/handlers/erc20/transfer2.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/erc20/transfer2.ts rename to packages/blue-sdk-viem-simulation/src/handlers/erc20/transfer2.ts diff --git a/packages/blue-sdk-simulation/src/handlers/erc20/unwrap.ts b/packages/blue-sdk-viem-simulation/src/handlers/erc20/unwrap.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/erc20/unwrap.ts rename to packages/blue-sdk-viem-simulation/src/handlers/erc20/unwrap.ts diff --git a/packages/blue-sdk-simulation/src/handlers/erc20/wrap.ts b/packages/blue-sdk-viem-simulation/src/handlers/erc20/wrap.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/erc20/wrap.ts rename to packages/blue-sdk-viem-simulation/src/handlers/erc20/wrap.ts diff --git a/packages/blue-sdk-simulation/src/handlers/index.ts b/packages/blue-sdk-viem-simulation/src/handlers/index.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/index.ts rename to packages/blue-sdk-viem-simulation/src/handlers/index.ts diff --git a/packages/blue-sdk-simulation/src/handlers/metamorpho/accrueInterest.ts b/packages/blue-sdk-viem-simulation/src/handlers/metamorpho/accrueInterest.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/metamorpho/accrueInterest.ts rename to packages/blue-sdk-viem-simulation/src/handlers/metamorpho/accrueInterest.ts diff --git a/packages/blue-sdk-simulation/src/handlers/metamorpho/deposit.ts b/packages/blue-sdk-viem-simulation/src/handlers/metamorpho/deposit.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/metamorpho/deposit.ts rename to packages/blue-sdk-viem-simulation/src/handlers/metamorpho/deposit.ts diff --git a/packages/blue-sdk-simulation/src/handlers/metamorpho/index.ts b/packages/blue-sdk-viem-simulation/src/handlers/metamorpho/index.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/metamorpho/index.ts rename to packages/blue-sdk-viem-simulation/src/handlers/metamorpho/index.ts diff --git a/packages/blue-sdk-simulation/src/handlers/metamorpho/publicReallocate.ts b/packages/blue-sdk-viem-simulation/src/handlers/metamorpho/publicReallocate.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/metamorpho/publicReallocate.ts rename to packages/blue-sdk-viem-simulation/src/handlers/metamorpho/publicReallocate.ts diff --git a/packages/blue-sdk-simulation/src/handlers/metamorpho/reallocate.ts b/packages/blue-sdk-viem-simulation/src/handlers/metamorpho/reallocate.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/metamorpho/reallocate.ts rename to packages/blue-sdk-viem-simulation/src/handlers/metamorpho/reallocate.ts diff --git a/packages/blue-sdk-simulation/src/handlers/metamorpho/withdraw.ts b/packages/blue-sdk-viem-simulation/src/handlers/metamorpho/withdraw.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/metamorpho/withdraw.ts rename to packages/blue-sdk-viem-simulation/src/handlers/metamorpho/withdraw.ts diff --git a/packages/blue-sdk-simulation/src/handlers/types.ts b/packages/blue-sdk-viem-simulation/src/handlers/types.ts similarity index 100% rename from packages/blue-sdk-simulation/src/handlers/types.ts rename to packages/blue-sdk-viem-simulation/src/handlers/types.ts diff --git a/packages/blue-sdk-simulation/src/hooks/index.ts b/packages/blue-sdk-viem-simulation/src/hooks/index.ts similarity index 100% rename from packages/blue-sdk-simulation/src/hooks/index.ts rename to packages/blue-sdk-viem-simulation/src/hooks/index.ts diff --git a/packages/blue-sdk-simulation/src/hooks/useSimulationState.ts b/packages/blue-sdk-viem-simulation/src/hooks/useSimulationState.ts similarity index 100% rename from packages/blue-sdk-simulation/src/hooks/useSimulationState.ts rename to packages/blue-sdk-viem-simulation/src/hooks/useSimulationState.ts diff --git a/packages/blue-sdk-simulation/src/index.ts b/packages/blue-sdk-viem-simulation/src/index.ts similarity index 100% rename from packages/blue-sdk-simulation/src/index.ts rename to packages/blue-sdk-viem-simulation/src/index.ts diff --git a/packages/blue-sdk-simulation/src/operations.ts b/packages/blue-sdk-viem-simulation/src/operations.ts similarity index 100% rename from packages/blue-sdk-simulation/src/operations.ts rename to packages/blue-sdk-viem-simulation/src/operations.ts diff --git a/packages/blue-sdk-viem-simulation/tsconfig.build.json b/packages/blue-sdk-viem-simulation/tsconfig.build.json new file mode 100644 index 00000000..3c5447a8 --- /dev/null +++ b/packages/blue-sdk-viem-simulation/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["**/*.(spec|test|fixtures).ts"] +} diff --git a/packages/blue-sdk-viem-simulation/tsconfig.json b/packages/blue-sdk-viem-simulation/tsconfig.json new file mode 100644 index 00000000..3860b401 --- /dev/null +++ b/packages/blue-sdk-viem-simulation/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "lib", + "rootDir": ".", + "baseUrl": "." + } +} diff --git a/packages/blue-sdk-viem/src/abis.ts b/packages/blue-sdk-viem/src/abis.ts index 876bafdd..b05485db 100644 --- a/packages/blue-sdk-viem/src/abis.ts +++ b/packages/blue-sdk-viem/src/abis.ts @@ -1,34 +1,3 @@ -import { erc20Abi } from "viem"; - -export const bytes32Erc20Abi = [ - ...erc20Abi.filter( - ({ type, name }) => - type !== "function" || (name !== "name" && name !== "symbol"), - ), - { - type: "function", - name: "name", - stateMutability: "view", - inputs: [], - outputs: [ - { - type: "bytes32", - }, - ], - }, - { - type: "function", - name: "symbol", - stateMutability: "view", - inputs: [], - outputs: [ - { - type: "bytes32", - }, - ], - }, -] as const; - export const erc2612Abi = [ { inputs: [], diff --git a/packages/blue-sdk-viem/src/fetch/Token.ts b/packages/blue-sdk-viem/src/fetch/Token.ts index 79f025fd..46dffdf2 100644 --- a/packages/blue-sdk-viem/src/fetch/Token.ts +++ b/packages/blue-sdk-viem/src/fetch/Token.ts @@ -1,4 +1,11 @@ -import { Address, Client, erc20Abi, hexToString, isHex } from "viem"; +import { + Address, + Client, + erc20Abi, + erc20Abi_bytes32, + hexToString, + isHex, +} from "viem"; import { ChainUtils, @@ -10,7 +17,7 @@ import { getUnwrappedToken, } from "@morpho-org/blue-sdk"; import { getChainId, readContract } from "viem/actions"; -import { bytes32Erc20Abi, wstEthAbi } from "../abis"; +import { wstEthAbi } from "../abis"; import { abi, code } from "../queries/GetToken"; import { DeploylessFetchParameters } from "../types"; @@ -82,7 +89,7 @@ export async function fetchToken( readContract(client, { ...parameters, address, - abi: bytes32Erc20Abi, + abi: erc20Abi_bytes32, functionName: "symbol", }).then(decodeBytes32String), ), @@ -95,7 +102,7 @@ export async function fetchToken( readContract(client, { ...parameters, address, - abi: bytes32Erc20Abi, + abi: erc20Abi_bytes32, functionName: "name", }).then(decodeBytes32String), ), diff --git a/packages/blue-sdk-viem/src/index.ts b/packages/blue-sdk-viem/src/index.ts index 2234dba2..84347e49 100644 --- a/packages/blue-sdk-viem/src/index.ts +++ b/packages/blue-sdk-viem/src/index.ts @@ -1,9 +1,11 @@ export * from "./abis"; export * from "./fetch"; +export * from "./signatures"; export * from "./types"; export * from "./utils"; export * as abis from "./abis"; export * as fetch from "./fetch"; +export * as signatures from "./signatures"; export * as types from "./types"; export * as utils from "./utils"; diff --git a/packages/blue-sdk-viem/src/signatures/index.ts b/packages/blue-sdk-viem/src/signatures/index.ts index 745ca5ff..5704ad1d 100644 --- a/packages/blue-sdk-viem/src/signatures/index.ts +++ b/packages/blue-sdk-viem/src/signatures/index.ts @@ -1,5 +1,3 @@ export * from "./manager"; export * from "./permit"; export * from "./permit2"; -export * from "./utils"; -export * from "./types"; diff --git a/packages/blue-sdk-viem/src/signatures/manager.ts b/packages/blue-sdk-viem/src/signatures/manager.ts index 376452a1..dc415cda 100644 --- a/packages/blue-sdk-viem/src/signatures/manager.ts +++ b/packages/blue-sdk-viem/src/signatures/manager.ts @@ -1,9 +1,7 @@ import { ChainId, getChainAddresses } from "@morpho-org/blue-sdk"; +import { TypedDataDefinition } from "viem"; -import { HashTypedDataParameters, hashTypedData } from "viem"; -import { SignatureMessage } from "./types"; - -export interface ManagerApprovalSignatureArgs extends Record { +export interface AuthorizationArgs { authorizer: string; authorized: string; isAuthorized: boolean; @@ -11,49 +9,33 @@ export interface ManagerApprovalSignatureArgs extends Record { deadline: bigint; } -export const getManagerApprovalMessage = ( - args: ManagerApprovalSignatureArgs, - chainId: ChainId, -): SignatureMessage => { - const domain = { - chainId: chainId, - verifyingContract: getChainAddresses(chainId).morpho, - }; - - const types = { - Authorization: [ - { - name: "authorizer", - type: "address", - }, - { - name: "authorized", - type: "address", - }, - { - name: "isAuthorized", - type: "bool", - }, - { - name: "nonce", - type: "uint256", - }, - { - name: "deadline", - type: "uint256", - }, - ], - }; - - const data: HashTypedDataParameters = { - types, - message: args, - domain, - primaryType: "Authorization", - }; +const authorizationTypes = { + Authorization: [ + { name: "authorizer", type: "address" }, + { name: "authorized", type: "address" }, + { name: "isAuthorized", type: "bool" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], +}; +export const getAuthorizationTypedData = ( + { authorizer, authorized, isAuthorized, nonce, deadline }: AuthorizationArgs, + chainId: ChainId, +): TypedDataDefinition => { return { - data, - hash: hashTypedData(data), + domain: { + chainId: chainId, + verifyingContract: getChainAddresses(chainId).morpho, + }, + types: authorizationTypes, + message: { + authorizer, + authorized, + isAuthorized, + nonce, + deadline, + }, + primaryType: "Authorization", }; }; diff --git a/packages/blue-sdk-viem/src/signatures/permit.ts b/packages/blue-sdk-viem/src/signatures/permit.ts index a5d1da3b..6ea1fcdf 100644 --- a/packages/blue-sdk-viem/src/signatures/permit.ts +++ b/packages/blue-sdk-viem/src/signatures/permit.ts @@ -1,15 +1,9 @@ -import { - Address, - ChainId, - Token, - getChainAddresses, -} from "@morpho-org/blue-sdk"; - -import { HashTypedDataParameters, hashTypedData } from "viem"; -import { SignatureMessage } from "./types"; +import { Address, ChainId, getChainAddresses } from "@morpho-org/blue-sdk"; +import { TypedDataDefinition } from "viem"; export interface PermitArgs { - erc20: Token; + name: string; + address: Address; owner: Address; spender: Address; allowance: bigint; @@ -17,95 +11,88 @@ export interface PermitArgs { deadline: bigint; } +const permitTypes = { + Permit: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], +} as const; + /** * Permit signature for ERC20 tokens, following EIP-2612. * Docs: https://eips.ethereum.org/EIPS/eip-2612 */ -export const getPermitMessage = ( - { deadline, owner, nonce, spender, erc20, allowance }: PermitArgs, +export const getPermitTypedData = ( + { deadline, owner, nonce, spender, name, address, allowance }: PermitArgs, chainId: ChainId, -): SignatureMessage => { - const { usdc, dai } = getChainAddresses(chainId); +): TypedDataDefinition => { + const { usdc } = getChainAddresses(chainId); const domain = { - name: erc20.name, - version: erc20.address === usdc ? "2" : "1", + name: name, + version: address === usdc ? "2" : "1", chainId, - verifyingContract: erc20.address, + verifyingContract: address, }; - const data: HashTypedDataParameters = - erc20.address === dai - ? { - domain, - types: { - Permit: [ - { - name: "holder", - type: "address", - }, - { - name: "spender", - type: "address", - }, - { - name: "nonce", - type: "uint256", - }, - { - name: "expiry", - type: "uint256", - }, - { - name: "allowed", - type: "bool", - }, - ], - }, - message: { - holder: owner, - spender, - allowed: allowance > 0n, - nonce, - expiry: deadline, - }, - primaryType: "Permit", - } - : { - domain, - types: { - Permit: [ - { - name: "owner", - type: "address", - }, - { - name: "spender", - type: "address", - }, - { - name: "value", - type: "uint256", - }, - { - name: "nonce", - type: "uint256", - }, - { - name: "deadline", - type: "uint256", - }, - ], - }, - message: { - owner, - spender, - value: allowance, - nonce, - deadline, - }, - primaryType: "Permit", - }; + return { + domain, + types: permitTypes, + message: { + owner, + spender, + value: allowance, + nonce, + deadline, + }, + primaryType: "Permit", + }; +}; - return { data, hash: hashTypedData(data) }; +export interface DaiPermitArgs { + owner: Address; + spender: Address; + allowance: bigint; + nonce: bigint; + deadline: bigint; +} + +const daiPermitTypes = { + Permit: [ + { name: "holder", type: "address" }, + { name: "spender", type: "address" }, + { name: "nonce", type: "uint256" }, + { name: "expiry", type: "uint256" }, + { name: "allowed", type: "bool" }, + ], +} as const; + +export const getDaiPermitTypedData = ( + { deadline, owner, nonce, spender, allowance }: DaiPermitArgs, + chainId: ChainId, +): TypedDataDefinition => { + const { dai } = getChainAddresses(chainId); + + const domain = { + name: "DAI", + version: "1", + chainId, + verifyingContract: dai, + }; + + return { + domain, + types: daiPermitTypes, + message: { + holder: owner, + spender, + allowed: allowance > 0n, + nonce, + expiry: deadline, + }, + primaryType: "Permit", + }; }; diff --git a/packages/blue-sdk-viem/src/signatures/permit2.ts b/packages/blue-sdk-viem/src/signatures/permit2.ts index 9db5c661..bb72726f 100644 --- a/packages/blue-sdk-viem/src/signatures/permit2.ts +++ b/packages/blue-sdk-viem/src/signatures/permit2.ts @@ -5,16 +5,15 @@ import { getChainAddresses, } from "@morpho-org/blue-sdk"; -import { HashTypedDataParameters, hashTypedData } from "viem"; -import { SignatureMessage } from "./types"; +import { TypedDataDefinition } from "viem"; -export interface Permit2Args { +export interface Permit2PermitArgs { erc20: Address; allowance: bigint; - nonce: bigint; + nonce: number; deadline: bigint; spender: Address; - expiration?: bigint; + expiration?: number; } export interface Permit2TransferFromArgs { @@ -25,112 +24,86 @@ export interface Permit2TransferFromArgs { deadline: bigint; } -export const getPermit2TransferFromMessage = ( - args: Permit2TransferFromArgs, +const permit2PermitTypes = { + PermitSingle: [ + { name: "details", type: "PermitDetails" }, + { name: "spender", type: "address" }, + { name: "sigDeadline", type: "uint256" }, + ], + PermitDetails: [ + { name: "token", type: "address" }, + { name: "amount", type: "uint160" }, + { name: "expiration", type: "uint48" }, + { name: "nonce", type: "uint48" }, + ], +}; + +export const getPermit2PermitTypedData = ( + args: Permit2PermitArgs, chainId: ChainId, -): SignatureMessage => { - const domain = { - name: "Permit2", - chainId, - verifyingContract: getChainAddresses(chainId).permit2, - }; - const types = { - PermitTransferFrom: [ - { name: "permitted", type: "TokenPermissions" }, - { name: "spender", type: "address" }, - { name: "nonce", type: "uint256" }, - { name: "deadline", type: "uint256" }, - ], - TokenPermissions: [ - { name: "token", type: "address" }, - { name: "amount", type: "uint256" }, - ], - }; - const message = { - permitted: { - token: args.erc20, - amount: MathLib.min(args.allowance, MathLib.MAX_UINT_160), +): TypedDataDefinition => { + return { + domain: { + name: "Permit2", + chainId: chainId, + verifyingContract: getChainAddresses(chainId).permit2, }, - spender: args.spender, - nonce: args.nonce, - deadline: args.deadline, - }; - - const data: HashTypedDataParameters = { - domain, - types, - message, - primaryType: "PermitTransferFrom", + types: permit2PermitTypes, + message: { + details: { + token: args.erc20, + amount: MathLib.min(args.allowance, MathLib.MAX_UINT_160), + // Use an unlimited expiration because it most + // closely mimics how a standard approval works. + expiration: MathLib.min( + args.expiration ?? MathLib.MAX_UINT_48, + MathLib.MAX_UINT_48, + ), + nonce: args.nonce, + }, + spender: args.spender, + sigDeadline: args.deadline, + }, + primaryType: "PermitSingle", }; +}; - return { data, hash: hashTypedData(data) }; +const permit2TransferFromTypes = { + PermitTransferFrom: [ + { name: "permitted", type: "TokenPermissions" }, + { name: "spender", type: "address" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], + TokenPermissions: [ + { name: "token", type: "address" }, + { name: "amount", type: "uint256" }, + ], }; -export const getPermit2Message = ( - args: Permit2Args, +export const getPermit2TransferFromTypedData = ( + args: Permit2TransferFromArgs, chainId: ChainId, -): SignatureMessage => { - const domain = { - name: "Permit2", - chainId: chainId, - verifyingContract: getChainAddresses(chainId).permit2, - }; - const types = { - PermitSingle: [ - { - name: "details", - type: "PermitDetails", - }, - { - name: "spender", - type: "address", - }, - { - name: "sigDeadline", - type: "uint256", - }, - ], - PermitDetails: [ - { - name: "token", - type: "address", - }, - { - name: "amount", - type: "uint160", - }, - { - name: "expiration", - type: "uint48", - }, - { - name: "nonce", - type: "uint48", +): TypedDataDefinition< + typeof permit2TransferFromTypes, + "PermitTransferFrom" +> => { + return { + domain: { + name: "Permit2", + chainId, + verifyingContract: getChainAddresses(chainId).permit2, + }, + types: permit2TransferFromTypes, + message: { + permitted: { + token: args.erc20, + amount: MathLib.min(args.allowance, MathLib.MAX_UINT_160), }, - ], - }; - const message = { - details: { - token: args.erc20, - amount: MathLib.min(args.allowance, MathLib.MAX_UINT_160), - // Use an unlimited expiration because it most - // closely mimics how a standard approval works. - expiration: MathLib.min( - args.expiration ?? MathLib.MAX_UINT_48, - MathLib.MAX_UINT_48, - ), + spender: args.spender, nonce: args.nonce, + deadline: args.deadline, }, - spender: args.spender, - sigDeadline: args.deadline, - }; - - const data: HashTypedDataParameters = { - domain, - types, - message, - primaryType: "PermitSingle", + primaryType: "PermitTransferFrom", }; - - return { data, hash: hashTypedData(data) }; }; diff --git a/packages/blue-sdk-viem/src/signatures/types.ts b/packages/blue-sdk-viem/src/signatures/types.ts deleted file mode 100644 index 9d56c792..00000000 --- a/packages/blue-sdk-viem/src/signatures/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TypedDataDomain } from "ethers"; -import { HashTypedDataParameters, Hex } from "viem"; - -export interface SignatureData { - domain: TypedDataDomain; - types: Record< - string, - { - name: string; - type: string; - }[] - >; - message: Record; - primaryType: string; -} - -export interface SignatureMessage { - hash: Hex; - data: HashTypedDataParameters; -} diff --git a/packages/blue-sdk-viem/src/signatures/utils.ts b/packages/blue-sdk-viem/src/signatures/utils.ts deleted file mode 100644 index 9b4c6b7a..00000000 --- a/packages/blue-sdk-viem/src/signatures/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Address, InvalidSignatureError } from "@morpho-org/blue-sdk"; - -import { Hex, Signature, recoverAddress, zeroAddress } from "viem"; - -export async function verifySignature( - signature: Signature, - hash: Hex, - signerAddress: Address, -) { - const recoveredAddress = await recoverAddress({ hash, signature }); - - if (recoveredAddress === zeroAddress || recoveredAddress !== signerAddress) - throw new InvalidSignatureError(hash, signerAddress, recoveredAddress); -} diff --git a/packages/blue-sdk/src/errors.ts b/packages/blue-sdk/src/errors.ts index 6ba5d90c..0137015e 100644 --- a/packages/blue-sdk/src/errors.ts +++ b/packages/blue-sdk/src/errors.ts @@ -76,18 +76,6 @@ export namespace BlueErrors { } } -export class InvalidSignatureError extends Error { - constructor( - public readonly hash: string, - public readonly signer: Address, - public readonly recovered: Address, - ) { - super( - `invalid signature for hash ${hash}: expected ${signer}, recovered ${recovered}`, - ); - } -} - export interface ErrorClass { new (...args: any[]): E; } diff --git a/packages/morpho-ts/src/utils.ts b/packages/morpho-ts/src/utils.ts index 7a0128b0..66fb4438 100644 --- a/packages/morpho-ts/src/utils.ts +++ b/packages/morpho-ts/src/utils.ts @@ -124,3 +124,25 @@ export const retryPromiseLinearBackoff = async ( throw Error("too many retries"); }; + +export function getLast(array: [T, ...(T | null | undefined)[]]): T; +export function getLast(array: T[]): T | undefined; +export function getLast(array: T[]) { + return array[array.length - 1]; +} + +export function filterDefined( + array: [T, ...(T | null | undefined)[]], +): [T, ...T[]]; +export function filterDefined(array: (T | null | undefined)[]): T[]; +export function filterDefined(array: T[]) { + return array.filter(isDefined); +} + +export function getLastDefined(array: [T, ...(T | null | undefined)[]]): T; +export function getLastDefined( + array: (T | null | undefined)[], +): T | undefined; +export function getLastDefined(array: T[]) { + return getLast(filterDefined(array)); +} diff --git a/yarn.lock b/yarn.lock index d22337a2..3fedc585 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3069,9 +3069,53 @@ __metadata: languageName: unknown linkType: soft -"@morpho-org/blue-sdk-simulation@workspace:packages/blue-sdk-simulation": +"@morpho-org/blue-sdk-viem-bundler@workspace:packages/blue-sdk-viem-bundler": version: 0.0.0-use.local - resolution: "@morpho-org/blue-sdk-simulation@workspace:packages/blue-sdk-simulation" + resolution: "@morpho-org/blue-sdk-viem-bundler@workspace:packages/blue-sdk-viem-bundler" + dependencies: + "@morpho-org/blue-sdk": "workspace:^" + "@morpho-org/blue-sdk-viem": "workspace:^" + "@morpho-org/blue-sdk-viem-simulation": "workspace:^" + "@morpho-org/morpho-test": "workspace:^" + "@morpho-org/morpho-ts": "workspace:^" + "@nomicfoundation/hardhat-chai-matchers": "npm:^2.0.2" + "@nomicfoundation/hardhat-ethers": "npm:^3.0.6" + "@nomicfoundation/hardhat-network-helpers": "npm:^1.0.9" + "@types/chai": "npm:^4.3.17" + "@types/jest": "npm:^29.5.12" + "@types/lodash": "npm:^4.17.7" + "@types/mocha": "npm:^10.0.6" + "@types/node": "npm:^22.2.0" + "@types/sinon": "npm:^17.0.3" + "@types/sinon-chai": "npm:^3.2.12" + chai: "npm:^4.5.0" + dotenv: "npm:^16.3.1" + ethers: "npm:^6.12.1" + ethers-types: "npm:^3.17.1" + hardhat: "npm:^2.22.10" + hardhat-deal: "npm:^3.1.0" + jest: "npm:^29.6.2" + lodash: "npm:^4.17.21" + mocha: "npm:^10.4.0" + sinon: "npm:^19.0.2" + sinon-chai: "npm:^3.7.0" + ts-jest: "npm:^29.2.4" + ts-node: "npm:^10.9.2" + typescript: "npm:^5.6.2" + viem: "npm:^2.21.15" + peerDependencies: + "@morpho-org/blue-sdk": "workspace:^" + "@morpho-org/blue-sdk-viem": "workspace:^" + "@morpho-org/blue-sdk-viem-simulation": "workspace:^" + "@morpho-org/morpho-ts": "workspace:^" + mutative: ^1.0.8 + viem: ^2.0.0 + languageName: unknown + linkType: soft + +"@morpho-org/blue-sdk-viem-simulation@workspace:^, @morpho-org/blue-sdk-viem-simulation@workspace:packages/blue-sdk-viem-simulation": + version: 0.0.0-use.local + resolution: "@morpho-org/blue-sdk-viem-simulation@workspace:packages/blue-sdk-viem-simulation" dependencies: "@morpho-org/blue-sdk": "workspace:^" "@morpho-org/blue-sdk-viem": "workspace:^" @@ -4931,6 +4975,13 @@ __metadata: languageName: node linkType: hard +"@types/lodash@npm:^4.17.7": + version: 4.17.9 + resolution: "@types/lodash@npm:4.17.9" + checksum: 10c0/54de935e835508b5f835a5dfaedd2b9a299685a21d11e9c5cd2dde57331d03bc2f98b71d2424ca8460f447ecd55a673e45ccdb70e58f9f72745710f6b91abc60 + languageName: node + linkType: hard + "@types/lru-cache@npm:^5.1.0": version: 5.1.1 resolution: "@types/lru-cache@npm:5.1.1" @@ -4977,6 +5028,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^22.2.0": + version: 22.7.3 + resolution: "@types/node@npm:22.7.3" + dependencies: + undici-types: "npm:~6.19.2" + checksum: 10c0/0e579813528b0370454337a952f43b792cd12731e10fdca0fdb627158e980c1219bba99e9048c134b6a19325d817016059afe016ccd372326c838a1b85a51574 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.1, @types/normalize-package-data@npm:^2.4.3": version: 2.4.4 resolution: "@types/normalize-package-data@npm:2.4.4" @@ -7937,7 +7997,7 @@ __metadata: languageName: node linkType: hard -"ethers-types@npm:^3.15.0, ethers-types@npm:^3.17.0": +"ethers-types@npm:^3.15.0, ethers-types@npm:^3.17.0, ethers-types@npm:^3.17.1": version: 3.17.2 resolution: "ethers-types@npm:3.17.2" peerDependencies: @@ -10267,7 +10327,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:^29.7.0": +"jest@npm:^29.6.2, jest@npm:^29.7.0": version: 29.7.0 resolution: "jest@npm:29.7.0" dependencies: @@ -14614,6 +14674,28 @@ __metadata: languageName: node linkType: hard +"viem@npm:^2.21.15": + version: 2.21.15 + resolution: "viem@npm:2.21.15" + dependencies: + "@adraffy/ens-normalize": "npm:1.10.0" + "@noble/curves": "npm:1.4.0" + "@noble/hashes": "npm:1.4.0" + "@scure/bip32": "npm:1.4.0" + "@scure/bip39": "npm:1.4.0" + abitype: "npm:1.0.5" + isows: "npm:1.0.4" + webauthn-p256: "npm:0.0.5" + ws: "npm:8.17.1" + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/72549a364daf3f983637a127c5f73421e088babd76b6283f474154c565c550d1d670300ab178dda7c33fce98eac23a596ddb07d3c4cbc6f90252b8d81e649c49 + languageName: node + linkType: hard + "viem@npm:^2.21.8": version: 2.21.8 resolution: "viem@npm:2.21.8"