diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index 66721bc89d..874141201b 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -11,6 +11,14 @@ { "version": "3.2.0", "changes": [ + { + "note": "Add more types and functions to `IDydx`", + "pr": 2466 + }, + { + "note": "Rename `DydxBrigeAction.accountId` to `DydxBridgeAction.accountIdx`", + "pr": 2466 + }, { "note": "Fix broken tests.", "pr": 2462 diff --git a/contracts/asset-proxy/contracts/src/bridges/DydxBridge.sol b/contracts/asset-proxy/contracts/src/bridges/DydxBridge.sol index 1a5bf1bdd4..70c97f268f 100644 --- a/contracts/asset-proxy/contracts/src/bridges/DydxBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/DydxBridge.sol @@ -191,7 +191,7 @@ contract DydxBridge is depositAction = IDydx.ActionArgs({ actionType: IDydx.ActionType.Deposit, // deposit tokens. amount: dydxAmount, // amount to deposit. - accountId: bridgeAction.accountId, // index in the `accounts` when calling `operate`. + accountIdx: bridgeAction.accountIdx, // index in the `accounts` when calling `operate`. primaryMarketId: bridgeAction.marketId, // indicates which token to deposit. otherAddress: depositFrom, // deposit from the account owner. // unused parameters @@ -229,7 +229,7 @@ contract DydxBridge is withdrawAction = IDydx.ActionArgs({ actionType: IDydx.ActionType.Withdraw, // withdraw tokens. amount: amountToWithdraw, // amount to withdraw. - accountId: bridgeAction.accountId, // index in the `accounts` when calling `operate`. + accountIdx: bridgeAction.accountIdx, // index in the `accounts` when calling `operate`. primaryMarketId: bridgeAction.marketId, // indicates which token to withdraw. otherAddress: withdrawTo, // withdraw tokens to this address. // unused parameters diff --git a/contracts/asset-proxy/contracts/src/interfaces/IDydx.sol b/contracts/asset-proxy/contracts/src/interfaces/IDydx.sol index 56e5e9da36..d784bde2cc 100644 --- a/contracts/asset-proxy/contracts/src/interfaces/IDydx.sol +++ b/contracts/asset-proxy/contracts/src/interfaces/IDydx.sol @@ -45,7 +45,7 @@ interface IDydx { /// parsed into before being processed. struct ActionArgs { ActionType actionType; - uint256 accountId; + uint256 accountIdx; AssetAmount amount; uint256 primaryMarketId; uint256 secondaryMarketId; @@ -71,6 +71,31 @@ interface IDydx { uint256 value; } + struct D256 { + uint256 value; + } + + struct Value { + uint256 value; + } + + struct Price { + uint256 value; + } + + /// @dev The global risk parameters that govern the health and security of the system + struct RiskParams { + // Required ratio of over-collateralization + D256 marginRatio; + // Percentage penalty incurred by liquidated accounts + D256 liquidationSpread; + // Percentage of the borrower's interest fee that gets passed to the suppliers + D256 earningsRate; + // The minimum absolute borrow value of an account + // There must be sufficient incentivize to liquidate undercollateralized accounts + Value minBorrowedValue; + } + /// @dev The main entry-point to Solo that allows users and contracts to manage accounts. /// Take one or more actions on one or more accounts. The msg.sender must be the owner or /// operator of all accounts except for those being liquidated, vaporized, or traded with. @@ -86,4 +111,59 @@ interface IDydx { ActionArgs[] calldata actions ) external; + + /// @dev Return true if a particular address is approved as an operator for an owner's accounts. + /// Approved operators can act on the accounts of the owner as if it were the operator's own. + /// @param owner The owner of the accounts + /// @param operator The possible operator + /// @return isLocalOperator True if operator is approved for owner's accounts + function getIsLocalOperator( + address owner, + address operator + ) + external + view + returns (bool isLocalOperator); + + /// @dev Get the ERC20 token address for a market. + /// @param marketId The market to query + /// @return tokenAddress The token address + function getMarketTokenAddress( + uint256 marketId + ) + external + view + returns (address tokenAddress); + + /// @dev Get all risk parameters in a single struct. + /// @return riskParams All global risk parameters + function getRiskParams() + external + view + returns (RiskParams memory riskParams); + + /// @dev Get the price of the token for a market. + /// @param marketId The market to query + /// @return price The price of each atomic unit of the token + function getMarketPrice( + uint256 marketId + ) + external + view + returns (Price memory price); + + /// @dev Get the total supplied and total borrowed values of an account adjusted by the marginPremium + /// of each market. Supplied values are divided by (1 + marginPremium) for each market and + /// borrowed values are multiplied by (1 + marginPremium) for each market. Comparing these + /// adjusted values gives the margin-ratio of the account which will be compared to the global + /// margin-ratio when determining if the account can be liquidated. + /// @param account The account to query + /// @return supplyValue The supplied value of the account (adjusted for marginPremium) + /// @return borrowValue The borrowed value of the account (adjusted for marginPremium) + function getAdjustedAccountValues( + AccountInfo calldata account + ) + external + view + returns (Value memory supplyValue, Value memory borrowValue); } diff --git a/contracts/asset-proxy/contracts/src/interfaces/IDydxBridge.sol b/contracts/asset-proxy/contracts/src/interfaces/IDydxBridge.sol index 1b69179f49..779bf72fdc 100644 --- a/contracts/asset-proxy/contracts/src/interfaces/IDydxBridge.sol +++ b/contracts/asset-proxy/contracts/src/interfaces/IDydxBridge.sol @@ -29,7 +29,7 @@ interface IDydxBridge { struct BridgeAction { BridgeActionType actionType; // Action to run on dydx account. - uint256 accountId; // Index in `BridgeData.accountNumbers` for this action. + uint256 accountIdx; // Index in `BridgeData.accountNumbers` for this action. uint256 marketId; // Market to operate on. uint256 conversionRateNumerator; // Optional. If set, transfer amount is scaled by (conversionRateNumerator/conversionRateDenominator). uint256 conversionRateDenominator; // Optional. If set, transfer amount is scaled by (conversionRateNumerator/conversionRateDenominator). @@ -39,4 +39,4 @@ interface IDydxBridge { uint256[] accountNumbers; // Account number used to identify the owner's specific account. BridgeAction[] actions; // Actions to carry out on the owner's accounts. } -} \ No newline at end of file +} diff --git a/contracts/asset-proxy/contracts/test/TestDydxBridge.sol b/contracts/asset-proxy/contracts/test/TestDydxBridge.sol index 31dcc5359f..895e31231b 100644 --- a/contracts/asset-proxy/contracts/test/TestDydxBridge.sol +++ b/contracts/asset-proxy/contracts/test/TestDydxBridge.sol @@ -23,6 +23,7 @@ import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; import "../src/bridges/DydxBridge.sol"; +// solhint-disable no-empty-blocks contract TestDydxBridgeToken { uint256 private constant INIT_HOLDER_BALANCE = 10 * 10**18; // 10 tokens @@ -79,7 +80,7 @@ contract TestDydxBridge is event OperateAction( ActionType actionType, - uint256 accountId, + uint256 accountIdx, bool amountSign, AssetDenomination amountDenomination, AssetReference amountRef, @@ -120,7 +121,7 @@ contract TestDydxBridge is for (uint i = 0; i < actions.length; ++i) { emit OperateAction( actions[i].actionType, - actions[i].accountId, + actions[i].accountIdx, actions[i].amount.sign, actions[i].amount.denomination, actions[i].amount.ref, @@ -171,6 +172,50 @@ contract TestDydxBridge is return _testTokenAddress; } + /// @dev Unused. + function getIsLocalOperator( + address owner, + address operator + ) + external + view + returns (bool isLocalOperator) + {} + + /// @dev Unused. + function getMarketTokenAddress( + uint256 marketId + ) + external + view + returns (address tokenAddress) + {} + + /// @dev Unused. + function getRiskParams() + external + view + returns (RiskParams memory riskParams) + {} + + /// @dev Unsused. + function getMarketPrice( + uint256 marketId + ) + external + view + returns (Price memory price) + {} + + /// @dev Unused. + function getAdjustedAccountValues( + AccountInfo calldata account + ) + external + view + returns (Value memory supplyValue, Value memory borrowValue) + {} + /// @dev overrides `_getDydxAddress()` from `DeploymentConstants` to return this address. function _getDydxAddress() internal @@ -188,4 +233,4 @@ contract TestDydxBridge is { return msg.sender == ALWAYS_REVERT_ADDRESS ? address(0) : msg.sender; } -} \ No newline at end of file +} diff --git a/contracts/asset-proxy/src/dydx_bridge_encoder.ts b/contracts/asset-proxy/src/dydx_bridge_encoder.ts index 8d07ec65ab..63dcade973 100644 --- a/contracts/asset-proxy/src/dydx_bridge_encoder.ts +++ b/contracts/asset-proxy/src/dydx_bridge_encoder.ts @@ -7,7 +7,7 @@ export enum DydxBridgeActionType { export interface DydxBridgeAction { actionType: DydxBridgeActionType; - accountId: BigNumber; + accountIdx: BigNumber; marketId: BigNumber; conversionRateNumerator: BigNumber; conversionRateDenominator: BigNumber; @@ -29,7 +29,7 @@ export const dydxBridgeDataEncoder = AbiEncoder.create([ type: 'tuple[]', components: [ { name: 'actionType', type: 'uint8' }, - { name: 'accountId', type: 'uint256' }, + { name: 'accountIdx', type: 'uint256' }, { name: 'marketId', type: 'uint256' }, { name: 'conversionRateNumerator', type: 'uint256' }, { name: 'conversionRateDenominator', type: 'uint256' }, diff --git a/contracts/asset-proxy/test/dydx_bridge.ts b/contracts/asset-proxy/test/dydx_bridge.ts index 329291b0ca..3377ddfce4 100644 --- a/contracts/asset-proxy/test/dydx_bridge.ts +++ b/contracts/asset-proxy/test/dydx_bridge.ts @@ -17,14 +17,14 @@ blockchainTests.resets('DydxBridge unit tests', env => { const notAuthorized = '0x0000000000000000000000000000000000000001'; const defaultDepositAction = { actionType: DydxBridgeActionType.Deposit, - accountId: constants.ZERO_AMOUNT, + accountIdx: constants.ZERO_AMOUNT, marketId, conversionRateNumerator: constants.ZERO_AMOUNT, conversionRateDenominator: constants.ZERO_AMOUNT, }; const defaultWithdrawAction = { actionType: DydxBridgeActionType.Withdraw, - accountId: constants.ZERO_AMOUNT, + accountIdx: constants.ZERO_AMOUNT, marketId, conversionRateNumerator: constants.ZERO_AMOUNT, conversionRateDenominator: constants.ZERO_AMOUNT, @@ -118,7 +118,7 @@ blockchainTests.resets('DydxBridge unit tests', env => { for (const action of bridgeData.actions) { expectedOperateActionEvents.push({ actionType: action.actionType as number, - accountId: action.accountId, + accountIdx: action.accountIdx, amountSign: action.actionType === DydxBridgeActionType.Deposit ? true : false, amountDenomination: weiDenomination, amountRef: deltaAmountRef, diff --git a/contracts/dev-utils/CHANGELOG.json b/contracts/dev-utils/CHANGELOG.json index d04eb08985..0e8f288e99 100644 --- a/contracts/dev-utils/CHANGELOG.json +++ b/contracts/dev-utils/CHANGELOG.json @@ -18,6 +18,10 @@ { "note": "Remove `LibTransactionDecoder` export", "pr": 2464 + }, + { + "note": "Add `DydxBridge` order validation", + "pr": 2466 } ], "timestamp": 1581204851 diff --git a/contracts/dev-utils/contracts/src/Addresses.sol b/contracts/dev-utils/contracts/src/Addresses.sol index 3cd41a329c..12e58284fb 100644 --- a/contracts/dev-utils/contracts/src/Addresses.sol +++ b/contracts/dev-utils/contracts/src/Addresses.sol @@ -34,15 +34,18 @@ contract Addresses is address public erc1155ProxyAddress; address public staticCallProxyAddress; address public chaiBridgeAddress; + address public dydxBridgeAddress; constructor ( address exchange_, - address chaiBridge_ + address chaiBridge_, + address dydxBridge_ ) public { exchangeAddress = exchange_; chaiBridgeAddress = chaiBridge_; + dydxBridgeAddress = dydxBridge_; erc20ProxyAddress = IExchange(exchange_).getAssetProxy(IAssetData(address(0)).ERC20Token.selector); erc721ProxyAddress = IExchange(exchange_).getAssetProxy(IAssetData(address(0)).ERC721Token.selector); erc1155ProxyAddress = IExchange(exchange_).getAssetProxy(IAssetData(address(0)).ERC1155Assets.selector); diff --git a/contracts/dev-utils/contracts/src/AssetBalance.sol b/contracts/dev-utils/contracts/src/AssetBalance.sol index cc72becb46..45172c757e 100644 --- a/contracts/dev-utils/contracts/src/AssetBalance.sol +++ b/contracts/dev-utils/contracts/src/AssetBalance.sol @@ -28,7 +28,7 @@ import "@0x/contracts-erc1155/contracts/src/interfaces/IERC1155.sol"; import "@0x/contracts-asset-proxy/contracts/src/interfaces/IChai.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; import "./Addresses.sol"; -import "./LibAssetData.sol"; +import "./LibDydxBalance.sol"; contract AssetBalance is @@ -274,6 +274,9 @@ contract AssetBalance is uint256 chaiAllowance = LibERC20Token.allowance(_getChaiAddress(), ownerAddress, chaiBridgeAddress); // Dai allowance is unlimited if Chai allowance is unlimited allowance = chaiAllowance == _MAX_UINT256 ? _MAX_UINT256 : _convertChaiToDaiAmount(chaiAllowance); + } else if (bridgeAddress == dydxBridgeAddress) { + // Dydx bridges always have infinite allowance. + allowance = _MAX_UINT256; } // Allowance will be 0 if bridge is not supported } @@ -366,6 +369,17 @@ contract AssetBalance is if (order.makerAssetData.length < 4) { return (0, 0); } + bytes4 assetProxyId = order.makerAssetData.readBytes4(0); + // Handle dydx bridge assets. + if (assetProxyId == IAssetData(address(0)).ERC20Bridge.selector) { + (, , address bridgeAddress, ) = LibAssetData.decodeERC20BridgeAssetData(order.makerAssetData); + if (bridgeAddress == dydxBridgeAddress) { + return ( + LibDydxBalance.getDydxMakerBalance(order, dydxBridgeAddress), + _MAX_UINT256 + ); + } + } return ( getBalance(order.makerAddress, order.makerAssetData), getAssetProxyAllowance(order.makerAddress, order.makerAssetData) diff --git a/contracts/dev-utils/contracts/src/DevUtils.sol b/contracts/dev-utils/contracts/src/DevUtils.sol index 45ce184646..b275b3ccb1 100644 --- a/contracts/dev-utils/contracts/src/DevUtils.sol +++ b/contracts/dev-utils/contracts/src/DevUtils.sol @@ -40,12 +40,14 @@ contract DevUtils is { constructor ( address exchange_, - address chaiBridge_ + address chaiBridge_, + address dydxBridge_ ) public Addresses( exchange_, - chaiBridge_ + chaiBridge_, + dydxBridge_ ) LibEIP712ExchangeDomain(uint256(0), address(0)) // null args because because we only use constants {} diff --git a/contracts/dev-utils/contracts/src/LibDydxBalance.sol b/contracts/dev-utils/contracts/src/LibDydxBalance.sol new file mode 100644 index 0000000000..dac436f08d --- /dev/null +++ b/contracts/dev-utils/contracts/src/LibDydxBalance.sol @@ -0,0 +1,483 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.16; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetData.sol"; +import "@0x/contracts-asset-proxy/contracts/src/interfaces/IDydxBridge.sol"; +import "@0x/contracts-asset-proxy/contracts/src/interfaces/IDydx.sol"; +import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; +import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; +import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol"; +import "@0x/contracts-utils/contracts/src/LibBytes.sol"; +import "@0x/contracts-utils/contracts/src/LibFractions.sol"; +import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; +import "./LibAssetData.sol"; + + +// solhint-disable separate-by-one-line-in-contract +library LibDydxBalance { + + using LibBytes for bytes; + using LibSafeMath for uint256; + + /// @dev Decimal places for dydx value quantities. + uint256 private constant DYDX_UNITS_DECIMALS = 18; + /// @dev Base units for dydx value quantities. + uint256 private constant DYDX_UNITS_BASE = 10 ** DYDX_UNITS_DECIMALS; + + /// @dev A fraction/rate. + struct Fraction { + uint256 n; + uint256 d; + } + + /// @dev Structure that holds all pertinent info needed to perform a balance + /// check. + struct BalanceCheckInfo { + IDydx dydx; + address bridgeAddress; + address makerAddress; + address makerTokenAddress; + address takerTokenAddress; + uint256 makerAssetAmount; + uint256 takerAssetAmount; + uint256[] accounts; + IDydxBridge.BridgeAction[] actions; + } + + /// @dev Get the maker asset balance of an order with a `DydxBridge` maker asset. + /// @param order An order with a dydx maker asset. + /// @param dydx The address of the dydx contract. + /// @return balance The maker asset balance. + function getDydxMakerBalance(LibOrder.Order memory order, address dydx) + public + view + returns (uint256 balance) + { + BalanceCheckInfo memory info = _getBalanceCheckInfo(order, dydx); + // The Dydx bridge must be an operator for the maker. + if (!info.dydx.getIsLocalOperator(info.makerAddress, info.bridgeAddress)) { + return 0; + } + // Actions must be well-formed. + if (!_areActionsWellFormed(info)) { + return 0; + } + // If the rate we withdraw maker tokens is < 1, the asset proxy will + // throw because we will always transfer less maker tokens than asked. + if (_ltf(_getMakerTokenWithdrawRate(info), Fraction(1, 1))) { + return 0; + } + // The maker balance is the smaller of: + return LibSafeMath.min256( + // How many times we can execute all the deposit actions. + _getDepositableMakerAmount(info), + // How many times we can execute all the actions before the an + // account becomes undercollateralized. + _getSolventMakerAmount(info) + ); + } + + /// @dev Checks that: + /// 1. Actions are arranged as [...deposits, withdraw]. + /// 2. There is only one deposit for each market ID. + /// 3. Every action has a valid account index. + /// 4. There is exactly one withdraw at the end and it is for the + /// maker token. + /// @param info State from `_getBalanceCheckInfo()`. + /// @return areWellFormed Whether the actions are well-formed. + function _areActionsWellFormed(BalanceCheckInfo memory info) + internal + view + returns (bool areWellFormed) + { + if (info.actions.length == 0) { + return false; + } + uint256 depositCount = 0; + // Count the number of deposits. + for (; depositCount < info.actions.length; ++depositCount) { + IDydxBridge.BridgeAction memory action = info.actions[depositCount]; + if (action.actionType != IDydxBridge.BridgeActionType.Deposit) { + break; + } + // Search all prior actions for the same market ID. + uint256 marketId = action.marketId; + for (uint256 j = 0; j < depositCount; ++j) { + if (info.actions[j].marketId == marketId) { + // Market ID is not unique. + return false; + } + } + // Check that the account index is within the valid range. + if (action.accountIdx >= info.accounts.length) { + return false; + } + } + // There must be exactly one withdraw action at the end. + if (depositCount + 1 != info.actions.length) { + return false; + } + IDydxBridge.BridgeAction memory withdraw = info.actions[depositCount]; + if (withdraw.actionType != IDydxBridge.BridgeActionType.Withdraw) { + return false; + } + // And it must be for the maker token. + if (info.dydx.getMarketTokenAddress(withdraw.marketId) != info.makerTokenAddress) { + return false; + } + // Check the account index. + return withdraw.accountIdx < info.accounts.length; + } + + /// @dev Returns the rate at which we withdraw maker tokens. + /// @param info State from `_getBalanceCheckInfo()`. + /// @return makerTokenWithdrawRate Maker token withdraw rate. + function _getMakerTokenWithdrawRate(BalanceCheckInfo memory info) + internal + pure + returns (Fraction memory makerTokenWithdrawRate) + { + // The last action is always a withdraw for the maker token. + IDydxBridge.BridgeAction memory withdraw = info.actions[info.actions.length - 1]; + return _getActionRate(withdraw); + } + + /// @dev Get how much maker asset we can transfer before a deposit fails. + /// @param info State from `_getBalanceCheckInfo()`. + function _getDepositableMakerAmount(BalanceCheckInfo memory info) + internal + view + returns (uint256 depositableMakerAmount) + { + depositableMakerAmount = uint256(-1); + // The conversion rate from maker -> taker. + Fraction memory makerToTakerRate = Fraction( + info.takerAssetAmount, + info.makerAssetAmount + ); + // Take the minimum maker amount from all deposits. + for (uint256 i = 0; i < info.actions.length; ++i) { + IDydxBridge.BridgeAction memory action = info.actions[i]; + // Only looking at deposit actions. + if (action.actionType != IDydxBridge.BridgeActionType.Deposit) { + continue; + } + Fraction memory depositRate = _getActionRate(action); + // Taker tokens will be transferred to the maker for every fill, so + // we reduce the effective deposit rate if we're depositing the taker + // token. + address depositToken = info.dydx.getMarketTokenAddress(action.marketId); + if (info.takerTokenAddress != address(0) && depositToken == info.takerTokenAddress) { + // `depositRate = max(0, depositRate - makerToTakerRate)` + if (_ltf(makerToTakerRate, depositRate)) { + depositRate = _subf(depositRate, makerToTakerRate); + } else { + depositRate = Fraction(0, 1); + } + } + // If the deposit rate is > 0, we are limited by the transferrable + // token balance of the maker. + if (_gtf(depositRate, Fraction(0, 1))) { + uint256 supply = _getTransferabeTokenAmount( + depositToken, + info.makerAddress, + address(info.dydx) + ); + depositableMakerAmount = LibSafeMath.min256( + depositableMakerAmount, + LibMath.getPartialAmountFloor( + depositRate.d, + depositRate.n, + supply + ) + ); + } + } + } + + /// @dev Get how much maker asset we can transfer before an account + /// becomes insolvent. + /// @param info State from `_getBalanceCheckInfo()`. + function _getSolventMakerAmount(BalanceCheckInfo memory info) + internal + view + returns (uint256 solventMakerAmount) + { + solventMakerAmount = uint256(-1); + assert(info.actions.length >= 1); + IDydxBridge.BridgeAction memory withdraw = info.actions[info.actions.length - 1]; + assert(withdraw.actionType == IDydxBridge.BridgeActionType.Withdraw); + Fraction memory minCr = _getMinimumCollateralizationRatio(info.dydx); + // CR < 1 will cause math underflows. + require(_gtef(minCr, Fraction(1, 1)), "DevUtils/MIN_CR_MUST_BE_GTE_ONE"); + // Loop through the accounts. + for (uint256 accountIdx = 0; accountIdx < info.accounts.length; ++accountIdx) { + (uint256 supplyValue, uint256 borrowValue) = + _getAccountValues(info, info.accounts[accountIdx]); + // All accounts must currently be solvent. + if (borrowValue != 0 && _ltf(Fraction(supplyValue, borrowValue), minCr)) { + return 0; + } + // If this is the same account used to in the withdraw/borrow action, + // compute the maker amount at which it will become insolvent. + if (accountIdx != withdraw.accountIdx) { + continue; + } + // Compute the deposit/collateralization rate, which is the rate at + // which (USD) value is added to the account across all markets. + Fraction memory dd = Fraction(0, 1); + for (uint256 i = 0; i < info.actions.length - 1; ++i) { + IDydxBridge.BridgeAction memory deposit = info.actions[i]; + assert(deposit.actionType == IDydxBridge.BridgeActionType.Deposit); + if (deposit.accountIdx == accountIdx) { + dd = _addf( + dd, + _toQuoteValue( + info.dydx, + deposit.marketId, + _getActionRate(deposit) + ) + ); + } + } + // Compute the borrow/withdraw rate, which is the rate at which + // (USD) value is deducted from the account. + Fraction memory db = _toQuoteValue( + info.dydx, + withdraw.marketId, + _getActionRate(withdraw) + ); + // If the deposit to withdraw ratio is >= the minimum collateralization + // rate, then we will never become insolvent at these prices. + if (_gtef(_divf(dd, db), minCr)) { + continue; + } + // The collateralization ratio for this account, parameterized by + // `t` (maker amount), is given by: + // `cr = (supplyValue + t * dd) / (borrowValue + t * db)` + // Solving for `t` gives us: + // `t = (supplyValue - cr * borrowValue) / (cr * db - dd)` + // TODO(dorothy-zbornak): It'll also revert when getting extremely + // close to the minimum collateralization ratio. + Fraction memory t = _divf( + _subf( + Fraction(supplyValue, DYDX_UNITS_BASE), + _mulf(minCr, Fraction(borrowValue, DYDX_UNITS_BASE)) + ), + _subf(_mulf(minCr, db), dd) + ); + solventMakerAmount = LibSafeMath.min256( + solventMakerAmount, + t.n.safeDiv(t.d) + ); + } + } + + /// @dev Create a `BalanceCheckInfo` struct. + /// @param order An order with a `DydxBridge` maker asset. + /// @param dydx The address of the Dydx contract. + /// @return info The `BalanceCheckInfo` struct. + function _getBalanceCheckInfo(LibOrder.Order memory order, address dydx) + private + pure + returns (BalanceCheckInfo memory info) + { + bytes memory rawBridgeData; + (, info.makerTokenAddress, info.bridgeAddress, rawBridgeData) = + LibAssetData.decodeERC20BridgeAssetData(order.makerAssetData); + info.dydx = IDydx(dydx); + info.makerAddress = order.makerAddress; + if (order.takerAssetData.readBytes4(0) == IAssetData(0).ERC20Token.selector) { + (, info.takerTokenAddress) = + LibAssetData.decodeERC20AssetData(order.takerAssetData); + } + info.makerAssetAmount = order.makerAssetAmount; + info.takerAssetAmount = order.takerAssetAmount; + (IDydxBridge.BridgeData memory bridgeData) = + abi.decode(rawBridgeData, (IDydxBridge.BridgeData)); + info.accounts = bridgeData.accountNumbers; + info.actions = bridgeData.actions; + } + + /// @dev Returns the conversion rate for an action, treating infinites as 1. + /// @param action A `BridgeAction`. + function _getActionRate(IDydxBridge.BridgeAction memory action) + private + pure + returns (Fraction memory rate) + { + rate = action.conversionRateDenominator == 0 + ? Fraction(1, 1) + : _normalizef( + Fraction( + action.conversionRateNumerator, + action.conversionRateDenominator + ) + ); + } + + /// @dev Get the global minimum collateralization ratio required for + /// an account to be considered solvent. + /// @param dydx The Dydx interface. + function _getMinimumCollateralizationRatio(IDydx dydx) + private + view + returns (Fraction memory ratio) + { + IDydx.RiskParams memory riskParams = dydx.getRiskParams(); + return _normalizef( + Fraction( + riskParams.marginRatio.value, + DYDX_UNITS_BASE + ) + ); + } + + /// @dev Get the quote (USD) value of a rate within a market. + /// @param dydx The Dydx interface. + /// @param marketId Dydx market ID. + /// @param rate Rate to scale by price. + function _toQuoteValue(IDydx dydx, uint256 marketId, Fraction memory rate) + private + view + returns (Fraction memory quotedRate) + { + IDydx.Price memory price = dydx.getMarketPrice(marketId); + uint8 tokenDecimals = LibERC20Token.decimals(dydx.getMarketTokenAddress(marketId)); + return _mulf( + Fraction( + price.value, + 10 ** uint256(DYDX_UNITS_DECIMALS + tokenDecimals) + ), + rate + ); + } + + /// @dev Get the total supply and borrow values for an account across all markets. + /// @param info State from `_getBalanceCheckInfo()`. + /// @param account The Dydx account identifier. + function _getAccountValues(BalanceCheckInfo memory info, uint256 account) + private + view + returns (uint256 supplyValue, uint256 borrowValue) + { + (IDydx.Value memory supplyValue_, IDydx.Value memory borrowValue_) = + info.dydx.getAdjustedAccountValues(IDydx.AccountInfo( + info.makerAddress, + account + )); + return (supplyValue_.value, borrowValue_.value); + } + + /// @dev Get the amount of an ERC20 token held by `owner` that can be transferred + /// by `spender`. + /// @param tokenAddress The address of the ERC20 token. + /// @param owner The address of the token holder. + /// @param spender The address of the token spender. + function _getTransferabeTokenAmount( + address tokenAddress, + address owner, + address spender + ) + private + view + returns (uint256 transferableAmount) + { + return LibSafeMath.min256( + LibERC20Token.allowance(tokenAddress, owner, spender), + LibERC20Token.balanceOf(tokenAddress, owner) + ); + } + + /*** Fraction helpers ***/ + + /// @dev Check if `a < b`. + function _ltf(Fraction memory a, Fraction memory b) + private + pure + returns (bool isLessThan) + { + return LibFractions.cmp(a.n, a.d, b.n, b.d) == -1; + } + + /// @dev Check if `a > b`. + function _gtf(Fraction memory a, Fraction memory b) + private + pure + returns (bool isGreaterThan) + { + return LibFractions.cmp(a.n, a.d, b.n, b.d) == 1; + } + + /// @dev Check if `a >= b`. + function _gtef(Fraction memory a, Fraction memory b) + private + pure + returns (bool isGreaterThanOrEqual) + { + return !_ltf(a, b); + } + + /// @dev Compute `a + b`. + function _addf(Fraction memory a, Fraction memory b) + private + pure + returns (Fraction memory r) + { + (r.n, r.d) = LibFractions.add(a.n, a.d, b.n, b.d); + } + + /// @dev Compute `a - b`. + function _subf(Fraction memory a, Fraction memory b) + private + pure + returns (Fraction memory r) + { + (r.n, r.d) = LibFractions.sub(a.n, a.d, b.n, b.d); + } + + /// @dev Compute `a * b`. + function _mulf(Fraction memory a, Fraction memory b) + private + pure + returns (Fraction memory r) + { + (r.n, r.d) = LibFractions.mul(a.n, a.d, b.n, b.d); + } + + /// @dev Compute `a / b`. + function _divf(Fraction memory a, Fraction memory b) + private + pure + returns (Fraction memory r) + { + (r.n, r.d) = LibFractions.mul(a.n, a.d, b.d, b.n); + } + + /// @dev Normalize a fraction to prevent arithmetic overflows. + function _normalizef(Fraction memory f) + private + pure + returns (Fraction memory r) + { + (r.n, r.d) = LibFractions.normalize(f.n, f.d); + } +} diff --git a/contracts/dev-utils/contracts/test/TestDydx.sol b/contracts/dev-utils/contracts/test/TestDydx.sol new file mode 100644 index 0000000000..c3c7a62e1c --- /dev/null +++ b/contracts/dev-utils/contracts/test/TestDydx.sol @@ -0,0 +1,162 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.16; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-asset-proxy/contracts/src/interfaces/IDydx.sol"; +import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; + + +// solhint-disable separate-by-one-line-in-contract +contract TestDydx { + + struct OperatorConfig { + address owner; + address operator; + } + + struct AccountConfig { + address owner; + uint256 accountId; + int256[] balances; + } + + struct MarketInfo { + address token; + uint256 price; + } + + struct TestConfig { + uint256 marginRatio; + OperatorConfig[] operators; + AccountConfig[] accounts; + MarketInfo[] markets; + } + + mapping (bytes32 => bool) private _operators; + mapping (bytes32 => int256) private _balance; + MarketInfo[] private _markets; + uint256 private _marginRatio; + + constructor(TestConfig memory config) public { + _marginRatio = config.marginRatio; + for (uint256 marketId = 0; marketId < config.markets.length; ++marketId) { + _markets.push(config.markets[marketId]); + } + for (uint256 i = 0; i < config.operators.length; ++i) { + OperatorConfig memory op = config.operators[i]; + _operators[_getOperatorHash(op.owner, op.operator)] = true; + } + for (uint256 i = 0; i < config.accounts.length; ++i) { + AccountConfig memory acct = config.accounts[i]; + for (uint256 marketId = 0; marketId < acct.balances.length; ++marketId) { + _balance[_getBalanceHash(acct.owner, acct.accountId, marketId)] = + acct.balances[marketId]; + } + } + } + + function getIsLocalOperator( + address owner, + address operator + ) + external + view + returns (bool isLocalOperator) + { + return _operators[_getOperatorHash(owner, operator)]; + } + + function getMarketTokenAddress( + uint256 marketId + ) + external + view + returns (address tokenAddress) + { + return _markets[marketId].token; + } + + function getRiskParams() + external + view + returns (IDydx.RiskParams memory riskParams) + { + return IDydx.RiskParams({ + marginRatio: IDydx.D256(_marginRatio), + liquidationSpread: IDydx.D256(0), + earningsRate: IDydx.D256(0), + minBorrowedValue: IDydx.Value(0) + }); + } + + function getMarketPrice( + uint256 marketId + ) + external + view + returns (IDydx.Price memory price) + { + return IDydx.Price(_markets[marketId].price); + } + + function getAdjustedAccountValues( + IDydx.AccountInfo calldata account + ) + external + view + returns (IDydx.Value memory supplyValue, IDydx.Value memory borrowValue) + { + for (uint256 marketId = 0; marketId < _markets.length; ++marketId) { + MarketInfo memory market = _markets[marketId]; + int256 balance = + _balance[_getBalanceHash(account.owner, account.number, marketId)]; + uint256 decimals = LibERC20Token.decimals(market.token); + balance = balance * int256(market.price) / int256(10 ** decimals); + if (balance >= 0) { + supplyValue.value += uint256(balance); + } else { + borrowValue.value += uint256(-balance); + } + } + } + + function _getOperatorHash(address owner, address operator) + private + pure + returns (bytes32 operatorHash) + { + return keccak256(abi.encode( + owner, + operator + )); + } + + function _getBalanceHash(address owner, uint256 accountId, uint256 marketId) + private + pure + returns (bytes32 balanceHash) + { + return keccak256(abi.encode( + owner, + accountId, + marketId + )); + } +} diff --git a/contracts/dev-utils/contracts/test/TestLibDydxBalance.sol b/contracts/dev-utils/contracts/test/TestLibDydxBalance.sol new file mode 100644 index 0000000000..0ab8dc037d --- /dev/null +++ b/contracts/dev-utils/contracts/test/TestLibDydxBalance.sol @@ -0,0 +1,116 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.16; +pragma experimental ABIEncoderV2; + +import "../src/LibDydxBalance.sol"; + + +contract TestLibDydxBalanceToken { + + uint8 public decimals; + mapping (address => uint256) public balanceOf; + mapping (address => mapping (address => uint256)) public allowance; + + constructor(uint8 decimals_) public { + decimals = decimals_; + } + + function setBalance(address owner, uint256 balance) external { + balanceOf[owner] = balance; + } + + function setApproval( + address owner, + address spender, + uint256 allowance_ + ) + external + { + allowance[owner][spender] = allowance_; + } +} + + +contract TestLibDydxBalance { + + mapping (address => TestLibDydxBalanceToken) private tokens; + + function createToken(uint8 decimals) external returns (address) { + TestLibDydxBalanceToken token = new TestLibDydxBalanceToken(decimals); + return address(tokens[address(token)] = token); + } + + function setTokenBalance( + address tokenAddress, + address owner, + uint256 balance + ) + external + { + tokens[tokenAddress].setBalance(owner, balance); + } + + function setTokenApproval( + address tokenAddress, + address owner, + address spender, + uint256 allowance + ) + external + { + tokens[tokenAddress].setApproval(owner, spender, allowance); + } + + function getDydxMakerBalance(LibOrder.Order memory order, address dydx) + public + view + returns (uint256 balance) + { + return LibDydxBalance.getDydxMakerBalance(order, dydx); + } + + function getSolventMakerAmount( + LibDydxBalance.BalanceCheckInfo memory info + ) + public + view + returns (uint256 solventMakerAmount) + { + return LibDydxBalance._getSolventMakerAmount(info); + } + + function getDepositableMakerAmount( + LibDydxBalance.BalanceCheckInfo memory info + ) + public + view + returns (uint256 depositableMakerAmount) + { + return LibDydxBalance._getDepositableMakerAmount(info); + } + + function areActionsWellFormed(LibDydxBalance.BalanceCheckInfo memory info) + public + view + returns (bool areWellFormed) + { + return LibDydxBalance._areActionsWellFormed(info); + } +} diff --git a/contracts/dev-utils/package.json b/contracts/dev-utils/package.json index 830a721039..b3cf58b3ee 100644 --- a/contracts/dev-utils/package.json +++ b/contracts/dev-utils/package.json @@ -8,7 +8,7 @@ "main": "lib/src/index.js", "scripts": { "build": "yarn pre_build && tsc -b", - "test": "yarn assert_deployable", + "test": "yarn assert_deployable && yarn mocha -t 10000 -b ./lib/test/**_test.js", "assert_deployable": "node -e \"const bytecodeLen = (require('./generated-artifacts/DevUtils.json').compilerOutput.evm.bytecode.object.length-2)/2; assert(bytecodeLen<=0x6000,'DevUtils contract is too big to deploy, per EIP-170. '+bytecodeLen+'>'+0x6000)\"", "build:ci": "yarn build", "pre_build": "run-s compile quantify_bytecode contracts:gen generate_contract_wrappers contracts:copy", @@ -27,8 +27,8 @@ "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" }, "config": { - "publicInterfaceContracts": "DevUtils,LibAssetData,LibOrderTransferSimulation,LibTransactionDecoder", - "abis": "./test/generated-artifacts/@(Addresses|AssetBalance|DevUtils|EthBalanceChecker|ExternalFunctions|LibAssetData|LibOrderTransferSimulation|LibTransactionDecoder|OrderTransferSimulationUtils|OrderValidationUtils).json", + "publicInterfaceContracts": "DevUtils,LibAssetData,LibDydxBalance,LibOrderTransferSimulation,LibTransactionDecoder", + "abis": "./test/generated-artifacts/@(Addresses|AssetBalance|DevUtils|EthBalanceChecker|ExternalFunctions|LibAssetData|LibDydxBalance|LibOrderTransferSimulation|LibTransactionDecoder|OrderTransferSimulationUtils|OrderValidationUtils|TestDydx|TestLibDydxBalance).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/dev-utils/src/artifacts.ts b/contracts/dev-utils/src/artifacts.ts index 37178cef1e..bdc1925bbc 100644 --- a/contracts/dev-utils/src/artifacts.ts +++ b/contracts/dev-utils/src/artifacts.ts @@ -7,11 +7,13 @@ import { ContractArtifact } from 'ethereum-types'; import * as DevUtils from '../generated-artifacts/DevUtils.json'; import * as LibAssetData from '../generated-artifacts/LibAssetData.json'; +import * as LibDydxBalance from '../generated-artifacts/LibDydxBalance.json'; import * as LibOrderTransferSimulation from '../generated-artifacts/LibOrderTransferSimulation.json'; import * as LibTransactionDecoder from '../generated-artifacts/LibTransactionDecoder.json'; export const artifacts = { DevUtils: DevUtils as ContractArtifact, LibAssetData: LibAssetData as ContractArtifact, + LibDydxBalance: LibDydxBalance as ContractArtifact, LibOrderTransferSimulation: LibOrderTransferSimulation as ContractArtifact, LibTransactionDecoder: LibTransactionDecoder as ContractArtifact, }; diff --git a/contracts/dev-utils/src/wrappers.ts b/contracts/dev-utils/src/wrappers.ts index c00882a21a..e3af16194b 100644 --- a/contracts/dev-utils/src/wrappers.ts +++ b/contracts/dev-utils/src/wrappers.ts @@ -5,5 +5,6 @@ */ export * from '../generated-wrappers/dev_utils'; export * from '../generated-wrappers/lib_asset_data'; +export * from '../generated-wrappers/lib_dydx_balance'; export * from '../generated-wrappers/lib_order_transfer_simulation'; export * from '../generated-wrappers/lib_transaction_decoder'; diff --git a/contracts/dev-utils/test/artifacts.ts b/contracts/dev-utils/test/artifacts.ts index 7a57acf66b..e4c1802bc0 100644 --- a/contracts/dev-utils/test/artifacts.ts +++ b/contracts/dev-utils/test/artifacts.ts @@ -11,10 +11,13 @@ import * as DevUtils from '../test/generated-artifacts/DevUtils.json'; import * as EthBalanceChecker from '../test/generated-artifacts/EthBalanceChecker.json'; import * as ExternalFunctions from '../test/generated-artifacts/ExternalFunctions.json'; import * as LibAssetData from '../test/generated-artifacts/LibAssetData.json'; +import * as LibDydxBalance from '../test/generated-artifacts/LibDydxBalance.json'; import * as LibOrderTransferSimulation from '../test/generated-artifacts/LibOrderTransferSimulation.json'; import * as LibTransactionDecoder from '../test/generated-artifacts/LibTransactionDecoder.json'; import * as OrderTransferSimulationUtils from '../test/generated-artifacts/OrderTransferSimulationUtils.json'; import * as OrderValidationUtils from '../test/generated-artifacts/OrderValidationUtils.json'; +import * as TestDydx from '../test/generated-artifacts/TestDydx.json'; +import * as TestLibDydxBalance from '../test/generated-artifacts/TestLibDydxBalance.json'; export const artifacts = { Addresses: Addresses as ContractArtifact, AssetBalance: AssetBalance as ContractArtifact, @@ -22,8 +25,11 @@ export const artifacts = { EthBalanceChecker: EthBalanceChecker as ContractArtifact, ExternalFunctions: ExternalFunctions as ContractArtifact, LibAssetData: LibAssetData as ContractArtifact, + LibDydxBalance: LibDydxBalance as ContractArtifact, LibOrderTransferSimulation: LibOrderTransferSimulation as ContractArtifact, LibTransactionDecoder: LibTransactionDecoder as ContractArtifact, OrderTransferSimulationUtils: OrderTransferSimulationUtils as ContractArtifact, OrderValidationUtils: OrderValidationUtils as ContractArtifact, + TestDydx: TestDydx as ContractArtifact, + TestLibDydxBalance: TestLibDydxBalance as ContractArtifact, }; diff --git a/contracts/dev-utils/test/lib_dydx_balance_test.ts b/contracts/dev-utils/test/lib_dydx_balance_test.ts new file mode 100644 index 0000000000..04438f8bc7 --- /dev/null +++ b/contracts/dev-utils/test/lib_dydx_balance_test.ts @@ -0,0 +1,1166 @@ +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, 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; + makerAssetAmount: BigNumber; + takerAssetAmount: 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, + makerAssetAmount: fromTokenUnitAmount(10, MAKER_DECIMALS), + takerAssetAmount: fromTokenUnitAmount(5, TAKER_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.000001): BigNumber { + return withdrawRate.times((MAKER_PRICE / TAKER_PRICE) * MARGIN_RATIO).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({ + takerAssetAmount: fromTokenUnitAmount(10, TAKER_DECIMALS), + makerAssetAmount: 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({ + takerAssetAmount: fromTokenUnitAmount(10, TAKER_DECIMALS), + makerAssetAmount: 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({ + takerAssetAmount: fromTokenUnitAmount(10, TAKER_DECIMALS), + makerAssetAmount: 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({ + takerAssetAmount: fromTokenUnitAmount(exchangeRate, TAKER_DECIMALS), + makerAssetAmount: 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, + takerAssetAmount: new BigNumber(1), + makerAssetAmount: fromTokenUnitAmount(100, 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. + takerAssetAmount: fromTokenUnitAmount(10, TAKER_DECIMALS), + makerAssetAmount: 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({ + takerAssetAmount: fromTokenUnitAmount(exchangeRate, TAKER_DECIMALS), + makerAssetAmount: 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({ + takerAssetAmount: fromTokenUnitAmount(exchangeRate, TAKER_DECIMALS), + makerAssetAmount: 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.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({ + takerAssetAmount: fromTokenUnitAmount(exchangeRate, TAKER_DECIMALS), + makerAssetAmount: 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.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, TAKER_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(9e18), + conversionRateDenominator: new BigNumber(10e18), + }, + ], + }), + }); + const r = await testContract.getDydxMakerBalance(order, dydx.address).callAsync(); + expect(r).to.bignumber.eq(0); + }); + }); +}); +// tslint:disable-next-line: max-file-line-count diff --git a/contracts/dev-utils/test/wrappers.ts b/contracts/dev-utils/test/wrappers.ts index 0760a7390d..01f291fa09 100644 --- a/contracts/dev-utils/test/wrappers.ts +++ b/contracts/dev-utils/test/wrappers.ts @@ -9,7 +9,10 @@ export * from '../test/generated-wrappers/dev_utils'; export * from '../test/generated-wrappers/eth_balance_checker'; export * from '../test/generated-wrappers/external_functions'; export * from '../test/generated-wrappers/lib_asset_data'; +export * from '../test/generated-wrappers/lib_dydx_balance'; export * from '../test/generated-wrappers/lib_order_transfer_simulation'; export * from '../test/generated-wrappers/lib_transaction_decoder'; export * from '../test/generated-wrappers/order_transfer_simulation_utils'; export * from '../test/generated-wrappers/order_validation_utils'; +export * from '../test/generated-wrappers/test_dydx'; +export * from '../test/generated-wrappers/test_lib_dydx_balance'; diff --git a/contracts/dev-utils/tsconfig.json b/contracts/dev-utils/tsconfig.json index 9c2a01383f..eccc9bf633 100644 --- a/contracts/dev-utils/tsconfig.json +++ b/contracts/dev-utils/tsconfig.json @@ -5,6 +5,7 @@ "files": [ "generated-artifacts/DevUtils.json", "generated-artifacts/LibAssetData.json", + "generated-artifacts/LibDydxBalance.json", "generated-artifacts/LibOrderTransferSimulation.json", "generated-artifacts/LibTransactionDecoder.json", "test/generated-artifacts/Addresses.json", @@ -13,10 +14,13 @@ "test/generated-artifacts/EthBalanceChecker.json", "test/generated-artifacts/ExternalFunctions.json", "test/generated-artifacts/LibAssetData.json", + "test/generated-artifacts/LibDydxBalance.json", "test/generated-artifacts/LibOrderTransferSimulation.json", "test/generated-artifacts/LibTransactionDecoder.json", "test/generated-artifacts/OrderTransferSimulationUtils.json", - "test/generated-artifacts/OrderValidationUtils.json" + "test/generated-artifacts/OrderValidationUtils.json", + "test/generated-artifacts/TestDydx.json", + "test/generated-artifacts/TestLibDydxBalance.json" ], "exclude": ["./deploy/solc/solc_bin"] } diff --git a/contracts/integrations/CHANGELOG.json b/contracts/integrations/CHANGELOG.json index cfdc905da5..fd221e8db4 100644 --- a/contracts/integrations/CHANGELOG.json +++ b/contracts/integrations/CHANGELOG.json @@ -31,6 +31,10 @@ { "note": "Update tests for refactored `DevUtils`", "pr": 2464 + }, + { + "note": "Add DydxBridge validation", + "pr": 2466 } ], "timestamp": 1581204851 diff --git a/contracts/integrations/test/bridges/dydx_bridge_mainnet_test.ts b/contracts/integrations/test/bridges/dydx_bridge_mainnet_test.ts index 17b117a8f0..f4992117c7 100644 --- a/contracts/integrations/test/bridges/dydx_bridge_mainnet_test.ts +++ b/contracts/integrations/test/bridges/dydx_bridge_mainnet_test.ts @@ -23,14 +23,14 @@ blockchainTests.fork.resets('Mainnet dydx bridge tests', env => { const defaultAmount = toBaseUnitAmount(0.01); const defaultDepositAction = { actionType: DydxBridgeActionType.Deposit as number, - accountId: constants.ZERO_AMOUNT, + accountIdx: constants.ZERO_AMOUNT, marketId: daiMarketId, conversionRateNumerator: constants.ZERO_AMOUNT, conversionRateDenominator: constants.ZERO_AMOUNT, }; const defaultWithdrawAction = { actionType: DydxBridgeActionType.Withdraw as number, - accountId: constants.ZERO_AMOUNT, + accountIdx: constants.ZERO_AMOUNT, marketId: daiMarketId, // This ratio must be less than the `1` to account // for interest in dydx balances because the test @@ -71,7 +71,7 @@ blockchainTests.fork.resets('Mainnet dydx bridge tests', env => { case DydxBridgeActionType.Deposit: expectedDepositEvents.push({ accountOwner: dydxAccountOwner, - accountNumber: bridgeData.accountNumbers[action.accountId.toNumber()], + accountNumber: bridgeData.accountNumbers[action.accountIdx.toNumber()], market: action.marketId, update: { deltaWei: { sign: true, value: scaledAmount } }, from: dydxAccountOwner, @@ -81,7 +81,7 @@ blockchainTests.fork.resets('Mainnet dydx bridge tests', env => { case DydxBridgeActionType.Withdraw: expectedWithdrawEvents.push({ accountOwner: dydxAccountOwner, - accountNumber: bridgeData.accountNumbers[action.accountId.toNumber()], + accountNumber: bridgeData.accountNumbers[action.accountIdx.toNumber()], market: action.marketId, update: { deltaWei: { sign: false, value: scaledAmount } }, to: receiver, diff --git a/contracts/integrations/test/dev-utils/dev_utils_mainnet_test.ts b/contracts/integrations/test/dev-utils/dev_utils_mainnet_test.ts index 1aa36ff346..db00762350 100644 --- a/contracts/integrations/test/dev-utils/dev_utils_mainnet_test.ts +++ b/contracts/integrations/test/dev-utils/dev_utils_mainnet_test.ts @@ -34,6 +34,7 @@ blockchainTests.fork.resets('DevUtils mainnet tests', env => { devUtilsArtifacts, contractAddresses.exchange, contractAddresses.chaiBridge, + contractAddresses.dydxBridge, ); await dai.approve(chai.address, constants.MAX_UINT256).awaitTransactionSuccessAsync({ from: daiHolder }); await chai.join(daiHolder, daiDepositAmount).awaitTransactionSuccessAsync({ from: daiHolder }); diff --git a/contracts/integrations/test/dev-utils/get_order_hash.ts b/contracts/integrations/test/dev-utils/get_order_hash.ts index 862b9a188b..ca8e53430b 100644 --- a/contracts/integrations/test/dev-utils/get_order_hash.ts +++ b/contracts/integrations/test/dev-utils/get_order_hash.ts @@ -27,6 +27,7 @@ blockchainTests('DevUtils.getOrderHash', env => { artifacts, exchange.address, constants.NULL_ADDRESS, + constants.NULL_ADDRESS, ); }); diff --git a/contracts/integrations/test/dev-utils/lib_asset_data.ts b/contracts/integrations/test/dev-utils/lib_asset_data.ts index 7594a44f11..5592c2243d 100644 --- a/contracts/integrations/test/dev-utils/lib_asset_data.ts +++ b/contracts/integrations/test/dev-utils/lib_asset_data.ts @@ -81,6 +81,7 @@ blockchainTests.resets('LibAssetData', env => { artifacts, deployment.exchange.address, constants.NULL_ADDRESS, + constants.NULL_ADDRESS, ); staticCallTarget = await TestStaticCallTargetContract.deployFrom0xArtifactAsync( diff --git a/contracts/integrations/test/dev-utils/lib_transaction_decoder.ts b/contracts/integrations/test/dev-utils/lib_transaction_decoder.ts index 98313cd893..7b3cb76a5b 100644 --- a/contracts/integrations/test/dev-utils/lib_transaction_decoder.ts +++ b/contracts/integrations/test/dev-utils/lib_transaction_decoder.ts @@ -43,6 +43,7 @@ blockchainTests('LibTransactionDecoder', env => { artifacts, exchange.address, constants.NULL_ADDRESS, + constants.NULL_ADDRESS, ); }); diff --git a/contracts/integrations/test/exchange/fill_dydx_order_test.ts b/contracts/integrations/test/exchange/fill_dydx_order_test.ts index ba99d7fe4c..5b410a906f 100644 --- a/contracts/integrations/test/exchange/fill_dydx_order_test.ts +++ b/contracts/integrations/test/exchange/fill_dydx_order_test.ts @@ -34,14 +34,14 @@ blockchainTests.resets('Exchange fills dydx orders', env => { let testTokenAddress: string; const defaultDepositAction = { actionType: DydxBridgeActionType.Deposit as number, - accountId: constants.ZERO_AMOUNT, + accountIdx: constants.ZERO_AMOUNT, marketId, conversionRateNumerator: dydxConversionRateNumerator, conversionRateDenominator: dydxConversionRateDenominator, }; const defaultWithdrawAction = { actionType: DydxBridgeActionType.Withdraw as number, - accountId: constants.ZERO_AMOUNT, + accountIdx: constants.ZERO_AMOUNT, marketId, conversionRateNumerator: constants.ZERO_AMOUNT, conversionRateDenominator: constants.ZERO_AMOUNT, diff --git a/contracts/integrations/test/framework/deployment_manager.ts b/contracts/integrations/test/framework/deployment_manager.ts index 4b0dd9aa8b..fae0ffc519 100644 --- a/contracts/integrations/test/framework/deployment_manager.ts +++ b/contracts/integrations/test/framework/deployment_manager.ts @@ -203,6 +203,7 @@ export class DeploymentManager { devUtilsArtifacts, exchange.address, constants.NULL_ADDRESS, + constants.NULL_ADDRESS, ); // Construct the new instance and return it. diff --git a/contracts/utils/CHANGELOG.json b/contracts/utils/CHANGELOG.json index 251907e9c5..3551b3a3d1 100644 --- a/contracts/utils/CHANGELOG.json +++ b/contracts/utils/CHANGELOG.json @@ -37,6 +37,10 @@ { "note": "Export `EvmBytecodeOutputLinkReferences` type.", "pr": 2462 + }, + { + "note": "Add more functions to `LibFractions`.", + "pr": 2466 } ], "timestamp": 1580811564 diff --git a/contracts/utils/contracts/src/LibFractions.sol b/contracts/utils/contracts/src/LibFractions.sol index f5a05706c6..1582733988 100644 --- a/contracts/utils/contracts/src/LibFractions.sol +++ b/contracts/utils/contracts/src/LibFractions.sol @@ -37,7 +37,101 @@ library LibFractions { .safeMul(d2) .safeAdd(n2.safeMul(d1)); denominator = d1.safeMul(d2); - return (numerator, denominator); + return normalize(numerator, denominator); + } + + /// @dev Safely subracts two fractions `n1/d1 - n2/d2` + /// @param n1 numerator of `1` + /// @param d1 denominator of `1` + /// @param n2 numerator of `2` + /// @param d2 denominator of `2` + /// @return numerator Numerator of sum + /// @return denominator Denominator of sum + function sub( + uint256 n1, + uint256 d1, + uint256 n2, + uint256 d2 + ) + internal + pure + returns ( + uint256 numerator, + uint256 denominator + ) + { + if (n2 == 0) { + return (numerator = n1, denominator = d1); + } + numerator = n1 + .safeMul(d2) + .safeSub(n2.safeMul(d1)); + denominator = d1.safeMul(d2); + return normalize(numerator, denominator); + } + + /// @dev Multiply two fractions. + /// @param n1 numerator of `1` + /// @param d1 denominator of `1` + /// @param n2 numerator of `2` + /// @param d2 denominator of `2` + /// @return numerator numerator of product. + /// @return denominator numerator of product. + function mul( + uint256 n1, + uint256 d1, + uint256 n2, + uint256 d2 + ) + internal + pure + returns ( + uint256 numerator, + uint256 denominator + ) + { + return normalize(n1.safeMul(n2), d1.safeMul(d2)); + } + + /// @dev Compares two fractions. + /// @param n1 numerator of `1` + /// @param d1 denominator of `1` + /// @param n2 numerator of `2` + /// @param d2 denominator of `2` + /// @return compareResult + /// `-1` if `n1/d1 < n2/d2`. + /// `0` if `n1/d1 == n2/d2`. + /// `1` if `n1/d1 > n2/d2`. + function cmp( + uint256 n1, + uint256 d1, + uint256 n2, + uint256 d2 + ) + internal + pure + returns (int8 compareResult) + { + // Handle infinities. + if (d1 == 0) { + if (d2 == 0) { + return 0; + } + return 1; + } else { + if (d2 == 0) { + return -1; + } + } + uint256 nd1 = n1.safeMul(d2); + uint256 nd2 = n2.safeMul(d1); + if (nd1 > nd2) { + return 1; + } + if (nd1 < nd2) { + return -1; + } + return 0; } /// @dev Rescales a fraction to prevent overflows during addition if either diff --git a/packages/abi-gen/CHANGELOG.json b/packages/abi-gen/CHANGELOG.json index 13e746ff6a..03c143c90a 100644 --- a/packages/abi-gen/CHANGELOG.json +++ b/packages/abi-gen/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Support deploying contracts with unliked libraries through `deployWithLibrariesFrom0xArtifactAsync()`", "pr": 2463 + }, + { + "note": "Update reference outputs", + "pr": 2466 } ], "timestamp": 1581204851 diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index 54f09768de..b65cf98d4f 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -60,7 +60,7 @@ }, { "note": "Update snapshot addresses", - "pr": 2464 + "pr": 2466 } ], "timestamp": 1580811564 diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index bdb4821599..4b50df8450 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -131,18 +131,18 @@ "etherToken": "0x0b1ba0af832d7c05fd64161e0db78e85978e8082", "exchange": "0x48bacb9266a570d521063ef5dd96e61686dbe788", "assetProxyOwner": "0x0000000000000000000000000000000000000000", - "erc20BridgeProxy": "0x038f9b392fb9a9676dbaddf78ea5fdbf6c7d9710", + "erc20BridgeProxy": "0x371b13d97f4bf77d724e78c16b7dc74099f40e84", "zeroExGovernor": "0x0000000000000000000000000000000000000000", "forwarder": "0xe704967449b57b2382b7fa482718748c13c63190", "coordinatorRegistry": "0xaa86dda78e9434aca114b6676fc742a18d15a1cc", "coordinator": "0x4d3d5c850dd5bd9d6f4adda3dd039a3c8054ca29", "multiAssetProxy": "0xcfc18cec799fbd1793b5c43e773c98d4d61cc2db", "staticCallProxy": "0x6dfff22588be9b3ef8cf0ad6dc9b84796f9fb45f", - "devUtils": "0x74341e87b1c4db7d5ed95f92b37509f2525a7a90", + "devUtils": "0xb23672f74749bf7916ba6827c64111a4d6de7f11", "exchangeV2": "0x48bacb9266a570d521063ef5dd96e61686dbe788", - "zrxVault": "0xc4df27466183c0fe2a5924d6ea56e334deff146a", - "staking": "0xf23276778860e420acfc18ebeebf7e829b06965c", - "stakingProxy": "0x8a063452f7df2614db1bca3a85ef35da40cf0835", + "zrxVault": "0xf23276778860e420acfc18ebeebf7e829b06965c", + "staking": "0x8a063452f7df2614db1bca3a85ef35da40cf0835", + "stakingProxy": "0x59adefa01843c627ba5d6aa350292b4b7ccae67a", "uniswapBridge": "0x0000000000000000000000000000000000000000", "eth2DaiBridge": "0x0000000000000000000000000000000000000000", "erc20BridgeSampler": "0x0000000000000000000000000000000000000000", diff --git a/packages/contract-artifacts/CHANGELOG.json b/packages/contract-artifacts/CHANGELOG.json index 33a90c586a..7293dc0490 100644 --- a/packages/contract-artifacts/CHANGELOG.json +++ b/packages/contract-artifacts/CHANGELOG.json @@ -18,7 +18,7 @@ "changes": [ { "note": "Update `DevUtils` artifact", - "pr": 2464 + "pr": 2466 }, { "note": "Remove `LibTransactionDecoder` artifact", diff --git a/packages/migrations/CHANGELOG.json b/packages/migrations/CHANGELOG.json index 9127f14eec..dd8474eeb1 100644 --- a/packages/migrations/CHANGELOG.json +++ b/packages/migrations/CHANGELOG.json @@ -23,6 +23,10 @@ { "note": "Use contract package artifacts in ganache migrations", "pr": 2456 + }, + { + "note": "Update deployment for new DevUtils", + "pr": 2466 } ], "timestamp": 1581204851 diff --git a/packages/migrations/src/migration.ts b/packages/migrations/src/migration.ts index c6655b99d9..6c07f2c7c2 100644 --- a/packages/migrations/src/migration.ts +++ b/packages/migrations/src/migration.ts @@ -198,6 +198,7 @@ export async function runMigrationsAsync( allArtifacts, exchange.address, constants.NULL_ADDRESS, + constants.NULL_ADDRESS, ); // tslint:disable-next-line:no-unused-variable diff --git a/packages/migrations/src/testnet_migrations.ts b/packages/migrations/src/testnet_migrations.ts index f5b6f45324..df9aa2a779 100644 --- a/packages/migrations/src/testnet_migrations.ts +++ b/packages/migrations/src/testnet_migrations.ts @@ -120,7 +120,7 @@ export async function runMigrationsAsync(supportedProvider: SupportedProvider, t assetProxyArtifacts, ); - await DydxBridgeContract.deployFrom0xArtifactAsync( + const dydxBridge = await DydxBridgeContract.deployFrom0xArtifactAsync( assetProxyArtifacts.DydxBridge, provider, txDefaults, @@ -253,6 +253,7 @@ export async function runMigrationsAsync(supportedProvider: SupportedProvider, t devUtilsArtifacts, exchange.address, chaiBridge.address, + dydxBridge.address, ); await CoordinatorContract.deployFrom0xArtifactAsync(