diff --git a/projects/sdk/src/classes/Workflow.ts b/projects/sdk/src/classes/Workflow.ts index 5a5421b216..3f83045413 100644 --- a/projects/sdk/src/classes/Workflow.ts +++ b/projects/sdk/src/classes/Workflow.ts @@ -287,7 +287,7 @@ export abstract class Workflow< this.add(elem, options); // recurse } } else { - Workflow.sdk.debug(`[Workflow][${this.name}][add] ${input.name || ""}`); + if (input instanceof StepClass) { input.setSDK(Workflow.sdk); } @@ -297,16 +297,27 @@ export abstract class Workflow< switch (input.name) { case "pipelineDeposit": - pipelineOptions = { tag: `deposit${filteredOptions.length + 1}Amount` }; - Workflow.sdk.debug(`[Workflow][${this.name}][add] pipelineOptions: `, pipelineOptions); + pipelineOptions = { tag: `deposit${filteredOptions.length}Amount` }; + break; + case "pipelineUniV3Deposit": + pipelineOptions = { tag: `depositUniV3${filteredOptions.length}Amount` }; + break; + case "pipelineWellSwap": + pipelineOptions = { tag: `wellSwap${filteredOptions.length}Amount` }; + break; + case "pipelineUniswapV3Swap": + pipelineOptions = { tag: `uniswapV3Swap${filteredOptions.length}Amount` }; break; - case "pipelineBeanWethSwap": - pipelineOptions = { tag: `beanWethSwap${filteredOptions.length}Amount` }; - Workflow.sdk.debug(`[Workflow][${this.name}][add] pipelineOptions: `, pipelineOptions); + case "pipelineUniV3WellSwap": + pipelineOptions = { tag: `uniV3WellSwap${filteredOptions.length}Amount` }; + break; + case "pipelineWellSwapUniV3": + pipelineOptions = { tag: `wellSwapUniV3${filteredOptions.length}Amount` }; break; default: - Workflow.sdk.debug(`[Workflow][${this.name}][add] Not a bundle of Pipeline operations`); - } + }; + + Workflow.sdk.debug(`[Workflow][${this.name}][add] ${input.name || ""}`, pipelineOptions || ''); this._generators.push(input); this._options.push(pipelineOptions || options || null); // null = no options set diff --git a/projects/sdk/src/constants/abi/Uniswap/UniswapV3QuoterV2.json b/projects/sdk/src/constants/abi/Uniswap/UniswapV3QuoterV2.json new file mode 100644 index 0000000000..b68835015e --- /dev/null +++ b/projects/sdk/src/constants/abi/Uniswap/UniswapV3QuoterV2.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH9","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH9","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"uint256","name":"amountIn","type":"uint256"}],"name":"quoteExactInput","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint160[]","name":"sqrtPriceX96AfterList","type":"uint160[]"},{"internalType":"uint32[]","name":"initializedTicksCrossedList","type":"uint32[]"},{"internalType":"uint256","name":"gasEstimate","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct IQuoterV2.QuoteExactInputSingleParams","name":"params","type":"tuple"}],"name":"quoteExactInputSingle","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceX96After","type":"uint160"},{"internalType":"uint32","name":"initializedTicksCrossed","type":"uint32"},{"internalType":"uint256","name":"gasEstimate","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"uint256","name":"amountOut","type":"uint256"}],"name":"quoteExactOutput","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint160[]","name":"sqrtPriceX96AfterList","type":"uint160[]"},{"internalType":"uint32[]","name":"initializedTicksCrossedList","type":"uint32[]"},{"internalType":"uint256","name":"gasEstimate","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct IQuoterV2.QuoteExactOutputSingleParams","name":"params","type":"tuple"}],"name":"quoteExactOutputSingle","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceX96After","type":"uint160"},{"internalType":"uint32","name":"initializedTicksCrossed","type":"uint32"},{"internalType":"uint256","name":"gasEstimate","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"path","type":"bytes"}],"name":"uniswapV3SwapCallback","outputs":[],"stateMutability":"view","type":"function"}] diff --git a/projects/sdk/src/constants/abi/Uniswap/UniswapV3Router.json b/projects/sdk/src/constants/abi/Uniswap/UniswapV3Router.json new file mode 100644 index 0000000000..7946192319 --- /dev/null +++ b/projects/sdk/src/constants/abi/Uniswap/UniswapV3Router.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH9","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH9","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"}],"internalType":"struct ISwapRouter.ExactInputParams","name":"params","type":"tuple"}],"name":"exactInput","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct ISwapRouter.ExactInputSingleParams","name":"params","type":"tuple"}],"name":"exactInputSingle","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"}],"internalType":"struct ISwapRouter.ExactOutputParams","name":"params","type":"tuple"}],"name":"exactOutput","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct ISwapRouter.ExactOutputSingleParams","name":"params","type":"tuple"}],"name":"exactOutputSingle","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"refundETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowed","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowedIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"sweepToken","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"sweepTokenWithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"uniswapV3SwapCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"unwrapWETH9","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"unwrapWETH9WithFee","outputs":[],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}] diff --git a/projects/sdk/src/constants/addresses.ts b/projects/sdk/src/constants/addresses.ts index bcc866454c..d854dd5dd1 100644 --- a/projects/sdk/src/constants/addresses.ts +++ b/projects/sdk/src/constants/addresses.ts @@ -105,6 +105,12 @@ export const addresses = { // zap CURVE_ZAP: Address.make("0xA79828DF1850E8a3A3064576f380D90aECDD3359"), + // Uniswap V3 Router + UNISWAP_V3_ROUTER: Address.make("0xE592427A0AEce92De3Edee1F18E0157C05861564"), + + // Uniswap V3 Quoter V2 + UNISWAP_V3_QUOTER_V2: Address.make("0x61fFE014bA17989E743c5F6cB21bF9697530B21e"), + // BEAN_ETH_UNIV2_LP !! Deprecated BEAN_ETH_UNIV2_LP: Address.make("0x87898263B6C5BABe34b4ec53F22d98430b91e371"), diff --git a/projects/sdk/src/lib/contracts.ts b/projects/sdk/src/lib/contracts.ts index 0224aca07a..736d1ccc19 100644 --- a/projects/sdk/src/lib/contracts.ts +++ b/projects/sdk/src/lib/contracts.ts @@ -29,7 +29,12 @@ import { Math, Math__factory, UsdOracle, - UsdOracle__factory + UsdOracle__factory, + UniswapV3Router__factory, + UniswapV3Router, + UniswapV3QuoterV2__factory, + UniswapV3QuoterV2, + } from "src/constants/generated"; import { BaseContract } from "ethers"; @@ -65,6 +70,9 @@ export class Contracts { public readonly curve: CurveContracts; + public readonly uniswapV3Router: UniswapV3Router; + public readonly uniswapV3QuoterV2: UniswapV3QuoterV2; + // private chain: string; constructor(sdk: BeanstalkSDK) { @@ -89,6 +97,9 @@ export class Contracts { const cryptoFactoryAddress = sdk.addresses.CRYPTO_FACTORY.get(sdk.chainId); const zapAddress = sdk.addresses.CURVE_ZAP.get(sdk.chainId); + const uniswapV3RouterAddress = sdk.addresses.UNISWAP_V3_ROUTER.get(sdk.chainId); + const uniswapV3QuoterV2Address = sdk.addresses.UNISWAP_V3_QUOTER_V2.get(sdk.chainId); + // Instances this.beanstalk = Beanstalk__factory.connect(beanstalkAddress, sdk.providerOrSigner); this.beanstalkRead = Beanstalk__factory.connect(beanstalkAddress, sdk.readProvider ?? sdk.providerOrSigner); @@ -109,6 +120,9 @@ export class Contracts { const cryptoFactory = CurveCryptoFactory__factory.connect(cryptoFactoryAddress, sdk.providerOrSigner); const zap = CurveZap__factory.connect(zapAddress, sdk.providerOrSigner); + this.uniswapV3Router = UniswapV3Router__factory.connect(uniswapV3RouterAddress, sdk.providerOrSigner); + this.uniswapV3QuoterV2 = UniswapV3QuoterV2__factory.connect(uniswapV3QuoterV2Address, sdk.providerOrSigner); + this.curve = { pools: { beanCrv3, diff --git a/projects/sdk/src/lib/farm/LibraryPresets.ts b/projects/sdk/src/lib/farm/LibraryPresets.ts index ae501f24e4..6c03ae9695 100644 --- a/projects/sdk/src/lib/farm/LibraryPresets.ts +++ b/projects/sdk/src/lib/farm/LibraryPresets.ts @@ -23,8 +23,6 @@ export class LibraryPresets { public readonly weth2bean: ActionBuilder; public readonly bean2weth: ActionBuilder; public readonly weth2bean3crv: ActionBuilder; - public readonly wellWethBean; - public readonly wellAddLiquidity; public readonly usdc2bean: ActionBuilder; public readonly bean2usdc: ActionBuilder; @@ -43,6 +41,13 @@ export class LibraryPresets { public readonly usdt2beaneth; public readonly dai2beaneth; + public readonly wellSwap; + public readonly wellAddLiquidity; + public readonly uniswapV3Swap; + public readonly uniV3AddLiquidity; + public readonly uniV3WellSwap; + public readonly wellSwapUniV3; + /** * Load the Pipeline in preparation for a set Pipe actions. * @param _permit provide a permit directly, or provide a function to extract it from `context`. @@ -228,8 +233,7 @@ export class LibraryPresets { ///////// [ USDC, USDT, DAI ] -> BEANETH /////////// this.usdc2beaneth = (well: BasinWell, account: string, fromMode?: FarmFromMode, toMode?: FarmToMode) => [ - this.usdc2weth(fromMode, FarmToMode.INTERNAL) as StepGenerator, - this.wellAddLiquidity(well, sdk.tokens.WETH, account, FarmFromMode.INTERNAL, toMode) + this.uniV3AddLiquidity(well, account, sdk.tokens.USDC, sdk.tokens.WETH, 500, fromMode) ]; this.usdt2beaneth = (well: BasinWell, account: string, fromMode?: FarmFromMode, toMode?: FarmToMode) => [ @@ -238,67 +242,62 @@ export class LibraryPresets { ]; this.dai2beaneth = (well: BasinWell, account: string, fromMode?: FarmFromMode, toMode?: FarmToMode) => [ - this.dai2weth(fromMode, FarmToMode.INTERNAL) as StepGenerator, - this.wellAddLiquidity(well, sdk.tokens.WETH, account, FarmFromMode.INTERNAL, toMode) + this.uniV3AddLiquidity(well, account, sdk.tokens.DAI, sdk.tokens.WETH, 500, fromMode) ]; - this.wellWethBean = (fromToken: ERC20Token, toToken: ERC20Token, account: string, from?: FarmFromMode, to?: FarmToMode) => { - const WELL_ADDRESS = sdk.addresses.BEANWETH_WELL.get(sdk.chainId); + ///////// BEAN <> WETH /////////// + this.wellSwap = (well: BasinWell, fromToken: ERC20Token, toToken: ERC20Token, account: string, from?: FarmFromMode, to?: FarmToMode) => { const result = []; + // Set up the AdvancedPipe workflow that will call Wells via Pipeline + const advancedPipe = sdk.farm.createAdvancedPipe("pipelineWellSwap"); + // If the TO mode is INTERNAL that means this is not the last step of a swap/workflow. // We must transfer result of the swap back to User's INTERNAL balance on Beanstalk. - // This means setting the swap recipient to PIPELINE, have PIPELINE approve Beanstalk to spend - // the output token, then transfer the output token from PIPELINE's external balance to USER's internal balance + // This means setting the swap recipient to Pipeline, have Pipeline approve Beanstalk to spend + // the output token, then transfer the output token from Pipeline's external balance to User's INTERNAL balance const transferBack = to === FarmToMode.INTERNAL; - // Transfer input token to PIPELINE (via Beanstalk, so a beanstalk approval will be required, but - // that is a separate transaction, not part of this workflow) - const transfer = new sdk.farm.actions.TransferToken(fromToken.address, sdk.contracts.pipeline.address, from, FarmToMode.EXTERNAL); + // When transferBack is true, we tell Wells to send the swap result to Pipeline, otherwise + // send it directly to the User + const recipient = transferBack ? sdk.contracts.pipeline.address : account; - // This transfers the output token back to Beanstalk, from PIPELINE. Used when transferBack == true - const transferClipboard = { - tag: "swap", - copySlot: 0, - pasteSlot: 2 - } - const transferToBeanstalk = new sdk.farm.actions.TransferToken(toToken.address, account, FarmFromMode.EXTERNAL, FarmToMode.INTERNAL, transferClipboard); + // Transfer input token to Well + const transfer = new sdk.farm.actions.TransferToken(fromToken.address, well.address, from, FarmToMode.EXTERNAL); + + // Swap fromToken -> toToken on Well, send output back to recipient (either the User or Pipeline) + const swap = new sdk.farm.actions.WellShift(well.address, fromToken, toToken, recipient); // This approves the transferToBeanstalk operation. Used when transferBack == true const approveClipboard = { tag: "swap", copySlot: 0, pasteSlot: 1 - } + }; const approveBack = new sdk.farm.actions.ApproveERC20(toToken, sdk.contracts.beanstalk.address, approveClipboard); - // When transferBack is true, we tell Wells to send the swap result to PIPELINE, otherwise - // send it directly to the user - const recipient = transferBack ? sdk.contracts.pipeline.address : account; - - // Set up the AdvancedPipe workflow that will call Wells via PIPELINE - const advancedPipe = sdk.farm.createAdvancedPipe("pipelineBeanWethSwap"); - - // Approve WELL to spend PIPELINE's input token - const approve = new sdk.farm.actions.ApproveERC20(fromToken, WELL_ADDRESS); - // Swap opration executed on WELL, by PIPELINE - const swap = new sdk.farm.actions.WellSwap(WELL_ADDRESS, fromToken, toToken, recipient); + // This transfers the output token back to Beanstalk, from Pipeline. Used when transferBack == true + const transferClipboard = { + tag: "swap", + copySlot: 0, + pasteSlot: 2 + }; + const transferToBeanstalk = new sdk.farm.actions.TransferToken(toToken.address, account, FarmFromMode.EXTERNAL, FarmToMode.INTERNAL, transferClipboard); // Compose the steps - advancedPipe.add(approve); + result.push(transfer); advancedPipe.add(swap, { tag: "swap" }); if (transferBack) { advancedPipe.add(approveBack); advancedPipe.add(transferToBeanstalk); } - - result.push(transfer); result.push(advancedPipe); return result; }; + ///////// [ BEAN, WETH ] -> BEANETH /////////// this.wellAddLiquidity = (well: BasinWell, tokenIn: ERC20Token, account: string, from?: FarmFromMode, to?: FarmToMode) => { const result = []; const advancedPipe = sdk.farm.createAdvancedPipe("pipelineDeposit"); @@ -339,5 +338,202 @@ export class LibraryPresets { return result; }; + + this.uniswapV3Swap = (fromToken: ERC20Token, toToken: ERC20Token, account: string, uniswapFeeTier: number, from?: FarmFromMode, to?: FarmToMode) => { + const result = []; + const advancedPipe = sdk.farm.createAdvancedPipe("pipelineUniswapV3Swap"); + + const transferBack = to === FarmToMode.INTERNAL; + const recipient = transferBack ? sdk.contracts.pipeline.address : account; + + // Transfer fromToken to Pipeline + const transfer = new sdk.farm.actions.TransferToken(fromToken.address, sdk.contracts.pipeline.address, from, FarmToMode.EXTERNAL); + + // Approve Uniswap V3 to use fromToken + const approveUniswap = new sdk.farm.actions.ApproveERC20(fromToken, sdk.contracts.uniswapV3Router.address); + + // Swap fromToken -> toToken using Uniswap V3 + const swap = new sdk.farm.actions.UniswapV3Swap(fromToken, toToken, recipient, uniswapFeeTier); + + // This approves the transferToBeanstalk operation. + const approveClipboard = { + tag: "uniV3SwapAmount", + copySlot: 0, + pasteSlot: 1 + }; + const approveBack = new sdk.farm.actions.ApproveERC20(toToken, sdk.contracts.beanstalk.address, approveClipboard); + + // Transfers toToken back to Beanstalk, from Pipeline. + const transferClipboard = { + tag: "uniV3SwapAmount", + copySlot: 0, + pasteSlot: 2 + }; + const transferToBeanstalk = new sdk.farm.actions.TransferToken(toToken.address, account, FarmFromMode.EXTERNAL, FarmToMode.INTERNAL, transferClipboard); + + result.push(transfer); + advancedPipe.add(approveUniswap); + advancedPipe.add(swap, { tag: "uniV3SwapAmount" }); + if (transferBack) { + advancedPipe.add(approveBack); + advancedPipe.add(transferToBeanstalk); + } + + result.push(advancedPipe); + + return result; + }; + + this.uniV3AddLiquidity = (well: BasinWell, account: string, fromToken: ERC20Token, thruToken: ERC20Token, uniswapFeeTier: number, fromMode?: FarmFromMode) => { + const result = []; + const advancedPipe = sdk.farm.createAdvancedPipe("pipelineUniV3Deposit"); + + // Transfer fromToken to Pipeline + const transfer = new sdk.farm.actions.TransferToken(fromToken.address, sdk.contracts.pipeline.address, fromMode, FarmToMode.EXTERNAL); + + // Approve Uniswap V3 to use fromToken + const approveUniswap = new sdk.farm.actions.ApproveERC20(fromToken, sdk.contracts.uniswapV3Router.address); + + // Swap fromToken -> thruToken on Uniswap V3, output result to Well + const swap = new sdk.farm.actions.UniswapV3Swap(fromToken, thruToken, well.address, uniswapFeeTier); + + // Call sync on Well, send output (LP tokens) back to Pipeline + const addLiquidity = new sdk.farm.actions.WellSync(well, thruToken, sdk.contracts.pipeline.address); + + // This approves the transferToBeanstalk operation. + const approveClipboard = { + tag: "amountToDeposit", + copySlot: 0, + pasteSlot: 1 + }; + const approveBack = new sdk.farm.actions.ApproveERC20(well.lpToken, sdk.contracts.beanstalk.address, approveClipboard); + + // Transfers the output token back to Beanstalk, from Pipeline. + const transferClipboard = { + tag: "amountToDeposit", + copySlot: 0, + pasteSlot: 2 + }; + const transferToBeanstalk = new sdk.farm.actions.TransferToken(well.address, account, FarmFromMode.EXTERNAL, FarmToMode.INTERNAL, transferClipboard); + + result.push(transfer); + + advancedPipe.add(approveUniswap); + advancedPipe.add(swap); + advancedPipe.add(addLiquidity, { tag: "amountToDeposit" }); + advancedPipe.add(approveBack); + advancedPipe.add(transferToBeanstalk); + + result.push(advancedPipe); + return result; + }; + + this.uniV3WellSwap = (well: BasinWell, account: string, fromToken: ERC20Token, thruToken: ERC20Token, toToken: ERC20Token, uniswapFeeTier: number, fromMode?: FarmFromMode, toMode?: FarmToMode) => { + const result = []; + const advancedPipe = sdk.farm.createAdvancedPipe("pipelineUniV3WellSwap"); + + const transferBack = toMode === FarmToMode.INTERNAL; + const recipient = transferBack ? sdk.contracts.pipeline.address : account; + + // Transfer fromToken to Pipeline + const transfer = new sdk.farm.actions.TransferToken(fromToken.address, sdk.contracts.pipeline.address, fromMode, FarmToMode.EXTERNAL); + + // Approve Uniswap V3 to use fromToken + const approveUniswap = new sdk.farm.actions.ApproveERC20(fromToken, sdk.contracts.uniswapV3Router.address); + + // Swap fromToken -> thruToken on Uniswap V3, send output to Well + const swap = new sdk.farm.actions.UniswapV3Swap(fromToken, thruToken, well.address, uniswapFeeTier); + + // Swap thruToken -> toToken on Well, send output to recipient + const wellSwap = new sdk.farm.actions.WellShift(well.address, thruToken, toToken, recipient); + + // This approves the transferToBeanstalk operation. + const approveClipboard = { + tag: "swapOutput", + copySlot: 0, + pasteSlot: 1 + }; + const approveBack = new sdk.farm.actions.ApproveERC20(toToken, sdk.contracts.beanstalk.address, approveClipboard); + + // Transfers toToken back to Beanstalk, from Pipeline. + const transferClipboard = { + tag: "swapOutput", + copySlot: 0, + pasteSlot: 2 + }; + const transferToBeanstalk = new sdk.farm.actions.TransferToken(toToken.address, account, FarmFromMode.EXTERNAL, FarmToMode.INTERNAL, transferClipboard); + + result.push(transfer); + + advancedPipe.add(approveUniswap); + advancedPipe.add(swap); + advancedPipe.add(wellSwap, { tag: "swapOutput" }); + if (transferBack) { + advancedPipe.add(approveBack); + advancedPipe.add(transferToBeanstalk); + }; + + result.push(advancedPipe); + return result; + }; + + this.wellSwapUniV3 = (well: BasinWell, account: string, fromToken: ERC20Token, thruToken: ERC20Token, toToken: ERC20Token, uniswapFeeTier: number, fromMode?: FarmFromMode, toMode?: FarmToMode) => { + const result = []; + const advancedPipe = sdk.farm.createAdvancedPipe("pipelineWellSwapUniV3"); + + const transferBack = toMode === FarmToMode.INTERNAL; + const recipient = transferBack ? sdk.contracts.pipeline.address : account; + + // Transfer fromToken to Well + const transfer = new sdk.farm.actions.TransferToken(fromToken.address, well.address, fromMode, FarmToMode.EXTERNAL); + + // Swap fromToken -> thruToken on Well, send output back to Pipeline + const wellSwap = new sdk.farm.actions.WellShift(well.address, fromToken, thruToken, sdk.contracts.pipeline.address); + + // Approve Uniswap V3 to use thruToken + const uniApproveClipboard = { + tag: "swapOutput", + copySlot: 0, + pasteSlot: 1 + }; + const approveUniswap = new sdk.farm.actions.ApproveERC20(thruToken, sdk.contracts.uniswapV3Router.address, uniApproveClipboard); + + // Swap thruToken -> toToken on Uniswap V3, send output to recipient + const uniClipboard = { + tag: "swapOutput", + copySlot: 0, + pasteSlot: 5 + }; + const swap = new sdk.farm.actions.UniswapV3Swap(thruToken, toToken, recipient, uniswapFeeTier, undefined, uniClipboard); + + // This approves the transferToBeanstalk operation. + const transferApproveClipboard = { + tag: "uniV3Output", + copySlot: 0, + pasteSlot: 1 + }; + const approveBack = new sdk.farm.actions.ApproveERC20(toToken, sdk.contracts.beanstalk.address, transferApproveClipboard); + + // Transfers toToken back to Beanstalk, from Pipeline. + const transferClipboard = { + tag: "uniV3Output", + copySlot: 0, + pasteSlot: 2 + }; + const transferToBeanstalk = new sdk.farm.actions.TransferToken(toToken.address, account, FarmFromMode.EXTERNAL, FarmToMode.INTERNAL, transferClipboard); + + result.push(transfer); + + advancedPipe.add(wellSwap, { tag: "swapOutput" }); + advancedPipe.add(approveUniswap); + advancedPipe.add(swap, { tag: "uniV3Output" }); + if (transferBack) { + advancedPipe.add(approveBack); + advancedPipe.add(transferToBeanstalk); + }; + + result.push(advancedPipe); + return result; + }; } } diff --git a/projects/sdk/src/lib/farm/actions/Deposit.ts b/projects/sdk/src/lib/farm/actions/Deposit.ts index d6b1d41073..2bbf5803c0 100644 --- a/projects/sdk/src/lib/farm/actions/Deposit.ts +++ b/projects/sdk/src/lib/farm/actions/Deposit.ts @@ -19,15 +19,38 @@ export class Deposit extends StepClass { } async run(_amountInStep: ethers.BigNumber, context: RunContext) { - // Checking if the user isn't directly depositing BEANETH - const indirectBeanEth = this.token.symbol === "BEANETH" && context.step.index > 0; - const beanEthClipboard = { - tag: `deposit${context.step.index}Amount`, - copySlot: 6, - pasteSlot: 1 + const pipeDepositIndex = context.steps.findIndex(step => step.name === "pipelineDeposit"); + const pipeWellSwapDepositIndex = context.steps.findIndex(step => step.name === "pipelineWellSwap"); + const pipeUniV3DepositIndex = context.steps.findIndex(step => step.name === "pipelineUniV3Deposit"); + const pipeUniV3WellSwapIndex = context.steps.findIndex(step => step.name === "pipelineUniV3WellSwap"); + + if (!this.clipboard) { + if (pipeDepositIndex > 0) { + this.clipboard = { + tag: Object.keys(context.tagMap).find(tag => context.tagMap[tag] === pipeDepositIndex)!, + copySlot: 6, + pasteSlot: 1 + }; + } else if (pipeWellSwapDepositIndex > 0) { + this.clipboard = { + tag: Object.keys(context.tagMap).find(tag => context.tagMap[tag] === pipeWellSwapDepositIndex)!, + copySlot: 6, + pasteSlot: 1 + }; + } else if (pipeUniV3DepositIndex > 0) { + this.clipboard = { + tag: Object.keys(context.tagMap).find(tag => context.tagMap[tag] === pipeUniV3DepositIndex)!, + copySlot: 12, + pasteSlot: 1 + }; + } else if (pipeUniV3WellSwapIndex > 0) { + this.clipboard = { + tag: Object.keys(context.tagMap).find(tag => context.tagMap[tag] === pipeUniV3WellSwapIndex)!, + copySlot: 12, + pasteSlot: 1 + }; + }; }; - - if (indirectBeanEth && !this.clipboard) this.clipboard = beanEthClipboard; return { name: this.name, diff --git a/projects/sdk/src/lib/farm/actions/UniswapV3Swap.ts b/projects/sdk/src/lib/farm/actions/UniswapV3Swap.ts new file mode 100644 index 0000000000..b38bfec66b --- /dev/null +++ b/projects/sdk/src/lib/farm/actions/UniswapV3Swap.ts @@ -0,0 +1,146 @@ +import { BigNumberish, ethers } from "ethers"; +import { RunContext, RunMode, StepClass, Workflow } from "src/classes/Workflow"; +import { Token } from "src/classes/Token"; +import { AdvancedPipePreparedResult } from "src/lib/depot/pipe"; +import { deadlineSecondsToBlockchain } from "src/utils"; +import { TokenValue } from "@beanstalk/sdk-core"; +import { Clipboard } from "src/lib/depot"; +import { ClipboardSettings } from "src/types"; + +export class UniswapV3Swap extends StepClass { + public name: string = "uniswapV3Swap"; + private transactionDeadline: BigNumberish; + + constructor( + public readonly tokenIn: Token, + public readonly tokenOut: Token, + public readonly recipient: string, + public readonly feeTier: number, + public readonly deadline?: number, + public clipboard?: ClipboardSettings + ) { + super(); + this.transactionDeadline = deadline ? deadlineSecondsToBlockchain(deadline) : TokenValue.MAX_UINT256.toBlockchain(); + } + + async run(_amountInStep: ethers.BigNumber, context: RunContext) { + + if (!this.clipboard) { + const pipelineWellSwapIndex = context.steps.findIndex(step => step.name === "pipelineWellSwap"); + // If the action before (happens when reverse estimating) or after this one is a BEAN -> WETH swap through Pipeline... + if (pipelineWellSwapIndex >= 0 && Math.abs(pipelineWellSwapIndex - context.step.index) === 1) { + // We use clipboard... + this.clipboard = { + // Then find the correct tag in the tag map + tag: Object.keys(context.tagMap).find(tag => context.tagMap[tag] === pipelineWellSwapIndex)!, + copySlot: 6, + pasteSlot: 5 + }; + }; + }; + + const quoter = UniswapV3Swap.sdk.contracts.uniswapV3QuoterV2; + const reversed = context.runMode === RunMode.EstimateReversed; + let estimate: any; + + if (!reversed) { + estimate = await quoter.callStatic.quoteExactInputSingle({ + tokenIn: this.tokenIn.address, + tokenOut: this.tokenOut.address, + amountIn: _amountInStep, + fee: this.feeTier, + sqrtPriceLimitX96: 0, + }); + } else { + estimate = await quoter.callStatic.quoteExactOutputSingle({ + tokenIn: this.tokenIn.address, + tokenOut: this.tokenOut.address, + amount: _amountInStep, + fee: this.feeTier, + sqrtPriceLimitX96: 0, + }); + }; + + const swapFunctionName = reversed ? "exactOutputSingle" : "exactInputSingle"; + + return { + name: this.name, + amountOut: estimate[0], + prepare: () => { + + if (context.data.slippage === undefined) throw new Error("Exchange: slippage required"); + let callData; + const estimatedOutput = TokenValue.fromBlockchain(estimate[0].toString(), this.tokenOut.decimals); + const sqrtPriceX96After = estimate[1]; + + if (!reversed) { + const minAmountOut = estimatedOutput.subSlippage(context.data.slippage); + if (!minAmountOut) throw new Error("UniswapV3Swap: missing minAmountOut"); + + UniswapV3Swap.sdk.debug(`>[${this.name}.prepare()]`, { + tokenIn: this.tokenIn.symbol, + tokenOut: this.tokenOut.symbol, + fee: this.feeTier, + recipient: this.recipient, + deadline: this.transactionDeadline, + amountIn: _amountInStep, + amountOutMinimum: minAmountOut.toBlockchain().toString(), + sqrtPriceLimitX96: sqrtPriceX96After, + reversed, + method: "exactInputSingle", + context + }); + + callData = UniswapV3Swap.sdk.contracts.uniswapV3Router.interface.encodeFunctionData("exactInputSingle", [{ + tokenIn: this.tokenIn.address, + tokenOut: this.tokenOut.address, + fee: this.feeTier, + recipient: this.recipient, + deadline: this.transactionDeadline, + amountIn: _amountInStep, + amountOutMinimum: minAmountOut.toBlockchain().toString(), + sqrtPriceLimitX96: sqrtPriceX96After + }]); + + } else { + const maxAmountIn = estimatedOutput.addSlippage(context.data.slippage); + if (!maxAmountIn) throw new Error("UniswapV3Swap: missing maxAmountOut"); + + UniswapV3Swap.sdk.debug(`>[${this.name}.prepare()]`, { + tokenIn: this.tokenIn.symbol, + tokenOut: this.tokenOut.symbol, + fee: this.feeTier, + recipient: this.recipient, + deadline: this.transactionDeadline, + amountOut: _amountInStep, + amountInMaximum: maxAmountIn.toBlockchain().toString(), + sqrtPriceLimitX96: sqrtPriceX96After, + reversed, + method: "exactOutputSingle", + context + }); + + callData = UniswapV3Swap.sdk.contracts.uniswapV3Router.interface.encodeFunctionData("exactOutputSingle", [{ + tokenIn: this.tokenIn.address, + tokenOut: this.tokenOut.address, + fee: this.feeTier, + recipient: this.recipient, + deadline: this.transactionDeadline, + amountOut: _amountInStep, + amountInMaximum: maxAmountIn.toBlockchain().toString(), + sqrtPriceLimitX96: sqrtPriceX96After + }]); + }; + + return { + target: UniswapV3Swap.sdk.contracts.uniswapV3Router.address, + callData: callData, + clipboard: this.clipboard ? Clipboard.encodeSlot(context.step.findTag(this.clipboard.tag), this.clipboard.copySlot, this.clipboard.pasteSlot) : undefined + }; + }, + decode: (data: string) => UniswapV3Swap.sdk.contracts.uniswapV3Router.interface.decodeFunctionData(swapFunctionName, data), + // @ts-ignore + decodeResult: (result: string) => UniswapV3Swap.sdk.contracts.uniswapV3Router.interface.decodeFunctionResult(swapFunctionName, result) + }; + } +} diff --git a/projects/sdk/src/lib/farm/actions/UnwrapEth.ts b/projects/sdk/src/lib/farm/actions/UnwrapEth.ts index 5a740191f1..b6b7d42acd 100644 --- a/projects/sdk/src/lib/farm/actions/UnwrapEth.ts +++ b/projects/sdk/src/lib/farm/actions/UnwrapEth.ts @@ -13,14 +13,17 @@ export class UnwrapEth extends StepClass { async run(_amountInStep: ethers.BigNumber, context: RunContext) { if (!this.clipboard) { - const pipelineBeanWethSwapIndex = context.steps.findIndex(step => step.name === "pipelineBeanWethSwap"); - // If the action before (happens when reverse estimating) or after this one is a BEAN -> WETH swap through Pipeline... - if (pipelineBeanWethSwapIndex >= 0 && Math.abs(pipelineBeanWethSwapIndex - context.step.index) === 1) { + const pipelineWellSwapIndex = context.steps.findIndex(step => step.name === "pipelineWellSwap"); + const pipelineUniV3SwapIndex = context.steps.findIndex(step => step.name === "pipelineUniswapV3Swap"); + + const pipelineStepIndex = Math.max(pipelineWellSwapIndex, pipelineUniV3SwapIndex); + // If the action before or after (happens when reverse estimating) this one is a BEAN/USDC/DAI -> WETH swap through Pipeline... + if (pipelineStepIndex >= 0 && Math.abs(pipelineStepIndex - context.step.index) === 1) { // We use clipboard... this.clipboard = { // Then find the correct tag in the tag map - tag: Object.keys(context.tagMap).find(tag => context.tagMap[tag] === pipelineBeanWethSwapIndex)!, - copySlot: 9, + tag: Object.keys(context.tagMap).find(tag => context.tagMap[tag] === pipelineStepIndex)!, + copySlot: 6, pasteSlot: 0 }; }; diff --git a/projects/sdk/src/lib/farm/actions/WellShift.ts b/projects/sdk/src/lib/farm/actions/WellShift.ts index d11e009c54..c31363b3e6 100644 --- a/projects/sdk/src/lib/farm/actions/WellShift.ts +++ b/projects/sdk/src/lib/farm/actions/WellShift.ts @@ -10,7 +10,7 @@ import { AdvancedPipePreparedResult } from "src/lib/depot/pipe"; export class WellShift extends StepClass { public name: string = "shift"; - constructor(public wellAddress: string, public toToken: Token, public recipient: string) { + constructor(public wellAddress: string, public fromToken: Token, public toToken: Token, public recipient: string) { super(); } @@ -23,10 +23,21 @@ export class WellShift extends StepClass { const reversed = context.runMode === RunMode.EstimateReversed; - if (reversed) { - throw new Error("Reverse direction is not supported by shift()"); - } - const estimate = await well.shiftQuote(this.toToken); + let estimate: any; + + if (!reversed) { + estimate = await well.swapFromQuote( + this.fromToken, + this.toToken, + TokenValue.fromBlockchain(_amountInStep.toString(), this.fromToken.decimals) + ); + } else { + estimate = await well.swapToQuote( + this.fromToken, + this.toToken, + TokenValue.fromBlockchain(_amountInStep.toString(), this.toToken.decimals) + ); + }; return { name: this.name, diff --git a/projects/sdk/src/lib/farm/actions/WellSwap.ts b/projects/sdk/src/lib/farm/actions/WellSwap.ts index 562bf14b63..e871c1de04 100644 --- a/projects/sdk/src/lib/farm/actions/WellSwap.ts +++ b/projects/sdk/src/lib/farm/actions/WellSwap.ts @@ -3,13 +3,15 @@ import { BigNumberish, ethers } from "ethers"; import { Token } from "src/classes/Token"; import { RunContext, RunMode, Step, StepClass, Workflow } from "src/classes/Workflow"; import { AdvancedPipePreparedResult } from "src/lib/depot/pipe"; +import { Clipboard } from "src/lib/depot"; +import { ClipboardSettings } from "src/types"; import { deadlineSecondsToBlockchain } from "src/utils"; export class WellSwap extends StepClass { public name: string = "wellSwap"; private transactionDeadline: BigNumberish; - constructor(public wellAddress: string, public fromToken: Token, public toToken: Token, public recipient: string, deadline?: number) { + constructor(public wellAddress: string, public fromToken: Token, public toToken: Token, public recipient: string, deadline?: number, public clipboard?: ClipboardSettings) { super(); if (deadline !== null && deadline !== undefined && deadline <= 0) { throw new Error("Deadline must be greater than 0"); @@ -70,6 +72,9 @@ export class WellSwap extends StepClass { ]); } else { const maxAmountIn = estimate.addSlippage(context.data.slippage); + if (this.clipboard) { + this.clipboard.pasteSlot = 3; + }; WellSwap.sdk.debug(`>[${this.name}.prepare()]`, { well: well.name, tokenIn: this.fromToken.symbol, @@ -94,7 +99,8 @@ export class WellSwap extends StepClass { return { target: this.wellAddress, - callData: callData + callData: callData, + clipboard: this.clipboard ? Clipboard.encodeSlot(context.step.findTag(this.clipboard.tag), this.clipboard.copySlot, this.clipboard.pasteSlot) : undefined }; }, decode: (data: string) => well.contract.interface.decodeFunctionData(wellFunctionName, data), diff --git a/projects/sdk/src/lib/farm/actions/index.ts b/projects/sdk/src/lib/farm/actions/index.ts index 803bde6fed..2e4879a90d 100644 --- a/projects/sdk/src/lib/farm/actions/index.ts +++ b/projects/sdk/src/lib/farm/actions/index.ts @@ -16,7 +16,8 @@ import { ExchangeUnderlying } from "./ExchangeUnderlying"; import { RemoveLiquidityOneToken } from "./RemoveLiquidityOneToken"; import { WellSwap } from "./WellSwap"; import { WellShift } from "./WellShift"; -import { WellSync } from "./WellSync"; +import { WellSync } from "./WellSync"; +import { UniswapV3Swap } from "./UniswapV3Swap"; import { DevDebug } from "./_DevDebug"; export { @@ -51,6 +52,9 @@ export { WellShift, WellSync, + // DEX: Uniswap V3 + UniswapV3Swap, + // Developers DevDebug }; diff --git a/projects/sdk/src/lib/silo/Deposit.test.ts b/projects/sdk/src/lib/silo/Deposit.test.ts index 914092894b..4dc72dd539 100644 --- a/projects/sdk/src/lib/silo/Deposit.test.ts +++ b/projects/sdk/src/lib/silo/Deposit.test.ts @@ -23,15 +23,15 @@ const happyPaths: Record = { "BEAN:BEAN3CRV": "BEAN -> BEAN3CRV -> BEAN3CRV:SILO", "BEAN:BEANETH": "BEAN -> BEANETH -> BEANETH:SILO", - "3CRV:BEAN": "3CRV -> USDC -> WETH -> BEAN -> BEAN:SILO", // TODO: Fix this path + "3CRV:BEAN": "3CRV -> USDC -> BEAN -> BEAN:SILO", "3CRV:BEAN3CRV": "3CRV -> BEAN3CRV -> BEAN3CRV:SILO", "3CRV:BEANETH": "3CRV -> USDC -> BEANETH -> BEANETH:SILO", - "DAI:BEAN": "DAI -> WETH -> BEAN -> BEAN:SILO", + "DAI:BEAN": "DAI -> BEAN -> BEAN:SILO", "DAI:BEAN3CRV": "DAI -> 3CRV -> BEAN3CRV -> BEAN3CRV:SILO", "DAI:BEANETH": "DAI -> BEANETH -> BEANETH:SILO", - "USDC:BEAN": "USDC -> WETH -> BEAN -> BEAN:SILO", + "USDC:BEAN": "USDC -> BEAN -> BEAN:SILO", "USDC:BEAN3CRV": "USDC -> 3CRV -> BEAN3CRV -> BEAN3CRV:SILO", "USDC:BEANETH": "USDC -> BEANETH -> BEANETH:SILO", diff --git a/projects/sdk/src/lib/silo/depositGraph.ts b/projects/sdk/src/lib/silo/depositGraph.ts index 9130050db4..3d643a2ca1 100644 --- a/projects/sdk/src/lib/silo/depositGraph.ts +++ b/projects/sdk/src/lib/silo/depositGraph.ts @@ -230,37 +230,63 @@ export const getDepositGraph = (sdk: BeanstalkSDK): Graph => { }); graph.setEdge("USDC", "WETH", { - build: (_: string, from: FarmFromMode, to: FarmToMode) => sdk.farm.presets.usdc2weth(from, to), + build: (account: string, from: FarmFromMode, to: FarmToMode) => + sdk.farm.presets.uniswapV3Swap(sdk.tokens.USDC, sdk.tokens.WETH, account, 500, from, to), from: "USDC", to: "WETH", - label: "exchange" + label: "uniswapV3Swap" }); graph.setEdge("DAI", "WETH", { - build: (_: string, from: FarmFromMode, to: FarmToMode) => sdk.farm.presets.dai2weth(from, to), + build: (account: string, from: FarmFromMode, to: FarmToMode) => + sdk.farm.presets.uniswapV3Swap(sdk.tokens.DAI, sdk.tokens.WETH, account, 500, from, to), from: "DAI", to: "WETH", - label: "exchange" + label: "uniswapV3Swap" + }); + } + + + /** + * [ USDC, DAI ] => BEAN + */ + { + const well = sdk.pools.BEAN_ETH_WELL; + graph.setEdge("USDC", "BEAN", { + build: (account: string, from: FarmFromMode, to: FarmToMode) => + sdk.farm.presets.uniV3WellSwap(well, account, sdk.tokens.USDC, sdk.tokens.WETH, sdk.tokens.BEAN, 500, from, to), + from: "USDC", + to: "BEAN", + label: "uniV3WellSwap" + }); + + graph.setEdge("DAI", "BEAN", { + build: (account: string, from: FarmFromMode, to: FarmToMode) => + sdk.farm.presets.uniV3WellSwap(well, account, sdk.tokens.DAI, sdk.tokens.WETH, sdk.tokens.BEAN, 500, from, to), + from: "DAI", + to: "BEAN", + label: "uniV3WellSwap" }); } /** - * Well Swap: WETH => BEAN + * Well Swap: WETH <> BEAN */ { + const well = sdk.pools.BEAN_ETH_WELL; graph.setEdge("WETH", "BEAN", { build: (account: string, from: FarmFromMode, to: FarmToMode) => - sdk.farm.presets.wellWethBean(sdk.tokens.WETH, sdk.tokens.BEAN, account, from, to), + sdk.farm.presets.wellSwap(well, sdk.tokens.WETH, sdk.tokens.BEAN, account, from, to), from: "WETH", to: "BEAN", - label: "wellWethBean" + label: "wellSwap" }); graph.setEdge("BEAN", "WETH", { build: (account: string, from: FarmFromMode, to: FarmToMode) => - sdk.farm.presets.wellWethBean(sdk.tokens.BEAN, sdk.tokens.WETH, account, from, to), + sdk.farm.presets.wellSwap(well, sdk.tokens.BEAN, sdk.tokens.WETH, account, from, to), from: "BEAN", to: "WETH", - label: "wellWethBean" + label: "wellSwap" }); } diff --git a/projects/sdk/src/lib/swap/Swap.ts b/projects/sdk/src/lib/swap/Swap.ts index 1fc51620ae..80ab7d8f66 100644 --- a/projects/sdk/src/lib/swap/Swap.ts +++ b/projects/sdk/src/lib/swap/Swap.ts @@ -29,26 +29,10 @@ export class Swap { } public buildSwap(tokenIn: Token, tokenOut: Token, account: string, _from?: FarmFromMode, _to?: FarmToMode) { + const route = this.router.getRoute(tokenIn.symbol, tokenOut.symbol); + const workflow = Swap.sdk.farm.createAdvancedFarm(`Swap ${tokenIn.symbol}->${tokenOut.symbol}`); - let workflow; - let useAdvancedFarm = false; - - for (let i = 0; i < route.length; i++) { - if (route.getStep(i - 1)) { - if (route.getStep(i - 1).from === "BEAN" && route.getStep(i - 1).to === "WETH" && - route.getStep(i).from === "WETH" && route.getStep(i).to === "ETH") { - useAdvancedFarm = true; - break; - }; - }; - }; - - if (useAdvancedFarm) { - workflow = Swap.sdk.farm.createAdvancedFarm(`Swap ${tokenIn.symbol}->${tokenOut.symbol}`); - } else { - workflow = Swap.sdk.farm.create(`Swap ${tokenIn.symbol}->${tokenOut.symbol}`); - } // Handle Farm Modes // For a single step swap (ex, ETH > WETH, or BEAN > BEAN), use the passed modes, if available if (route.length === 1) { diff --git a/projects/sdk/src/lib/swap/graph.ts b/projects/sdk/src/lib/swap/graph.ts index 736db459eb..58f9cf7acd 100644 --- a/projects/sdk/src/lib/swap/graph.ts +++ b/projects/sdk/src/lib/swap/graph.ts @@ -112,18 +112,78 @@ export const getSwapGraph = (sdk: BeanstalkSDK): Graph => { // BEAN<>WETH via Basin Well graph.setEdge("BEAN", "WETH", { build: (account: string, from: FarmFromMode, to: FarmToMode) => - sdk.farm.presets.wellWethBean(sdk.tokens.BEAN, sdk.tokens.WETH, account, from, to), + sdk.farm.presets.wellSwap(sdk.pools.BEAN_ETH_WELL, sdk.tokens.BEAN, sdk.tokens.WETH, account, from, to), from: "BEAN", to: "WETH" }); graph.setEdge("WETH", "BEAN", { build: (account: string, from: FarmFromMode, to: FarmToMode) => - sdk.farm.presets.wellWethBean(sdk.tokens.WETH, sdk.tokens.BEAN, account, from, to), + sdk.farm.presets.wellSwap(sdk.pools.BEAN_ETH_WELL, sdk.tokens.WETH, sdk.tokens.BEAN, account, from, to), from: "WETH", to: "BEAN" }); + // USDC<>WETH via Uniswap V3 + graph.setEdge("USDC", "WETH", { + build: (account: string, from: FarmFromMode, to: FarmToMode) => + sdk.farm.presets.uniswapV3Swap(sdk.tokens.USDC, sdk.tokens.WETH, account, 500, from, to), + from: "USDC", + to: "WETH" + }); + + graph.setEdge("WETH", "USDC", { + build: (account: string, from: FarmFromMode, to: FarmToMode) => + sdk.farm.presets.uniswapV3Swap(sdk.tokens.WETH, sdk.tokens.USDC, account, 500, from, to), + from: "WETH", + to: "USDC" + }); + + // DAI<>WETH via Uniswap V3 + graph.setEdge("DAI", "WETH", { + build: (account: string, from: FarmFromMode, to: FarmToMode) => + sdk.farm.presets.uniswapV3Swap(sdk.tokens.DAI, sdk.tokens.WETH, account, 500, from, to), + from: "DAI", + to: "WETH" + }); + + graph.setEdge("WETH", "DAI", { + build: (account: string, from: FarmFromMode, to: FarmToMode) => + sdk.farm.presets.uniswapV3Swap(sdk.tokens.WETH, sdk.tokens.DAI, account, 500, from, to), + from: "WETH", + to: "DAI" + }); + + //BEAN<>USDC via Pipeline + graph.setEdge("USDC", "BEAN", { + build: (account: string, from: FarmFromMode, to: FarmToMode) => + sdk.farm.presets.uniV3WellSwap(sdk.pools.BEAN_ETH_WELL, account, sdk.tokens.USDC, sdk.tokens.WETH, sdk.tokens.BEAN, 500, from, to), + from: "USDC", + to: "BEAN" + }); + + graph.setEdge("BEAN", "USDC", { + build: (account: string, from: FarmFromMode, to: FarmToMode) => + sdk.farm.presets.wellSwapUniV3(sdk.pools.BEAN_ETH_WELL, account, sdk.tokens.BEAN, sdk.tokens.WETH, sdk.tokens.USDC, 500, from, to), + from: "BEAN", + to: "USDC" + }); + + //BEAN<>DAI via Pipeline + graph.setEdge("DAI", "BEAN", { + build: (account: string, from: FarmFromMode, to: FarmToMode) => + sdk.farm.presets.uniV3WellSwap(sdk.pools.BEAN_ETH_WELL, account, sdk.tokens.DAI, sdk.tokens.WETH, sdk.tokens.BEAN, 500, from, to), + from: "DAI", + to: "BEAN" + }); + + graph.setEdge("BEAN", "DAI", { + build: (account: string, from: FarmFromMode, to: FarmToMode) => + sdk.farm.presets.wellSwapUniV3(sdk.pools.BEAN_ETH_WELL, account, sdk.tokens.BEAN, sdk.tokens.WETH, sdk.tokens.DAI, 500, from, to), + from: "BEAN", + to: "DAI" + }); + /// 3CRV<>Stables via 3Pool Add/Remove Liquidity // HEADS UP: the ordering of these tokens needs to match their indexing in the 3CRV LP token. diff --git a/projects/ui/src/components/Common/Form/TokenInputField.tsx b/projects/ui/src/components/Common/Form/TokenInputField.tsx index 1afd1280ed..fc2af426c6 100644 --- a/projects/ui/src/components/Common/Form/TokenInputField.tsx +++ b/projects/ui/src/components/Common/Form/TokenInputField.tsx @@ -252,7 +252,7 @@ const TokenInput: FC = ({ } } }, - [form, field.name, field.value, onChange, clamp] + [form, field.name, field.value, onChange, clamp, balance] ); // diff --git a/projects/ui/src/components/Silo/Actions/Deposit.tsx b/projects/ui/src/components/Silo/Actions/Deposit.tsx index 9f64e8b3d0..64f08f9d1e 100644 --- a/projects/ui/src/components/Silo/Actions/Deposit.tsx +++ b/projects/ui/src/components/Silo/Actions/Deposit.tsx @@ -487,7 +487,7 @@ const DepositPropProvider: FC<{ throw new Error('Only one token supported at this time'); } - const { BEAN, BEAN_ETH_WELL_LP } = sdk.tokens; + const { BEAN } = sdk.tokens; const formData = values.tokens[0]; const claimData = values.claimableBeans; @@ -542,8 +542,8 @@ const DepositPropProvider: FC<{ depositTxn, amountIn, values.settings.slippage, - target.equals(BEAN_ETH_WELL_LP) ? 1.2 : undefined, - tokenIn.symbol !== "BEANETH" && target.equals(BEAN_ETH_WELL_LP) + 1.2, + true ); const txn = await execute();