From dd3c3a1328bcded5d99b23a498a073abe63ce314 Mon Sep 17 00:00:00 2001 From: Brendan Chou Date: Tue, 20 Aug 2019 17:00:44 -0700 Subject: [PATCH] add concluder contract --- .soliumignore | 1 + contracts/external/traders/Expiry.sol | 150 +++---- contracts/external/traders/ExpiryOld.sol | 477 +++++++++++++++++++++++ 3 files changed, 535 insertions(+), 93 deletions(-) create mode 100644 contracts/external/traders/ExpiryOld.sol diff --git a/.soliumignore b/.soliumignore index e0f4d8e3..1465fcfa 100644 --- a/.soliumignore +++ b/.soliumignore @@ -1,3 +1,4 @@ node_modules +contracts/external/traders/Expiry.sol contracts/testing contracts/Migrations.sol diff --git a/contracts/external/traders/Expiry.sol b/contracts/external/traders/Expiry.sol index bde4a8bf..4c0b8bee 100644 --- a/contracts/external/traders/Expiry.sol +++ b/contracts/external/traders/Expiry.sol @@ -28,7 +28,6 @@ import { Decimal } from "../../protocol/lib/Decimal.sol"; import { Math } from "../../protocol/lib/Math.sol"; import { Monetary } from "../../protocol/lib/Monetary.sol"; import { Require } from "../../protocol/lib/Require.sol"; -import { Time } from "../../protocol/lib/Time.sol"; import { Types } from "../../protocol/lib/Types.sol"; import { OnlySolo } from "../helpers/OnlySolo.sol"; @@ -37,9 +36,8 @@ import { OnlySolo } from "../helpers/OnlySolo.sol"; * @title Expiry * @author dYdX * - * Sets the negative balance for an account to expire at a certain time. This allows any other - * account to repay that negative balance after expiry using any positive balance in the same - * account. The arbitrage incentive is the same as liquidation in the base protocol. + * Copy of Expiry contract that also allows approved senders to set expiry to be 28 days in the + * future. */ contract Expiry is Ownable, @@ -47,7 +45,6 @@ contract Expiry is ICallee, IAutoTrader { - using SafeMath for uint32; using SafeMath for uint256; using Types for Types.Par; using Types for Types.Wei; @@ -56,23 +53,41 @@ contract Expiry is bytes32 constant FILE = "Expiry"; + uint256 constant FOUR_WEEKS_IN_SECONDS = 60 * 60 * 24 * 28; + + // ============ Structs ============ + + struct ExpiryArg { + Account.Info account; + uint256 marketId; + } + // ============ Events ============ event ExpirySet( address owner, uint256 number, uint256 marketId, - uint32 time + uint256 time ); event LogExpiryRampTimeSet( uint256 expiryRampTime ); + event LogSenderApproved( + address approver, + address sender, + bool approved + ); + // ============ Storage ============ // owner => number => market => time - mapping (address => mapping (uint256 => mapping (uint256 => uint32))) g_expiries; + mapping (address => mapping (uint256 => mapping (uint256 => uint256))) g_expiries; + + // owner => sender => approved + mapping (address => mapping (address => bool)) public g_approvedSender; // time over which the liquidation ratio goes from zero to maximum uint256 public g_expiryRampTime; @@ -101,6 +116,18 @@ contract Expiry is g_expiryRampTime = newExpiryRampTime; } + // ============ Approval Functions ============ + + function approveSender( + address sender, + bool approved + ) + external + { + emit LogSenderApproved(msg.sender, sender, approved); + g_approvedSender[msg.sender][sender] = approved; + } + // ============ Getters ============ function getExpiry( @@ -109,7 +136,7 @@ contract Expiry is ) public view - returns (uint32) + returns (uint256) { return g_expiries[account.owner][account.number][marketId]; } @@ -117,7 +144,7 @@ contract Expiry is function getSpreadAdjustedPrices( uint256 heldMarketId, uint256 owedMarketId, - uint32 expiry + uint256 expiry ) public view @@ -131,7 +158,7 @@ contract Expiry is owedMarketId ); - uint256 expiryAge = Time.currentTime().sub(expiry); + uint256 expiryAge = block.timestamp.sub(expiry); if (expiryAge < g_expiryRampTime) { spread.value = Math.getPartial(spread.value, expiryAge, g_expiryRampTime); @@ -154,17 +181,21 @@ contract Expiry is public onlySolo(msg.sender) { - ( - uint256 marketId, - uint32 expiryTime - ) = parseCallArgs(data); + ExpiryArg[] memory expiries = abi.decode(data, (ExpiryArg[])); - // don't set expiry time for accounts with positive balance - if (expiryTime != 0 && !SOLO_MARGIN.getAccountPar(account, marketId).isNegative()) { - return; + for (uint256 i = 0; i < expiries.length; i++) { + ExpiryArg memory exp = expiries[i]; + Require.that( + g_approvedSender[exp.account.owner][account.owner], + FILE, + "Unapproved sender" + ); + Types.Par memory par = SOLO_MARGIN.getAccountPar(exp.account, exp.marketId); + uint256 expiryTime = par.isNegative() + ? block.timestamp.add(FOUR_WEEKS_IN_SECONDS) + : 0; + setExpiry(exp.account, exp.marketId, expiryTime); } - - setExpiry(account, marketId, expiryTime); } function getTradeCost( @@ -191,12 +222,9 @@ contract Expiry is }); } - ( - uint256 owedMarketId, - uint32 maxExpiry - ) = parseTradeArgs(data); + (uint256 owedMarketId, uint256 maxExpiry) = abi.decode(data, (uint256, uint256)); - uint32 expiry = getExpiry(makerAccount, owedMarketId); + uint256 expiry = getExpiry(makerAccount, owedMarketId); // validate expiry Require.that( @@ -208,7 +236,7 @@ contract Expiry is owedMarketId ); Require.that( - expiry <= Time.currentTime(), + expiry <= block.timestamp, FILE, "Borrow not yet expired", expiry @@ -242,7 +270,7 @@ contract Expiry is Types.Par memory newInputPar, Types.Wei memory inputWei, uint256 owedMarketId, - uint32 expiry + uint256 expiry ) private returns (Types.AssetAmount memory) @@ -331,7 +359,7 @@ contract Expiry is function setExpiry( Account.Info memory account, uint256 marketId, - uint32 time + uint256 time ) private { @@ -349,7 +377,7 @@ contract Expiry is Types.Wei memory heldWei, uint256 heldMarketId, uint256 owedMarketId, - uint32 expiry + uint256 expiry ) private view @@ -382,7 +410,7 @@ contract Expiry is Types.Wei memory owedWei, uint256 heldMarketId, uint256 owedMarketId, - uint32 expiry + uint256 expiry ) private view @@ -410,68 +438,4 @@ contract Expiry is value: heldAmount }); } - - function parseCallArgs( - bytes memory data - ) - private - pure - returns ( - uint256, - uint32 - ) - { - Require.that( - data.length == 64, - FILE, - "Call data invalid length", - data.length - ); - - uint256 marketId; - uint256 rawExpiry; - - /* solium-disable-next-line security/no-inline-assembly */ - assembly { - marketId := mload(add(data, 32)) - rawExpiry := mload(add(data, 64)) - } - - return ( - marketId, - Math.to32(rawExpiry) - ); - } - - function parseTradeArgs( - bytes memory data - ) - private - pure - returns ( - uint256, - uint32 - ) - { - Require.that( - data.length == 64, - FILE, - "Trade data invalid length", - data.length - ); - - uint256 owedMarketId; - uint256 rawExpiry; - - /* solium-disable-next-line security/no-inline-assembly */ - assembly { - owedMarketId := mload(add(data, 32)) - rawExpiry := mload(add(data, 64)) - } - - return ( - owedMarketId, - Math.to32(rawExpiry) - ); - } } diff --git a/contracts/external/traders/ExpiryOld.sol b/contracts/external/traders/ExpiryOld.sol new file mode 100644 index 00000000..449b9fcf --- /dev/null +++ b/contracts/external/traders/ExpiryOld.sol @@ -0,0 +1,477 @@ +/* + + Copyright 2019 dYdX Trading Inc. + + 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.7; +pragma experimental ABIEncoderV2; + +import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import { Ownable } from "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import { IAutoTrader } from "../../protocol/interfaces/IAutoTrader.sol"; +import { ICallee } from "../../protocol/interfaces/ICallee.sol"; +import { Account } from "../../protocol/lib/Account.sol"; +import { Decimal } from "../../protocol/lib/Decimal.sol"; +import { Math } from "../../protocol/lib/Math.sol"; +import { Monetary } from "../../protocol/lib/Monetary.sol"; +import { Require } from "../../protocol/lib/Require.sol"; +import { Time } from "../../protocol/lib/Time.sol"; +import { Types } from "../../protocol/lib/Types.sol"; +import { OnlySolo } from "../helpers/OnlySolo.sol"; + + +/** + * @title ExpiryOld + * @author dYdX + * + * Sets the negative balance for an account to expire at a certain time. This allows any other + * account to repay that negative balance after expiry using any positive balance in the same + * account. The arbitrage incentive is the same as liquidation in the base protocol. + */ +contract ExpiryOld is + Ownable, + OnlySolo, + ICallee, + IAutoTrader +{ + using SafeMath for uint32; + using SafeMath for uint256; + using Types for Types.Par; + using Types for Types.Wei; + + // ============ Constants ============ + + bytes32 constant FILE = "Expiry"; + + // ============ Events ============ + + event ExpirySet( + address owner, + uint256 number, + uint256 marketId, + uint32 time + ); + + event LogExpiryRampTimeSet( + uint256 expiryRampTime + ); + + // ============ Storage ============ + + // owner => number => market => time + mapping (address => mapping (uint256 => mapping (uint256 => uint32))) g_expiries; + + // time over which the liquidation ratio goes from zero to maximum + uint256 public g_expiryRampTime; + + // ============ Constructor ============ + + constructor ( + address soloMargin, + uint256 expiryRampTime + ) + public + OnlySolo(soloMargin) + { + g_expiryRampTime = expiryRampTime; + } + + // ============ Owner Functions ============ + + function ownerSetExpiryRampTime( + uint256 newExpiryRampTime + ) + external + onlyOwner + { + emit LogExpiryRampTimeSet(newExpiryRampTime); + g_expiryRampTime = newExpiryRampTime; + } + + // ============ Getters ============ + + function getExpiry( + Account.Info memory account, + uint256 marketId + ) + public + view + returns (uint32) + { + return g_expiries[account.owner][account.number][marketId]; + } + + function getSpreadAdjustedPrices( + uint256 heldMarketId, + uint256 owedMarketId, + uint32 expiry + ) + public + view + returns ( + Monetary.Price memory, + Monetary.Price memory + ) + { + Decimal.D256 memory spread = SOLO_MARGIN.getLiquidationSpreadForPair( + heldMarketId, + owedMarketId + ); + + uint256 expiryAge = Time.currentTime().sub(expiry); + + if (expiryAge < g_expiryRampTime) { + spread.value = Math.getPartial(spread.value, expiryAge, g_expiryRampTime); + } + + Monetary.Price memory heldPrice = SOLO_MARGIN.getMarketPrice(heldMarketId); + Monetary.Price memory owedPrice = SOLO_MARGIN.getMarketPrice(owedMarketId); + owedPrice.value = owedPrice.value.add(Decimal.mul(owedPrice.value, spread)); + + return (heldPrice, owedPrice); + } + + // ============ Only-Solo Functions ============ + + function callFunction( + address /* sender */, + Account.Info memory account, + bytes memory data + ) + public + onlySolo(msg.sender) + { + ( + uint256 marketId, + uint32 expiryTime + ) = parseCallArgs(data); + + // don't set expiry time for accounts with positive balance + if (expiryTime != 0 && !SOLO_MARGIN.getAccountPar(account, marketId).isNegative()) { + return; + } + + setExpiry(account, marketId, expiryTime); + } + + function getTradeCost( + uint256 inputMarketId, + uint256 outputMarketId, + Account.Info memory makerAccount, + Account.Info memory /* takerAccount */, + Types.Par memory oldInputPar, + Types.Par memory newInputPar, + Types.Wei memory inputWei, + bytes memory data + ) + public + onlySolo(msg.sender) + returns (Types.AssetAmount memory) + { + // return zero if input amount is zero + if (inputWei.isZero()) { + return Types.AssetAmount({ + sign: true, + denomination: Types.AssetDenomination.Par, + ref: Types.AssetReference.Delta, + value: 0 + }); + } + + ( + uint256 owedMarketId, + uint32 maxExpiry + ) = parseTradeArgs(data); + + uint32 expiry = getExpiry(makerAccount, owedMarketId); + + // validate expiry + Require.that( + expiry != 0, + FILE, + "Expiry not set", + makerAccount.owner, + makerAccount.number, + owedMarketId + ); + Require.that( + expiry <= Time.currentTime(), + FILE, + "Borrow not yet expired", + expiry + ); + Require.that( + expiry <= maxExpiry, + FILE, + "Expiry past maxExpiry", + expiry + ); + + return getTradeCostInternal( + inputMarketId, + outputMarketId, + makerAccount, + oldInputPar, + newInputPar, + inputWei, + owedMarketId, + expiry + ); + } + + // ============ Private Functions ============ + + function getTradeCostInternal( + uint256 inputMarketId, + uint256 outputMarketId, + Account.Info memory makerAccount, + Types.Par memory oldInputPar, + Types.Par memory newInputPar, + Types.Wei memory inputWei, + uint256 owedMarketId, + uint32 expiry + ) + private + returns (Types.AssetAmount memory) + { + Types.AssetAmount memory output; + Types.Wei memory maxOutputWei = SOLO_MARGIN.getAccountWei(makerAccount, outputMarketId); + + if (inputWei.isPositive()) { + Require.that( + inputMarketId == owedMarketId, + FILE, + "inputMarket mismatch", + inputMarketId + ); + Require.that( + !newInputPar.isPositive(), + FILE, + "Borrows cannot be overpaid", + newInputPar.value + ); + assert(oldInputPar.isNegative()); + Require.that( + maxOutputWei.isPositive(), + FILE, + "Collateral must be positive", + outputMarketId, + maxOutputWei.value + ); + output = owedWeiToHeldWei( + inputWei, + outputMarketId, + inputMarketId, + expiry + ); + + // clear expiry if borrow is fully repaid + if (newInputPar.isZero()) { + setExpiry(makerAccount, owedMarketId, 0); + } + } else { + Require.that( + outputMarketId == owedMarketId, + FILE, + "outputMarket mismatch", + outputMarketId + ); + Require.that( + !newInputPar.isNegative(), + FILE, + "Collateral cannot be overused", + newInputPar.value + ); + assert(oldInputPar.isPositive()); + Require.that( + maxOutputWei.isNegative(), + FILE, + "Borrows must be negative", + outputMarketId, + maxOutputWei.value + ); + output = heldWeiToOwedWei( + inputWei, + inputMarketId, + outputMarketId, + expiry + ); + + // clear expiry if borrow is fully repaid + if (output.value == maxOutputWei.value) { + setExpiry(makerAccount, owedMarketId, 0); + } + } + + Require.that( + output.value <= maxOutputWei.value, + FILE, + "outputMarket too small", + output.value, + maxOutputWei.value + ); + assert(output.sign != maxOutputWei.sign); + + return output; + } + + function setExpiry( + Account.Info memory account, + uint256 marketId, + uint32 time + ) + private + { + g_expiries[account.owner][account.number][marketId] = time; + + emit ExpirySet( + account.owner, + account.number, + marketId, + time + ); + } + + function heldWeiToOwedWei( + Types.Wei memory heldWei, + uint256 heldMarketId, + uint256 owedMarketId, + uint32 expiry + ) + private + view + returns (Types.AssetAmount memory) + { + ( + Monetary.Price memory heldPrice, + Monetary.Price memory owedPrice + ) = getSpreadAdjustedPrices( + heldMarketId, + owedMarketId, + expiry + ); + + uint256 owedAmount = Math.getPartialRoundUp( + heldWei.value, + heldPrice.value, + owedPrice.value + ); + + return Types.AssetAmount({ + sign: true, + denomination: Types.AssetDenomination.Wei, + ref: Types.AssetReference.Delta, + value: owedAmount + }); + } + + function owedWeiToHeldWei( + Types.Wei memory owedWei, + uint256 heldMarketId, + uint256 owedMarketId, + uint32 expiry + ) + private + view + returns (Types.AssetAmount memory) + { + ( + Monetary.Price memory heldPrice, + Monetary.Price memory owedPrice + ) = getSpreadAdjustedPrices( + heldMarketId, + owedMarketId, + expiry + ); + + uint256 heldAmount = Math.getPartial( + owedWei.value, + owedPrice.value, + heldPrice.value + ); + + return Types.AssetAmount({ + sign: false, + denomination: Types.AssetDenomination.Wei, + ref: Types.AssetReference.Delta, + value: heldAmount + }); + } + + function parseCallArgs( + bytes memory data + ) + private + pure + returns ( + uint256, + uint32 + ) + { + Require.that( + data.length == 64, + FILE, + "Call data invalid length", + data.length + ); + + uint256 marketId; + uint256 rawExpiry; + + /* solium-disable-next-line security/no-inline-assembly */ + assembly { + marketId := mload(add(data, 32)) + rawExpiry := mload(add(data, 64)) + } + + return ( + marketId, + Math.to32(rawExpiry) + ); + } + + function parseTradeArgs( + bytes memory data + ) + private + pure + returns ( + uint256, + uint32 + ) + { + Require.that( + data.length == 64, + FILE, + "Trade data invalid length", + data.length + ); + + uint256 owedMarketId; + uint256 rawExpiry; + + /* solium-disable-next-line security/no-inline-assembly */ + assembly { + owedMarketId := mload(add(data, 32)) + rawExpiry := mload(add(data, 64)) + } + + return ( + owedMarketId, + Math.to32(rawExpiry) + ); + } +}