import { DydxBridgeAction, DydxBridgeActionType, DydxBridgeData, dydxBridgeDataEncoder, IAssetDataContract, } from '@0x/contracts-asset-proxy'; import { blockchainTests, constants, expect, getRandomFloat, getRandomInteger, Numberish, randomAddress, } from '@0x/contracts-test-utils'; import { Order } from '@0x/types'; import { BigNumber, fromTokenUnitAmount, toTokenUnitAmount } from '@0x/utils'; import { artifacts as devUtilsArtifacts } from './artifacts'; import { TestDydxContract, TestLibDydxBalanceContract } from './wrappers'; blockchainTests('LibDydxBalance', env => { interface TestDydxConfig { marginRatio: BigNumber; operators: Array<{ owner: string; operator: string; }>; accounts: Array<{ owner: string; accountId: BigNumber; balances: BigNumber[]; }>; markets: Array<{ token: string; decimals: number; price: BigNumber; }>; } const MARGIN_RATIO = 1.5; const PRICE_DECIMALS = 18; const MAKER_DECIMALS = 6; const TAKER_DECIMALS = 18; const INITIAL_TAKER_TOKEN_BALANCE = fromTokenUnitAmount(1000, TAKER_DECIMALS); const BRIDGE_ADDRESS = randomAddress(); const ACCOUNT_OWNER = randomAddress(); const MAKER_PRICE = 150; const TAKER_PRICE = 100; const SOLVENT_ACCOUNT_IDX = 0; // const MIN_SOLVENT_ACCOUNT_IDX = 1; const INSOLVENT_ACCOUNT_IDX = 2; const ZERO_BALANCE_ACCOUNT_IDX = 3; const DYDX_CONFIG: TestDydxConfig = { marginRatio: fromTokenUnitAmount(MARGIN_RATIO - 1, PRICE_DECIMALS), operators: [{ owner: ACCOUNT_OWNER, operator: BRIDGE_ADDRESS }], accounts: [ { owner: ACCOUNT_OWNER, accountId: getRandomInteger(1, 2 ** 64), // Account exceeds collateralization. balances: [fromTokenUnitAmount(10, TAKER_DECIMALS), fromTokenUnitAmount(-1, MAKER_DECIMALS)], }, { owner: ACCOUNT_OWNER, accountId: getRandomInteger(1, 2 ** 64), // Account is at minimum collateralization. balances: [ fromTokenUnitAmount((MAKER_PRICE / TAKER_PRICE) * MARGIN_RATIO * 5, TAKER_DECIMALS), fromTokenUnitAmount(-5, MAKER_DECIMALS), ], }, { owner: ACCOUNT_OWNER, accountId: getRandomInteger(1, 2 ** 64), // Account is undercollateralized.. balances: [fromTokenUnitAmount(1, TAKER_DECIMALS), fromTokenUnitAmount(-2, MAKER_DECIMALS)], }, { owner: ACCOUNT_OWNER, accountId: getRandomInteger(1, 2 ** 64), // Account has no balance. balances: [fromTokenUnitAmount(0, TAKER_DECIMALS), fromTokenUnitAmount(0, MAKER_DECIMALS)], }, ], markets: [ { token: constants.NULL_ADDRESS, // TBD decimals: TAKER_DECIMALS, price: fromTokenUnitAmount(TAKER_PRICE, PRICE_DECIMALS), }, { token: constants.NULL_ADDRESS, // TBD decimals: MAKER_DECIMALS, price: fromTokenUnitAmount(MAKER_PRICE, PRICE_DECIMALS), }, ], }; let dydx: TestDydxContract; let testContract: TestLibDydxBalanceContract; let assetDataContract: IAssetDataContract; let takerTokenAddress: string; let makerTokenAddress: string; before(async () => { assetDataContract = new IAssetDataContract(constants.NULL_ADDRESS, env.provider); testContract = await TestLibDydxBalanceContract.deployWithLibrariesFrom0xArtifactAsync( devUtilsArtifacts.TestLibDydxBalance, devUtilsArtifacts, env.provider, env.txDefaults, {}, ); // Create tokens. takerTokenAddress = await testContract.createToken(TAKER_DECIMALS).callAsync(); await testContract.createToken(TAKER_DECIMALS).awaitTransactionSuccessAsync(); makerTokenAddress = await testContract.createToken(MAKER_DECIMALS).callAsync(); await testContract.createToken(MAKER_DECIMALS).awaitTransactionSuccessAsync(); DYDX_CONFIG.markets[0].token = takerTokenAddress; DYDX_CONFIG.markets[1].token = makerTokenAddress; dydx = await TestDydxContract.deployFrom0xArtifactAsync( devUtilsArtifacts.TestDydx, env.provider, env.txDefaults, {}, DYDX_CONFIG, ); // Mint taker tokens. await testContract .setTokenBalance(takerTokenAddress, ACCOUNT_OWNER, INITIAL_TAKER_TOKEN_BALANCE) .awaitTransactionSuccessAsync(); // Approve the Dydx contract to spend takerToken. await testContract .setTokenApproval(takerTokenAddress, ACCOUNT_OWNER, dydx.address, constants.MAX_UINT256) .awaitTransactionSuccessAsync(); }); interface BalanceCheckInfo { dydx: string; bridgeAddress: string; makerAddress: string; makerTokenAddress: string; takerTokenAddress: string; orderMakerToTakerRate: BigNumber; accounts: BigNumber[]; actions: DydxBridgeAction[]; } function createBalanceCheckInfo(fields: Partial = {}): BalanceCheckInfo { return { dydx: dydx.address, bridgeAddress: BRIDGE_ADDRESS, makerAddress: ACCOUNT_OWNER, makerTokenAddress: DYDX_CONFIG.markets[1].token, takerTokenAddress: DYDX_CONFIG.markets[0].token, orderMakerToTakerRate: fromTokenUnitAmount( fromTokenUnitAmount(10, TAKER_DECIMALS).div(fromTokenUnitAmount(5, MAKER_DECIMALS)), ), accounts: [DYDX_CONFIG.accounts[SOLVENT_ACCOUNT_IDX].accountId], actions: [], ...fields, }; } function getFilledAccountCollateralizations( config: TestDydxConfig, checkInfo: BalanceCheckInfo, makerAssetFillAmount: BigNumber, ): BigNumber[] { const values: BigNumber[][] = checkInfo.accounts.map((accountId, accountIdx) => { const accountBalances = config.accounts[accountIdx].balances.slice(); for (const action of checkInfo.actions) { const actionMarketId = action.marketId.toNumber(); const actionAccountIdx = action.accountIdx.toNumber(); if (checkInfo.accounts[actionAccountIdx] !== accountId) { continue; } const rate = action.conversionRateDenominator.eq(0) ? new BigNumber(1) : action.conversionRateNumerator.div(action.conversionRateDenominator); const change = makerAssetFillAmount.times( action.actionType === DydxBridgeActionType.Deposit ? rate : rate.negated(), ); accountBalances[actionMarketId] = change.plus(accountBalances[actionMarketId]); } return accountBalances.map((b, marketId) => toTokenUnitAmount(b, config.markets[marketId].decimals).times( toTokenUnitAmount(config.markets[marketId].price, PRICE_DECIMALS), ), ); }); return values .map(accountValues => { return [ // supply BigNumber.sum(...accountValues.filter(b => b.gte(0))), // borrow BigNumber.sum(...accountValues.filter(b => b.lt(0))).abs(), ]; }) .map(([supply, borrow]) => supply.div(borrow)); } function getRandomRate(): BigNumber { return getRandomFloat(0, 1); } // Computes a deposit rate that is the minimum to keep an account solvent // perpetually. function getBalancedDepositRate(withdrawRate: BigNumber, scaling: Numberish = 1): BigNumber { // Add a small amount to the margin ratio to stay just above insolvency. return withdrawRate.times((MAKER_PRICE / TAKER_PRICE) * (MARGIN_RATIO + 1.1e-4)).times(scaling); } function takerToMakerAmount(takerAmount: BigNumber): BigNumber { return takerAmount.times(new BigNumber(10).pow(MAKER_DECIMALS - TAKER_DECIMALS)); } describe('_getSolventMakerAmount()', () => { it('computes fillable amount for a solvent maker', async () => { // Deposit collateral at a rate low enough to steadily reduce the // withdraw account's collateralization ratio. const withdrawRate = getRandomRate(); const depositRate = getBalancedDepositRate(withdrawRate, Math.random()); const checkInfo = createBalanceCheckInfo({ actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: fromTokenUnitAmount(withdrawRate), conversionRateDenominator: fromTokenUnitAmount(1), }, ], }); const makerAssetFillAmount = await testContract.getSolventMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.not.bignumber.eq(constants.MAX_UINT256); // The collateralization ratio after filling `makerAssetFillAmount` // should be exactly at `MARGIN_RATIO`. const cr = getFilledAccountCollateralizations(DYDX_CONFIG, checkInfo, makerAssetFillAmount); expect(cr[0].dp(2)).to.bignumber.eq(MARGIN_RATIO); }); it('computes fillable amount for a solvent maker with zero-sized deposits', async () => { const withdrawRate = getRandomRate(); const depositRate = new BigNumber(0); const checkInfo = createBalanceCheckInfo({ actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: fromTokenUnitAmount(withdrawRate), conversionRateDenominator: fromTokenUnitAmount(1), }, ], }); const makerAssetFillAmount = await testContract.getSolventMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.not.bignumber.eq(constants.MAX_UINT256); // The collateralization ratio after filling `makerAssetFillAmount` // should be exactly at `MARGIN_RATIO`. const cr = getFilledAccountCollateralizations(DYDX_CONFIG, checkInfo, makerAssetFillAmount); expect(cr[0].dp(2)).to.bignumber.eq(MARGIN_RATIO); }); it('computes fillable amount for a solvent maker with no deposits', async () => { const withdrawRate = getRandomRate(); const checkInfo = createBalanceCheckInfo({ actions: [ { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: fromTokenUnitAmount(withdrawRate), conversionRateDenominator: fromTokenUnitAmount(1), }, ], }); const makerAssetFillAmount = await testContract.getSolventMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.not.bignumber.eq(constants.MAX_UINT256); // The collateralization ratio after filling `makerAssetFillAmount` // should be exactly at `MARGIN_RATIO`. const cr = getFilledAccountCollateralizations(DYDX_CONFIG, checkInfo, makerAssetFillAmount); expect(cr[0].dp(2)).to.bignumber.eq(MARGIN_RATIO); }); it('computes fillable amount for a solvent maker with multiple deposits', async () => { // Deposit collateral at a rate low enough to steadily reduce the // withdraw account's collateralization ratio. const withdrawRate = getRandomRate(); const depositRate = getBalancedDepositRate(withdrawRate, Math.random()); const checkInfo = createBalanceCheckInfo({ actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate.times(0.75), TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate.times(0.25), TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: fromTokenUnitAmount(withdrawRate), conversionRateDenominator: fromTokenUnitAmount(1), }, ], }); const makerAssetFillAmount = await testContract.getSolventMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.not.bignumber.eq(constants.MAX_UINT256); // The collateralization ratio after filling `makerAssetFillAmount` // should be exactly at `MARGIN_RATIO`. const cr = getFilledAccountCollateralizations(DYDX_CONFIG, checkInfo, makerAssetFillAmount); expect(cr[0].dp(2)).to.bignumber.eq(MARGIN_RATIO); }); it('returns infinite amount for a perpetually solvent maker', async () => { // Deposit collateral at a rate that keeps the withdraw account's // collateralization ratio constant. const withdrawRate = getRandomRate(); const depositRate = getBalancedDepositRate(withdrawRate); const checkInfo = createBalanceCheckInfo({ // Deposit/Withdraw at a rate == marginRatio. actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: fromTokenUnitAmount(withdrawRate), conversionRateDenominator: fromTokenUnitAmount(1), }, ], }); const makerAssetFillAmount = await testContract.getSolventMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.bignumber.eq(constants.MAX_UINT256); }); it('returns infinite amount for a perpetually solvent maker with multiple deposits', async () => { // Deposit collateral at a rate that keeps the withdraw account's // collateralization ratio constant. const withdrawRate = getRandomRate(); const depositRate = getBalancedDepositRate(withdrawRate); const checkInfo = createBalanceCheckInfo({ // Deposit/Withdraw at a rate == marginRatio. actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate.times(0.25), TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate.times(0.75), TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: fromTokenUnitAmount(withdrawRate), conversionRateDenominator: fromTokenUnitAmount(1), }, ], }); const makerAssetFillAmount = await testContract.getSolventMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.bignumber.eq(constants.MAX_UINT256); }); it('does not count deposits to other accounts', async () => { // Deposit collateral at a rate that keeps the withdraw account's // collateralization ratio constant, BUT we split it in two deposits // and one will go into a different account. const withdrawRate = getRandomRate(); const depositRate = getBalancedDepositRate(withdrawRate); const checkInfo = createBalanceCheckInfo({ actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate.times(0.5), TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Deposit, // Deposit enough to balance out withdraw, but // into a different account. accountIdx: new BigNumber(1), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate.times(0.5), TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: fromTokenUnitAmount(withdrawRate), conversionRateDenominator: fromTokenUnitAmount(1), }, ], }); const makerAssetFillAmount = await testContract.getSolventMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.not.bignumber.eq(constants.MAX_UINT256); }); it('returns zero on an account that is under-collateralized', async () => { // Even though the deposit rate is enough to meet the minimum collateralization ratio, // the account is under-collateralized from the start, so cannot be filled. const withdrawRate = getRandomRate(); const depositRate = getBalancedDepositRate(withdrawRate); const checkInfo = createBalanceCheckInfo({ accounts: [DYDX_CONFIG.accounts[INSOLVENT_ACCOUNT_IDX].accountId], actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: fromTokenUnitAmount(withdrawRate), conversionRateDenominator: fromTokenUnitAmount(1), }, ], }); const makerAssetFillAmount = await testContract.getSolventMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.bignumber.eq(0); }); it( 'returns zero on an account that has no balance if deposit ' + 'to withdraw ratio is < the minimum collateralization rate', async () => { // If the deposit rate is not enough to meet the minimum collateralization ratio, // the fillable maker amount is zero because it will become insolvent as soon as // the withdraw occurs. const withdrawRate = getRandomRate(); const depositRate = getBalancedDepositRate(withdrawRate, 0.99); const checkInfo = createBalanceCheckInfo({ accounts: [DYDX_CONFIG.accounts[ZERO_BALANCE_ACCOUNT_IDX].accountId], actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: fromTokenUnitAmount(withdrawRate), conversionRateDenominator: fromTokenUnitAmount(1), }, ], }); const makerAssetFillAmount = await testContract.getSolventMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.bignumber.eq(0); }, ); it( 'returns infinite on an account that has no balance if deposit ' + 'to withdraw ratio is >= the minimum collateralization rate', async () => { const withdrawRate = getRandomRate(); const depositRate = getBalancedDepositRate(withdrawRate); const checkInfo = createBalanceCheckInfo({ accounts: [DYDX_CONFIG.accounts[ZERO_BALANCE_ACCOUNT_IDX].accountId], actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: fromTokenUnitAmount(withdrawRate), conversionRateDenominator: fromTokenUnitAmount(1), }, ], }); const makerAssetFillAmount = await testContract.getSolventMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.bignumber.eq(constants.MAX_UINT256); }, ); }); blockchainTests.resets('_getDepositableMakerAmount()', () => { it('returns infinite if no deposit action', async () => { const checkInfo = createBalanceCheckInfo({ orderMakerToTakerRate: fromTokenUnitAmount( fromTokenUnitAmount(10, TAKER_DECIMALS).div(fromTokenUnitAmount(100, MAKER_DECIMALS)), ), actions: [], }); const makerAssetFillAmount = await testContract.getDepositableMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.bignumber.eq(constants.MAX_UINT256); }); it('returns infinite if deposit rate is zero', async () => { const checkInfo = createBalanceCheckInfo({ orderMakerToTakerRate: fromTokenUnitAmount( fromTokenUnitAmount(10, TAKER_DECIMALS).div(fromTokenUnitAmount(100, MAKER_DECIMALS)), ), actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(0, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, ], }); const makerAssetFillAmount = await testContract.getDepositableMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.bignumber.eq(constants.MAX_UINT256); }); it('returns infinite if taker tokens cover the deposit rate', async () => { const checkInfo = createBalanceCheckInfo({ orderMakerToTakerRate: fromTokenUnitAmount( fromTokenUnitAmount(10, TAKER_DECIMALS).div(fromTokenUnitAmount(100, MAKER_DECIMALS)), ), actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(Math.random() * 0.1, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, ], }); const makerAssetFillAmount = await testContract.getDepositableMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.bignumber.eq(constants.MAX_UINT256); }); it('returns correct amount if taker tokens only partially cover deposit rate', async () => { // The taker tokens getting exchanged in will only partially cover the deposit. const exchangeRate = 0.1; const depositRate = Math.random() + exchangeRate; const checkInfo = createBalanceCheckInfo({ orderMakerToTakerRate: fromTokenUnitAmount( fromTokenUnitAmount(exchangeRate, TAKER_DECIMALS).div(fromTokenUnitAmount(1, MAKER_DECIMALS)), ), actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, ], }); const makerAssetFillAmount = await testContract.getDepositableMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.not.bignumber.eq(constants.MAX_UINT256); // Compute the equivalent taker asset fill amount. const takerAssetFillAmount = fromTokenUnitAmount( toTokenUnitAmount(makerAssetFillAmount, MAKER_DECIMALS) // Reduce the deposit rate by the exchange rate. .times(depositRate - exchangeRate), TAKER_DECIMALS, ); // Which should equal the entire taker token balance of the account owner. // We do some rounding to account for integer vs FP vs symbolic precision differences. expect(toTokenUnitAmount(takerAssetFillAmount, TAKER_DECIMALS).dp(5)).to.bignumber.eq( toTokenUnitAmount(INITIAL_TAKER_TOKEN_BALANCE, TAKER_DECIMALS).dp(5), ); }); it('returns correct amount if the taker asset not an ERC20', async () => { const depositRate = 0.1; const checkInfo = createBalanceCheckInfo({ // The `takerTokenAddress` will be zero if the asset is not an ERC20. takerTokenAddress: constants.NULL_ADDRESS, orderMakerToTakerRate: fromTokenUnitAmount(fromTokenUnitAmount(0.1, MAKER_DECIMALS)), actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, ], }); const makerAssetFillAmount = await testContract.getDepositableMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.not.bignumber.eq(constants.MAX_UINT256); // Compute the equivalent taker asset fill amount. const takerAssetFillAmount = fromTokenUnitAmount( toTokenUnitAmount(makerAssetFillAmount, MAKER_DECIMALS) // Reduce the deposit rate by the exchange rate. .times(depositRate), TAKER_DECIMALS, ); // Which should equal the entire taker token balance of the account owner. // We do some rounding to account for integer vs FP vs symbolic precision differences. expect(toTokenUnitAmount(takerAssetFillAmount, TAKER_DECIMALS).dp(6)).to.bignumber.eq( toTokenUnitAmount(INITIAL_TAKER_TOKEN_BALANCE, TAKER_DECIMALS).dp(6), ); }); it('returns the correct amount if taker:maker deposit rate is 1:1 and' + 'token != taker token', async () => { const checkInfo = createBalanceCheckInfo({ takerTokenAddress: randomAddress(), // These amounts should be effectively ignored in the final computation // because the token being deposited is not the taker token. orderMakerToTakerRate: fromTokenUnitAmount( fromTokenUnitAmount(10, TAKER_DECIMALS).div(fromTokenUnitAmount(100, MAKER_DECIMALS)), ), actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(1, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, ], }); const makerAssetFillAmount = await testContract.getDepositableMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.bignumber.eq(takerToMakerAmount(INITIAL_TAKER_TOKEN_BALANCE)); }); it('returns the smallest viable maker amount with multiple deposits', async () => { // The taker tokens getting exchanged in will only partially cover the deposit. const exchangeRate = 0.1; const checkInfo = createBalanceCheckInfo({ orderMakerToTakerRate: fromTokenUnitAmount( fromTokenUnitAmount(exchangeRate, TAKER_DECIMALS).div(fromTokenUnitAmount(1, MAKER_DECIMALS)), ), actions: [ // Technically, deposits of the same token are not allowed, but the // check isn't done in this function so we'll do this to simulate // two deposits to distinct tokens. { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(Math.random() + exchangeRate, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(Math.random() + exchangeRate, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, ], }); const makerAssetFillAmount = await testContract.getDepositableMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.not.bignumber.eq(constants.MAX_UINT256); // Extract the deposit rates. const depositRates = checkInfo.actions.map(a => toTokenUnitAmount(a.conversionRateNumerator, TAKER_DECIMALS).div( toTokenUnitAmount(a.conversionRateDenominator, MAKER_DECIMALS), ), ); // The largest deposit rate will result in the smallest maker asset fill amount. const maxDepositRate = BigNumber.max(...depositRates); // Compute the equivalent taker asset fill amounts. const takerAssetFillAmount = fromTokenUnitAmount( toTokenUnitAmount(makerAssetFillAmount, MAKER_DECIMALS) // Reduce the deposit rate by the exchange rate. .times(maxDepositRate.minus(exchangeRate)), TAKER_DECIMALS, ); // Which should equal the entire taker token balance of the account owner. // We do some rounding to account for integer vs FP vs symbolic precision differences. expect(toTokenUnitAmount(takerAssetFillAmount, TAKER_DECIMALS).dp(5)).to.bignumber.eq( toTokenUnitAmount(INITIAL_TAKER_TOKEN_BALANCE, TAKER_DECIMALS).dp(5), ); }); it( 'returns zero if the maker has no taker tokens and the deposit rate is' + 'greater than the exchange rate', async () => { await testContract .setTokenBalance(takerTokenAddress, ACCOUNT_OWNER, constants.ZERO_AMOUNT) .awaitTransactionSuccessAsync(); // The taker tokens getting exchanged in will only partially cover the deposit. const exchangeRate = 0.1; const depositRate = Math.random() + exchangeRate; const checkInfo = createBalanceCheckInfo({ orderMakerToTakerRate: fromTokenUnitAmount(fromTokenUnitAmount(1 / exchangeRate, MAKER_DECIMALS)), actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, ], }); const makerAssetFillAmount = await testContract.getDepositableMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.bignumber.eq(0); }, ); it( 'returns zero if dydx has no taker token allowance and the deposit rate is' + 'greater than the exchange rate', async () => { await testContract .setTokenApproval(takerTokenAddress, ACCOUNT_OWNER, dydx.address, constants.ZERO_AMOUNT) .awaitTransactionSuccessAsync(); // The taker tokens getting exchanged in will only partially cover the deposit. const exchangeRate = 0.1; const depositRate = Math.random() + exchangeRate; const checkInfo = createBalanceCheckInfo({ orderMakerToTakerRate: fromTokenUnitAmount(fromTokenUnitAmount(1 / exchangeRate, MAKER_DECIMALS)), actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(depositRate, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, ], }); const makerAssetFillAmount = await testContract.getDepositableMakerAmount(checkInfo).callAsync(); expect(makerAssetFillAmount).to.bignumber.eq(0); }, ); }); describe('_areActionsWellFormed()', () => { it('Returns false if no actions', async () => { const checkInfo = createBalanceCheckInfo({ actions: [], }); const r = await testContract.areActionsWellFormed(checkInfo).callAsync(); expect(r).to.be.false(); }); it('Returns false if there is an account index out of range in deposits', async () => { const checkInfo = createBalanceCheckInfo({ accounts: DYDX_CONFIG.accounts.slice(0, 2).map(a => a.accountId), actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(2), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, ], }); const r = await testContract.areActionsWellFormed(checkInfo).callAsync(); expect(r).to.be.false(); }); it('Returns false if a market is not unique among deposits', async () => { const checkInfo = createBalanceCheckInfo({ actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, ], }); const r = await testContract.areActionsWellFormed(checkInfo).callAsync(); expect(r).to.be.false(); }); it('Returns false if no withdraw at the end', async () => { const checkInfo = createBalanceCheckInfo({ actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, ], }); const r = await testContract.areActionsWellFormed(checkInfo).callAsync(); expect(r).to.be.false(); }); it('Returns false if a withdraw comes before a deposit', async () => { const checkInfo = createBalanceCheckInfo({ actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, ], }); const r = await testContract.areActionsWellFormed(checkInfo).callAsync(); expect(r).to.be.false(); }); it('Returns false if more than one withdraw', async () => { const checkInfo = createBalanceCheckInfo({ actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, ], }); const r = await testContract.areActionsWellFormed(checkInfo).callAsync(); expect(r).to.be.false(); }); it('Returns false if withdraw is not for maker token', async () => { const checkInfo = createBalanceCheckInfo({ actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, ], }); const r = await testContract.areActionsWellFormed(checkInfo).callAsync(); expect(r).to.be.false(); }); it('Returns false if withdraw is for an out of range account', async () => { const checkInfo = createBalanceCheckInfo({ accounts: DYDX_CONFIG.accounts.slice(0, 2).map(a => a.accountId), actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(2), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, ], }); const r = await testContract.areActionsWellFormed(checkInfo).callAsync(); expect(r).to.be.false(); }); it('Can return true if no deposit', async () => { const checkInfo = createBalanceCheckInfo({ actions: [ { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, ], }); const r = await testContract.areActionsWellFormed(checkInfo).callAsync(); expect(r).to.be.true(); }); it('Can return true if no deposit', async () => { const checkInfo = createBalanceCheckInfo({ actions: [ { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, ], }); const r = await testContract.areActionsWellFormed(checkInfo).callAsync(); expect(r).to.be.true(); }); it('Can return true with multiple deposits', async () => { const checkInfo = createBalanceCheckInfo({ actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, ], }); const r = await testContract.areActionsWellFormed(checkInfo).callAsync(); expect(r).to.be.true(); }); }); function createERC20AssetData(tokenAddress: string): string { return assetDataContract.ERC20Token(tokenAddress).getABIEncodedTransactionData(); } function createERC721AssetData(tokenAddress: string, tokenId: BigNumber): string { return assetDataContract.ERC721Token(tokenAddress, tokenId).getABIEncodedTransactionData(); } function createBridgeAssetData( makerTokenAddress_: string, bridgeAddress: string, data: Partial = {}, ): string { return assetDataContract .ERC20Bridge( makerTokenAddress_, bridgeAddress, dydxBridgeDataEncoder.encode({ bridgeData: { accountNumbers: DYDX_CONFIG.accounts.slice(0, 1).map(a => a.accountId), actions: [ { actionType: DydxBridgeActionType.Deposit, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: fromTokenUnitAmount(1, TAKER_DECIMALS), conversionRateDenominator: fromTokenUnitAmount(1, MAKER_DECIMALS), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, ], ...data, }, }), ) .getABIEncodedTransactionData(); } function createOrder(orderFields: Partial = {}): Order { return { chainId: 1, exchangeAddress: randomAddress(), salt: getRandomInteger(1, constants.MAX_UINT256), expirationTimeSeconds: getRandomInteger(1, constants.MAX_UINT256), feeRecipientAddress: randomAddress(), makerAddress: ACCOUNT_OWNER, takerAddress: constants.NULL_ADDRESS, senderAddress: constants.NULL_ADDRESS, makerFee: getRandomInteger(1, constants.MAX_UINT256), takerFee: getRandomInteger(1, constants.MAX_UINT256), makerAssetAmount: fromTokenUnitAmount(100, MAKER_DECIMALS), takerAssetAmount: fromTokenUnitAmount(10, TAKER_DECIMALS), makerAssetData: createBridgeAssetData(makerTokenAddress, BRIDGE_ADDRESS), takerAssetData: createERC20AssetData(takerTokenAddress), makerFeeAssetData: constants.NULL_BYTES, takerFeeAssetData: constants.NULL_BYTES, ...orderFields, }; } describe('getDydxMakerBalance()', () => { it('returns nonzero with valid order', async () => { const order = createOrder(); const r = await testContract.getDydxMakerBalance(order, dydx.address).callAsync(); expect(r).to.not.bignumber.eq(0); }); it('returns nonzero with valid order with an ERC721 taker asset', async () => { const order = createOrder({ takerAssetData: createERC721AssetData(randomAddress(), getRandomInteger(1, constants.MAX_UINT256)), }); const r = await testContract.getDydxMakerBalance(order, dydx.address).callAsync(); expect(r).to.not.bignumber.eq(0); }); it('returns 0 if bridge is not a local operator', async () => { const order = createOrder({ makerAssetData: createBridgeAssetData(ACCOUNT_OWNER, randomAddress()), }); const r = await testContract.getDydxMakerBalance(order, dydx.address).callAsync(); expect(r).to.bignumber.eq(0); }); it('returns 0 if bridge data does not have well-formed actions', async () => { const order = createOrder({ makerAssetData: createBridgeAssetData(takerTokenAddress, BRIDGE_ADDRESS, { // Two withdraw actions is invalid. actions: [ { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(0), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: new BigNumber(0), conversionRateDenominator: new BigNumber(0), }, ], }), }); const r = await testContract.getDydxMakerBalance(order, dydx.address).callAsync(); expect(r).to.bignumber.eq(0); }); it('returns 0 if the maker token withdraw rate is < 1', async () => { const order = createOrder({ makerAssetData: createBridgeAssetData(takerTokenAddress, BRIDGE_ADDRESS, { actions: [ { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), conversionRateNumerator: new BigNumber(0.99e18), conversionRateDenominator: new BigNumber(1e18), }, ], }), }); const r = await testContract.getDydxMakerBalance(order, dydx.address).callAsync(); expect(r).to.bignumber.eq(0); }); }); }); // tslint:disable-next-line: max-file-line-count