From 52ebdbfb3f51feffcdf014b73a38e4991cbf599f Mon Sep 17 00:00:00 2001 From: Georges KABBOUCHI Date: Sat, 4 Sep 2021 20:39:05 +0300 Subject: [PATCH] basic compound strategies --- composables/protocols/useCompoundPosition.ts | 4 + composables/protocols/useLiquityPosition.ts | 5 + composables/protocols/useMakerdaoPosition.ts | 4 + composables/useStrategy.ts | 4 +- core/strategies/helpers/index.ts | 8 + core/strategies/index.ts | 2 + .../protocols/compound/deposit-and-borrow.ts | 182 +++++++++++++++++ core/strategies/protocols/compound/index.ts | 8 + .../compound/payback-and-withdraw.ts | 186 ++++++++++++++++++ pages/mainnet/compound.vue | 45 ++++- 10 files changed, 438 insertions(+), 10 deletions(-) create mode 100644 core/strategies/protocols/compound/deposit-and-borrow.ts create mode 100644 core/strategies/protocols/compound/index.ts create mode 100644 core/strategies/protocols/compound/payback-and-withdraw.ts diff --git a/composables/protocols/useCompoundPosition.ts b/composables/protocols/useCompoundPosition.ts index 0578618..ece0239 100644 --- a/composables/protocols/useCompoundPosition.ts +++ b/composables/protocols/useCompoundPosition.ts @@ -14,6 +14,7 @@ import addresses from "~/constant/addresses"; import ctokens from "~/constant/ctokens"; import tokenIdMapping from "~/constant/tokenIdMapping"; import { useSorting } from "~/composables/useSorting"; +import useEventBus from "../useEventBus"; const { times, @@ -62,6 +63,7 @@ export function useCompoundPosition( ) { overridePosition = overridePosition || (pos => pos); + const { onEvent } = useEventBus() const { web3, networkName } = useWeb3(); const { activeAccount } = useDSA(); const { getTokenByKey } = useToken(); @@ -100,6 +102,8 @@ export function useCompoundPosition( position.value = await fetchPosition(); }; + onEvent("protocol::compound::refresh", refreshPosition); + watch( web3, async val => { diff --git a/composables/protocols/useLiquityPosition.ts b/composables/protocols/useLiquityPosition.ts index 9bc529f..b0ced4f 100644 --- a/composables/protocols/useLiquityPosition.ts +++ b/composables/protocols/useLiquityPosition.ts @@ -9,6 +9,7 @@ BigNumber.config({ POW_PRECISION: 200 }); import abis from "~/constant/abis"; import addresses from "~/constant/addresses"; import { useDSA } from "../useDSA"; +import useEventBus from "../useEventBus"; export const trove = ref({ collateral: "0", @@ -49,6 +50,7 @@ export function useLiquityPosition( collateralAmountRef: Ref = null, debtAmountRef: Ref = null ) { + const { onEvent } = useEventBus() const { web3 } = useWeb3(); const { activeAccount } = useDSA(); @@ -178,6 +180,9 @@ export function useLiquityPosition( } } + onEvent("protocol::liquity::refresh", fetchPosition); + + watch( web3, async val => { diff --git a/composables/protocols/useMakerdaoPosition.ts b/composables/protocols/useMakerdaoPosition.ts index 9ec5910..aad5397 100644 --- a/composables/protocols/useMakerdaoPosition.ts +++ b/composables/protocols/useMakerdaoPosition.ts @@ -9,6 +9,7 @@ import { useDSA } from "~/composables/useDSA"; import { useToken } from "~/composables/useToken"; import { useWeb3 } from "~/composables/useWeb3"; import { AbiItem } from "web3-utils"; +import useEventBus from "../useEventBus"; const defaultVault = { id: null, @@ -54,6 +55,7 @@ export function useMakerdaoPosition( collateralAmountRef: Ref = null, debtAmountRef: Ref = null ) { + const { onEvent } = useEventBus() const { web3, chainId, networkName } = useWeb3(); const { activeAccount } = useDSA(); const { isZero, ensureValue, times, div, max, gt } = useBigNumber(); @@ -132,6 +134,8 @@ export function useMakerdaoPosition( } }; + onEvent("protocol::makerdao::refresh", fetchPosition); + watch( web3, async val => { diff --git a/composables/useStrategy.ts b/composables/useStrategy.ts index a87be44..1cb7e48 100644 --- a/composables/useStrategy.ts +++ b/composables/useStrategy.ts @@ -23,6 +23,7 @@ import { useSidebar } from "./useSidebar"; import { useToken } from "./useToken"; import { useWeb3 } from "./useWeb3"; import { useBigNumber } from "./useBigNumber"; +import tokenIdMapping from "~/constant/tokenIdMapping"; export function useStrategy(defineStrategy: DefineStrategy) { const { web3, networkName, account } = useWeb3(); @@ -84,7 +85,8 @@ export function useStrategy(defineStrategy: DefineStrategy) { convertTokenAmountToWei: valInt, getTokenByKey, toBN, - position + position, + tokenIdMapping }); }); diff --git a/core/strategies/helpers/index.ts b/core/strategies/helpers/index.ts index 06ecb27..05b6768 100644 --- a/core/strategies/helpers/index.ts +++ b/core/strategies/helpers/index.ts @@ -3,10 +3,16 @@ import Web3 from "web3"; import slugify from "slugify"; import { Strategy } from "./strategy"; import BigNumber from "bignumber.js"; +import tokenIdMapping from "~/constant/tokenIdMapping"; + export interface IStrategyContext { dsa: DSA; web3: Web3; inputs: IStrategyInput[]; + + + + // TODO: add types in useStrategy.ts dsaBalances?: { [address: string]: IStrategyToken }; userBalances?: { [address: string]: IStrategyToken }; tokens?: { [address: string]: IStrategyToken }; @@ -15,6 +21,8 @@ export interface IStrategyContext { position?: any; variables?: { [key: string]: any }; toBN?: (value: any) => BigNumber; + tokenIdMapping?: typeof tokenIdMapping; + } export interface IStrategyToken { diff --git a/core/strategies/index.ts b/core/strategies/index.ts index fdc6272..38eb9ae 100644 --- a/core/strategies/index.ts +++ b/core/strategies/index.ts @@ -1,7 +1,9 @@ import AaveV2 from "./protocols/aave-v2" +import Compound from "./protocols/compound" export const protocolStrategies = { aaveV2 : AaveV2, + compound : Compound, } export * from "./helpers" \ No newline at end of file diff --git a/core/strategies/protocols/compound/deposit-and-borrow.ts b/core/strategies/protocols/compound/deposit-and-borrow.ts new file mode 100644 index 0000000..f24cbc6 --- /dev/null +++ b/core/strategies/protocols/compound/deposit-and-borrow.ts @@ -0,0 +1,182 @@ +import BigNumber from "bignumber.js"; +import { + defineStrategy, + defineInput, + StrategyInputType, + StrategyProtocol +} from "../../helpers"; + +export default defineStrategy({ + protocol: StrategyProtocol.COMPOUND, + name: "Deposit & Borrow", + description: "Deposit collateral & borrow asset in a single txn.", + + details: `

This strategy executes:

+
    +
  • Deposit collateral
  • +
  • Borrow Debt
  • +
`, + + submitText: "Deposit & Borrow", + + author: "Instadapp Team", + + variables: { + collateralTokenKey: "eth", + debtTokenKey: "dai" + }, + + inputs: [ + defineInput({ + type: StrategyInputType.INPUT_WITH_TOKEN, + name: "Collateral", + placeholder: ({ input }) => + input.token ? `${input.token.symbol} to Deposit` : "", + validate: ({ input, dsaBalances, toBN }) => { + if (!input.token) { + return "Collateral token is required"; + } + + if (!input.value) { + return "Collateral amount is required"; + } + + const collateralBalance = toBN( + dsaBalances[input.token.address]?.balance + ); + + if (toBN(collateralBalance).lt(input.value)) { + const collateralBalanceFormatted = collateralBalance.toFixed(2); + return `Your amount exceeds your maximum limit of ${collateralBalanceFormatted} ${input.token.symbol}`; + } + }, + defaults: ({ getTokenByKey, variables }) => ({ + token: getTokenByKey?.(variables.collateralTokenKey) + }) + }), + + defineInput({ + type: StrategyInputType.INPUT_WITH_TOKEN, + name: "Debt", + placeholder: ({ input }) => + input.token ? `${input.token.symbol} to Borrow` : "", + validate: ({ input }) => { + if (!input.token) { + return "Debt token is required"; + } + + if (!input.value) { + return "Debt amount is required"; + } + }, + defaults: ({ getTokenByKey, variables }) => ({ + token: getTokenByKey?.(variables.debtTokenKey) + }) + }) + ], + + validate: async ({ position, inputs, toBN, tokenIdMapping }) => { + if (toBN(inputs[0].value).isZero() && toBN(inputs[1].value).isZero()) { + return; + } + const { tokenToId } = tokenIdMapping; + + const newPositionData = position.data.map(position => { + const changedPosition = { ...position }; + if (tokenToId.compound[inputs[1].token.key] === position.cTokenId) { + changedPosition.borrow = BigNumber.max( + toBN(position.borrow).plus(inputs[1].value), + "0" + ).toFixed(); + } + + if (tokenToId.compound[inputs[0].token.key] === position.cTokenId) { + changedPosition.supply = BigNumber.max( + toBN(position.supply).plus(inputs[0].value), + "0" + ).toFixed(); + } + + return changedPosition; + }); + + const stats = newPositionData.reduce( + (stats, { key, supply, borrow, priceInEth, factor }) => { + if (key === "eth") { + stats.ethSupplied = supply; + } + + + stats.totalSupplyInEth = toBN(supply) + .times(priceInEth) + .plus(stats.totalSupplyInEth) + .toFixed(); + stats.totalBorrowInEth = toBN(borrow) + .times(priceInEth) + .plus(stats.totalBorrowInEth) + .toFixed(); + + stats.totalMaxBorrowLimitInEth = toBN(priceInEth) + .times(factor) + .times(supply) + .plus(stats.totalMaxBorrowLimitInEth) + .toFixed(); + + return stats; + }, + { + totalSupplyInEth: "0", + totalBorrowInEth: "0", + totalMaxBorrowLimitInEth: "0", + ethSupplied: "0" + } + ); + + let liquidation = "0"; + if (!toBN(stats.totalSupplyInEth).isZero()) { + liquidation = BigNumber.max( + toBN(stats.totalMaxBorrowLimitInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + } + + const status = BigNumber.max( + toBN(stats.totalBorrowInEth).div(stats.totalSupplyInEth), + "0" + ); + + if (status.gt(toBN(liquidation).minus("0.0001"))) { + return "Position will liquidate."; + } + }, + + spells: async ({ inputs, convertTokenAmountToWei, tokenIdMapping }) => { + const { tokenToId } = tokenIdMapping; + + const collateralTokenId = tokenToId.compound[inputs[0].token.key]; + const debtTokenId = tokenToId.compound[inputs[1].token.key]; + + return [ + { + connector: "compound", + method: "deposit", + args: [ + collateralTokenId, + convertTokenAmountToWei(inputs[0].value, inputs[0].token.decimals), + 0, + 0 + ] + }, + { + connector: "compound", + method: "borrow", + args: [ + debtTokenId, + convertTokenAmountToWei(inputs[1].value, inputs[1].token.decimals), + 0, + 0 + ] + } + ]; + } +}); diff --git a/core/strategies/protocols/compound/index.ts b/core/strategies/protocols/compound/index.ts new file mode 100644 index 0000000..7a63aaa --- /dev/null +++ b/core/strategies/protocols/compound/index.ts @@ -0,0 +1,8 @@ +import depositAndBorrow from "./deposit-and-borrow" +import paybackAndWithdraw from "./payback-and-withdraw" + +export default [ + depositAndBorrow, + paybackAndWithdraw, +] + \ No newline at end of file diff --git a/core/strategies/protocols/compound/payback-and-withdraw.ts b/core/strategies/protocols/compound/payback-and-withdraw.ts new file mode 100644 index 0000000..8defa7f --- /dev/null +++ b/core/strategies/protocols/compound/payback-and-withdraw.ts @@ -0,0 +1,186 @@ +import BigNumber from "bignumber.js"; +import { + defineStrategy, + defineInput, + StrategyInputType, + StrategyProtocol +} from "../../helpers"; + +export default defineStrategy({ + protocol: StrategyProtocol.COMPOUND, + name: "Payback & Withdraw", + description: "Payback debt & withdraw collateral in a single txn.", + author: "Instadapp Team", + + submitText: "Payback & Withdraw", + + details: `

This strategy executes:

+
    +
  • Payback debt
  • +
  • Withdraw collateral
  • +
`, + + inputs: [ + defineInput({ + type: StrategyInputType.INPUT_WITH_TOKEN, + name: "Debt", + placeholder: ({ input }) => + input.token ? `${input.token.symbol} to Payback` : "", + validate: ({ input, toBN, dsaBalances }) => { + if (!input.token) { + return "Debt token is required"; + } + + const balance = toBN(dsaBalances[input.token.address]?.balance); + + if (toBN(balance).lt(input.value)) { + return "You don't have enough balance to payback."; + } + }, + defaults: ({ getTokenByKey }) => ({ + token: getTokenByKey?.("dai") + }) + }), + defineInput({ + type: StrategyInputType.INPUT_WITH_TOKEN, + name: "Collateral", + placeholder: ({ input }) => + input.token ? `${input.token.symbol} to Withdraw` : "", + validate: ({ input, position, toBN, tokenIdMapping }) => { + if (!input.token) { + return "Collateral token is required"; + } + + if (!input.value) { + return "Collateral amount is required"; + } + + const { tokenToId } = tokenIdMapping; + + if (position) { + const collateralBalance = toBN( + position.data.find( + pos => pos.cTokenId === tokenToId.compound[input.token.key] + )?.supply || "0" + ); + + if (collateralBalance.lt(input.value)) { + const collateralBalanceFormatted = collateralBalance.toFixed( + 2, + BigNumber.ROUND_FLOOR + ); + return `Your amount exceeds your maximum limit of ${collateralBalanceFormatted} ${input.token.symbol}`; + } + } + }, + defaults: ({ getTokenByKey }) => ({ + token: getTokenByKey?.("eth") + }) + }) + ], + + validate: async ({ position, inputs, toBN, tokenIdMapping }) => { + if (toBN(inputs[0].value).isZero() && toBN(inputs[1].value).isZero()) { + return; + } + const { tokenToId } = tokenIdMapping; + + const newPositionData = position.data.map(position => { + const changedPosition = { ...position }; + if (tokenToId.compound[inputs[0].token.key] === position.cTokenId) { + changedPosition.borrow = BigNumber.max( + toBN(position.borrow).minus(inputs[0].value), + "0" + ).toFixed(); + } + + if (tokenToId.compound[inputs[1].token.key] === position.cTokenId) { + changedPosition.supply = BigNumber.max( + toBN(position.supply).minus(inputs[1].value), + "0" + ).toFixed(); + } + + return changedPosition; + }); + + const stats = newPositionData.reduce( + (stats, { key, supply, borrow, priceInEth, factor }) => { + if (key === "eth") { + stats.ethSupplied = supply; + } + + stats.totalSupplyInEth = toBN(supply) + .times(priceInEth) + .plus(stats.totalSupplyInEth) + .toFixed(); + stats.totalBorrowInEth = toBN(borrow) + .times(priceInEth) + .plus(stats.totalBorrowInEth) + .toFixed(); + + stats.totalMaxBorrowLimitInEth = toBN(priceInEth) + .times(factor) + .times(supply) + .plus(stats.totalMaxBorrowLimitInEth) + .toFixed(); + + return stats; + }, + { + totalSupplyInEth: "0", + totalBorrowInEth: "0", + totalMaxBorrowLimitInEth: "0", + ethSupplied: '0', + } + ); + + let liquidation = "0"; + + if (!toBN(stats.totalSupplyInEth).isZero()) { + liquidation = BigNumber.max( + toBN(stats.totalMaxBorrowLimitInEth).div(stats.totalSupplyInEth), + "0" + ).toFixed(); + } + + const status = BigNumber.max( + toBN(stats.totalBorrowInEth).div(stats.totalSupplyInEth), + "0" + ); + + if (status.gt(toBN(liquidation).minus("0.0001"))) { + return "Position will liquidate."; + } + }, + + spells: async ({ inputs, convertTokenAmountToWei, tokenIdMapping }) => { + const { tokenToId } = tokenIdMapping; + + const debtTokenId = tokenToId.compound[inputs[0].token.key]; + const collateralTokenId = tokenToId.compound[inputs[1].token.key]; + + return [ + { + connector: "compound", + method: "payback", + args: [ + debtTokenId, + convertTokenAmountToWei(inputs[0].value, inputs[0].token.decimals), + 0, + 0 + ] + }, + { + connector: "compound", + method: "withdraw", + args: [ + collateralTokenId, + convertTokenAmountToWei(inputs[1].value, inputs[1].token.decimals), + 0, + 0 + ] + } + ]; + } +}); diff --git a/pages/mainnet/compound.vue b/pages/mainnet/compound.vue index d4e8f2c..a8d6372 100644 --- a/pages/mainnet/compound.vue +++ b/pages/mainnet/compound.vue @@ -10,18 +10,43 @@ -
-
+
+
- +
+ +
+

Compound

-

Compound

+ + + Strategies + + + + +
@@ -163,13 +188,15 @@ import { useSearchFilter } from "~/composables/useSearchFilter"; import { useStatus } from "~/composables/useStatus"; import { useBigNumber } from "~/composables/useBigNumber"; import CardCompound from "~/components/protocols/CardCompound.vue"; -import CompoundIcon from '~/assets/icons/compound.svg?inline' +import CompoundIcon from "~/assets/icons/compound.svg?inline"; +import ButtonCTAOutlined from "~/components/common/input/ButtonCTAOutlined.vue"; export default defineComponent({ components: { BackIcon, CardCompound, CompoundIcon, + ButtonCTAOutlined, }, setup() { const {