diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index 874141201b..a8342ec103 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -30,6 +30,10 @@ { "note": "Add asset data decoding functions", "pr": 2462 + }, + { + "note": "Add `setOperators()` to `IDydx`", + "pr": "TODO" } ], "timestamp": 1581204851 diff --git a/contracts/asset-proxy/contracts/src/bridges/DydxBridge.sol b/contracts/asset-proxy/contracts/src/bridges/DydxBridge.sol index 70c97f268f..8176c2507c 100644 --- a/contracts/asset-proxy/contracts/src/bridges/DydxBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/DydxBridge.sol @@ -196,7 +196,7 @@ contract DydxBridge is otherAddress: depositFrom, // deposit from the account owner. // unused parameters secondaryMarketId: 0, - otherAccountId: 0, + otherAccountIdx: 0, data: hex'' }); } @@ -234,7 +234,7 @@ contract DydxBridge is otherAddress: withdrawTo, // withdraw tokens to this address. // unused parameters secondaryMarketId: 0, - otherAccountId: 0, + otherAccountIdx: 0, data: hex'' }); } diff --git a/contracts/asset-proxy/contracts/src/interfaces/IDydx.sol b/contracts/asset-proxy/contracts/src/interfaces/IDydx.sol index d784bde2cc..2f2296bd88 100644 --- a/contracts/asset-proxy/contracts/src/interfaces/IDydx.sol +++ b/contracts/asset-proxy/contracts/src/interfaces/IDydx.sol @@ -50,7 +50,7 @@ interface IDydx { uint256 primaryMarketId; uint256 secondaryMarketId; address otherAddress; - uint256 otherAccountId; + uint256 otherAccountIdx; bytes data; } @@ -83,6 +83,11 @@ interface IDydx { uint256 value; } + struct OperatorArg { + address operator; + bool trusted; + } + /// @dev The global risk parameters that govern the health and security of the system struct RiskParams { // Required ratio of over-collateralization @@ -166,4 +171,13 @@ interface IDydx { external view returns (Value memory supplyValue, Value memory borrowValue); + + // @dev Approves/disapproves any number of operators. An operator is an external address that has the + // same permissions to manipulate an account as the owner of the account. Operators are simply + // addresses and therefore may either be externally-owned Ethereum accounts OR smart contracts. + // Operators are also able to act as AutoTrader contracts on behalf of the account owner if the + // operator is a smart contract and implements the IAutoTrader interface. + // @param args A list of OperatorArgs which have an address and a boolean. The boolean value + // denotes whether to approve (true) or revoke approval (false) for that address. + function setOperators(OperatorArg[] calldata args) external; } diff --git a/contracts/asset-proxy/contracts/test/TestDydxBridge.sol b/contracts/asset-proxy/contracts/test/TestDydxBridge.sol index 895e31231b..9345e21cdc 100644 --- a/contracts/asset-proxy/contracts/test/TestDydxBridge.sol +++ b/contracts/asset-proxy/contracts/test/TestDydxBridge.sol @@ -129,7 +129,7 @@ contract TestDydxBridge is actions[i].primaryMarketId, actions[i].secondaryMarketId, actions[i].otherAddress, - actions[i].otherAccountId, + actions[i].otherAccountIdx, actions[i].data ); @@ -216,6 +216,9 @@ contract TestDydxBridge is returns (Value memory supplyValue, Value memory borrowValue) {} + /// @dev Unused. + function setOperators(OperatorArg[] calldata args) external {} + /// @dev overrides `_getDydxAddress()` from `DeploymentConstants` to return this address. function _getDydxAddress() internal diff --git a/contracts/asset-proxy/src/index.ts b/contracts/asset-proxy/src/index.ts index 155cc1b51e..89d3053fa6 100644 --- a/contracts/asset-proxy/src/index.ts +++ b/contracts/asset-proxy/src/index.ts @@ -1,21 +1,22 @@ export { artifacts } from './artifacts'; export { + ChaiBridgeContract, ERC1155ProxyContract, ERC20BridgeProxyContract, ERC20ProxyContract, ERC721ProxyContract, Eth2DaiBridgeContract, DydxBridgeContract, - TestDydxBridgeContract, IAssetDataContract, IAssetProxyContract, + IChaiContract, + IDydxContract, + KyberBridgeContract, MultiAssetProxyContract, StaticCallProxyContract, + TestDydxBridgeContract, TestStaticCallTargetContract, UniswapBridgeContract, - KyberBridgeContract, - ChaiBridgeContract, - IChaiContract, } from './wrappers'; export { ERC20Wrapper } from './erc20_wrapper'; diff --git a/contracts/dev-utils/contracts/src/LibDydxBalance.sol b/contracts/dev-utils/contracts/src/LibDydxBalance.sol index 2cff51812e..20095ef30e 100644 --- a/contracts/dev-utils/contracts/src/LibDydxBalance.sol +++ b/contracts/dev-utils/contracts/src/LibDydxBalance.sol @@ -33,6 +33,12 @@ import "./D18.sol"; library LibDydxBalance { using LibBytes for bytes; + using LibSafeMath for uint256; + + /// @dev Padding % added to the minimum collateralization ratio to + /// prevent withdrawing exactly the amount that would make an account + /// insolvent. 1 bps. + int256 private constant MARGIN_RATIO_PADDING = 0.0001e18; /// @dev Structure that holds all pertinent info needed to perform a balance /// check. @@ -42,7 +48,7 @@ library LibDydxBalance { address makerAddress; address makerTokenAddress; address takerTokenAddress; - int256 orderTakerToMakerRate; + int256 orderMakerToTakerRate; uint256[] accounts; IDydxBridge.BridgeAction[] actions; } @@ -65,10 +71,10 @@ library LibDydxBalance { if (!_areActionsWellFormed(info)) { return 0; } - // If the rate we withdraw maker tokens is less than the order conversion - // rate , the asset proxy will throw because we will always transfer - // less maker tokens than asked. - if (_getMakerTokenWithdrawRate(info) < info.orderTakerToMakerRate) { + // If the rate we withdraw maker tokens is less than one, the asset + // proxy will throw because we will always transfer less maker tokens + // than asked. + if (_getMakerTokenWithdrawRate(info) < D18.one()) { return 0; } // The maker balance is the smaller of: @@ -154,7 +160,6 @@ library LibDydxBalance { returns (uint256 depositableMakerAmount) { depositableMakerAmount = uint256(-1); - int256 orderMakerToTakerRate = D18.div(D18.one(), info.orderTakerToMakerRate); // Take the minimum maker amount from all deposits. for (uint256 i = 0; i < info.actions.length; ++i) { IDydxBridge.BridgeAction memory action = info.actions[i]; @@ -162,13 +167,15 @@ library LibDydxBalance { if (action.actionType != IDydxBridge.BridgeActionType.Deposit) { continue; } + // `depositRate` is the rate at which we convert a maker token into + // a taker token for deposit. int256 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 = D18.sub(depositRate, orderMakerToTakerRate); + depositRate = D18.sub(depositRate, info.orderMakerToTakerRate); } // If the deposit rate is > 0, we are limited by the transferrable // token balance of the maker. @@ -198,11 +205,11 @@ library LibDydxBalance { assert(info.actions.length >= 1); IDydxBridge.BridgeAction memory withdraw = info.actions[info.actions.length - 1]; assert(withdraw.actionType == IDydxBridge.BridgeActionType.Withdraw); - int256 minCr = _getMinimumCollateralizationRatio(info.dydx); + int256 minCr = D18.add(_getMinimumCollateralizationRatio(info.dydx), MARGIN_RATIO_PADDING); // Loop through the accounts. for (uint256 accountIdx = 0; accountIdx < info.accounts.length; ++accountIdx) { (uint256 supplyValue, uint256 borrowValue) = - _getAccountValues(info, info.accounts[accountIdx]); + _getAccountMarketValues(info, info.accounts[accountIdx]); // All accounts must currently be solvent. if (borrowValue != 0 && D18.div(supplyValue, borrowValue) < minCr) { return 0; @@ -221,20 +228,18 @@ library LibDydxBalance { if (deposit.accountIdx == accountIdx) { dd = D18.add( dd, - _toQuoteValue( - info.dydx, - deposit.marketId, - _getActionRate(deposit) + _getActionRateValue( + info, + deposit ) ); } } // Compute the borrow/withdraw rate, which is the rate at which // (USD) value is deducted from the account. - int256 db = _toQuoteValue( - info.dydx, - withdraw.marketId, - _getActionRate(withdraw) + int256 db = _getActionRateValue( + info, + withdraw ); // If the deposit to withdraw ratio is >= the minimum collateralization // ratio, then we will never become insolvent at these prices. @@ -252,7 +257,8 @@ library LibDydxBalance { ); solventMakerAmount = LibSafeMath.min256( solventMakerAmount, - uint256(D18.clip(t)) + // `t` is in maker token units, so convert it to maker wei. + _toWei(info.makerTokenAddress, uint256(D18.clip(t))) ); } } @@ -275,14 +281,14 @@ library LibDydxBalance { (, info.takerTokenAddress) = LibAssetData.decodeERC20AssetData(order.takerAssetData); } - info.orderTakerToMakerRate = D18.div(order.makerAssetAmount, order.takerAssetAmount); + info.orderMakerToTakerRate = D18.div(order.takerAssetAmount, order.makerAssetAmount); (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. + /// @dev Returns the conversion rate for an action. /// @param action A `BridgeAction`. function _getActionRate(IDydxBridge.BridgeAction memory action) private @@ -297,6 +303,59 @@ library LibDydxBalance { ); } + /// @dev Returns the USD value of an action based on its conversion rate + /// and market prices. + /// @param info State from `_getBalanceCheckInfo()`. + /// @param action A `BridgeAction`. + function _getActionRateValue( + BalanceCheckInfo memory info, + IDydxBridge.BridgeAction memory action + ) + private + view + returns (int256 value) + { + address toToken = info.dydx.getMarketTokenAddress(action.marketId); + uint256 fromTokenDecimals = LibERC20Token.decimals(info.makerTokenAddress); + uint256 toTokenDecimals = LibERC20Token.decimals(toToken); + // First express the rate as 18-decimal units. + value = toTokenDecimals > fromTokenDecimals + ? int256( + uint256(_getActionRate(action)) + .safeDiv(10 ** (toTokenDecimals - fromTokenDecimals)) + ) + : int256( + uint256(_getActionRate(action)) + .safeMul(10 ** (fromTokenDecimals - toTokenDecimals)) + ); + // Prices have 18 + (18 - TOKEN_DECIMALS) decimal places because + // consistency is stupid. + uint256 price = info.dydx.getMarketPrice(action.marketId).value; + // Make prices have 18 decimals. + if (toTokenDecimals > 18) { + price = price.safeMul(10 ** (toTokenDecimals - 18)); + } else { + price = price.safeDiv(10 ** (18 - toTokenDecimals)); + } + // The action value is the action rate times the price. + value = D18.mul(price, value); + } + + /// @dev Returns the conversion rate for an action, expressed as units + /// of the market token. + /// @param token Address the of the token. + /// @param units Token units expressed with 18 digit precision. + function _toWei(address token, uint256 units) + private + view + returns (uint256 rate) + { + uint256 decimals = LibERC20Token.decimals(token); + rate = decimals > 18 + ? units.safeMul(10 ** (decimals - 18)) + : units.safeDiv(10 ** (18 - decimals)); + } + /// @dev Get the global minimum collateralization ratio required for /// an account to be considered solvent. /// @param dydx The Dydx interface. @@ -309,24 +368,10 @@ library LibDydxBalance { return D18.add(D18.one(), D18.toSigned(riskParams.marginRatio.value)); } - /// @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, int256 rate) - private - view - returns (int256 quotedRate) - { - IDydx.Price memory price = dydx.getMarketPrice(marketId); - uint8 tokenDecimals = LibERC20Token.decimals(dydx.getMarketTokenAddress(marketId)); - return D18.mul(D18.div(price.value, 10 ** uint256(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) + function _getAccountMarketValues(BalanceCheckInfo memory info, uint256 account) private view returns (uint256 supplyValue, uint256 borrowValue) @@ -336,7 +381,9 @@ library LibDydxBalance { info.makerAddress, account )); - return (supplyValue_.value, borrowValue_.value); + // Account values have 36 decimal places because dydx likes to make sure + // you're paying attention. + return (supplyValue_.value / 1e18, borrowValue_.value / 1e18); } /// @dev Get the amount of an ERC20 token held by `owner` that can be transferred diff --git a/contracts/dev-utils/contracts/test/TestDydx.sol b/contracts/dev-utils/contracts/test/TestDydx.sol index c3c7a62e1c..be251af314 100644 --- a/contracts/dev-utils/contracts/test/TestDydx.sol +++ b/contracts/dev-utils/contracts/test/TestDydx.sol @@ -106,16 +106,6 @@ contract TestDydx { }); } - function getMarketPrice( - uint256 marketId - ) - external - view - returns (IDydx.Price memory price) - { - return IDydx.Price(_markets[marketId].price); - } - function getAdjustedAccountValues( IDydx.AccountInfo calldata account ) @@ -124,11 +114,13 @@ contract TestDydx { 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); + // Account values have 36 decimal places. + // `getMarketPrice()` returns a unit with + // 18 + (18 - TOKEN_DECIMALS) decimal places so multiplying the price + // with the wei balance will result in a 36 decimal value. + balance = balance * int256(getMarketPrice(marketId).value); if (balance >= 0) { supplyValue.value += uint256(balance); } else { @@ -137,6 +129,24 @@ contract TestDydx { } } + function getMarketPrice( + uint256 marketId + ) + public + view + returns (IDydx.Price memory price) + { + MarketInfo memory market = _markets[marketId]; + uint256 decimals = LibERC20Token.decimals(market.token); + price.value = _markets[marketId].price; + // Market prices have 18 + (18 - TOKEN_DECIMALS) + if (decimals > 18) { + price.value /= 10 ** (decimals - 18); + } else { + price.value *= 10 ** (18 - decimals); + } + } + function _getOperatorHash(address owner, address operator) private pure diff --git a/contracts/dev-utils/test/lib_dydx_balance_test.ts b/contracts/dev-utils/test/lib_dydx_balance_test.ts index 9ca1775d27..6d06adfb56 100644 --- a/contracts/dev-utils/test/lib_dydx_balance_test.ts +++ b/contracts/dev-utils/test/lib_dydx_balance_test.ts @@ -148,7 +148,7 @@ blockchainTests('LibDydxBalance', env => { makerAddress: string; makerTokenAddress: string; takerTokenAddress: string; - orderTakerToMakerRate: BigNumber; + orderMakerToTakerRate: BigNumber; accounts: BigNumber[]; actions: DydxBridgeAction[]; } @@ -160,8 +160,8 @@ blockchainTests('LibDydxBalance', env => { makerAddress: ACCOUNT_OWNER, makerTokenAddress: DYDX_CONFIG.markets[1].token, takerTokenAddress: DYDX_CONFIG.markets[0].token, - orderTakerToMakerRate: fromTokenUnitAmount( - fromTokenUnitAmount(5, MAKER_DECIMALS).div(fromTokenUnitAmount(10, TAKER_DECIMALS)), + orderMakerToTakerRate: fromTokenUnitAmount( + fromTokenUnitAmount(10, TAKER_DECIMALS).div(fromTokenUnitAmount(5, MAKER_DECIMALS)), ), accounts: [DYDX_CONFIG.accounts[SOLVENT_ACCOUNT_IDX].accountId], actions: [], @@ -214,8 +214,9 @@ blockchainTests('LibDydxBalance', env => { // 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 getBalancedDepositRate(withdrawRate: BigNumber, scaling: Numberish = 1): BigNumber { + // Add a small amount to the margin ratio to stay just above insolvency. + return withdrawRate.times((MAKER_PRICE / TAKER_PRICE) * (MARGIN_RATIO + 1.1e-4)).times(scaling); } function takerToMakerAmount(takerAmount: BigNumber): BigNumber { @@ -537,8 +538,8 @@ blockchainTests('LibDydxBalance', env => { blockchainTests.resets('_getDepositableMakerAmount()', () => { it('returns infinite if no deposit action', async () => { const checkInfo = createBalanceCheckInfo({ - orderTakerToMakerRate: fromTokenUnitAmount( - fromTokenUnitAmount(100, MAKER_DECIMALS).div(fromTokenUnitAmount(10, TAKER_DECIMALS)), + orderMakerToTakerRate: fromTokenUnitAmount( + fromTokenUnitAmount(10, TAKER_DECIMALS).div(fromTokenUnitAmount(100, MAKER_DECIMALS)), ), actions: [], }); @@ -548,8 +549,8 @@ blockchainTests('LibDydxBalance', env => { it('returns infinite if deposit rate is zero', async () => { const checkInfo = createBalanceCheckInfo({ - orderTakerToMakerRate: fromTokenUnitAmount( - fromTokenUnitAmount(100, MAKER_DECIMALS).div(fromTokenUnitAmount(10, TAKER_DECIMALS)), + orderMakerToTakerRate: fromTokenUnitAmount( + fromTokenUnitAmount(10, TAKER_DECIMALS).div(fromTokenUnitAmount(100, MAKER_DECIMALS)), ), actions: [ { @@ -567,8 +568,8 @@ blockchainTests('LibDydxBalance', env => { it('returns infinite if taker tokens cover the deposit rate', async () => { const checkInfo = createBalanceCheckInfo({ - orderTakerToMakerRate: fromTokenUnitAmount( - fromTokenUnitAmount(100, MAKER_DECIMALS).div(fromTokenUnitAmount(10, TAKER_DECIMALS)), + orderMakerToTakerRate: fromTokenUnitAmount( + fromTokenUnitAmount(10, TAKER_DECIMALS).div(fromTokenUnitAmount(100, MAKER_DECIMALS)), ), actions: [ { @@ -589,8 +590,8 @@ blockchainTests('LibDydxBalance', env => { const exchangeRate = 0.1; const depositRate = Math.random() + exchangeRate; const checkInfo = createBalanceCheckInfo({ - orderTakerToMakerRate: fromTokenUnitAmount( - fromTokenUnitAmount(1, MAKER_DECIMALS).div(fromTokenUnitAmount(exchangeRate, TAKER_DECIMALS)), + orderMakerToTakerRate: fromTokenUnitAmount( + fromTokenUnitAmount(exchangeRate, TAKER_DECIMALS).div(fromTokenUnitAmount(1, MAKER_DECIMALS)), ), actions: [ { @@ -623,7 +624,7 @@ blockchainTests('LibDydxBalance', env => { const checkInfo = createBalanceCheckInfo({ // The `takerTokenAddress` will be zero if the asset is not an ERC20. takerTokenAddress: constants.NULL_ADDRESS, - orderTakerToMakerRate: fromTokenUnitAmount(fromTokenUnitAmount(100, MAKER_DECIMALS)), + orderMakerToTakerRate: fromTokenUnitAmount(fromTokenUnitAmount(0.1, MAKER_DECIMALS)), actions: [ { actionType: DydxBridgeActionType.Deposit, @@ -655,8 +656,8 @@ blockchainTests('LibDydxBalance', env => { takerTokenAddress: randomAddress(), // These amounts should be effectively ignored in the final computation // because the token being deposited is not the taker token. - orderTakerToMakerRate: fromTokenUnitAmount( - fromTokenUnitAmount(100, MAKER_DECIMALS).div(fromTokenUnitAmount(10, TAKER_DECIMALS)), + orderMakerToTakerRate: fromTokenUnitAmount( + fromTokenUnitAmount(10, TAKER_DECIMALS).div(fromTokenUnitAmount(100, MAKER_DECIMALS)), ), actions: [ { @@ -676,8 +677,8 @@ blockchainTests('LibDydxBalance', env => { // The taker tokens getting exchanged in will only partially cover the deposit. const exchangeRate = 0.1; const checkInfo = createBalanceCheckInfo({ - orderTakerToMakerRate: fromTokenUnitAmount( - fromTokenUnitAmount(1, MAKER_DECIMALS).div(fromTokenUnitAmount(exchangeRate, TAKER_DECIMALS)), + orderMakerToTakerRate: fromTokenUnitAmount( + fromTokenUnitAmount(exchangeRate, TAKER_DECIMALS).div(fromTokenUnitAmount(1, MAKER_DECIMALS)), ), actions: [ // Technically, deposits of the same token are not allowed, but the @@ -733,7 +734,7 @@ blockchainTests('LibDydxBalance', env => { const exchangeRate = 0.1; const depositRate = Math.random() + exchangeRate; const checkInfo = createBalanceCheckInfo({ - orderTakerToMakerRate: fromTokenUnitAmount(fromTokenUnitAmount(1 / exchangeRate, MAKER_DECIMALS)), + orderMakerToTakerRate: fromTokenUnitAmount(fromTokenUnitAmount(1 / exchangeRate, MAKER_DECIMALS)), actions: [ { actionType: DydxBridgeActionType.Deposit, @@ -760,7 +761,7 @@ blockchainTests('LibDydxBalance', env => { const exchangeRate = 0.1; const depositRate = Math.random() + exchangeRate; const checkInfo = createBalanceCheckInfo({ - orderTakerToMakerRate: fromTokenUnitAmount(fromTokenUnitAmount(1 / exchangeRate, MAKER_DECIMALS)), + orderMakerToTakerRate: fromTokenUnitAmount(fromTokenUnitAmount(1 / exchangeRate, MAKER_DECIMALS)), actions: [ { actionType: DydxBridgeActionType.Deposit, @@ -1155,8 +1156,8 @@ blockchainTests('LibDydxBalance', env => { actionType: DydxBridgeActionType.Withdraw, accountIdx: new BigNumber(0), marketId: new BigNumber(1), - conversionRateNumerator: new BigNumber(9e18), - conversionRateDenominator: new BigNumber(10e18), + conversionRateNumerator: new BigNumber(0.99e18), + conversionRateDenominator: new BigNumber(1e18), }, ], }),