diff --git a/core/src/abis/MCD_JOIN.json b/core/src/abis/MCD_JOIN.json new file mode 100644 index 000000000..a6c854301 --- /dev/null +++ b/core/src/abis/MCD_JOIN.json @@ -0,0 +1,236 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "vat_", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "ilk_", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "gem_", + "type": "address" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": true, + "inputs": [ + { + "indexed": true, + "internalType": "bytes4", + "name": "sig", + "type": "bytes4" + }, + { + "indexed": true, + "internalType": "address", + "name": "usr", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "arg1", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "arg2", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "LogNote", + "type": "event" + }, + { + "constant": false, + "inputs": [], + "name": "cage", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "dec", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "usr", + "type": "address" + } + ], + "name": "deny", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "usr", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "exit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "gem", + "outputs": [ + { + "internalType": "contract GemLike", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "ilk", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "usr", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "join", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "live", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "usr", + "type": "address" + } + ], + "name": "rely", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "vat", + "outputs": [ + { + "internalType": "contract VatLike", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "wards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] diff --git a/core/src/auctions.ts b/core/src/auctions.ts index f20f381a8..93c99bb53 100644 --- a/core/src/auctions.ts +++ b/core/src/auctions.ts @@ -11,6 +11,7 @@ import { calculateAuctionPrice, calculateTransactionGrossProfit, calculateTransactionGrossProfitDate, + calculateTransactionCollateralOutcome, } from './price'; import { getSupportedCollateralTypes } from './addresses'; import getContract, { getClipperNameByCollateralType } from './contracts'; @@ -181,14 +182,14 @@ export const restartAuction = async function ( export const bidWithDai = async function ( network: string, - auction: Auction, + auction: AuctionTransaction, bidAmountDai: BigNumber, profitAddress: string, notifier?: Notifier ): Promise { const contractName = getClipperNameByCollateralType(auction.collateralType); const updatedAuction = await enrichAuctionWithActualNumbers(network, auction); - const collateralAmount = bidAmountDai.dividedBy(updatedAuction.unitPrice); + const collateralAmount = calculateTransactionCollateralOutcome(bidAmountDai, updatedAuction.unitPrice, auction); const contractParameters = [ convertNumberTo32Bytes(auction.index), collateralAmount.shiftedBy(WAD_NUMBER_OF_DIGITS).toFixed(0), diff --git a/core/src/contracts.ts b/core/src/contracts.ts index 756cfa13e..f510b0560 100644 --- a/core/src/contracts.ts +++ b/core/src/contracts.ts @@ -5,6 +5,7 @@ import { fetchContractAddressByNetwork } from './addresses'; import MCD_DAI from './abis/MCD_DAI.json'; import MCD_VAT from './abis/MCD_VAT.json'; import MCD_JOIN_DAI from './abis/MCD_JOIN_DAI.json'; +import MCD_JOIN from './abis/MCD_JOIN.json'; import MCD_CLIP_CALC from './abis/MCD_CLIP_CALC.json'; import MCD_CLIP from './abis/MCD_CLIP.json'; import MCD_DOG from './abis/MCD_DOG.json'; @@ -41,6 +42,9 @@ const getContractInterfaceByName = async function (contractName: string): Promis if (contractName === 'MCD_JOIN_DAI') { return MCD_JOIN_DAI; } + if (contractName.startsWith('MCD_JOIN_')) { + return MCD_JOIN; + } if (contractName.startsWith('MCD_CLIP_CALC_')) { return MCD_CLIP_CALC; } diff --git a/core/src/fetch.ts b/core/src/fetch.ts index 59e593826..7d8fe5823 100644 --- a/core/src/fetch.ts +++ b/core/src/fetch.ts @@ -1,9 +1,8 @@ import type { AuctionInitialInfo, AuctionStatus } from './types'; -import { ethers } from 'ethers'; import memoizee from 'memoizee'; import BigNumber from './bignumber'; import getContract, { getClipperNameByCollateralType } from './contracts'; -import { RAD, RAY, WAD } from './constants/UNITS'; +import { RAD, RAD_NUMBER_OF_DIGITS, RAY, WAD } from './constants/UNITS'; import { getCollateralConfigByType } from './constants/COLLATERALS'; import convertNumberTo32Bytes from './helpers/convertNumberTo32Bytes'; @@ -25,11 +24,10 @@ const fetchMaximumAuctionDurationInSeconds = memoizee(_fetchMaximumAuctionDurati }); const _fetchMinimumBidDai = async function (network: string, collateralType: string): Promise { - const dogContract = await getContract(network, 'MCD_DOG'); - const encodedCollateralType = ethers.utils.formatBytes32String(collateralType); - const collateralParameters = await dogContract.ilks(encodedCollateralType); - const minimumBidDai = new BigNumber(collateralParameters.dirt._hex).div(RAD); - return minimumBidDai; + const contractName = getClipperNameByCollateralType(collateralType); + const contract = await getContract(network, contractName); + const minimumBidRaw = await contract.chost(); + return new BigNumber(minimumBidRaw._hex).shiftedBy(-RAD_NUMBER_OF_DIGITS); }; export const fetchMinimumBidDai = memoizee(_fetchMinimumBidDai, { diff --git a/core/src/price.ts b/core/src/price.ts index 672e46fcd..e1e67c395 100644 --- a/core/src/price.ts +++ b/core/src/price.ts @@ -1,4 +1,4 @@ -import type { Auction } from './types'; +import type { Auction, AuctionTransaction } from './types'; import BigNumber from './bignumber'; import { addSeconds } from 'date-fns'; @@ -54,6 +54,44 @@ export const calculateTransactionGrossProfit = function (auction: Auction): BigN return totalMarketPriceLimitedByDebt.minus(auction.debtDAI); }; +export const calculateTransactionCollateralOutcome = function ( + bidAmountDai: BigNumber, + unitPrice: BigNumber, + auction: AuctionTransaction +): BigNumber { + // Based on the clipper contract logic + // https://github.com/makerdao/dss/blob/60690042965500992490f695cf259256cc94c140/src/clip.sol#L357-L380 + const collateralToBuyForTheBid = bidAmountDai.dividedBy(unitPrice); + const potentialOutcomeCollateralAmount = BigNumber.minimum(collateralToBuyForTheBid, auction.collateralAmount); // slice + const potentialOutcomeTotalPrice = potentialOutcomeCollateralAmount.multipliedBy(unitPrice); // owe + if ( + // if owe > tab + potentialOutcomeTotalPrice.isGreaterThan(auction.debtDAI) + ) { + return auction.debtDAI.dividedBy(unitPrice); // return tab / price + } else if ( + // if owe < tab && slice < lot + potentialOutcomeTotalPrice.isLessThan(auction.debtDAI) && + potentialOutcomeCollateralAmount.isLessThan(auction.collateralAmount) + ) { + if ( + // if tab - owe < _chost + auction.debtDAI.minus(potentialOutcomeTotalPrice).isLessThan(auction.minimumBidDai) + ) { + if ( + // if tab > _chost + auction.debtDAI.isLessThanOrEqualTo(auction.minimumBidDai) + ) { + // shouldn't be possible to left less than minimumBidDai + return new BigNumber(NaN); + } + // tab - _chost / price + return auction.debtDAI.minus(auction.minimumBidDai).dividedBy(unitPrice); + } + } + return potentialOutcomeCollateralAmount; +}; + export const calculateTransactionGrossProfitDate = function (auction: Auction, currentDate: Date): Date | undefined { if ( auction.secondsBetweenPriceDrops === undefined || diff --git a/core/src/wallet.ts b/core/src/wallet.ts index ac62947cd..e1948c631 100644 --- a/core/src/wallet.ts +++ b/core/src/wallet.ts @@ -1,7 +1,8 @@ import type { Notifier, WalletBalances } from './types'; +import { ethers } from 'ethers'; import getProvider from './provider'; import BigNumber from './bignumber'; -import getContract from './contracts'; +import getContract, { getJoinNameByCollateralType } from './contracts'; import executeTransaction from './execute'; import { DAI_NUMBER_OF_DIGITS, @@ -31,6 +32,17 @@ export const fetchVATbalanceDAI = async function (network: string, walletAddress return walletVatDAI.decimalPlaces(WAD_NUMBER_OF_DIGITS, BigNumber.ROUND_DOWN); }; +export const fetchCollateralVatBalance = async function ( + network: string, + walletAddress: string, + collateralType: string +): Promise { + const contract = await getContract(network, 'MCD_VAT'); + const encodedCollateralType = ethers.utils.formatBytes32String(collateralType); + const wadAmount = await contract.gem(encodedCollateralType, walletAddress); + return new BigNumber(wadAmount._hex).shiftedBy(-WAD_NUMBER_OF_DIGITS); +}; + export const fetchWalletBalances = async function (network: string, walletAddress: string): Promise { return { walletETH: await fetchBalanceETH(network, walletAddress), @@ -59,3 +71,16 @@ export const withdrawFromVAT = async function ( const wadAmount = amount.shiftedBy(WAD_NUMBER_OF_DIGITS).toFixed(0, BigNumber.ROUND_DOWN); await executeTransaction(network, 'MCD_JOIN_DAI', 'exit', [walletAddress, wadAmount], notifier, true); }; + +export const withdrawCollateralFromVat = async function ( + network: string, + walletAddress: string, + collateralType: string, + amount: BigNumber | undefined, + notifier?: Notifier +): Promise { + const withdrawalAmount = amount || (await fetchCollateralVatBalance(network, walletAddress, collateralType)); + const withdrawalAmountWad = withdrawalAmount.shiftedBy(WAD_NUMBER_OF_DIGITS).toFixed(0, BigNumber.ROUND_DOWN); + const contractName = getJoinNameByCollateralType(collateralType); + await executeTransaction(network, contractName, 'exit', [walletAddress, withdrawalAmountWad], notifier, true); +}; diff --git a/frontend/components/MainFlow.vue b/frontend/components/MainFlow.vue index b18329c27..f84712892 100644 --- a/frontend/components/MainFlow.vue +++ b/frontend/components/MainFlow.vue @@ -69,6 +69,10 @@ :wallet-address="walletAddress" :wallet-dai="walletDai" :wallet-vat-dai="walletVatDai" + :collateral-vat-balance="collateralVatBalance" + :is-fetching-collateral-vat-balance="isFetchingCollateralVatBalance" + @fetchCollateralVatBalance="$emit('fetchCollateralVatBalance', $event)" + @withdrawAllCollateralFromVat="$emit('withdrawAllCollateralFromVat', $event)" @connect="$emit('connect')" @disconnect="$emit('disconnect')" @manageVat="$emit('manageVat')" @@ -154,6 +158,14 @@ export default Vue.extend({ type: Object as Vue.PropType, default: null, }, + collateralVatBalanceStore: { + type: Object as Vue.PropType>, + default: undefined, + }, + isFetchingCollateralVatBalance: { + type: Boolean, + default: false, + }, isExplanationsShown: { type: Boolean, default: true, @@ -176,6 +188,12 @@ export default Vue.extend({ isStagingEnvironment(): boolean { return !!process.env.STAGING_BANNER_URL; }, + collateralVatBalance(): BigNumber | undefined { + if (!this.collateralVatBalanceStore || !this.selectedAuction) { + return; + } + return this.collateralVatBalanceStore[this.selectedAuction.collateralType]; + }, }, watch: { selectedAuctionId: { diff --git a/frontend/components/common/BaseValueInput.vue b/frontend/components/common/BaseValueInput.vue index 9479f903c..2a3543616 100644 --- a/frontend/components/common/BaseValueInput.vue +++ b/frontend/components/common/BaseValueInput.vue @@ -74,7 +74,10 @@ export default Vue.extend({ }, watch: { inputText(_newInputText: string, oldInputText: string) { - if (this.errorMessage || !this.inputTextParsed) { + if (!this.inputTextParsed) { + return; + } + if (this.errorMessage) { this.$emit('update:inputValue', new BigNumber(NaN)); return; } diff --git a/frontend/components/transaction/AuthorisationBlock.vue b/frontend/components/transaction/AuthorisationBlock.vue index 66d926f69..b3d8ce7c6 100644 --- a/frontend/components/transaction/AuthorisationBlock.vue +++ b/frontend/components/transaction/AuthorisationBlock.vue @@ -49,11 +49,9 @@ > Authorize DAI Transactions - -
- Authorize DAI Transactions -
-
+ + DAI Transactions Authorized +
@@ -94,22 +92,15 @@ > Authorizing... - - -
- - Authorize - Transactions - -
-
+ + Transactions Authorized +
diff --git a/frontend/components/utils/BidInput.vue b/frontend/components/utils/BidInput.vue index 9a1fde4c5..5b2baa21d 100644 --- a/frontend/components/utils/BidInput.vue +++ b/frontend/components/utils/BidInput.vue @@ -1,49 +1,71 @@ diff --git a/frontend/store/auctions.ts b/frontend/store/auctions.ts index 30b6ca551..d7ed9e84e 100644 --- a/frontend/store/auctions.ts +++ b/frontend/store/auctions.ts @@ -230,6 +230,7 @@ export const actions = { notifier ); await dispatch('wallet/fetchWalletBalances', undefined, { root: true }); + await dispatch('wallet/fetchCollateralVatBalance', auction.collateralType, { root: true }); await dispatch('fetch'); } catch (error) { console.error('Bidding error', error); diff --git a/frontend/store/wallet.ts b/frontend/store/wallet.ts index 49646f646..815602a8a 100644 --- a/frontend/store/wallet.ts +++ b/frontend/store/wallet.ts @@ -2,7 +2,13 @@ import type { WalletBalances } from 'auctions-core/src/types'; import { ActionContext } from 'vuex'; import { message } from 'ant-design-vue'; import BigNumber from 'bignumber.js'; -import { fetchWalletBalances, depositToVAT, withdrawFromVAT } from 'auctions-core/src/wallet'; +import { + fetchWalletBalances, + depositToVAT, + withdrawFromVAT, + fetchCollateralVatBalance, + withdrawCollateralFromVat, +} from 'auctions-core/src/wallet'; import getWallet, { WALLETS } from '~/lib/wallet'; import notifier from '~/lib/notifier'; import { getContractAddressByName } from '~/../core/src/contracts'; @@ -15,6 +21,8 @@ interface State { isFetchingBalances: boolean; isDepositingOrWithdrawing: boolean; tokenAddressDai: string | undefined; + isFetchingCollateralVatBalance: boolean; + collateralVatBalanceStore: Record; } export const state = (): State => ({ @@ -25,6 +33,8 @@ export const state = (): State => ({ isFetchingBalances: false, isDepositingOrWithdrawing: false, tokenAddressDai: undefined, + isFetchingCollateralVatBalance: false, + collateralVatBalanceStore: {}, }); export const getters = { @@ -52,6 +62,12 @@ export const getters = { tokenAddressDai(state: State) { return state.tokenAddressDai; }, + isFetchingCollateralVatBalance(state: State) { + return state.isFetchingCollateralVatBalance; + }, + collateralVatBalanceStore(state: State) { + return state.collateralVatBalanceStore; + }, }; export const mutations = { @@ -79,6 +95,15 @@ export const mutations = { setTokenAddressDai(state: State, tokenAddressDai: string): void { state.tokenAddressDai = tokenAddressDai; }, + setIsFetchingCollateralVatBalance(state: State, isFetchingCollateralVatBalance: boolean): void { + state.isFetchingCollateralVatBalance = isFetchingCollateralVatBalance; + }, + setCollateralVatBalance( + state: State, + { collateralType, balance }: { collateralType: string; balance: BigNumber } + ): void { + state.collateralVatBalanceStore[collateralType] = balance; + }, }; export const actions = { @@ -191,4 +216,40 @@ export const actions = { message.error(`Error while fetching tokenAddressDai: ${error.message}`); } }, + async fetchCollateralVatBalance( + { commit, getters, rootGetters }: ActionContext, + collateralType: string + ): Promise { + const network = rootGetters['network/getMakerNetwork']; + const walletAddress = getters.getAddress; + if (!walletAddress) { + commit('setCollateralVatBalance', { collateralType, balance: undefined }); + return; + } + commit('setIsFetchingCollateralVatBalance', true); + try { + const collateralVatBalance = await fetchCollateralVatBalance(network, walletAddress, collateralType); + commit('setCollateralVatBalance', { collateralType, balance: collateralVatBalance }); + } catch (error) { + commit('setCollateralVatBalance', { collateralType, balance: undefined }); + message.error(`Error while fetching "${collateralType}" collateral vat balance: ${error.message}`); + } finally { + commit('setIsFetchingCollateralVatBalance', false); + } + }, + async withdrawAllCollateralFromVat( + { commit, dispatch, getters, rootGetters }: ActionContext, + collateralType: string + ): Promise { + const network = rootGetters['network/getMakerNetwork']; + commit('setIsDepositingOrWithdrawing', true); + try { + await withdrawCollateralFromVat(network, getters.getAddress, collateralType, undefined, notifier); + await dispatch('fetchCollateralVatBalance', collateralType); + } catch (error) { + message.error(`Error while withdrawing "${collateralType}" collateral from VAT: ${error.message}`); + } finally { + commit('setIsDepositingOrWithdrawing', false); + } + }, };