diff --git a/composables/protocols/useLiquityPosition.ts b/composables/protocols/useLiquityPosition.ts index b0ced4f..1a51382 100644 --- a/composables/protocols/useLiquityPosition.ts +++ b/composables/protocols/useLiquityPosition.ts @@ -27,7 +27,7 @@ export const trove = ref({ liquidation: "0" }); -const troveTypes = ref([ +export const troveTypes = ref([ { totalCollateral: "0", price: "0", @@ -42,7 +42,7 @@ const troveTypes = ref([ } ]); -const troveOverallDetails = computed(() => +export const troveOverallDetails = computed(() => troveTypes.value.find(t => t.tokenKey === trove.value.tokenKey) ); diff --git a/composables/useStrategy.ts b/composables/useStrategy.ts index 1cb7e48..834421c 100644 --- a/composables/useStrategy.ts +++ b/composables/useStrategy.ts @@ -14,7 +14,7 @@ import { import { position as aaveV2Position } from "./protocols/useAaveV2Position"; import { position as compoundPosition } from "./protocols/useCompoundPosition"; import { vault as makerPosition } from "./protocols/useMakerdaoPosition"; -import { trove as liquityPosition } from "./protocols/useLiquityPosition"; +import { trove as liquityPosition, troveTypes, troveOverallDetails } from "./protocols/useLiquityPosition"; import { useBalances } from "./useBalances"; import { useDSA } from "./useDSA"; import useEventBus from "./useEventBus"; @@ -70,6 +70,7 @@ export function useStrategy(defineStrategy: DefineStrategy) { watchEffect(() => { let position = null; + let positionExtra = {} if (strategy.schema.protocol == StrategyProtocol.AAVE_V2) { position = aaveV2Position.value; @@ -79,6 +80,9 @@ export function useStrategy(defineStrategy: DefineStrategy) { position = compoundPosition.value; } else if (strategy.schema.protocol == StrategyProtocol.LIQUITY) { position = liquityPosition.value; + + positionExtra["troveTypes"] = troveTypes.value; + positionExtra["troveOverallDetails"] = troveOverallDetails.value; } strategy.setProps({ @@ -86,6 +90,7 @@ export function useStrategy(defineStrategy: DefineStrategy) { getTokenByKey, toBN, position, + positionExtra, tokenIdMapping }); }); diff --git a/core/strategies/helpers/index.ts b/core/strategies/helpers/index.ts index 05b6768..cb4f0bb 100644 --- a/core/strategies/helpers/index.ts +++ b/core/strategies/helpers/index.ts @@ -19,6 +19,7 @@ export interface IStrategyContext { convertTokenAmountToWei?: (value: any, decimals: any) => string; getTokenByKey?: (key: string) => IStrategyToken; position?: any; + positionExtra?: { [key: string]: any }; variables?: { [key: string]: any }; toBN?: (value: any) => BigNumber; tokenIdMapping?: typeof tokenIdMapping; @@ -30,6 +31,7 @@ export interface IStrategyToken { key: string; symbol: string; balance: string; + decimals: string; // supply: string; // borrow: string; diff --git a/core/strategies/index.ts b/core/strategies/index.ts index 38eb9ae..1cd44ac 100644 --- a/core/strategies/index.ts +++ b/core/strategies/index.ts @@ -1,9 +1,11 @@ import AaveV2 from "./protocols/aave-v2" import Compound from "./protocols/compound" +import Liquity from "./protocols/liquity" export const protocolStrategies = { aaveV2 : AaveV2, compound : Compound, + liquity : Liquity, } export * from "./helpers" \ No newline at end of file diff --git a/core/strategies/protocols/liquity/deposit-and-borrow.ts b/core/strategies/protocols/liquity/deposit-and-borrow.ts new file mode 100644 index 0000000..edb5707 --- /dev/null +++ b/core/strategies/protocols/liquity/deposit-and-borrow.ts @@ -0,0 +1,216 @@ +import BigNumber from "bignumber.js"; +import abis from "~/constant/abis"; +import addresses from "~/constant/addresses"; +import { + defineStrategy, + defineInput, + StrategyInputType, + StrategyProtocol +} from "../../helpers"; + +export default defineStrategy({ + protocol: StrategyProtocol.LIQUITY, + name: "Deposit & Borrow", + description: "Deposit collateral & borrow asset in a single txn.", + + details: `

This strategy executes:

+ `, + + submitText: "Deposit & Borrow", + + author: "Instadapp Team", + + variables: { + collateralTokenKey: "eth", + debtTokenKey: "lusd" + }, + + 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, toBN, position, positionExtra }) => { + if (!input.token) { + return "Debt token is required"; + } + + if (!input.value) { + return "Debt amount is required"; + } + + const troveOverallDetails = positionExtra["troveOverallDetails"]; + + const borrowFeeAmount = toBN(input.value) + .times(troveOverallDetails.borrowFee) + .toFixed(); + + const debtInputAmountWithFee = toBN(input.value) + .plus(borrowFeeAmount) + .toFixed(); + + const changedDebt = toBN(position.debt).plus(debtInputAmountWithFee); + + const totalDebt = toBN(changedDebt).plus( + troveOverallDetails.liquidationReserve + ); + + if (totalDebt.isZero()) + return `Minimum total debt requirement is ${troveOverallDetails.minDebt} LUSD`; + + if (totalDebt.lt(troveOverallDetails.minDebt) && totalDebt.gt("0")) { + return `Minimum total debt requirement is ${troveOverallDetails.minDebt} LUSD`; + } + }, + defaults: ({ getTokenByKey, variables }) => ({ + token: getTokenByKey?.(variables.debtTokenKey) + }) + }) + ], + + validate: async ({ position, positionExtra, inputs, toBN }) => { + if (toBN(inputs[0].value).isZero() && toBN(inputs[1].value).isZero()) { + return; + } + const troveOpened = + !toBN(position.collateral).isZero() && !toBN(position.debt).isZero(); + + if (!troveOpened) { + return "You should open new trove first"; + } + + // const troveOverallDetails = positionExtra["troveOverallDetails"]; + + // const status = + // toBN(inputs[0].value).isZero() && !toBN(inputs[1].value).isZero() + // ? toBN("1.1") + // : toBN(inputs[0].value) + // .times(position.price) + // .div(inputs[1].value); + + // console.log(status.toFixed(), troveOverallDetails.liquidation); + + // if (status.gt(toBN(troveOverallDetails.liquidation).minus("0.0001"))) { + // return "Position will liquidate."; + // } + }, + + spells: async ({ + inputs, + position, + positionExtra, + getTokenByKey, + convertTokenAmountToWei, + web3, + toBN + }) => { + const troveOverallDetails = positionExtra["troveOverallDetails"]; + const collateralToken = getTokenByKey("eth"); + const collateralInWei = convertTokenAmountToWei( + position.collateral, + collateralToken.decimals + ); + + const depositAmountInWei = convertTokenAmountToWei( + inputs[0].value, + inputs[0].token.decimals + ); + const totalDepositAmountInWei = toBN(depositAmountInWei) + .plus(collateralInWei) + .toFixed(); + + const borrowFeeAmount = toBN(inputs[1].value) + .times(troveOverallDetails.borrowFee) + .toFixed(); + const debtInputAmountWithFee = toBN(inputs[1].value) + .plus(borrowFeeAmount) + .toFixed(); + const changedDebt = toBN(position.debt) + .plus(debtInputAmountWithFee) + .toFixed(); + const borrowAmountInWei = convertTokenAmountToWei( + inputs[1].value, + inputs[1].value.decimals + ); + const totalBorrowAmountInWei = convertTokenAmountToWei( + changedDebt, + inputs[1].value.decimals + ); + + const withdrawAmountInWei = "0"; + const paybackAmountInWei = "0"; + + const liquityInstance = new web3.eth.Contract( + abis.resolver.liquity as any, + addresses.mainnet.resolver.liquity + ); + + const { + upperHint, + lowerHint + } = await liquityInstance.methods + .getTrovePositionHints( + totalDepositAmountInWei.toString(), + totalBorrowAmountInWei.toString(), + 0, + 0 + ) + .call(); + + const getIds = [0, 0, 0, 0]; + const setIds = [0, 0, 0, 0]; + + return [ + { + connector: "LIQUITY-A", + method: "adjust", + args: [ + toBN(troveOverallDetails.borrowFee) + .times("100") + .times("1e18") + .toFixed(), + depositAmountInWei, + withdrawAmountInWei, + borrowAmountInWei, + paybackAmountInWei, + upperHint, + lowerHint, + getIds, + setIds + ] + } + ]; + } +}); diff --git a/core/strategies/protocols/liquity/index.ts b/core/strategies/protocols/liquity/index.ts new file mode 100644 index 0000000..7a63aaa --- /dev/null +++ b/core/strategies/protocols/liquity/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/liquity/payback-and-withdraw.ts b/core/strategies/protocols/liquity/payback-and-withdraw.ts new file mode 100644 index 0000000..4cf0916 --- /dev/null +++ b/core/strategies/protocols/liquity/payback-and-withdraw.ts @@ -0,0 +1,194 @@ +import BigNumber from "bignumber.js"; +import abis from "~/constant/abis"; +import addresses from "~/constant/addresses"; +import { + defineStrategy, + defineInput, + StrategyInputType, + StrategyProtocol +} from "../../helpers"; + +export default defineStrategy({ + protocol: StrategyProtocol.LIQUITY, + name: "Payback & Withdraw", + description: "Payback debt & withdraw collateral in a single txn.", + author: "Instadapp Team", + + submitText: "Payback & Withdraw", + + details: `

This strategy executes:

+ `, + + inputs: [ + defineInput({ + type: StrategyInputType.INPUT_WITH_TOKEN, + name: "Debt", + placeholder: ({ input }) => + input.token ? `${input.token.symbol} to Payback` : "", + validate: ({ input, toBN, position, positionExtra }) => { + if (!input.token) { + return "Debt token is required"; + } + + if (!input.value) { + return "Debt amount is required"; + } + + const troveOverallDetails = positionExtra["troveOverallDetails"]; + + const borrowFeeAmount = toBN(input.value) + .times(troveOverallDetails.borrowFee) + .toFixed(); + + const debtInputAmountWithFee = toBN(input.value) + .plus(borrowFeeAmount) + .toFixed(); + + const changedDebt = toBN(position.debt).plus(debtInputAmountWithFee); + + const totalDebt = toBN(changedDebt).plus( + troveOverallDetails.liquidationReserve + ); + + if (totalDebt.isZero()) + return `Minimum total debt requirement is ${troveOverallDetails.minDebt} LUSD`; + + if (totalDebt.lt(troveOverallDetails.minDebt) && totalDebt.gt("0")) { + return `Minimum total debt requirement is ${troveOverallDetails.minDebt} LUSD`; + } + }, + defaults: ({ getTokenByKey }) => ({ + token: getTokenByKey?.("lusd") + }) + }), + defineInput({ + type: StrategyInputType.INPUT_WITH_TOKEN, + name: "Collateral", + placeholder: ({ input }) => + input.token ? `${input.token.symbol} to Withdraw` : "", + 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 }) => ({ + token: getTokenByKey?.("eth") + }) + }) + ], + + validate: async ({ position, inputs, toBN }) => { + if (toBN(inputs[0].value).isZero() && toBN(inputs[1].value).isZero()) { + return; + } + const troveOpened = + !toBN(position.collateral).isZero() && !toBN(position.debt).isZero(); + + if (!troveOpened) { + return "You should open new trove first"; + } + }, + + spells: async ({ + inputs, + position, + positionExtra, + getTokenByKey, + convertTokenAmountToWei, + web3, + toBN + }) => { + const troveOverallDetails = positionExtra["troveOverallDetails"]; + const collateralToken = getTokenByKey("eth"); + const collateralInWei = convertTokenAmountToWei( + position.collateral, + collateralToken.decimals + ); + + const withdrawAmountInWei = convertTokenAmountToWei( + inputs[0].value, + inputs[0].token.decimals + ); + const totalDepositAmountInWei = toBN(withdrawAmountInWei) + .plus(collateralInWei) + .toFixed(); + + const borrowFeeAmount = toBN(inputs[1].value) + .times(troveOverallDetails.borrowFee) + .toFixed(); + const debtInputAmountWithFee = toBN(inputs[1].value) + .plus(borrowFeeAmount) + .toFixed(); + const changedDebt = toBN(position.debt) + .plus(debtInputAmountWithFee) + .toFixed(); + const paybackAmountInWei = convertTokenAmountToWei( + inputs[1].value, + inputs[1].value.decimals + ); + const totalBorrowAmountInWei = convertTokenAmountToWei( + changedDebt, + inputs[1].value.decimals + ); + + const depositAmountInWei = "0"; + const borrowAmountInWei = "0"; + + const liquityInstance = new web3.eth.Contract( + abis.resolver.liquity as any, + addresses.mainnet.resolver.liquity + ); + + const { + upperHint, + lowerHint + } = await liquityInstance.methods + .getTrovePositionHints( + totalDepositAmountInWei.toString(), + totalBorrowAmountInWei.toString(), + 0, + 0 + ) + .call(); + + const getIds = [0, 0, 0, 0]; + const setIds = [0, 0, 0, 0]; + + return [ + { + connector: "LIQUITY-A", + method: "adjust", + args: [ + toBN(troveOverallDetails.borrowFee) + .times("100") + .times("1e18") + .toFixed(), + depositAmountInWei, + withdrawAmountInWei, + borrowAmountInWei, + paybackAmountInWei, + upperHint, + lowerHint, + getIds, + setIds + ] + } + ]; + } +}); diff --git a/pages/mainnet/liquity.vue b/pages/mainnet/liquity.vue index c6b0714..b945dc6 100644 --- a/pages/mainnet/liquity.vue +++ b/pages/mainnet/liquity.vue @@ -10,18 +10,43 @@ -
-
+
+
- +
+ +
+

Liquity

-

Liquity

+ + + Strategies + + + + +
@@ -150,6 +175,7 @@ import { useFormatting } from "~/composables/useFormatting"; import { useStatus } from "~/composables/useStatus"; import { useBigNumber } from "~/composables/useBigNumber"; import CardLiquityTrove from "~/components/protocols/liquity/CardLiquityTrove.vue"; +import ButtonCTAOutlined from "~/components/common/input/ButtonCTAOutlined.vue"; export default defineComponent({ components: { @@ -159,7 +185,8 @@ export default defineComponent({ SVGAdd, SVGBalance, SVGPercent, - CardLiquityTrove + CardLiquityTrove, + ButtonCTAOutlined, }, setup() { const router = useRouter();