-
+
{
- setAmountShares(e.value)
+ setDisplayAmountToSell(e.value)
setAmountSharesToDisplay('')
}}
style={{ width: 0 }}
- value={amountShares}
+ value={displaySellShares}
valueToDisplay={amountSharesToDisplay}
/>
}
onClickMaxButton={() => {
- setAmountShares(balanceItem.shares)
- setAmountSharesToDisplay(formatBigNumber(balanceItem.shares, collateral.decimals, 5))
+ setAmountSharesFromInput(displaySelectedOutcomeBalanceValue)
+ setAmountSharesToDisplay(formatBigNumber(displaySelectedOutcomeBalanceValue, baseCollateral.decimals, 5))
}}
shouldDisplayMaxButton
symbol={'Shares'}
/>
{amountError && {amountError}}
+ {currencySelect}
@@ -251,20 +403,24 @@ const MarketSellWrapper: React.FC = (props: Props) => {
value={`${formatNumber(formatBigNumber(amountShares || Zero, collateral.decimals))} Shares`}
/>
= (props: Props) => {
}
title={'Total'}
value={`${
- tradedCollateral ? formatNumber(formatBigNumber(tradedCollateral, collateral.decimals, 2)) : '0.00'
- } ${collateral.symbol}`}
+ normalizedTradedCollateral
+ ? formatNumber(formatBigNumber(normalizedTradedCollateral, displayCollateral.decimals, 2))
+ : '0.00'
+ } ${displayCollateral.symbol}`}
/>
diff --git a/app/src/components/market/sections/market_sell/market_sell_container.tsx b/app/src/components/market/sections/market_sell/market_sell_container.tsx
index c9dafceb06..6206ab6438 100644
--- a/app/src/components/market/sections/market_sell/market_sell_container.tsx
+++ b/app/src/components/market/sections/market_sell/market_sell_container.tsx
@@ -1,11 +1,13 @@
import React from 'react'
+import { CompoundService } from '../../../../services'
import { MarketDetailsTab, MarketMakerData } from '../../../../util/types'
import { MarketSell } from './market_sell'
import { ScalarMarketSell } from './scalar_market_sell'
interface Props {
+ compoundService: CompoundService
isScalar: boolean
marketMakerData: MarketMakerData
switchMarketTab: (arg0: MarketDetailsTab) => void
diff --git a/app/src/global.d.ts b/app/src/global.d.ts
index 82e9089189..f068436dfd 100644
--- a/app/src/global.d.ts
+++ b/app/src/global.d.ts
@@ -10,6 +10,6 @@ declare module 'react-share'
declare type Maybe = T | null
-declare type KnownToken = 'cdai' | 'usdc' | 'dai' | 'weth' | 'owl' | 'chai' | 'gno' | 'pnk' | 'dxd' | 'wspoa' | 'wxdai'
+declare type KnownToken = 'cdai' | 'cbat' | 'ceth' | 'cusdc' | 'cusdt' | 'cwbtc' | 'cuni' | 'eth' | 'usdc' | 'dai' | 'weth' | 'owl' | 'chai' | 'gno' | 'pnk' | 'dxd' | 'wspoa' | 'wxdai'
declare type KnownArbitrator = 'kleros' | 'unknown'
diff --git a/app/src/hooks/useBlockchainMarketMakerData.tsx b/app/src/hooks/useBlockchainMarketMakerData.tsx
index df871cb9d4..26d5068dbd 100644
--- a/app/src/hooks/useBlockchainMarketMakerData.tsx
+++ b/app/src/hooks/useBlockchainMarketMakerData.tsx
@@ -172,6 +172,7 @@ export const useBlockchainMarketMakerData = (graphMarketMakerData: Maybe {
let collateralBalance = new BigNumber(0)
+ setCollateralBalance(collateralBalance)
if (account) {
if (collateral.address === pseudoNativeAssetAddress) {
collateralBalance = await provider.getBalance(account)
@@ -28,7 +29,6 @@ export const useCollateralBalance = (
collateralBalance = await collateralService.getCollateral(account)
}
}
-
setCollateralBalance(collateralBalance)
}
diff --git a/app/src/services/compound_service.ts b/app/src/services/compound_service.ts
new file mode 100644
index 0000000000..fa777dfe65
--- /dev/null
+++ b/app/src/services/compound_service.ts
@@ -0,0 +1,158 @@
+import Big from 'big.js'
+import { Contract, Wallet, ethers, utils } from 'ethers'
+import { BigNumber, formatUnits, parseUnits } from 'ethers/utils'
+
+import { cBATAbi, cDaiAbi, cETHAbi, cUNIAbi, cUSDCAbi, cUSDTAbi, cWBTCAbi } from '../abi/compound_abi'
+import { roundNumberStringToSignificantDigits } from '../util/tools'
+import { Token } from '../util/types'
+
+// use floor as rounding method
+Big.RM = 0
+const RoundingFactor = 100000
+
+class CompoundService {
+ contract: Contract
+ signerAddress: Maybe
+ provider: any
+ exchangeRate: number
+ constructor(address: string, symbol: string, provider: any, signerAddress: Maybe) {
+ const cTokenABI = CompoundService.getABI(symbol)
+ if (signerAddress) {
+ const signer: Wallet = provider.getSigner()
+ this.contract = new ethers.Contract(address, cTokenABI, provider).connect(signer)
+ } else {
+ this.contract = new ethers.Contract(address, cTokenABI, provider)
+ }
+ this.exchangeRate = 0
+ this.signerAddress = signerAddress
+ this.provider = provider
+ }
+
+ init = async () => {
+ this.exchangeRate = await this.calculateExchangeRate()
+ }
+
+ calculateSupplyRateAPY = async (): Promise => {
+ const supplyRate: number = await this.contract.supplyRatePerBlock()
+ const supplyMantissa = 1e18
+ const blocksPerDay = 4 * 60 * 24
+ const daysPerYear = 365
+ const supplyApy = (Math.pow((supplyRate / supplyMantissa) * blocksPerDay + 1, daysPerYear - 1) - 1) * 100
+ return supplyApy
+ }
+
+ calculateCTokenToBaseExchange = (baseToken: Token, cTokenFunding: BigNumber): BigNumber => {
+ const cTokenDecimals = 8
+ const bigTen = new Big(10)
+ const userCTokenAmount = new Big(formatUnits(cTokenFunding, cTokenDecimals))
+ const exchangeRate = new Big(this.exchangeRate)
+ const baseTokenDecimals = Number(baseToken.decimals)
+ const mantissa = 18 + baseTokenDecimals - cTokenDecimals
+ if (exchangeRate.eq(0)) {
+ return new BigNumber('0')
+ }
+ const exp = bigTen.pow(mantissa)
+ const oneCTokenInUnderlying = exchangeRate.div(exp)
+ const amountUnderlyingTokens = userCTokenAmount.mul(oneCTokenInUnderlying)
+ let amountUnderlyingTokensBoundToPrecision = roundNumberStringToSignificantDigits(
+ amountUnderlyingTokens.toString(),
+ 4,
+ )
+ try {
+ const underlyingBigNumber = parseUnits(amountUnderlyingTokensBoundToPrecision, baseTokenDecimals)
+ return underlyingBigNumber
+ } catch (e) {
+ const amountUnderlyingTokensNumber = new Big(amountUnderlyingTokensBoundToPrecision).mul(RoundingFactor)
+ amountUnderlyingTokensBoundToPrecision = roundNumberStringToSignificantDigits(
+ amountUnderlyingTokensNumber.toString(),
+ 4,
+ )
+ let underlyingBigNumber = parseUnits(amountUnderlyingTokensBoundToPrecision.toString(), baseTokenDecimals)
+ underlyingBigNumber = underlyingBigNumber.div(RoundingFactor)
+ return underlyingBigNumber
+ }
+ }
+
+ calculateBaseToCTokenExchange = (userInputToken: Token, userInputTokenFunding: BigNumber): BigNumber => {
+ const cTokenDecimals = 8
+ const bigTen = new Big(10)
+ const userInputAmount = formatUnits(userInputTokenFunding, userInputToken.decimals)
+ const userInputTokenAmount = new Big(userInputAmount).round(cTokenDecimals, 0)
+ const underlyingDecimals = Number(userInputToken.decimals)
+ const exchangeRate = new Big(this.exchangeRate)
+ const mantissa = 18 + underlyingDecimals - cTokenDecimals
+ const divisor = bigTen.pow(mantissa)
+ if (exchangeRate.eq(0)) {
+ return new BigNumber('0')
+ }
+ const oneUnderlyingInCToken = divisor.div(exchangeRate)
+ const amountCTokens = userInputTokenAmount.times(oneUnderlyingInCToken)
+ let amountCTokensBoundToPrecision = roundNumberStringToSignificantDigits(amountCTokens.toString(), 4)
+ try {
+ const amountCTokenBigNumber = parseUnits(amountCTokensBoundToPrecision, cTokenDecimals)
+ return amountCTokenBigNumber
+ } catch (e) {
+ const amountCTokenNumber = new Big(amountCTokensBoundToPrecision).mul(RoundingFactor)
+ amountCTokensBoundToPrecision = roundNumberStringToSignificantDigits(amountCTokenNumber.toString(), 4)
+ if (amountCTokensBoundToPrecision === '0') {
+ return new BigNumber('0')
+ }
+ let amountCTokenBigNumber = parseUnits(amountCTokensBoundToPrecision.toString(), cTokenDecimals)
+ amountCTokenBigNumber = amountCTokenBigNumber.div(RoundingFactor)
+ return amountCTokenBigNumber
+ }
+ }
+
+ calculateExchangeRate = async (): Promise => {
+ const exchangeRate = Number(await this.contract.functions.exchangeRateStored())
+ return exchangeRate
+ }
+
+ static encodeMintTokens = (tokenSymbol: string, amountWei: string): string => {
+ if (tokenSymbol.toLowerCase() === 'ceth') {
+ const tokenABI = CompoundService.getABI(tokenSymbol)
+ const mintInterface = new utils.Interface(tokenABI)
+ return mintInterface.functions.mint.encode([])
+ } else {
+ const tokenABI = CompoundService.getABI(tokenSymbol)
+ const mintInterface = new utils.Interface(tokenABI)
+ return mintInterface.functions.mint.encode([amountWei])
+ }
+ }
+
+ static encodeRedeemTokens = (tokenSymbol: string, amountRedeem: string): string => {
+ const tokenABI = CompoundService.getABI(tokenSymbol)
+ const mintInterface = new utils.Interface(tokenABI)
+ return mintInterface.functions.redeem.encode([amountRedeem])
+ }
+
+ static encodeApproveUnlimited = (tokenSymbol: string, spenderAccount: string): string => {
+ const tokenABI = CompoundService.getABI(tokenSymbol)
+ const approveInterface = new utils.Interface(tokenABI)
+ return approveInterface.functions.approve.encode([spenderAccount, ethers.constants.MaxUint256])
+ }
+
+ static getABI = (symbol: string) => {
+ const symbolLowerCase = symbol.toLowerCase()
+ switch (symbolLowerCase) {
+ case 'cdai':
+ return cDaiAbi
+ case 'cwbtc':
+ return cWBTCAbi
+ case 'ceth':
+ return cETHAbi
+ case 'cbat':
+ return cBATAbi
+ case 'cuni':
+ return cUNIAbi
+ case 'cusdt':
+ return cUSDTAbi
+ case 'cusdc':
+ return cUSDCAbi
+ default:
+ return []
+ }
+ }
+}
+
+export { CompoundService }
diff --git a/app/src/services/cpk.ts b/app/src/services/cpk.ts
index 6766e9305f..e4f58c4e95 100644
--- a/app/src/services/cpk.ts
+++ b/app/src/services/cpk.ts
@@ -10,6 +10,8 @@ import { getLogger } from '../util/logger'
import {
getContractAddress,
getTargetSafeImplementation,
+ getToken,
+ getTokenFromAddress,
getWrapToken,
pseudoNativeAssetAddress,
waitForBlockToSync,
@@ -17,6 +19,7 @@ import {
import { calcDistributionHint, clampBigNumber, waitABit } from '../util/tools'
import { MarketData, Question, Token } from '../util/types'
+import { CompoundService } from './compound_service'
import { ConditionalTokenService } from './conditional_token'
import { ERC20Service } from './erc20'
import { MarketMakerService } from './market_maker'
@@ -30,39 +33,50 @@ const logger = getLogger('Services::CPKService')
interface CPKBuyOutcomesParams {
amount: BigNumber
collateral: Token
+ compoundService?: CompoundService | null
outcomeIndex: number
+ useBaseToken?: boolean
marketMaker: MarketMakerService
}
interface CPKSellOutcomesParams {
amount: BigNumber
+ compoundService?: CompoundService | null
outcomeIndex: number
marketMaker: MarketMakerService
+ useBaseToken?: boolean
conditionalTokens: ConditionalTokenService
}
interface CPKCreateMarketParams {
+ compoundService?: CompoundService | null
+ compoundTokenDetails?: Token
marketData: MarketData
conditionalTokens: ConditionalTokenService
realitio: RealitioService
marketMakerFactory: MarketMakerFactoryService
+ useCompoundReserve?: boolean
}
interface CPKAddFundingParams {
amount: BigNumber
collateral: Token
+ compoundService?: CompoundService | null
marketMaker: MarketMakerService
+ useBaseToken?: boolean
}
interface CPKRemoveFundingParams {
amountToMerge: BigNumber
collateralAddress: string
+ compoundService?: CompoundService | null
conditionId: string
conditionalTokens: ConditionalTokenService
earnings: BigNumber
marketMaker: MarketMakerService
outcomesCount: number
sharesToBurn: BigNumber
+ useBaseToken?: boolean
}
interface CPKRedeemParams {
@@ -172,8 +186,10 @@ class CPKService {
buyOutcomes = async ({
amount,
collateral,
+ compoundService,
marketMaker,
outcomeIndex,
+ useBaseToken = false,
}: CPKBuyOutcomesParams): Promise => {
try {
const signer = this.provider.getSigner()
@@ -185,11 +201,17 @@ class CPKService {
const txOptions: TxOptions = {}
- if (this.cpk.isSafeApp() || collateral.address === pseudoNativeAssetAddress) {
- txOptions.gas = 500000
- }
-
let collateralAddress
+ let collateralSymbol = ''
+ let userInputCollateralSymbol: KnownToken
+ let userInputCollateral: Token = collateral
+ let minCollateralAmount = amount
+ if (useBaseToken && compoundService != null) {
+ collateralSymbol = collateral.symbol.toLowerCase()
+ userInputCollateralSymbol = collateralSymbol.substring(1, collateralSymbol.length) as KnownToken
+ userInputCollateral = getToken(networkId, userInputCollateralSymbol)
+ minCollateralAmount = compoundService.calculateBaseToCTokenExchange(userInputCollateral, amount)
+ }
if (collateral.address === pseudoNativeAssetAddress) {
// ultimately WETH will be the collateral if we fund with native ether
collateralAddress = getWrapToken(networkId).address
@@ -198,30 +220,63 @@ class CPKService {
if (!this.cpk.isSafeApp()) {
txOptions.value = amount
}
-
+ if (this.cpk.isSafeApp()) {
+ txOptions.gas = 500000
+ }
// Step 0: Wrap ether
transactions.push({
to: collateralAddress,
value: amount,
})
+ } else if (useBaseToken) {
+ if (userInputCollateral.address === pseudoNativeAssetAddress) {
+ // If base token is ETH then we don't need to transfer to cpk
+ if (!this.cpk.isSafeApp()) {
+ txOptions.value = amount
+ }
+ if (this.cpk.isSafeApp()) {
+ txOptions.gas = 500000
+ }
+ const encodedMintFunction = CompoundService.encodeMintTokens(collateralSymbol, amount.toString())
+ transactions.push({
+ to: collateral.address,
+ data: encodedMintFunction,
+ value: amount.toString(),
+ })
+ } else {
+ // Transfer the base token to cpk
+ // Mint cTokens in the cpk
+ transactions.push({
+ to: userInputCollateral.address,
+ data: ERC20Service.encodeTransferFrom(account, this.cpk.address, amount),
+ })
+ const encodedMintFunction = CompoundService.encodeMintTokens(collateralSymbol, amount.toString())
+ // Approve cToken for the cpk contract
+ transactions.push({
+ to: userInputCollateral.address,
+ data: ERC20Service.encodeApproveUnlimited(collateral.address),
+ })
+ // Mint ctokens from the underlying token
+ transactions.push({
+ to: collateral.address,
+ data: encodedMintFunction,
+ })
+ }
+ collateralAddress = await marketMaker.getCollateralToken()
} else {
collateralAddress = await marketMaker.getCollateralToken()
}
-
const marketMakerAddress = marketMaker.address
-
const collateralService = new ERC20Service(this.provider, account, collateralAddress)
-
logger.log(`CPK address: ${this.cpk.address}`)
-
- const outcomeTokensToBuy = await marketMaker.calcBuyAmount(amount, outcomeIndex)
+ const outcomeTokensToBuy = await marketMaker.calcBuyAmount(minCollateralAmount, outcomeIndex)
logger.log(`Min outcome tokens to buy: ${outcomeTokensToBuy}`)
// Check if the allowance of the CPK to the market maker is enough.
const hasCPKEnoughAlowance = await collateralService.hasEnoughAllowance(
this.cpk.address,
marketMakerAddress,
- amount,
+ minCollateralAmount,
)
if (!hasCPKEnoughAlowance) {
@@ -235,17 +290,17 @@ class CPKService {
// Step 2: Transfer the amount of collateral being spent from the user to the CPK
// If we are funding with native ether we can skip this step
// If we are signed in as a safe we don't need to transfer
- if (!this.cpk.isSafeApp() && collateral.address !== pseudoNativeAssetAddress) {
+ if (!this.cpk.isSafeApp() && collateral.address !== pseudoNativeAssetAddress && !useBaseToken) {
+ // Step 2: Transfer the amount of collateral being spent from the user to the CPK
transactions.push({
to: collateralAddress,
data: ERC20Service.encodeTransferFrom(account, this.cpk.address, amount),
})
}
-
// Step 3: Buy outcome tokens with the CPK
transactions.push({
to: marketMakerAddress,
- data: MarketMakerService.encodeBuy(amount, outcomeIndex, outcomeTokensToBuy),
+ data: MarketMakerService.encodeBuy(minCollateralAmount, outcomeIndex, outcomeTokensToBuy),
})
const txObject = await this.cpk.execTransactions(transactions, txOptions)
@@ -257,13 +312,25 @@ class CPKService {
}
createMarket = async ({
+ compoundService,
+ compoundTokenDetails,
conditionalTokens,
marketData,
marketMakerFactory,
realitio,
+ useCompoundReserve,
}: CPKCreateMarketParams): Promise => {
try {
- const { arbitrator, category, loadedQuestionId, outcomes, question, resolution, spread } = marketData
+ const {
+ arbitrator,
+ category,
+ loadedQuestionId,
+ outcomes,
+ question,
+ resolution,
+ spread,
+ userInputCollateral,
+ } = marketData
if (!resolution) {
throw new Error('Resolution time was not specified')
@@ -292,7 +359,6 @@ class CPKService {
if (marketData.collateral.address === pseudoNativeAssetAddress) {
// ultimately WETH will be the collateral if we fund with native ether
collateral = getWrapToken(networkId)
-
// we need to send the funding amount in native ether
if (!this.cpk.isSafeApp()) {
txOptions.value = marketData.funding
@@ -303,6 +369,45 @@ class CPKService {
to: collateral.address,
value: marketData.funding,
})
+ } else if (useCompoundReserve && compoundTokenDetails) {
+ if (userInputCollateral.address === pseudoNativeAssetAddress) {
+ // If user chosen collateral is ETH
+ collateral = marketData.collateral
+ if (!this.cpk.isSafeApp()) {
+ txOptions.value = marketData.funding
+ }
+ const encodedMintFunction = CompoundService.encodeMintTokens(
+ compoundTokenDetails.symbol,
+ marketData.funding.toString(),
+ )
+ transactions.push({
+ to: collateral.address,
+ data: encodedMintFunction,
+ value: marketData.funding,
+ })
+ } else {
+ collateral = marketData.collateral
+ // For any other compound pair that is not ETH
+ const encodedMintFunction = CompoundService.encodeMintTokens(
+ compoundTokenDetails.symbol,
+ marketData.funding.toString(),
+ )
+ // Transfer user input collateral to cpk
+ transactions.push({
+ to: userInputCollateral.address,
+ data: ERC20Service.encodeTransferFrom(account, this.cpk.address, marketData.funding),
+ })
+ // Approve cToken for the cpk contract
+ transactions.push({
+ to: userInputCollateral.address,
+ data: ERC20Service.encodeApproveUnlimited(collateral.address),
+ })
+ // Mint ctokens from the underlying token
+ transactions.push({
+ to: collateral.address,
+ data: encodedMintFunction,
+ })
+ }
} else {
collateral = marketData.collateral
}
@@ -360,17 +465,23 @@ class CPKService {
to: collateral.address,
data: ERC20Service.encodeApproveUnlimited(marketMakerFactory.address),
})
-
+ let minCollateralAmount = marketData.funding
+ if (useCompoundReserve && compoundService) {
+ minCollateralAmount = compoundService.calculateBaseToCTokenExchange(userInputCollateral, marketData.funding)
+ }
// Step 4: Transfer funding from user
// If we are funding with native ether we can skip this step
// If we are signed in as a safe we don't need to transfer
if (!this.cpk.isSafeApp() && marketData.collateral.address !== pseudoNativeAssetAddress) {
- transactions.push({
- to: collateral.address,
- data: ERC20Service.encodeTransferFrom(account, this.cpk.address, marketData.funding),
- })
+ // If we are using compound reserve then we don't need to transfer
+ // since we have already transferred the userinput collateral and minted cTokens
+ if (!useCompoundReserve) {
+ transactions.push({
+ to: collateral.address,
+ data: ERC20Service.encodeTransferFrom(account, this.cpk.address, marketData.funding),
+ })
+ }
}
-
// Step 5: Create market maker
const saltNonce = Math.round(Math.random() * 1000000)
const predictedMarketMakerAddress = await marketMakerFactory.predictMarketMakerAddress(
@@ -391,7 +502,7 @@ class CPKService {
collateral.address,
conditionId,
spread,
- marketData.funding,
+ minCollateralAmount,
distributionHint,
),
})
@@ -608,9 +719,11 @@ class CPKService {
sellOutcomes = async ({
amount,
+ compoundService,
conditionalTokens,
marketMaker,
outcomeIndex,
+ useBaseToken,
}: CPKSellOutcomesParams): Promise => {
try {
const signer = this.provider.getSigner()
@@ -642,14 +755,48 @@ class CPKService {
to: marketMaker.address,
data: MarketMakerService.encodeSell(amount, outcomeIndex, outcomeTokensToSell),
})
-
// If we are signed in as a safe we don't need to transfer
if (!this.cpk.isSafeApp()) {
- // Step 4: Transfer funding to user
- transactions.push({
- to: collateralAddress,
- data: ERC20Service.encodeTransfer(account, amount),
- })
+ if (useBaseToken && compoundService != null) {
+ const network = await this.provider.getNetwork()
+ const networkId = network.chainId
+ const collateralToken = getTokenFromAddress(networkId, collateralAddress)
+ const collateralSymbol = collateralToken.symbol.toLowerCase()
+ const userInputCollateralSymbol = collateralSymbol.substring(1, collateralSymbol.length) as KnownToken
+ const userInputCollateral = getToken(networkId, userInputCollateralSymbol)
+ const minCollateralAmount = compoundService.calculateCTokenToBaseExchange(userInputCollateral, amount)
+ // Convert cpk token to base token if user wants to redeem in base
+ const encodedRedeemFunction = CompoundService.encodeRedeemTokens(collateralSymbol, amount.toString())
+ // Approve cToken for the cpk contract
+ transactions.push({
+ to: userInputCollateral.address,
+ data: ERC20Service.encodeApproveUnlimited(collateralToken.address),
+ })
+ // Redeem underlying token from the ctoken
+ transactions.push({
+ to: collateralToken.address,
+ data: encodedRedeemFunction,
+ })
+ // Transfer base token to the user
+ if (userInputCollateral.address === pseudoNativeAssetAddress) {
+ // If user wants to withdraw ether then simply transfer the amount
+ transactions.push({
+ to: account,
+ value: minCollateralAmount.toString(),
+ })
+ } else {
+ transactions.push({
+ to: userInputCollateral.address,
+ data: ERC20Service.encodeTransfer(account, minCollateralAmount),
+ })
+ }
+ } else {
+ // Step 4: Transfer funding to user
+ transactions.push({
+ to: collateralAddress,
+ data: ERC20Service.encodeTransfer(account, amount),
+ })
+ }
}
const txObject = await this.cpk.execTransactions(transactions, txOptions)
@@ -660,7 +807,13 @@ class CPKService {
}
}
- addFunding = async ({ amount, collateral, marketMaker }: CPKAddFundingParams): Promise => {
+ addFunding = async ({
+ amount,
+ collateral,
+ compoundService,
+ marketMaker,
+ useBaseToken,
+ }: CPKAddFundingParams): Promise => {
try {
const signer = this.provider.getSigner()
const account = await signer.getAddress()
@@ -672,6 +825,10 @@ class CPKService {
const txOptions: TxOptions = {}
+ let collateralSymbol = ''
+ let userInputCollateralSymbol: KnownToken
+ let userInputCollateral: Token = collateral
+
if (this.cpk.isSafeApp() || collateral.address === pseudoNativeAssetAddress) {
txOptions.gas = 500000
}
@@ -694,16 +851,13 @@ class CPKService {
} else {
collateralAddress = collateral.address
}
-
- // Check if the allowance of the CPK to the market maker is enough.
const collateralService = new ERC20Service(this.provider, account, collateralAddress)
-
+ // Check if the allowance of the CPK to the market maker is enough.
const hasCPKEnoughAlowance = await collateralService.hasEnoughAllowance(
this.cpk.address,
marketMaker.address,
amount,
)
-
if (!hasCPKEnoughAlowance) {
// Step 1: Approve unlimited amount to be transferred to the market maker
transactions.push({
@@ -711,21 +865,59 @@ class CPKService {
data: ERC20Service.encodeApproveUnlimited(marketMaker.address),
})
}
-
- // Step 2: Transfer funding from user
- // If we are funding with native ether we can skip this step
+ let minCollateralAmount = amount
+ if (useBaseToken && compoundService != null) {
+ collateralSymbol = collateral.symbol.toLowerCase()
+ userInputCollateralSymbol = collateralSymbol.substring(1, collateralSymbol.length) as KnownToken
+ userInputCollateral = getToken(networkId, userInputCollateralSymbol)
+ minCollateralAmount = compoundService.calculateBaseToCTokenExchange(userInputCollateral, amount)
+ }
// If we are signed in as a safe we don't need to transfer
if (!this.cpk.isSafeApp() && collateral.address !== pseudoNativeAssetAddress) {
- transactions.push({
- to: collateral.address,
- data: ERC20Service.encodeTransferFrom(account, this.cpk.address, amount),
- })
+ // Step 4: Transfer funding from user
+ if (useBaseToken) {
+ // If use base token then transfer the base token amount from the user
+ if (collateral.address !== pseudoNativeAssetAddress) {
+ transactions.push({
+ to: userInputCollateral.address,
+ data: ERC20Service.encodeTransferFrom(account, this.cpk.address, amount),
+ })
+ }
+ } else {
+ // If use collateral token then transfer the collateral token amount from the user
+ transactions.push({
+ to: collateral.address,
+ data: ERC20Service.encodeTransferFrom(account, this.cpk.address, minCollateralAmount),
+ })
+ }
+ }
+ if (useBaseToken) {
+ // get base token
+ const encodedMintFunction = CompoundService.encodeMintTokens(collateralSymbol, amount.toString())
+ // Approve cToken for the cpk contract
+ if (userInputCollateral.address === pseudoNativeAssetAddress) {
+ txOptions.value = amount
+ transactions.push({
+ to: collateral.address,
+ data: encodedMintFunction,
+ value: amount,
+ })
+ } else {
+ transactions.push({
+ to: userInputCollateral.address,
+ data: ERC20Service.encodeApproveUnlimited(collateral.address),
+ })
+ // Mint ctokens from the underlying token
+ transactions.push({
+ to: collateral.address,
+ data: encodedMintFunction,
+ })
+ }
}
-
// Step 3: Add funding to market
transactions.push({
to: marketMaker.address,
- data: MarketMakerService.encodeAddFunding(amount),
+ data: MarketMakerService.encodeAddFunding(minCollateralAmount),
})
const txObject = await this.cpk.execTransactions(transactions, txOptions)
@@ -739,17 +931,19 @@ class CPKService {
removeFunding = async ({
amountToMerge,
collateralAddress,
+ compoundService,
conditionId,
conditionalTokens,
earnings,
marketMaker,
outcomesCount,
sharesToBurn,
+ useBaseToken,
}: CPKRemoveFundingParams): Promise => {
try {
const signer = this.provider.getSigner()
const account = await signer.getAddress()
-
+ const transactions = []
const removeFundingTx = {
to: marketMaker.address,
data: MarketMakerService.encodeRemoveFunding(sharesToBurn),
@@ -764,22 +958,64 @@ class CPKService {
amountToMerge,
),
}
-
- const transactions = [removeFundingTx, mergePositionsTx]
+ transactions.push(removeFundingTx)
+ transactions.push(mergePositionsTx)
const txOptions: TxOptions = {}
if (this.cpk.isSafeApp()) {
txOptions.gas = 500000
}
-
+ const network = await this.provider.getNetwork()
+ const networkId = network.chainId
+ const collateralToken = getTokenFromAddress(networkId, collateralAddress)
+ const collateralSymbol = collateralToken.symbol.toLowerCase()
+ let userInputCollateral = collateralToken
+ const totalAmountEarned = amountToMerge.add(earnings)
+ // transfer to the user the merged collateral plus the earned fees
+ if (useBaseToken && compoundService != null) {
+ const userInputCollateralSymbol = collateralSymbol.substring(1, collateralSymbol.length) as KnownToken
+ userInputCollateral = getToken(networkId, userInputCollateralSymbol)
+ // Convert cpk token to base token if user wants to redeem in base
+ const encodedRedeemFunction = CompoundService.encodeRedeemTokens(collateralSymbol, totalAmountEarned.toString())
+ // Approve cToken for the cpk contract
+ transactions.push({
+ to: userInputCollateral.address,
+ data: ERC20Service.encodeApproveUnlimited(collateralToken.address),
+ })
+ // redeeem underlying token from the ctoken token
+ transactions.push({
+ to: collateralToken.address,
+ data: encodedRedeemFunction,
+ })
+ }
// If we are signed in as a safe we don't need to transfer
if (!this.cpk.isSafeApp()) {
// transfer to the user the merged collateral plus the earned fees
- transactions.push({
- to: collateralAddress,
- data: ERC20Service.encodeTransfer(account, amountToMerge.add(earnings)),
- })
+ if (useBaseToken && compoundService != null) {
+ const minCollateralAmount = compoundService.calculateCTokenToBaseExchange(
+ userInputCollateral,
+ totalAmountEarned,
+ )
+ if (userInputCollateral.address === pseudoNativeAssetAddress) {
+ // If user wants to redeem in ether simply transfer the funds back to user
+ transactions.push({
+ to: account,
+ value: minCollateralAmount.toString(),
+ })
+ } else {
+ // Transfer base token to the user
+ transactions.push({
+ to: userInputCollateral.address,
+ data: ERC20Service.encodeTransfer(account, minCollateralAmount),
+ })
+ }
+ } else {
+ transactions.push({
+ to: collateralAddress,
+ data: ERC20Service.encodeTransfer(account, totalAmountEarned),
+ })
+ }
}
const txObject = await this.cpk.execTransactions(transactions, txOptions)
diff --git a/app/src/services/index.ts b/app/src/services/index.ts
index 0ed6543fd1..c40b224034 100644
--- a/app/src/services/index.ts
+++ b/app/src/services/index.ts
@@ -1,3 +1,4 @@
+export { CompoundService } from './compound_service'
export { ConditionalTokenService } from './conditional_token'
export { RealitioService } from './realitio'
export { ERC20Service } from './erc20'
diff --git a/app/src/theme/index.js b/app/src/theme/index.js
index d083225431..bb2e589bec 100644
--- a/app/src/theme/index.js
+++ b/app/src/theme/index.js
@@ -101,6 +101,7 @@ const theme = {
textColor: '#757575',
textColorDark: '#37474F',
textColorDarker: '#333',
+ compound: '#00897B',
textColorLight: '#999',
textColorLighter: '#86909E',
textColorLightish: '#7D8189',
diff --git a/app/src/util/networks.ts b/app/src/util/networks.ts
index 429f9c1fec..47a34831d5 100644
--- a/app/src/util/networks.ts
+++ b/app/src/util/networks.ts
@@ -260,13 +260,66 @@ export const getInfuraUrl = (networkId: number): string => {
export const knownTokens: { [name in KnownToken]: KnownTokenData } = {
cdai: {
symbol: 'cDAI',
- decimals: 18,
+ decimals: 8,
addresses: {
[networkIds.MAINNET]: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643',
- [networkIds.RINKEBY]: '0x7a978b38d5af06ff929ca06647e025b759479318',
+ [networkIds.RINKEBY]: '0x6d7f0754ffeb405d23c51ce938289d4835be3b14',
},
order: 2,
},
+ cbat: {
+ symbol: 'cBAT',
+ decimals: 8,
+ addresses: {
+ [networkIds.MAINNET]: '0x6c8c6b02e7b2be14d4fa6022dfd6d75921d90e4e',
+ [networkIds.RINKEBY]: '0xebf1a11532b93a529b5bc942b4baa98647913002',
+ },
+ order: 10,
+ },
+ ceth: {
+ symbol: 'cETH',
+ decimals: 8,
+ addresses: {
+ [networkIds.MAINNET]: '0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5',
+ [networkIds.RINKEBY]: '0xd6801a1dffcd0a410336ef88def4320d6df1883e',
+ },
+ order: 11,
+ },
+ cusdc: {
+ symbol: 'cUSDC',
+ decimals: 8,
+ addresses: {
+ [networkIds.MAINNET]: '0x39aa39c021dfbae8fac545936693ac917d5e7563',
+ [networkIds.RINKEBY]: '0x5b281a6dda0b271e91ae35de655ad301c976edb1',
+ },
+ order: 12,
+ },
+ cusdt: {
+ symbol: 'cUSDT',
+ decimals: 8,
+ addresses: {
+ [networkIds.MAINNET]: '0xf650c3d88d12db855b8bf7d11be6c55a4e07dcc9',
+ [networkIds.RINKEBY]: '0x2fb298bdbef468638ad6653ff8376575ea41e768',
+ },
+ order: 13,
+ },
+ cwbtc: {
+ symbol: 'cWBTC',
+ decimals: 8,
+ addresses: {
+ [networkIds.MAINNET]: '0xc11b1268c1a384e55c48c2391d8d480264a3a7f4',
+ [networkIds.RINKEBY]: '0x0014f450b8ae7708593f4a46f8fa6e5d50620f96',
+ },
+ order: 14,
+ },
+ cuni: {
+ symbol: 'cUNI',
+ decimals: 8,
+ addresses: {
+ [networkIds.MAINNET]: '0x35a18000230da775cac24873d00ff85bccded550',
+ },
+ order: 15,
+ },
dai: {
symbol: 'DAI',
decimals: 18,
@@ -352,6 +405,15 @@ export const knownTokens: { [name in KnownToken]: KnownTokenData } = {
},
order: 9,
},
+ eth: {
+ symbol: 'ETH',
+ decimals: 18,
+ addresses: {
+ [networkIds.MAINNET]: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
+ [networkIds.RINKEBY]: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
+ },
+ order: 18,
+ },
}
const validNetworkId = (networkId: number): networkId is NetworkId => {
diff --git a/app/src/util/tools.ts b/app/src/util/tools.ts
index 35568d2e84..d3240d8252 100644
--- a/app/src/util/tools.ts
+++ b/app/src/util/tools.ts
@@ -9,8 +9,10 @@ import {
REALITIO_SCALAR_ADAPTER_ADDRESS_SOKOL,
REALITIO_SCALAR_ADAPTER_ADDRESS_XDAI,
} from '../common/constants'
+import { CompoundService } from '../services/compound_service'
import { getLogger } from './logger'
+import { BalanceItem, CompoundEnabledTokenType, Token } from './types'
const logger = getLogger('Tools')
@@ -228,6 +230,86 @@ export const calcPoolTokens = (
}
}
+/**
+ * Round a given number string to a fixed number of significant digits
+ */
+export const roundNumberStringToSignificantDigits = (value: string, sd: number): string => {
+ const r = new Big(value)
+ const preciseValue = (r as any).prec(sd, 0)
+ if (preciseValue.gt(0)) {
+ return preciseValue.toString()
+ } else {
+ return '0'
+ }
+}
+
+/**
+ * Gets the corresponding cToken for a given token symbol.
+ * Empty string if corresponding cToken doesn't exist
+ */
+export const getCTokenForToken = (token: string): string => {
+ const tokenSymbol = token.toLowerCase()
+ if (tokenSymbol in CompoundEnabledTokenType) {
+ if (tokenSymbol === 'eth' || tokenSymbol === 'weth') {
+ return 'ceth'
+ } else {
+ return `c${tokenSymbol}`
+ }
+ } else {
+ return ''
+ }
+}
+
+/**
+ * Gets base token symbol for a given ctoken
+ */
+export const getBaseTokenForCToken = (token: string): string => {
+ const tokenSymbol = token.toLowerCase()
+ if (tokenSymbol.startsWith('c')) {
+ return tokenSymbol.substring(1, tokenSymbol.length)
+ }
+ return ''
+}
+/**
+ * Calculates balances in base token for a given c token
+ */
+export const getBalancesInBaseToken = (
+ balances: BalanceItem[],
+ compoundService: CompoundService,
+ displayCollateral: Token,
+): BalanceItem[] => {
+ const displayBalances = balances.map(function(bal) {
+ const cTokenPrecision = 8
+ const cTokenWithPrecision = roundNumberStringToSignificantDigits(bal.currentPrice.toString(), 4)
+ let basePrice = '0'
+ try {
+ const cTokenPriceAmount = parseUnits(cTokenWithPrecision, cTokenPrecision)
+ const baseTokenPrice = compoundService.calculateCTokenToBaseExchange(displayCollateral, cTokenPriceAmount)
+ basePrice = formatBigNumber(baseTokenPrice, displayCollateral.decimals)
+ } catch (e) {
+ basePrice = '0'
+ }
+ return Object.assign({}, bal, {
+ currentPrice: basePrice,
+ })
+ })
+ return displayBalances
+}
+
+export const getSharesInBaseToken = (
+ balances: BalanceItem[],
+ compoundService: CompoundService,
+ displayCollateral: Token,
+): BalanceItem[] => {
+ const displayBalances = balances.map(function(bal) {
+ const baseTokenPrice = compoundService.calculateCTokenToBaseExchange(displayCollateral, bal.shares)
+ return Object.assign({}, bal, {
+ shares: baseTokenPrice,
+ })
+ })
+ return displayBalances
+}
+
/**
* Compute the number of outcomes that will be sent to the user by the Market Maker
* after funding it for the first time with `addedFunds` of collateral.
diff --git a/app/src/util/types.ts b/app/src/util/types.ts
index 88cc4418bb..145ebeae73 100644
--- a/app/src/util/types.ts
+++ b/app/src/util/types.ts
@@ -195,8 +195,11 @@ export enum Wallet {
export interface MarketData {
collateral: Token
+ userInputCollateral: Token
+ userInputToken: Token
arbitratorsCustom: Arbitrator[]
categoriesCustom: string[]
+ compoundInterestRate: string
question: string
category: string
resolution: Date | null
@@ -205,6 +208,7 @@ export interface MarketData {
funding: BigNumber
outcomes: Outcome[]
loadedQuestionId: Maybe
+ useCompoundReserve: boolean
verifiedLabel?: string
lowerBound: Maybe
upperBound: Maybe
@@ -284,6 +288,7 @@ export interface MarketMakerData {
balances: BalanceItem[]
creationTimestamp: string
collateral: Token
+ userInputCollateral: Token | null
fee: BigNumber
isConditionResolved: boolean
isQuestionFinalized: boolean
@@ -446,6 +451,24 @@ export enum MarketState {
none = '',
}
+export enum CompoundTokenType {
+ cdai = 'cdai',
+ cusdc = 'cusdc',
+ cusdt = 'cusdt',
+ cuni = 'cuni',
+ cbat = 'cbat',
+ ceth = 'ceth',
+}
+
+export enum CompoundEnabledTokenType {
+ dai = 'dai',
+ usdc = 'usdc',
+ usdt = 'usdt',
+ uni = 'uni',
+ bat = 'bat',
+ eth = 'eth',
+}
+
export const INVALID_ANSWER_ID = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
export enum FormState {
diff --git a/app/yarn.lock b/app/yarn.lock
index 64f2d5e7ba..29a90aeb48 100644
--- a/app/yarn.lock
+++ b/app/yarn.lock
@@ -6044,6 +6044,11 @@ big.js@^5.1.2, big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
+big.js@^6.0.3:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.0.3.tgz#8b4d99ac7023668e0e465d3f78c23b8ac29ad381"
+ integrity sha512-n6yn1FyVL1EW2DBAr4jlU/kObhRzmr+NNRESl65VIOT8WBJj/Kezpx2zFdhJUqYI6qrtTW7moCStYL5VxeVdPA==
+
bignumber.js@*, bignumber.js@9.0.0, bignumber.js@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075"
diff --git a/omen-exchange b/omen-exchange
new file mode 160000
index 0000000000..3671ab6a5c
--- /dev/null
+++ b/omen-exchange
@@ -0,0 +1 @@
+Subproject commit 3671ab6a5c2ab05016df84ed2f8297101b8c9e02