Created
June 8, 2020 11:25
-
-
Save Pet3ris/9cc3f4b9d2ea2157041353484d4f0086 to your computer and use it in GitHub Desktop.
dYdX solo in one file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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; | |
contract TestCounter | |
{ | |
// ============ Storage ============ | |
uint256 public counterFallback; | |
uint256 public counterOne; | |
mapping (uint256 => uint256) public counterTwo; | |
mapping (uint256 => mapping (uint256 => uint256)) public counterThree; | |
// ============ Functions ============ | |
function() | |
external | |
payable | |
{ | |
counterFallback++; | |
} | |
function functionOne() | |
public | |
{ | |
counterOne++; | |
} | |
function functionTwo( | |
uint256 input | |
) | |
public | |
{ | |
counterTwo[input]++; | |
} | |
function functionThree( | |
uint256 input1, | |
uint256 input2 | |
) | |
public | |
{ | |
counterThree[input1][input2]++; | |
} | |
} | |
library Exchange { | |
using Types for Types.Wei; | |
// ============ Constants ============ | |
bytes32 constant FILE = "Exchange"; | |
// ============ Library Functions ============ | |
function transferOut( | |
address token, | |
address to, | |
Types.Wei memory deltaWei | |
) | |
internal | |
{ | |
Require.that( | |
!deltaWei.isPositive(), | |
FILE, | |
"Cannot transferOut positive", | |
deltaWei.value | |
); | |
Token.transfer( | |
token, | |
to, | |
deltaWei.value | |
); | |
} | |
function transferIn( | |
address token, | |
address from, | |
Types.Wei memory deltaWei | |
) | |
internal | |
{ | |
Require.that( | |
!deltaWei.isNegative(), | |
FILE, | |
"Cannot transferIn negative", | |
deltaWei.value | |
); | |
Token.transferFrom( | |
token, | |
from, | |
address(this), | |
deltaWei.value | |
); | |
} | |
function getCost( | |
address exchangeWrapper, | |
address supplyToken, | |
address borrowToken, | |
Types.Wei memory desiredAmount, | |
bytes memory orderData | |
) | |
internal | |
view | |
returns (Types.Wei memory) | |
{ | |
Require.that( | |
!desiredAmount.isNegative(), | |
FILE, | |
"Cannot getCost negative", | |
desiredAmount.value | |
); | |
Types.Wei memory result; | |
result.sign = false; | |
result.value = IExchangeWrapper(exchangeWrapper).getExchangeCost( | |
supplyToken, | |
borrowToken, | |
desiredAmount.value, | |
orderData | |
); | |
return result; | |
} | |
function exchange( | |
address exchangeWrapper, | |
address accountOwner, | |
address supplyToken, | |
address borrowToken, | |
Types.Wei memory requestedFillAmount, | |
bytes memory orderData | |
) | |
internal | |
returns (Types.Wei memory) | |
{ | |
Require.that( | |
!requestedFillAmount.isPositive(), | |
FILE, | |
"Cannot exchange positive", | |
requestedFillAmount.value | |
); | |
transferOut(borrowToken, exchangeWrapper, requestedFillAmount); | |
Types.Wei memory result; | |
result.sign = true; | |
result.value = IExchangeWrapper(exchangeWrapper).exchange( | |
accountOwner, | |
address(this), | |
supplyToken, | |
borrowToken, | |
requestedFillAmount.value, | |
orderData | |
); | |
transferIn(supplyToken, exchangeWrapper, result); | |
return result; | |
} | |
} | |
contract TestLib { | |
// ============ Constants ============ | |
bytes32 constant FILE = "TestLib"; | |
// ============ TypedSignature Functions ============ | |
function TypedSignatureRecover( | |
bytes32 hash, | |
bytes calldata signatureWithType | |
) | |
external | |
pure | |
returns (address) | |
{ | |
return TypedSignature.recover(hash, signatureWithType); | |
} | |
// ============ Math Functions ============ | |
function MathGetPartial( | |
uint256 target, | |
uint256 numerator, | |
uint256 denominator | |
) | |
external | |
pure | |
returns (uint256) | |
{ | |
return Math.getPartial(target, numerator, denominator); | |
} | |
function MathGetPartialRoundUp( | |
uint256 target, | |
uint256 numerator, | |
uint256 denominator | |
) | |
external | |
pure | |
returns (uint256) | |
{ | |
return Math.getPartialRoundUp(target, numerator, denominator); | |
} | |
function MathTo128( | |
uint256 x | |
) | |
external | |
pure | |
returns (uint128) | |
{ | |
return Math.to128(x); | |
} | |
function MathTo96( | |
uint256 x | |
) | |
external | |
pure | |
returns (uint96) | |
{ | |
return Math.to96(x); | |
} | |
function MathTo32( | |
uint256 x | |
) | |
external | |
pure | |
returns (uint32) | |
{ | |
return Math.to32(x); | |
} | |
// ============ Require Functions ============ | |
function RequireThat0( | |
bytes32 reason | |
) | |
external | |
pure | |
{ | |
Require.that( | |
false, | |
FILE, | |
reason | |
); | |
} | |
function RequireThat1( | |
bytes32 reason, | |
uint256 payloadA | |
) | |
external | |
pure | |
{ | |
Require.that( | |
false, | |
FILE, | |
reason, | |
payloadA | |
); | |
} | |
function RequireThat2( | |
bytes32 reason, | |
uint256 payloadA, | |
uint256 payloadB | |
) | |
external | |
pure | |
{ | |
Require.that( | |
false, | |
FILE, | |
reason, | |
payloadA, | |
payloadB | |
); | |
} | |
function RequireThatA0( | |
bytes32 reason, | |
address payloadA | |
) | |
external | |
pure | |
{ | |
Require.that( | |
false, | |
FILE, | |
reason, | |
payloadA | |
); | |
} | |
function RequireThatA1( | |
bytes32 reason, | |
address payloadA, | |
uint256 payloadB | |
) | |
external | |
pure | |
{ | |
Require.that( | |
false, | |
FILE, | |
reason, | |
payloadA, | |
payloadB | |
); | |
} | |
function RequireThatA2( | |
bytes32 reason, | |
address payloadA, | |
uint256 payloadB, | |
uint256 payloadC | |
) | |
external | |
pure | |
{ | |
Require.that( | |
false, | |
FILE, | |
reason, | |
payloadA, | |
payloadB, | |
payloadC | |
); | |
} | |
function RequireThatB0( | |
bytes32 reason, | |
bytes32 payloadA | |
) | |
external | |
pure | |
{ | |
Require.that( | |
false, | |
FILE, | |
reason, | |
payloadA | |
); | |
} | |
function RequireThatB2( | |
bytes32 reason, | |
bytes32 payloadA, | |
uint256 payloadB, | |
uint256 payloadC | |
) | |
external | |
pure | |
{ | |
Require.that( | |
false, | |
FILE, | |
reason, | |
payloadA, | |
payloadB, | |
payloadC | |
); | |
} | |
// ============ Time Functions ============ | |
function TimeCurrentTime() | |
external | |
view | |
returns (uint32) | |
{ | |
return Time.currentTime(); | |
} | |
// ============ Token Functions ============ | |
function TokenBalanceOf( | |
address token, | |
address owner | |
) | |
external | |
view | |
returns (uint256) | |
{ | |
return Token.balanceOf(token, owner); | |
} | |
function TokenAllowance( | |
address token, | |
address owner, | |
address spender | |
) | |
external | |
view | |
returns (uint256) | |
{ | |
return Token.allowance(token, owner, spender); | |
} | |
function TokenApprove( | |
address token, | |
address spender, | |
uint256 amount | |
) | |
external | |
{ | |
Token.approve(token, spender, amount); | |
} | |
function TokenApproveMax( | |
address token, | |
address spender | |
) | |
external | |
{ | |
Token.approveMax(token, spender); | |
} | |
function TokenTransfer( | |
address token, | |
address to, | |
uint256 amount | |
) | |
external | |
{ | |
Token.transfer(token, to, amount); | |
} | |
function TokenTransferFrom( | |
address token, | |
address from, | |
address to, | |
uint256 amount | |
) | |
external | |
{ | |
Token.transferFrom( | |
token, | |
from, | |
to, | |
amount | |
); | |
} | |
// ============ Types Functions ============ | |
function TypesZeroPar() | |
external | |
pure | |
returns (Types.Par memory) | |
{ | |
return Types.zeroPar(); | |
} | |
function TypesParSub( | |
Types.Par memory a, | |
Types.Par memory b | |
) | |
public | |
pure | |
returns (Types.Par memory) | |
{ | |
return Types.sub(a, b); | |
} | |
function TypesParAdd( | |
Types.Par memory a, | |
Types.Par memory b | |
) | |
public | |
pure | |
returns (Types.Par memory) | |
{ | |
return Types.add(a, b); | |
} | |
function TypesParEquals( | |
Types.Par memory a, | |
Types.Par memory b | |
) | |
public | |
pure | |
returns (bool) | |
{ | |
return Types.equals(a, b); | |
} | |
function TypesParNegative( | |
Types.Par memory a | |
) | |
public | |
pure | |
returns (Types.Par memory) | |
{ | |
return Types.negative(a); | |
} | |
function TypesParIsNegative( | |
Types.Par memory a | |
) | |
public | |
pure | |
returns (bool) | |
{ | |
return Types.isNegative(a); | |
} | |
function TypesParIsPositive( | |
Types.Par memory a | |
) | |
public | |
pure | |
returns (bool) | |
{ | |
return Types.isPositive(a); | |
} | |
function TypesParIsZero( | |
Types.Par memory a | |
) | |
public | |
pure | |
returns (bool) | |
{ | |
return Types.isZero(a); | |
} | |
function TypesZeroWei() | |
external | |
pure | |
returns (Types.Wei memory) | |
{ | |
return Types.zeroWei(); | |
} | |
function TypesWeiSub( | |
Types.Wei memory a, | |
Types.Wei memory b | |
) | |
public | |
pure | |
returns (Types.Wei memory) | |
{ | |
return Types.sub(a, b); | |
} | |
function TypesWeiAdd( | |
Types.Wei memory a, | |
Types.Wei memory b | |
) | |
public | |
pure | |
returns (Types.Wei memory) | |
{ | |
return Types.add(a, b); | |
} | |
function TypesWeiEquals( | |
Types.Wei memory a, | |
Types.Wei memory b | |
) | |
public | |
pure | |
returns (bool) | |
{ | |
return Types.equals(a, b); | |
} | |
function TypesWeiNegative( | |
Types.Wei memory a | |
) | |
public | |
pure | |
returns (Types.Wei memory) | |
{ | |
return Types.negative(a); | |
} | |
function TypesWeiIsNegative( | |
Types.Wei memory a | |
) | |
public | |
pure | |
returns (bool) | |
{ | |
return Types.isNegative(a); | |
} | |
function TypesWeiIsPositive( | |
Types.Wei memory a | |
) | |
public | |
pure | |
returns (bool) | |
{ | |
return Types.isPositive(a); | |
} | |
function TypesWeiIsZero( | |
Types.Wei memory a | |
) | |
public | |
pure | |
returns (bool) | |
{ | |
return Types.isZero(a); | |
} | |
} | |
contract IPriceOracle { | |
// ============ Constants ============ | |
uint256 public constant ONE_DOLLAR = 10 ** 36; | |
// ============ Public Functions ============ | |
/** | |
* Get the price of a token | |
* | |
* @param token The ERC20 token address of the market | |
* @return The USD price of a base unit of the token, then multiplied by 10^36. | |
* So a USD-stable coin with 18 decimal places would return 10^18. | |
* This is the price of the base unit rather than the price of a "human-readable" | |
* token amount. Every ERC20 may have a different number of decimals. | |
*/ | |
function getPrice( | |
address token | |
) | |
public | |
view | |
returns (Monetary.Price memory); | |
} | |
library Math { | |
using SafeMath for uint256; | |
// ============ Constants ============ | |
bytes32 constant FILE = "Math"; | |
// ============ Library Functions ============ | |
/* | |
* Return target * (numerator / denominator). | |
*/ | |
function getPartial( | |
uint256 target, | |
uint256 numerator, | |
uint256 denominator | |
) | |
internal | |
pure | |
returns (uint256) | |
{ | |
return target.mul(numerator).div(denominator); | |
} | |
/* | |
* Return target * (numerator / denominator), but rounded up. | |
*/ | |
function getPartialRoundUp( | |
uint256 target, | |
uint256 numerator, | |
uint256 denominator | |
) | |
internal | |
pure | |
returns (uint256) | |
{ | |
if (target == 0 || numerator == 0) { | |
// SafeMath will check for zero denominator | |
return SafeMath.div(0, denominator); | |
} | |
return target.mul(numerator).sub(1).div(denominator).add(1); | |
} | |
function to128( | |
uint256 number | |
) | |
internal | |
pure | |
returns (uint128) | |
{ | |
uint128 result = uint128(number); | |
Require.that( | |
result == number, | |
FILE, | |
"Unsafe cast to uint128" | |
); | |
return result; | |
} | |
function to96( | |
uint256 number | |
) | |
internal | |
pure | |
returns (uint96) | |
{ | |
uint96 result = uint96(number); | |
Require.that( | |
result == number, | |
FILE, | |
"Unsafe cast to uint96" | |
); | |
return result; | |
} | |
function to32( | |
uint256 number | |
) | |
internal | |
pure | |
returns (uint32) | |
{ | |
uint32 result = uint32(number); | |
Require.that( | |
result == number, | |
FILE, | |
"Unsafe cast to uint32" | |
); | |
return result; | |
} | |
function min( | |
uint256 a, | |
uint256 b | |
) | |
internal | |
pure | |
returns (uint256) | |
{ | |
return a < b ? a : b; | |
} | |
function max( | |
uint256 a, | |
uint256 b | |
) | |
internal | |
pure | |
returns (uint256) | |
{ | |
return a > b ? a : b; | |
} | |
} | |
contract State | |
{ | |
Storage.State g_state; | |
} | |
library Token { | |
// ============ Constants ============ | |
bytes32 constant FILE = "Token"; | |
// ============ Library Functions ============ | |
function balanceOf( | |
address token, | |
address owner | |
) | |
internal | |
view | |
returns (uint256) | |
{ | |
return IErc20(token).balanceOf(owner); | |
} | |
function allowance( | |
address token, | |
address owner, | |
address spender | |
) | |
internal | |
view | |
returns (uint256) | |
{ | |
return IErc20(token).allowance(owner, spender); | |
} | |
function approve( | |
address token, | |
address spender, | |
uint256 amount | |
) | |
internal | |
{ | |
IErc20(token).approve(spender, amount); | |
Require.that( | |
checkSuccess(), | |
FILE, | |
"Approve failed" | |
); | |
} | |
function approveMax( | |
address token, | |
address spender | |
) | |
internal | |
{ | |
approve( | |
token, | |
spender, | |
uint256(-1) | |
); | |
} | |
function transfer( | |
address token, | |
address to, | |
uint256 amount | |
) | |
internal | |
{ | |
if (amount == 0 || to == address(this)) { | |
return; | |
} | |
IErc20(token).transfer(to, amount); | |
Require.that( | |
checkSuccess(), | |
FILE, | |
"Transfer failed" | |
); | |
} | |
function transferFrom( | |
address token, | |
address from, | |
address to, | |
uint256 amount | |
) | |
internal | |
{ | |
if (amount == 0 || to == from) { | |
return; | |
} | |
IErc20(token).transferFrom(from, to, amount); | |
Require.that( | |
checkSuccess(), | |
FILE, | |
"TransferFrom failed" | |
); | |
} | |
// ============ Private Functions ============ | |
/** | |
* Check the return value of the previous function up to 32 bytes. Return true if the previous | |
* function returned 0 bytes or 32 bytes that are not all-zero. | |
*/ | |
function checkSuccess( | |
) | |
private | |
pure | |
returns (bool) | |
{ | |
uint256 returnValue = 0; | |
/* solium-disable-next-line security/no-inline-assembly */ | |
assembly { | |
// check number of bytes returned from last function call | |
switch returndatasize | |
// no bytes returned: assume success | |
case 0x0 { | |
returnValue := 1 | |
} | |
// 32 bytes returned: check if non-zero | |
case 0x20 { | |
// copy 32 bytes into scratch space | |
returndatacopy(0x0, 0x0, 0x20) | |
// load those bytes into returnValue | |
returnValue := mload(0x0) | |
} | |
// not sure what was returned: don't mark as success | |
default { } | |
} | |
return returnValue != 0; | |
} | |
} | |
library Events { | |
using Types for Types.Wei; | |
using Storage for Storage.State; | |
// ============ Events ============ | |
event LogIndexUpdate( | |
uint256 indexed market, | |
Interest.Index index | |
); | |
event LogOperation( | |
address sender | |
); | |
event LogDeposit( | |
address indexed accountOwner, | |
uint256 accountNumber, | |
uint256 market, | |
BalanceUpdate update, | |
address from | |
); | |
event LogWithdraw( | |
address indexed accountOwner, | |
uint256 accountNumber, | |
uint256 market, | |
BalanceUpdate update, | |
address to | |
); | |
event LogTransfer( | |
address indexed accountOneOwner, | |
uint256 accountOneNumber, | |
address indexed accountTwoOwner, | |
uint256 accountTwoNumber, | |
uint256 market, | |
BalanceUpdate updateOne, | |
BalanceUpdate updateTwo | |
); | |
event LogBuy( | |
address indexed accountOwner, | |
uint256 accountNumber, | |
uint256 takerMarket, | |
uint256 makerMarket, | |
BalanceUpdate takerUpdate, | |
BalanceUpdate makerUpdate, | |
address exchangeWrapper | |
); | |
event LogSell( | |
address indexed accountOwner, | |
uint256 accountNumber, | |
uint256 takerMarket, | |
uint256 makerMarket, | |
BalanceUpdate takerUpdate, | |
BalanceUpdate makerUpdate, | |
address exchangeWrapper | |
); | |
event LogTrade( | |
address indexed takerAccountOwner, | |
uint256 takerAccountNumber, | |
address indexed makerAccountOwner, | |
uint256 makerAccountNumber, | |
uint256 inputMarket, | |
uint256 outputMarket, | |
BalanceUpdate takerInputUpdate, | |
BalanceUpdate takerOutputUpdate, | |
BalanceUpdate makerInputUpdate, | |
BalanceUpdate makerOutputUpdate, | |
address autoTrader | |
); | |
event LogCall( | |
address indexed accountOwner, | |
uint256 accountNumber, | |
address callee | |
); | |
event LogLiquidate( | |
address indexed solidAccountOwner, | |
uint256 solidAccountNumber, | |
address indexed liquidAccountOwner, | |
uint256 liquidAccountNumber, | |
uint256 heldMarket, | |
uint256 owedMarket, | |
BalanceUpdate solidHeldUpdate, | |
BalanceUpdate solidOwedUpdate, | |
BalanceUpdate liquidHeldUpdate, | |
BalanceUpdate liquidOwedUpdate | |
); | |
event LogVaporize( | |
address indexed solidAccountOwner, | |
uint256 solidAccountNumber, | |
address indexed vaporAccountOwner, | |
uint256 vaporAccountNumber, | |
uint256 heldMarket, | |
uint256 owedMarket, | |
BalanceUpdate solidHeldUpdate, | |
BalanceUpdate solidOwedUpdate, | |
BalanceUpdate vaporOwedUpdate | |
); | |
// ============ Structs ============ | |
struct BalanceUpdate { | |
Types.Wei deltaWei; | |
Types.Par newPar; | |
} | |
// ============ Internal Functions ============ | |
function logIndexUpdate( | |
uint256 marketId, | |
Interest.Index memory index | |
) | |
internal | |
{ | |
emit LogIndexUpdate( | |
marketId, | |
index | |
); | |
} | |
function logOperation() | |
internal | |
{ | |
emit LogOperation(msg.sender); | |
} | |
function logDeposit( | |
Storage.State storage state, | |
Actions.DepositArgs memory args, | |
Types.Wei memory deltaWei | |
) | |
internal | |
{ | |
emit LogDeposit( | |
args.account.owner, | |
args.account.number, | |
args.market, | |
getBalanceUpdate( | |
state, | |
args.account, | |
args.market, | |
deltaWei | |
), | |
args.from | |
); | |
} | |
function logWithdraw( | |
Storage.State storage state, | |
Actions.WithdrawArgs memory args, | |
Types.Wei memory deltaWei | |
) | |
internal | |
{ | |
emit LogWithdraw( | |
args.account.owner, | |
args.account.number, | |
args.market, | |
getBalanceUpdate( | |
state, | |
args.account, | |
args.market, | |
deltaWei | |
), | |
args.to | |
); | |
} | |
function logTransfer( | |
Storage.State storage state, | |
Actions.TransferArgs memory args, | |
Types.Wei memory deltaWei | |
) | |
internal | |
{ | |
emit LogTransfer( | |
args.accountOne.owner, | |
args.accountOne.number, | |
args.accountTwo.owner, | |
args.accountTwo.number, | |
args.market, | |
getBalanceUpdate( | |
state, | |
args.accountOne, | |
args.market, | |
deltaWei | |
), | |
getBalanceUpdate( | |
state, | |
args.accountTwo, | |
args.market, | |
deltaWei.negative() | |
) | |
); | |
} | |
function logBuy( | |
Storage.State storage state, | |
Actions.BuyArgs memory args, | |
Types.Wei memory takerWei, | |
Types.Wei memory makerWei | |
) | |
internal | |
{ | |
emit LogBuy( | |
args.account.owner, | |
args.account.number, | |
args.takerMarket, | |
args.makerMarket, | |
getBalanceUpdate( | |
state, | |
args.account, | |
args.takerMarket, | |
takerWei | |
), | |
getBalanceUpdate( | |
state, | |
args.account, | |
args.makerMarket, | |
makerWei | |
), | |
args.exchangeWrapper | |
); | |
} | |
function logSell( | |
Storage.State storage state, | |
Actions.SellArgs memory args, | |
Types.Wei memory takerWei, | |
Types.Wei memory makerWei | |
) | |
internal | |
{ | |
emit LogSell( | |
args.account.owner, | |
args.account.number, | |
args.takerMarket, | |
args.makerMarket, | |
getBalanceUpdate( | |
state, | |
args.account, | |
args.takerMarket, | |
takerWei | |
), | |
getBalanceUpdate( | |
state, | |
args.account, | |
args.makerMarket, | |
makerWei | |
), | |
args.exchangeWrapper | |
); | |
} | |
function logTrade( | |
Storage.State storage state, | |
Actions.TradeArgs memory args, | |
Types.Wei memory inputWei, | |
Types.Wei memory outputWei | |
) | |
internal | |
{ | |
BalanceUpdate[4] memory updates = [ | |
getBalanceUpdate( | |
state, | |
args.takerAccount, | |
args.inputMarket, | |
inputWei.negative() | |
), | |
getBalanceUpdate( | |
state, | |
args.takerAccount, | |
args.outputMarket, | |
outputWei.negative() | |
), | |
getBalanceUpdate( | |
state, | |
args.makerAccount, | |
args.inputMarket, | |
inputWei | |
), | |
getBalanceUpdate( | |
state, | |
args.makerAccount, | |
args.outputMarket, | |
outputWei | |
) | |
]; | |
emit LogTrade( | |
args.takerAccount.owner, | |
args.takerAccount.number, | |
args.makerAccount.owner, | |
args.makerAccount.number, | |
args.inputMarket, | |
args.outputMarket, | |
updates[0], | |
updates[1], | |
updates[2], | |
updates[3], | |
args.autoTrader | |
); | |
} | |
function logCall( | |
Actions.CallArgs memory args | |
) | |
internal | |
{ | |
emit LogCall( | |
args.account.owner, | |
args.account.number, | |
args.callee | |
); | |
} | |
function logLiquidate( | |
Storage.State storage state, | |
Actions.LiquidateArgs memory args, | |
Types.Wei memory heldWei, | |
Types.Wei memory owedWei | |
) | |
internal | |
{ | |
BalanceUpdate memory solidHeldUpdate = getBalanceUpdate( | |
state, | |
args.solidAccount, | |
args.heldMarket, | |
heldWei.negative() | |
); | |
BalanceUpdate memory solidOwedUpdate = getBalanceUpdate( | |
state, | |
args.solidAccount, | |
args.owedMarket, | |
owedWei.negative() | |
); | |
BalanceUpdate memory liquidHeldUpdate = getBalanceUpdate( | |
state, | |
args.liquidAccount, | |
args.heldMarket, | |
heldWei | |
); | |
BalanceUpdate memory liquidOwedUpdate = getBalanceUpdate( | |
state, | |
args.liquidAccount, | |
args.owedMarket, | |
owedWei | |
); | |
emit LogLiquidate( | |
args.solidAccount.owner, | |
args.solidAccount.number, | |
args.liquidAccount.owner, | |
args.liquidAccount.number, | |
args.heldMarket, | |
args.owedMarket, | |
solidHeldUpdate, | |
solidOwedUpdate, | |
liquidHeldUpdate, | |
liquidOwedUpdate | |
); | |
} | |
function logVaporize( | |
Storage.State storage state, | |
Actions.VaporizeArgs memory args, | |
Types.Wei memory heldWei, | |
Types.Wei memory owedWei, | |
Types.Wei memory excessWei | |
) | |
internal | |
{ | |
BalanceUpdate memory solidHeldUpdate = getBalanceUpdate( | |
state, | |
args.solidAccount, | |
args.heldMarket, | |
heldWei.negative() | |
); | |
BalanceUpdate memory solidOwedUpdate = getBalanceUpdate( | |
state, | |
args.solidAccount, | |
args.owedMarket, | |
owedWei.negative() | |
); | |
BalanceUpdate memory vaporOwedUpdate = getBalanceUpdate( | |
state, | |
args.vaporAccount, | |
args.owedMarket, | |
owedWei.add(excessWei) | |
); | |
emit LogVaporize( | |
args.solidAccount.owner, | |
args.solidAccount.number, | |
args.vaporAccount.owner, | |
args.vaporAccount.number, | |
args.heldMarket, | |
args.owedMarket, | |
solidHeldUpdate, | |
solidOwedUpdate, | |
vaporOwedUpdate | |
); | |
} | |
// ============ Private Functions ============ | |
function getBalanceUpdate( | |
Storage.State storage state, | |
Account.Info memory account, | |
uint256 market, | |
Types.Wei memory deltaWei | |
) | |
private | |
view | |
returns (BalanceUpdate memory) | |
{ | |
return BalanceUpdate({ | |
deltaWei: deltaWei, | |
newPar: state.getPar(account, market) | |
}); | |
} | |
} | |
interface IMakerOracle { | |
// Event that is logged when the `note` modifier is used | |
event LogNote( | |
bytes4 indexed msgSig, | |
address indexed msgSender, | |
bytes32 indexed arg1, | |
bytes32 indexed arg2, | |
uint256 msgValue, | |
bytes msgData | |
) anonymous; | |
// returns the current value (ETH/USD * 10**18) as a bytes32 | |
function peek() | |
external | |
view | |
returns (bytes32, bool); | |
// requires a fresh price and then returns the current value | |
function read() | |
external | |
view | |
returns (bytes32); | |
} | |
contract IAutoTrader { | |
// ============ Public Functions ============ | |
/** | |
* Allows traders to make trades approved by this smart contract. The active trader's account is | |
* the takerAccount and the passive account (for which this contract approves trades | |
* on-behalf-of) is the makerAccount. | |
* | |
* @param inputMarketId The market for which the trader specified the original amount | |
* @param outputMarketId The market for which the trader wants the resulting amount specified | |
* @param makerAccount The account for which this contract is making trades | |
* @param takerAccount The account requesting the trade | |
* @param oldInputPar The old principal amount for the makerAccount for the inputMarketId | |
* @param newInputPar The new principal amount for the makerAccount for the inputMarketId | |
* @param inputWei The change in token amount for the makerAccount for the inputMarketId | |
* @param data Arbitrary data passed in by the trader | |
* @return The AssetAmount for the makerAccount for the outputMarketId | |
*/ | |
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 | |
returns (Types.AssetAmount memory); | |
} | |
library Types { | |
using Math for uint256; | |
// ============ AssetAmount ============ | |
enum AssetDenomination { | |
Wei, // the amount is denominated in wei | |
Par // the amount is denominated in par | |
} | |
enum AssetReference { | |
Delta, // the amount is given as a delta from the current value | |
Target // the amount is given as an exact number to end up at | |
} | |
struct AssetAmount { | |
bool sign; // true if positive | |
AssetDenomination denomination; | |
AssetReference ref; | |
uint256 value; | |
} | |
// ============ Par (Principal Amount) ============ | |
// Total borrow and supply values for a market | |
struct TotalPar { | |
uint128 borrow; | |
uint128 supply; | |
} | |
// Individual principal amount for an account | |
struct Par { | |
bool sign; // true if positive | |
uint128 value; | |
} | |
function zeroPar() | |
internal | |
pure | |
returns (Par memory) | |
{ | |
return Par({ | |
sign: false, | |
value: 0 | |
}); | |
} | |
function sub( | |
Par memory a, | |
Par memory b | |
) | |
internal | |
pure | |
returns (Par memory) | |
{ | |
return add(a, negative(b)); | |
} | |
function add( | |
Par memory a, | |
Par memory b | |
) | |
internal | |
pure | |
returns (Par memory) | |
{ | |
Par memory result; | |
if (a.sign == b.sign) { | |
result.sign = a.sign; | |
result.value = SafeMath.add(a.value, b.value).to128(); | |
} else { | |
if (a.value >= b.value) { | |
result.sign = a.sign; | |
result.value = SafeMath.sub(a.value, b.value).to128(); | |
} else { | |
result.sign = b.sign; | |
result.value = SafeMath.sub(b.value, a.value).to128(); | |
} | |
} | |
return result; | |
} | |
function equals( | |
Par memory a, | |
Par memory b | |
) | |
internal | |
pure | |
returns (bool) | |
{ | |
if (a.value == b.value) { | |
if (a.value == 0) { | |
return true; | |
} | |
return a.sign == b.sign; | |
} | |
return false; | |
} | |
function negative( | |
Par memory a | |
) | |
internal | |
pure | |
returns (Par memory) | |
{ | |
return Par({ | |
sign: !a.sign, | |
value: a.value | |
}); | |
} | |
function isNegative( | |
Par memory a | |
) | |
internal | |
pure | |
returns (bool) | |
{ | |
return !a.sign && a.value > 0; | |
} | |
function isPositive( | |
Par memory a | |
) | |
internal | |
pure | |
returns (bool) | |
{ | |
return a.sign && a.value > 0; | |
} | |
function isZero( | |
Par memory a | |
) | |
internal | |
pure | |
returns (bool) | |
{ | |
return a.value == 0; | |
} | |
// ============ Wei (Token Amount) ============ | |
// Individual token amount for an account | |
struct Wei { | |
bool sign; // true if positive | |
uint256 value; | |
} | |
function zeroWei() | |
internal | |
pure | |
returns (Wei memory) | |
{ | |
return Wei({ | |
sign: false, | |
value: 0 | |
}); | |
} | |
function sub( | |
Wei memory a, | |
Wei memory b | |
) | |
internal | |
pure | |
returns (Wei memory) | |
{ | |
return add(a, negative(b)); | |
} | |
function add( | |
Wei memory a, | |
Wei memory b | |
) | |
internal | |
pure | |
returns (Wei memory) | |
{ | |
Wei memory result; | |
if (a.sign == b.sign) { | |
result.sign = a.sign; | |
result.value = SafeMath.add(a.value, b.value); | |
} else { | |
if (a.value >= b.value) { | |
result.sign = a.sign; | |
result.value = SafeMath.sub(a.value, b.value); | |
} else { | |
result.sign = b.sign; | |
result.value = SafeMath.sub(b.value, a.value); | |
} | |
} | |
return result; | |
} | |
function equals( | |
Wei memory a, | |
Wei memory b | |
) | |
internal | |
pure | |
returns (bool) | |
{ | |
if (a.value == b.value) { | |
if (a.value == 0) { | |
return true; | |
} | |
return a.sign == b.sign; | |
} | |
return false; | |
} | |
function negative( | |
Wei memory a | |
) | |
internal | |
pure | |
returns (Wei memory) | |
{ | |
return Wei({ | |
sign: !a.sign, | |
value: a.value | |
}); | |
} | |
function isNegative( | |
Wei memory a | |
) | |
internal | |
pure | |
returns (bool) | |
{ | |
return !a.sign && a.value > 0; | |
} | |
function isPositive( | |
Wei memory a | |
) | |
internal | |
pure | |
returns (bool) | |
{ | |
return a.sign && a.value > 0; | |
} | |
function isZero( | |
Wei memory a | |
) | |
internal | |
pure | |
returns (bool) | |
{ | |
return a.value == 0; | |
} | |
} | |
contract TestOasisDex { | |
// ============ Constants ============ | |
uint256 constant WAD = 10 ** 18; | |
uint256 constant RAY = 10 ** 27; | |
// ============ Events ============ | |
event LogItemUpdate(uint256 id); | |
event LogTrade( | |
uint256 pay_amt, | |
address indexed pay_gem, | |
uint256 buy_amt, | |
address indexed buy_gem | |
); | |
event LogMake( | |
bytes32 indexed id, | |
bytes32 indexed pair, | |
address indexed maker, | |
address pay_gem, | |
address buy_gem, | |
uint128 pay_amt, | |
uint128 buy_amt, | |
uint64 timestamp | |
); | |
event LogBump( | |
bytes32 indexed id, | |
bytes32 indexed pair, | |
address indexed maker, | |
address pay_gem, | |
address buy_gem, | |
uint128 pay_amt, | |
uint128 buy_amt, | |
uint64 timestamp | |
); | |
event LogTake( | |
bytes32 id, | |
bytes32 indexed pair, | |
address indexed maker, | |
address pay_gem, | |
address buy_gem, | |
address indexed taker, | |
uint128 take_amt, | |
uint128 give_amt, | |
uint64 timestamp | |
); | |
event LogKill( | |
bytes32 indexed id, | |
bytes32 indexed pair, | |
address indexed maker, | |
address pay_gem, | |
address buy_gem, | |
uint128 pay_amt, | |
uint128 buy_amt, | |
uint64 timestamp | |
); | |
event LogBuyEnabled(bool isEnabled); | |
event LogMinSell(address pay_gem, uint256 min_amount); | |
event LogMatchingEnabled(bool isEnabled); | |
event LogUnsortedOffer(uint256 id); | |
event LogSortedOffer(uint256 id); | |
event LogAddTokenPairWhitelist(address baseToken, address quoteToken); | |
event LogRemTokenPairWhitelist(address baseToken, address quoteToken); | |
event LogInsert(address keeper, uint256 id); | |
event LogDelete(address keeper, uint256 id); | |
// ============ Structs ============ | |
struct OfferInfo { | |
uint256 pay_amt; | |
address pay_gem; | |
uint256 buy_amt; | |
address buy_gem; | |
address owner; | |
uint64 timestamp; | |
} | |
struct sortInfo { | |
uint256 next; //points to id of next higher offer | |
uint256 prev; //points to id of previous lower offer | |
uint256 delb; //the blocknumber where this entry was marked for delete | |
} | |
// ============ Storage ============ | |
bool public stopped; | |
bool public buyEnabled = true; | |
bool public matchingEnabled = true; | |
mapping(uint256 => sortInfo) public _rank; | |
mapping(address => mapping(address => uint256)) public _best; | |
mapping(address => mapping(address => uint256)) public _span; | |
mapping(address => uint256) public _dust; | |
mapping(uint256 => uint256) public _near; | |
uint256 _head; | |
uint256 public dustId; | |
uint256 public last_offer_id; | |
mapping (uint256 => OfferInfo) public offers; | |
// ============ Modifiers ============ | |
modifier can_offer { | |
require(!isClosed()); | |
_; | |
} | |
modifier can_buy(uint256 id) { | |
require(isActive(id)); | |
require(!isClosed()); | |
_; | |
} | |
modifier can_cancel(uint256 id) { | |
require(isActive(id), "Offer was deleted or taken, or never existed."); | |
require( | |
isClosed() || msg.sender == getOwner(id) || id == dustId, | |
"Offer can not be cancelled because user is not owner, and market is open, and offer sells required amount of tokens." | |
); | |
_; | |
} | |
// ============ Functions ============ | |
function isClosed() public view returns (bool closed) { | |
return stopped; | |
} | |
function getTime() public view returns (uint64) { | |
return uint64(now); | |
} | |
function stop() public { | |
stopped = true; | |
} | |
function isActive(uint256 id) public view returns (bool active) { | |
return offers[id].timestamp > 0; | |
} | |
function getOwner(uint256 id) public view returns (address owner) { | |
return offers[id].owner; | |
} | |
function getOffer(uint256 id) public view returns (uint256, address, uint256, address) { | |
OfferInfo memory _offer = offers[id]; | |
return (_offer.pay_amt, _offer.pay_gem, | |
_offer.buy_amt, _offer.buy_gem); | |
} | |
function bump(bytes32 id_) | |
public | |
can_buy(uint256(id_)) | |
{ | |
uint256 id = uint256(id_); | |
emit LogBump( | |
id_, | |
keccak256(abi.encodePacked(offers[id].pay_gem, offers[id].buy_gem)), | |
offers[id].owner, | |
offers[id].pay_gem, | |
offers[id].buy_gem, | |
uint128(offers[id].pay_amt), | |
uint128(offers[id].buy_amt), | |
offers[id].timestamp | |
); | |
} | |
function make( | |
address pay_gem, | |
address buy_gem, | |
uint128 pay_amt, | |
uint128 buy_amt | |
) | |
public | |
returns (bytes32) | |
{ | |
return bytes32(offer(pay_amt, pay_gem, buy_amt, buy_gem)); | |
} | |
function take(bytes32 id, uint128 maxTakeAmount) public { | |
require(buy(uint256(id), maxTakeAmount)); | |
} | |
function kill(bytes32 id) public { | |
require(cancel(uint256(id))); | |
} | |
// Make a new offer. Takes funds from the caller into market escrow. | |
// | |
// If matching is enabled: | |
// * creates new offer without putting it in | |
// the sorted list. | |
// * available to authorized contracts only! | |
// * keepers should call insert(id,pos) | |
// to put offer in the sorted list. | |
// | |
// If matching is disabled: | |
// * calls expiring market's offer(). | |
// * available to everyone without authorization. | |
// * no sorting is done. | |
// | |
function offer( | |
uint256 pay_amt, //maker (ask) sell how much | |
address pay_gem, //maker (ask) sell which token | |
uint256 buy_amt, //taker (ask) buy how much | |
address buy_gem //taker (ask) buy which token | |
) | |
public | |
can_offer | |
returns (uint256) | |
{ | |
return _offeru(pay_amt, pay_gem, buy_amt, buy_gem); | |
} | |
// Make a new offer. Takes funds from the caller into market escrow. | |
function offer( | |
uint256 pay_amt, //maker (ask) sell how much | |
address pay_gem, //maker (ask) sell which token | |
uint256 buy_amt, //maker (ask) buy how much | |
address buy_gem, //maker (ask) buy which token | |
uint256 pos //position to insert offer, 0 should be used if unknown | |
) | |
public | |
can_offer | |
returns (uint256) | |
{ | |
return offer(pay_amt, pay_gem, buy_amt, buy_gem, pos, true); | |
} | |
function offer( | |
uint256 pay_amt, //maker (ask) sell how much | |
address pay_gem, //maker (ask) sell which token | |
uint256 buy_amt, //maker (ask) buy how much | |
address buy_gem, //maker (ask) buy which token | |
uint256 pos, //position to insert offer, 0 should be used if unknown | |
bool rounding //match "close enough" orders? | |
) | |
public | |
can_offer | |
returns (uint256) | |
{ | |
require(_dust[pay_gem] <= pay_amt); | |
return _matcho(pay_amt, pay_gem, buy_amt, buy_gem, pos, rounding); | |
} | |
//Transfers funds from caller to offer maker, and from market to caller. | |
function buy(uint256 id, uint256 amount) | |
public | |
can_buy(id) | |
returns (bool) | |
{ | |
return _buys(id, amount); | |
} | |
// Cancel an offer. Refunds offer maker. | |
function cancel(uint256 id) | |
public | |
can_cancel(id) | |
returns (bool success) | |
{ | |
if (isOfferSorted(id)) { | |
require(_unsort(id)); | |
} else { | |
require(_hide(id)); | |
} | |
// read-only offer. Modify an offer by directly accessing offers[id] | |
OfferInfo memory _offer = offers[id]; | |
delete offers[id]; | |
IErc20(_offer.pay_gem).transfer(_offer.owner, _offer.pay_amt); | |
emit LogItemUpdate(id); | |
emit LogKill( | |
bytes32(id), | |
keccak256(abi.encodePacked(_offer.pay_gem, _offer.buy_gem)), | |
_offer.owner, | |
_offer.pay_gem, | |
_offer.buy_gem, | |
uint128(_offer.pay_amt), | |
uint128(_offer.buy_amt), | |
uint64(now) | |
); | |
success = true; | |
} | |
//insert offer into the sorted list | |
//keepers need to use this function | |
function insert( | |
uint256 id, //maker (ask) id | |
uint256 pos //position to insert into | |
) | |
public | |
returns (bool) | |
{ | |
require(!isOfferSorted(id)); //make sure offers[id] is not yet sorted | |
require(isActive(id)); //make sure offers[id] is active | |
_hide(id); //remove offer from unsorted offers list | |
_sort(id, pos); //put offer into the sorted offers list | |
emit LogInsert(msg.sender, id); | |
return true; | |
} | |
//deletes _rank [id] | |
// Function should be called by keepers. | |
function del_rank(uint256 id) | |
public | |
returns (bool) | |
{ | |
require(!isActive(id) && _rank[id].delb != 0 && _rank[id].delb < block.number - 10); | |
delete _rank[id]; | |
emit LogDelete(msg.sender, id); | |
return true; | |
} | |
//set the minimum sell amount for a token | |
// Function is used to avoid "dust offers" that have | |
// very small amount of tokens to sell, and it would | |
// cost more gas to accept the offer, than the value | |
// of tokens received. | |
function setMinSell( | |
address pay_gem, //token to assign minimum sell amount to | |
uint256 dust //maker (ask) minimum sell amount | |
) | |
public | |
returns (bool) | |
{ | |
_dust[pay_gem] = dust; | |
emit LogMinSell(pay_gem, dust); | |
return true; | |
} | |
//returns the minimum sell amount for an offer | |
function getMinSell( | |
address pay_gem //token for which minimum sell amount is queried | |
) | |
public | |
view | |
returns (uint256) | |
{ | |
return _dust[pay_gem]; | |
} | |
//set buy functionality enabled/disabled | |
function setBuyEnabled(bool buyEnabled_) public returns (bool) { | |
buyEnabled = buyEnabled_; | |
emit LogBuyEnabled(buyEnabled); | |
return true; | |
} | |
//set matching enabled/disabled | |
// If matchingEnabled true(default), then inserted offers are matched. | |
// Except the ones inserted by contracts, because those end up | |
// in the unsorted list of offers, that must be later sorted by | |
// keepers using insert(). | |
// If matchingEnabled is false then MatchingMarket is reverted to ExpiringMarket, | |
// and matching is not done, and sorted lists are disabled. | |
function setMatchingEnabled(bool matchingEnabled_) public returns (bool) { | |
matchingEnabled = matchingEnabled_; | |
emit LogMatchingEnabled(matchingEnabled); | |
return true; | |
} | |
//return the best offer for a token pair | |
// the best offer is the lowest one if it's an ask, | |
// and highest one if it's a bid offer | |
function getBestOffer(address sell_gem, address buy_gem) public view returns(uint256) { | |
return _best[sell_gem][buy_gem]; | |
} | |
//return the next worse offer in the sorted list | |
// the worse offer is the higher one if its an ask, | |
// a lower one if its a bid offer, | |
// and in both cases the newer one if they're equal. | |
function getWorseOffer(uint256 id) public view returns(uint256) { | |
return _rank[id].prev; | |
} | |
//return the next better offer in the sorted list | |
// the better offer is in the lower priced one if its an ask, | |
// the next higher priced one if its a bid offer | |
// and in both cases the older one if they're equal. | |
function getBetterOffer(uint256 id) public view returns(uint256) { | |
return _rank[id].next; | |
} | |
//return the amount of better offers for a token pair | |
function getOfferCount(address sell_gem, address buy_gem) public view returns(uint256) { | |
return _span[sell_gem][buy_gem]; | |
} | |
//get the first unsorted offer that was inserted by a contract | |
// Contracts can't calculate the insertion position of their offer because it is not an O(1) operation. | |
// Their offers get put in the unsorted list of offers. | |
// Keepers can calculate the insertion position offchain and pass it to the insert() function to insert | |
// the unsorted offer into the sorted list. Unsorted offers will not be matched, but can be bought with buy(). | |
function getFirstUnsortedOffer() public view returns(uint256) { | |
return _head; | |
} | |
//get the next unsorted offer | |
// Can be used to cycle through all the unsorted offers. | |
function getNextUnsortedOffer(uint256 id) public view returns(uint256) { | |
return _near[id]; | |
} | |
function isOfferSorted(uint256 id) public view returns(bool) { | |
return _rank[id].next != 0 | |
|| _rank[id].prev != 0 | |
|| _best[offers[id].pay_gem][offers[id].buy_gem] == id; | |
} | |
function sellAllAmount(address pay_gem, uint256 pay_amt, address buy_gem, uint256 min_fill_amount) | |
public | |
returns (uint256 fill_amt) | |
{ | |
uint256 offerId; | |
while (pay_amt > 0) { //while there is amount to sell | |
offerId = getBestOffer(buy_gem, pay_gem); //Get the best offer for the token pair | |
require(offerId != 0); //Fails if there are not more offers | |
// There is a chance that pay_amt is smaller than 1 wei of the other token | |
if (pay_amt * 1 ether < wdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)) { | |
break; //We consider that all amount is sold | |
} | |
if (pay_amt >= offers[offerId].buy_amt) { //If amount to sell is higher or equal than current offer amount to buy | |
fill_amt = SafeMath.add(fill_amt, offers[offerId].pay_amt); //Add amount bought to acumulator | |
pay_amt = SafeMath.sub(pay_amt, offers[offerId].buy_amt); //Decrease amount to sell | |
take(bytes32(offerId), uint128(offers[offerId].pay_amt)); //We take the whole offer | |
} else { // if lower | |
uint256 baux = rmul(pay_amt * 10 ** 9, rdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)) / 10 ** 9; | |
fill_amt = SafeMath.add(fill_amt, baux); //Add amount bought to acumulator | |
take(bytes32(offerId), uint128(baux)); //We take the portion of the offer that we need | |
pay_amt = 0; //All amount is sold | |
} | |
} | |
require(fill_amt >= min_fill_amount); | |
} | |
function buyAllAmount(address buy_gem, uint256 buy_amt, address pay_gem, uint256 max_fill_amount) | |
public | |
returns (uint256 fill_amt) | |
{ | |
uint256 offerId; | |
while (buy_amt > 0) { //Meanwhile there is amount to buy | |
offerId = getBestOffer(buy_gem, pay_gem); //Get the best offer for the token pair | |
require(offerId != 0); | |
// There is a chance that buy_amt is smaller than 1 wei of the other token | |
if (buy_amt * 1 ether < wdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)) { | |
break; //We consider that all amount is sold | |
} | |
if (buy_amt >= offers[offerId].pay_amt) { //If amount to buy is higher or equal than current offer amount to sell | |
fill_amt = SafeMath.add(fill_amt, offers[offerId].buy_amt); //Add amount sold to acumulator | |
buy_amt = SafeMath.sub(buy_amt, offers[offerId].pay_amt); //Decrease amount to buy | |
take(bytes32(offerId), uint128(offers[offerId].pay_amt)); //We take the whole offer | |
} else { //if lower | |
fill_amt = SafeMath.add(fill_amt, rmul(buy_amt * 10 ** 9, rdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)) / 10 ** 9); //Add amount sold to acumulator | |
take(bytes32(offerId), uint128(buy_amt)); //We take the portion of the offer that we need | |
buy_amt = 0; //All amount is bought | |
} | |
} | |
require(fill_amt <= max_fill_amount); | |
} | |
function getBuyAmount(address buy_gem, address pay_gem, uint256 pay_amt) public view returns (uint256 fill_amt) { | |
uint256 offerId = getBestOffer(buy_gem, pay_gem); //Get best offer for the token pair | |
while (pay_amt > offers[offerId].buy_amt) { | |
fill_amt = SafeMath.add(fill_amt, offers[offerId].pay_amt); //Add amount to buy accumulator | |
pay_amt = SafeMath.sub(pay_amt, offers[offerId].buy_amt); //Decrease amount to pay | |
if (pay_amt > 0) { //If we still need more offers | |
offerId = getWorseOffer(offerId); //We look for the next best offer | |
require(offerId != 0); //Fails if there are not enough offers to complete | |
} | |
} | |
fill_amt = SafeMath.add(fill_amt, rmul(pay_amt * 10 ** 9, rdiv(offers[offerId].pay_amt, offers[offerId].buy_amt)) / 10 ** 9); //Add proportional amount of last offer to buy accumulator | |
} | |
function getPayAmount(address pay_gem, address buy_gem, uint256 buy_amt) public view returns (uint256 fill_amt) { | |
uint256 offerId = getBestOffer(buy_gem, pay_gem); //Get best offer for the token pair | |
while (buy_amt > offers[offerId].pay_amt) { | |
fill_amt = SafeMath.add(fill_amt, offers[offerId].buy_amt); //Add amount to pay accumulator | |
buy_amt = SafeMath.sub(buy_amt, offers[offerId].pay_amt); //Decrease amount to buy | |
if (buy_amt > 0) { //If we still need more offers | |
offerId = getWorseOffer(offerId); //We look for the next best offer | |
require(offerId != 0); //Fails if there are not enough offers to complete | |
} | |
} | |
fill_amt = SafeMath.add(fill_amt, rmul(buy_amt * 10 ** 9, rdiv(offers[offerId].buy_amt, offers[offerId].pay_amt)) / 10 ** 9); //Add proportional amount of last offer to pay accumulator | |
} | |
// ============ Internal Functions ============ | |
function _next_id() | |
internal | |
returns (uint256) | |
{ | |
last_offer_id++; return last_offer_id; | |
} | |
function _buys(uint256 id, uint256 amount) | |
internal | |
returns (bool) | |
{ | |
require(buyEnabled); | |
if (amount == offers[id].pay_amt) { | |
if (isOfferSorted(id)) { | |
//offers[id] must be removed from sorted list because all of it is bought | |
_unsort(id); | |
}else{ | |
_hide(id); | |
} | |
} | |
require(super_buy(id, amount)); | |
// If offer has become dust during buy, we cancel it | |
if (isActive(id) && offers[id].pay_amt < _dust[offers[id].pay_gem]) { | |
dustId = id; //enable current msg.sender to call cancel(id) | |
cancel(id); | |
} | |
return true; | |
} | |
//find the id of the next higher offer after offers[id] | |
function _find(uint256 id) | |
internal | |
view | |
returns (uint256) | |
{ | |
require(id > 0); | |
address buy_gem = offers[id].buy_gem; | |
address pay_gem = offers[id].pay_gem; | |
uint256 top = _best[pay_gem][buy_gem]; | |
uint256 old_top = 0; | |
// Find the larger-than-id order whose successor is less-than-id. | |
while (top != 0 && _isPricedLtOrEq(id, top)) { | |
old_top = top; | |
top = _rank[top].prev; | |
} | |
return old_top; | |
} | |
//find the id of the next higher offer after offers[id] | |
function _findpos(uint256 id, uint256 pos) | |
internal | |
view | |
returns (uint256) | |
{ | |
require(id > 0); | |
// Look for an active order. | |
while (pos != 0 && !isActive(pos)) { | |
pos = _rank[pos].prev; | |
} | |
if (pos == 0) { | |
//if we got to the end of list without a single active offer | |
return _find(id); | |
} else { | |
// if we did find a nearby active offer | |
// Walk the order book down from there... | |
if(_isPricedLtOrEq(id, pos)) { | |
uint256 old_pos; | |
// Guaranteed to run at least once because of | |
// the prior if statements. | |
while (pos != 0 && _isPricedLtOrEq(id, pos)) { | |
old_pos = pos; | |
pos = _rank[pos].prev; | |
} | |
return old_pos; | |
// ...or walk it up. | |
} else { | |
while (pos != 0 && !_isPricedLtOrEq(id, pos)) { | |
pos = _rank[pos].next; | |
} | |
return pos; | |
} | |
} | |
} | |
//return true if offers[low] priced less than or equal to offers[high] | |
function _isPricedLtOrEq( | |
uint256 low, //lower priced offer's id | |
uint256 high //higher priced offer's id | |
) | |
internal | |
view | |
returns (bool) | |
{ | |
return SafeMath.mul(offers[low].buy_amt, offers[high].pay_amt) | |
>= SafeMath.mul(offers[high].buy_amt, offers[low].pay_amt); | |
} | |
//these variables are global only because of solidity local variable limit | |
//match offers with taker offer, and execute token transactions | |
function _matcho( | |
uint256 t_pay_amt, //taker sell how much | |
address t_pay_gem, //taker sell which token | |
uint256 t_buy_amt, //taker buy how much | |
address t_buy_gem, //taker buy which token | |
uint256 pos, //position id | |
bool rounding //match "close enough" orders? | |
) | |
internal | |
returns (uint256 id) | |
{ | |
uint256 best_maker_id; //highest maker id | |
uint256 t_buy_amt_old; //taker buy how much saved | |
uint256 m_buy_amt; //maker offer wants to buy this much token | |
uint256 m_pay_amt; //maker offer wants to sell this much token | |
// there is at least one offer stored for token pair | |
while (_best[t_buy_gem][t_pay_gem] > 0) { | |
best_maker_id = _best[t_buy_gem][t_pay_gem]; | |
m_buy_amt = offers[best_maker_id].buy_amt; | |
m_pay_amt = offers[best_maker_id].pay_amt; | |
// Ugly hack to work around rounding errors. Based on the idea that | |
// the furthest the amounts can stray from their "true" values is 1. | |
// Ergo the worst case has t_pay_amt and m_pay_amt at +1 away from | |
// their "correct" values and m_buy_amt and t_buy_amt at -1. | |
// Since (c - 1) * (d - 1) > (a + 1) * (b + 1) is equivalent to | |
// c * d > a * b + a + b + c + d, we write... | |
if (SafeMath.mul(m_buy_amt, t_buy_amt) > SafeMath.mul(t_pay_amt, m_pay_amt) + | |
(rounding ? m_buy_amt + t_buy_amt + t_pay_amt + m_pay_amt : 0)) | |
{ | |
break; | |
} | |
// ^ The `rounding` parameter is a compromise borne of a couple days | |
// of discussion. | |
buy(best_maker_id, Math.min(m_pay_amt, t_buy_amt)); | |
t_buy_amt_old = t_buy_amt; | |
t_buy_amt = SafeMath.sub(t_buy_amt, Math.min(m_pay_amt, t_buy_amt)); | |
t_pay_amt = SafeMath.mul(t_buy_amt, t_pay_amt) / t_buy_amt_old; | |
if (t_pay_amt == 0 || t_buy_amt == 0) { | |
break; | |
} | |
} | |
if (t_buy_amt > 0 && t_pay_amt > 0 && t_pay_amt >= _dust[t_pay_gem]) { | |
//new offer should be created | |
id = super_offer(t_pay_amt, t_pay_gem, t_buy_amt, t_buy_gem); | |
//insert offer into the sorted list | |
_sort(id, pos); | |
} | |
} | |
// Make a new offer without putting it in the sorted list. | |
// Takes funds from the caller into market escrow. | |
// ****Available to authorized contracts only!********** | |
// Keepers should call insert(id,pos) to put offer in the sorted list. | |
function _offeru( | |
uint256 pay_amt, //maker (ask) sell how much | |
address pay_gem, //maker (ask) sell which token | |
uint256 buy_amt, //maker (ask) buy how much | |
address buy_gem //maker (ask) buy which token | |
) | |
internal | |
returns (uint256 id) | |
{ | |
require(_dust[pay_gem] <= pay_amt); | |
id = super_offer(pay_amt, pay_gem, buy_amt, buy_gem); | |
_near[id] = _head; | |
_head = id; | |
emit LogUnsortedOffer(id); | |
} | |
//put offer into the sorted list | |
function _sort( | |
uint256 id, //maker (ask) id | |
uint256 pos //position to insert into | |
) | |
internal | |
{ | |
require(isActive(id)); | |
address buy_gem = offers[id].buy_gem; | |
address pay_gem = offers[id].pay_gem; | |
uint256 prev_id; //maker (ask) id | |
pos = pos == 0 || offers[pos].pay_gem != pay_gem || offers[pos].buy_gem != buy_gem || !isOfferSorted(pos) | |
? | |
_find(id) | |
: | |
_findpos(id, pos); | |
if (pos != 0) { //offers[id] is not the highest offer | |
//requirement below is satisfied by statements above | |
//require(_isPricedLtOrEq(id, pos)); | |
prev_id = _rank[pos].prev; | |
_rank[pos].prev = id; | |
_rank[id].next = pos; | |
} else { //offers[id] is the highest offer | |
prev_id = _best[pay_gem][buy_gem]; | |
_best[pay_gem][buy_gem] = id; | |
} | |
if (prev_id != 0) { //if lower offer does exist | |
//requirement below is satisfied by statements above | |
//require(!_isPricedLtOrEq(id, prev_id)); | |
_rank[prev_id].next = id; | |
_rank[id].prev = prev_id; | |
} | |
_span[pay_gem][buy_gem]++; | |
emit LogSortedOffer(id); | |
} | |
// Remove offer from the sorted list (does not cancel offer) | |
function _unsort( | |
uint256 id //id of maker (ask) offer to remove from sorted list | |
) | |
internal | |
returns (bool) | |
{ | |
address buy_gem = offers[id].buy_gem; | |
address pay_gem = offers[id].pay_gem; | |
require(_span[pay_gem][buy_gem] > 0); | |
require(_rank[id].delb == 0 && //assert id is in the sorted list | |
isOfferSorted(id)); | |
if (id != _best[pay_gem][buy_gem]) { // offers[id] is not the highest offer | |
require(_rank[_rank[id].next].prev == id); | |
_rank[_rank[id].next].prev = _rank[id].prev; | |
} else { //offers[id] is the highest offer | |
_best[pay_gem][buy_gem] = _rank[id].prev; | |
} | |
if (_rank[id].prev != 0) { //offers[id] is not the lowest offer | |
require(_rank[_rank[id].prev].next == id); | |
_rank[_rank[id].prev].next = _rank[id].next; | |
} | |
_span[pay_gem][buy_gem]--; | |
_rank[id].delb = block.number; //mark _rank[id] for deletion | |
return true; | |
} | |
//Hide offer from the unsorted order book (does not cancel offer) | |
function _hide( | |
uint256 id //id of maker offer to remove from unsorted list | |
) | |
internal | |
returns (bool) | |
{ | |
uint256 uid = _head; //id of an offer in unsorted offers list | |
uint256 pre = uid; //id of previous offer in unsorted offers list | |
require(!isOfferSorted(id)); //make sure offer id is not in sorted offers list | |
if (_head == id) { //check if offer is first offer in unsorted offers list | |
_head = _near[id]; //set head to new first unsorted offer | |
_near[id] = 0; //delete order from unsorted order list | |
return true; | |
} | |
while (uid > 0 && uid != id) { //find offer in unsorted order list | |
pre = uid; | |
uid = _near[uid]; | |
} | |
if (uid != id) { //did not find offer id in unsorted offers list | |
return false; | |
} | |
_near[pre] = _near[id]; //set previous unsorted offer to point to offer after offer id | |
_near[id] = 0; //delete order from unsorted order list | |
return true; | |
} | |
// Accept given `quantity` of an offer. Transfers funds from caller to | |
// offer maker, and from market to caller. | |
function super_buy(uint256 id, uint256 quantity) | |
internal | |
returns (bool) | |
{ | |
OfferInfo memory _offer = offers[id]; | |
uint256 spend = SafeMath.mul(quantity, _offer.buy_amt) / _offer.pay_amt; | |
require(uint128(spend) == spend); | |
require(uint128(quantity) == quantity); | |
// For backwards semantic compatibility. | |
if (quantity == 0 || spend == 0 || | |
quantity > _offer.pay_amt || spend > _offer.buy_amt) | |
{ | |
return false; | |
} | |
offers[id].pay_amt = SafeMath.sub(_offer.pay_amt, quantity); | |
offers[id].buy_amt = SafeMath.sub(_offer.buy_amt, spend); | |
IErc20(_offer.buy_gem).transferFrom(msg.sender, _offer.owner, spend); | |
IErc20(_offer.pay_gem).transfer(msg.sender, quantity); | |
emit LogItemUpdate(id); | |
emit LogTake( | |
bytes32(id), | |
keccak256(abi.encodePacked(_offer.pay_gem, _offer.buy_gem)), | |
_offer.owner, | |
_offer.pay_gem, | |
_offer.buy_gem, | |
msg.sender, | |
uint128(quantity), | |
uint128(spend), | |
uint64(now) | |
); | |
emit LogTrade(quantity, _offer.pay_gem, spend, _offer.buy_gem); | |
if (offers[id].pay_amt == 0) { | |
delete offers[id]; | |
} | |
return true; | |
} | |
// Make a new offer. Takes funds from the caller into market escrow. | |
function super_offer(uint256 pay_amt, address pay_gem, uint256 buy_amt, address buy_gem) | |
internal | |
returns (uint256 id) | |
{ | |
require(uint128(pay_amt) == pay_amt); | |
require(uint128(buy_amt) == buy_amt); | |
require(pay_amt > 0); | |
require(pay_gem != address(0x0)); | |
require(buy_amt > 0); | |
require(buy_gem != address(0x0)); | |
require(pay_gem != buy_gem); | |
OfferInfo memory info; | |
info.pay_amt = pay_amt; | |
info.pay_gem = pay_gem; | |
info.buy_amt = buy_amt; | |
info.buy_gem = buy_gem; | |
info.owner = msg.sender; | |
info.timestamp = uint64(now); | |
id = _next_id(); | |
offers[id] = info; | |
IErc20(pay_gem).transferFrom(msg.sender, address(this), pay_amt); | |
emit LogItemUpdate(id); | |
emit LogMake( | |
bytes32(id), | |
keccak256(abi.encodePacked(pay_gem, buy_gem)), | |
msg.sender, | |
pay_gem, | |
buy_gem, | |
uint128(pay_amt), | |
uint128(buy_amt), | |
uint64(now) | |
); | |
} | |
// ============ Math Functions ============ | |
function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) { | |
z = SafeMath.add(SafeMath.mul(x, y), RAY / 2) / RAY; | |
} | |
function wdiv(uint256 x, uint256 y) internal pure returns (uint256 z) { | |
z = SafeMath.add(SafeMath.mul(x, WAD), y / 2) / y; | |
} | |
function rdiv(uint256 x, uint256 y) internal pure returns (uint256 z) { | |
z = SafeMath.add(SafeMath.mul(x, RAY), y / 2) / y; | |
} | |
} | |
contract TestToken { | |
using SafeMath for uint256; | |
uint256 supply; | |
mapping (address => uint256) balances; | |
mapping (address => mapping (address => uint256)) allowed; | |
event Transfer(address token, address from, address to, uint256 value); | |
event Approval(address token, address owner, address spender, uint256 value); | |
event Issue(address token, address owner, uint256 value); | |
// Allow anyone to get new token | |
function issue(uint256 amount) public { | |
issueTo(msg.sender, amount); | |
} | |
function issueTo(address who, uint256 amount) public { | |
supply = supply.add(amount); | |
balances[who] = balances[who].add(amount); | |
emit Issue(address(this), who, amount); | |
} | |
function totalSupply() public view returns (uint256) { | |
return supply; | |
} | |
function balanceOf(address who) public view returns (uint256) { | |
return balances[who]; | |
} | |
function allowance(address owner, address spender) public view returns (uint256) { | |
return allowed[owner][spender]; | |
} | |
function symbol() public pure returns (string memory) { | |
return "TEST"; | |
} | |
function name() public pure returns (string memory) { | |
return "Test Token"; | |
} | |
function decimals() public pure returns (uint8) { | |
return 18; | |
} | |
function transfer(address to, uint256 value) public returns (bool) { | |
if (balances[msg.sender] >= value) { | |
balances[msg.sender] = balances[msg.sender].sub(value); | |
balances[to] = balances[to].add(value); | |
emit Transfer( | |
address(this), | |
msg.sender, | |
to, | |
value | |
); | |
return true; | |
} else { | |
return false; | |
} | |
} | |
function transferFrom(address from, address to, uint256 value) public returns (bool) { | |
if (balances[from] >= value && allowed[from][msg.sender] >= value) { | |
balances[to] = balances[to].add(value); | |
balances[from] = balances[from].sub(value); | |
allowed[from][msg.sender] = allowed[from][msg.sender].sub(value); | |
emit Transfer( | |
address(this), | |
from, | |
to, | |
value | |
); | |
return true; | |
} else { | |
return false; | |
} | |
} | |
function approve(address spender, uint256 value) public returns (bool) { | |
allowed[msg.sender][spender] = value; | |
emit Approval( | |
address(this), | |
msg.sender, | |
spender, | |
value | |
); | |
return true; | |
} | |
} | |
contract TokenB is TestToken { | |
function decimals() public pure returns (uint8) { | |
return 22; | |
} | |
function symbol() public pure returns (string memory) { | |
return "BBB"; | |
} | |
function name() public pure returns (string memory) { | |
return "Test Token B"; | |
} | |
} | |
library SafeMath { | |
/** | |
* @dev Multiplies two unsigned integers, reverts on overflow. | |
*/ | |
function mul(uint256 a, uint256 b) internal pure returns (uint256) { | |
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the | |
// benefit is lost if 'b' is also tested. | |
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 | |
if (a == 0) { | |
return 0; | |
} | |
uint256 c = a * b; | |
require(c / a == b); | |
return c; | |
} | |
/** | |
* @dev Integer division of two unsigned integers truncating the quotient, reverts on division by zero. | |
*/ | |
function div(uint256 a, uint256 b) internal pure returns (uint256) { | |
// Solidity only automatically asserts when dividing by 0 | |
require(b > 0); | |
uint256 c = a / b; | |
// assert(a == b * c + a % b); // There is no case in which this doesn't hold | |
return c; | |
} | |
/** | |
* @dev Subtracts two unsigned integers, reverts on overflow (i.e. if subtrahend is greater than minuend). | |
*/ | |
function sub(uint256 a, uint256 b) internal pure returns (uint256) { | |
require(b <= a); | |
uint256 c = a - b; | |
return c; | |
} | |
/** | |
* @dev Adds two unsigned integers, reverts on overflow. | |
*/ | |
function add(uint256 a, uint256 b) internal pure returns (uint256) { | |
uint256 c = a + b; | |
require(c >= a); | |
return c; | |
} | |
/** | |
* @dev Divides two unsigned integers and returns the remainder (unsigned integer modulo), | |
* reverts when dividing by zero. | |
*/ | |
function mod(uint256 a, uint256 b) internal pure returns (uint256) { | |
require(b != 0); | |
return a % b; | |
} | |
} | |
contract TestPriceOracle is IPriceOracle { | |
mapping (address => uint256) public g_prices; | |
function setPrice( | |
address token, | |
uint256 price | |
) | |
external | |
{ | |
g_prices[token] = price; | |
} | |
function getPrice( | |
address token | |
) | |
public | |
view | |
returns (Monetary.Price memory) | |
{ | |
return Monetary.Price({ | |
value: g_prices[token] | |
}); | |
} | |
} | |
contract Permission is | |
State | |
{ | |
// ============ Events ============ | |
event LogOperatorSet( | |
address indexed owner, | |
address operator, | |
bool trusted | |
); | |
// ============ Structs ============ | |
struct OperatorArg { | |
address operator; | |
bool trusted; | |
} | |
// ============ Public Functions ============ | |
/** | |
* 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[] memory args | |
) | |
public | |
{ | |
for (uint256 i = 0; i < args.length; i++) { | |
address operator = args[i].operator; | |
bool trusted = args[i].trusted; | |
g_state.operators[msg.sender][operator] = trusted; | |
emit LogOperatorSet(msg.sender, operator, trusted); | |
} | |
} | |
} | |
library AdminImpl { | |
using Storage for Storage.State; | |
using Token for address; | |
using Types for Types.Wei; | |
// ============ Constants ============ | |
bytes32 constant FILE = "AdminImpl"; | |
// ============ Events ============ | |
event LogWithdrawExcessTokens( | |
address token, | |
uint256 amount | |
); | |
event LogAddMarket( | |
uint256 marketId, | |
address token | |
); | |
event LogSetIsClosing( | |
uint256 marketId, | |
bool isClosing | |
); | |
event LogSetPriceOracle( | |
uint256 marketId, | |
address priceOracle | |
); | |
event LogSetInterestSetter( | |
uint256 marketId, | |
address interestSetter | |
); | |
event LogSetMarginPremium( | |
uint256 marketId, | |
Decimal.D256 marginPremium | |
); | |
event LogSetSpreadPremium( | |
uint256 marketId, | |
Decimal.D256 spreadPremium | |
); | |
event LogSetMarginRatio( | |
Decimal.D256 marginRatio | |
); | |
event LogSetLiquidationSpread( | |
Decimal.D256 liquidationSpread | |
); | |
event LogSetEarningsRate( | |
Decimal.D256 earningsRate | |
); | |
event LogSetMinBorrowedValue( | |
Monetary.Value minBorrowedValue | |
); | |
event LogSetGlobalOperator( | |
address operator, | |
bool approved | |
); | |
// ============ Token Functions ============ | |
function ownerWithdrawExcessTokens( | |
Storage.State storage state, | |
uint256 marketId, | |
address recipient | |
) | |
public | |
returns (uint256) | |
{ | |
_validateMarketId(state, marketId); | |
Types.Wei memory excessWei = state.getNumExcessTokens(marketId); | |
Require.that( | |
!excessWei.isNegative(), | |
FILE, | |
"Negative excess" | |
); | |
address token = state.getToken(marketId); | |
uint256 actualBalance = token.balanceOf(address(this)); | |
if (excessWei.value > actualBalance) { | |
excessWei.value = actualBalance; | |
} | |
token.transfer(recipient, excessWei.value); | |
emit LogWithdrawExcessTokens(token, excessWei.value); | |
return excessWei.value; | |
} | |
function ownerWithdrawUnsupportedTokens( | |
Storage.State storage state, | |
address token, | |
address recipient | |
) | |
public | |
returns (uint256) | |
{ | |
_requireNoMarket(state, token); | |
uint256 balance = token.balanceOf(address(this)); | |
token.transfer(recipient, balance); | |
emit LogWithdrawExcessTokens(token, balance); | |
return balance; | |
} | |
// ============ Market Functions ============ | |
function ownerAddMarket( | |
Storage.State storage state, | |
address token, | |
IPriceOracle priceOracle, | |
IInterestSetter interestSetter, | |
Decimal.D256 memory marginPremium, | |
Decimal.D256 memory spreadPremium | |
) | |
public | |
{ | |
_requireNoMarket(state, token); | |
uint256 marketId = state.numMarkets; | |
state.numMarkets++; | |
state.markets[marketId].token = token; | |
state.markets[marketId].index = Interest.newIndex(); | |
emit LogAddMarket(marketId, token); | |
_setPriceOracle(state, marketId, priceOracle); | |
_setInterestSetter(state, marketId, interestSetter); | |
_setMarginPremium(state, marketId, marginPremium); | |
_setSpreadPremium(state, marketId, spreadPremium); | |
} | |
function ownerSetIsClosing( | |
Storage.State storage state, | |
uint256 marketId, | |
bool isClosing | |
) | |
public | |
{ | |
_validateMarketId(state, marketId); | |
state.markets[marketId].isClosing = isClosing; | |
emit LogSetIsClosing(marketId, isClosing); | |
} | |
function ownerSetPriceOracle( | |
Storage.State storage state, | |
uint256 marketId, | |
IPriceOracle priceOracle | |
) | |
public | |
{ | |
_validateMarketId(state, marketId); | |
_setPriceOracle(state, marketId, priceOracle); | |
} | |
function ownerSetInterestSetter( | |
Storage.State storage state, | |
uint256 marketId, | |
IInterestSetter interestSetter | |
) | |
public | |
{ | |
_validateMarketId(state, marketId); | |
_setInterestSetter(state, marketId, interestSetter); | |
} | |
function ownerSetMarginPremium( | |
Storage.State storage state, | |
uint256 marketId, | |
Decimal.D256 memory marginPremium | |
) | |
public | |
{ | |
_validateMarketId(state, marketId); | |
_setMarginPremium(state, marketId, marginPremium); | |
} | |
function ownerSetSpreadPremium( | |
Storage.State storage state, | |
uint256 marketId, | |
Decimal.D256 memory spreadPremium | |
) | |
public | |
{ | |
_validateMarketId(state, marketId); | |
_setSpreadPremium(state, marketId, spreadPremium); | |
} | |
// ============ Risk Functions ============ | |
function ownerSetMarginRatio( | |
Storage.State storage state, | |
Decimal.D256 memory ratio | |
) | |
public | |
{ | |
Require.that( | |
ratio.value <= state.riskLimits.marginRatioMax, | |
FILE, | |
"Ratio too high" | |
); | |
Require.that( | |
ratio.value > state.riskParams.liquidationSpread.value, | |
FILE, | |
"Ratio cannot be <= spread" | |
); | |
state.riskParams.marginRatio = ratio; | |
emit LogSetMarginRatio(ratio); | |
} | |
function ownerSetLiquidationSpread( | |
Storage.State storage state, | |
Decimal.D256 memory spread | |
) | |
public | |
{ | |
Require.that( | |
spread.value <= state.riskLimits.liquidationSpreadMax, | |
FILE, | |
"Spread too high" | |
); | |
Require.that( | |
spread.value < state.riskParams.marginRatio.value, | |
FILE, | |
"Spread cannot be >= ratio" | |
); | |
state.riskParams.liquidationSpread = spread; | |
emit LogSetLiquidationSpread(spread); | |
} | |
function ownerSetEarningsRate( | |
Storage.State storage state, | |
Decimal.D256 memory earningsRate | |
) | |
public | |
{ | |
Require.that( | |
earningsRate.value <= state.riskLimits.earningsRateMax, | |
FILE, | |
"Rate too high" | |
); | |
state.riskParams.earningsRate = earningsRate; | |
emit LogSetEarningsRate(earningsRate); | |
} | |
function ownerSetMinBorrowedValue( | |
Storage.State storage state, | |
Monetary.Value memory minBorrowedValue | |
) | |
public | |
{ | |
Require.that( | |
minBorrowedValue.value <= state.riskLimits.minBorrowedValueMax, | |
FILE, | |
"Value too high" | |
); | |
state.riskParams.minBorrowedValue = minBorrowedValue; | |
emit LogSetMinBorrowedValue(minBorrowedValue); | |
} | |
// ============ Global Operator Functions ============ | |
function ownerSetGlobalOperator( | |
Storage.State storage state, | |
address operator, | |
bool approved | |
) | |
public | |
{ | |
state.globalOperators[operator] = approved; | |
emit LogSetGlobalOperator(operator, approved); | |
} | |
// ============ Private Functions ============ | |
function _setPriceOracle( | |
Storage.State storage state, | |
uint256 marketId, | |
IPriceOracle priceOracle | |
) | |
private | |
{ | |
// require oracle can return non-zero price | |
address token = state.markets[marketId].token; | |
Require.that( | |
priceOracle.getPrice(token).value != 0, | |
FILE, | |
"Invalid oracle price" | |
); | |
state.markets[marketId].priceOracle = priceOracle; | |
emit LogSetPriceOracle(marketId, address(priceOracle)); | |
} | |
function _setInterestSetter( | |
Storage.State storage state, | |
uint256 marketId, | |
IInterestSetter interestSetter | |
) | |
private | |
{ | |
// ensure interestSetter can return a value without reverting | |
address token = state.markets[marketId].token; | |
interestSetter.getInterestRate(token, 0, 0); | |
state.markets[marketId].interestSetter = interestSetter; | |
emit LogSetInterestSetter(marketId, address(interestSetter)); | |
} | |
function _setMarginPremium( | |
Storage.State storage state, | |
uint256 marketId, | |
Decimal.D256 memory marginPremium | |
) | |
private | |
{ | |
Require.that( | |
marginPremium.value <= state.riskLimits.marginPremiumMax, | |
FILE, | |
"Margin premium too high" | |
); | |
state.markets[marketId].marginPremium = marginPremium; | |
emit LogSetMarginPremium(marketId, marginPremium); | |
} | |
function _setSpreadPremium( | |
Storage.State storage state, | |
uint256 marketId, | |
Decimal.D256 memory spreadPremium | |
) | |
private | |
{ | |
Require.that( | |
spreadPremium.value <= state.riskLimits.spreadPremiumMax, | |
FILE, | |
"Spread premium too high" | |
); | |
state.markets[marketId].spreadPremium = spreadPremium; | |
emit LogSetSpreadPremium(marketId, spreadPremium); | |
} | |
function _requireNoMarket( | |
Storage.State storage state, | |
address token | |
) | |
private | |
view | |
{ | |
uint256 numMarkets = state.numMarkets; | |
bool marketExists = false; | |
for (uint256 m = 0; m < numMarkets; m++) { | |
if (state.markets[m].token == token) { | |
marketExists = true; | |
break; | |
} | |
} | |
Require.that( | |
!marketExists, | |
FILE, | |
"Market exists" | |
); | |
} | |
function _validateMarketId( | |
Storage.State storage state, | |
uint256 marketId | |
) | |
private | |
view | |
{ | |
Require.that( | |
marketId < state.numMarkets, | |
FILE, | |
"Market OOB", | |
marketId | |
); | |
} | |
} | |
contract MultiSig { | |
// ============ Events ============ | |
event Confirmation(address indexed sender, uint256 indexed transactionId); | |
event Revocation(address indexed sender, uint256 indexed transactionId); | |
event Submission(uint256 indexed transactionId); | |
event Execution(uint256 indexed transactionId); | |
event ExecutionFailure(uint256 indexed transactionId); | |
event OwnerAddition(address indexed owner); | |
event OwnerRemoval(address indexed owner); | |
event RequirementChange(uint256 required); | |
// ============ Constants ============ | |
uint256 constant public MAX_OWNER_COUNT = 50; | |
address constant ADDRESS_ZERO = address(0x0); | |
// ============ Storage ============ | |
mapping (uint256 => Transaction) public transactions; | |
mapping (uint256 => mapping (address => bool)) public confirmations; | |
mapping (address => bool) public isOwner; | |
address[] public owners; | |
uint256 public required; | |
uint256 public transactionCount; | |
// ============ Structs ============ | |
struct Transaction { | |
address destination; | |
uint256 value; | |
bytes data; | |
bool executed; | |
} | |
// ============ Modifiers ============ | |
modifier onlyWallet() { | |
/* solium-disable-next-line error-reason */ | |
require(msg.sender == address(this)); | |
_; | |
} | |
modifier ownerDoesNotExist( | |
address owner | |
) { | |
/* solium-disable-next-line error-reason */ | |
require(!isOwner[owner]); | |
_; | |
} | |
modifier ownerExists( | |
address owner | |
) { | |
/* solium-disable-next-line error-reason */ | |
require(isOwner[owner]); | |
_; | |
} | |
modifier transactionExists( | |
uint256 transactionId | |
) { | |
/* solium-disable-next-line error-reason */ | |
require(transactions[transactionId].destination != ADDRESS_ZERO); | |
_; | |
} | |
modifier confirmed( | |
uint256 transactionId, | |
address owner | |
) { | |
/* solium-disable-next-line error-reason */ | |
require(confirmations[transactionId][owner]); | |
_; | |
} | |
modifier notConfirmed( | |
uint256 transactionId, | |
address owner | |
) { | |
/* solium-disable-next-line error-reason */ | |
require(!confirmations[transactionId][owner]); | |
_; | |
} | |
modifier notExecuted( | |
uint256 transactionId | |
) { | |
/* solium-disable-next-line error-reason */ | |
require(!transactions[transactionId].executed); | |
_; | |
} | |
modifier notNull( | |
address _address | |
) { | |
/* solium-disable-next-line error-reason */ | |
require(_address != ADDRESS_ZERO); | |
_; | |
} | |
modifier validRequirement( | |
uint256 ownerCount, | |
uint256 _required | |
) { | |
/* solium-disable-next-line error-reason */ | |
require( | |
ownerCount <= MAX_OWNER_COUNT | |
&& _required <= ownerCount | |
&& _required != 0 | |
&& ownerCount != 0 | |
); | |
_; | |
} | |
// ============ Constructor ============ | |
/** | |
* Contract constructor sets initial owners and required number of confirmations. | |
* | |
* @param _owners List of initial owners. | |
* @param _required Number of required confirmations. | |
*/ | |
constructor( | |
address[] memory _owners, | |
uint256 _required | |
) | |
public | |
validRequirement(_owners.length, _required) | |
{ | |
for (uint256 i = 0; i < _owners.length; i++) { | |
/* solium-disable-next-line error-reason */ | |
require(!isOwner[_owners[i]] && _owners[i] != ADDRESS_ZERO); | |
isOwner[_owners[i]] = true; | |
} | |
owners = _owners; | |
required = _required; | |
} | |
// ============ Wallet-Only Functions ============ | |
/** | |
* Allows to add a new owner. Transaction has to be sent by wallet. | |
* | |
* @param owner Address of new owner. | |
*/ | |
function addOwner( | |
address owner | |
) | |
public | |
onlyWallet | |
ownerDoesNotExist(owner) | |
notNull(owner) | |
validRequirement(owners.length + 1, required) | |
{ | |
isOwner[owner] = true; | |
owners.push(owner); | |
emit OwnerAddition(owner); | |
} | |
/** | |
* Allows to remove an owner. Transaction has to be sent by wallet. | |
* | |
* @param owner Address of owner. | |
*/ | |
function removeOwner( | |
address owner | |
) | |
public | |
onlyWallet | |
ownerExists(owner) | |
{ | |
isOwner[owner] = false; | |
for (uint256 i = 0; i < owners.length - 1; i++) { | |
if (owners[i] == owner) { | |
owners[i] = owners[owners.length - 1]; | |
break; | |
} | |
} | |
owners.length -= 1; | |
if (required > owners.length) { | |
changeRequirement(owners.length); | |
} | |
emit OwnerRemoval(owner); | |
} | |
/** | |
* Allows to replace an owner with a new owner. Transaction has to be sent by wallet. | |
* | |
* @param owner Address of owner to be replaced. | |
* @param newOwner Address of new owner. | |
*/ | |
function replaceOwner( | |
address owner, | |
address newOwner | |
) | |
public | |
onlyWallet | |
ownerExists(owner) | |
ownerDoesNotExist(newOwner) | |
notNull(newOwner) | |
{ | |
for (uint256 i = 0; i < owners.length; i++) { | |
if (owners[i] == owner) { | |
owners[i] = newOwner; | |
break; | |
} | |
} | |
isOwner[owner] = false; | |
isOwner[newOwner] = true; | |
emit OwnerRemoval(owner); | |
emit OwnerAddition(newOwner); | |
} | |
/** | |
* Allows to change the number of required confirmations. Transaction has to be sent by wallet. | |
* | |
* @param _required Number of required confirmations. | |
*/ | |
function changeRequirement( | |
uint256 _required | |
) | |
public | |
onlyWallet | |
validRequirement(owners.length, _required) | |
{ | |
required = _required; | |
emit RequirementChange(_required); | |
} | |
// ============ Admin Functions ============ | |
/** | |
* Allows an owner to submit and confirm a transaction. | |
* | |
* @param destination Transaction target address. | |
* @param value Transaction ether value. | |
* @param data Transaction data payload. | |
* @return Transaction ID. | |
*/ | |
function submitTransaction( | |
address destination, | |
uint256 value, | |
bytes memory data | |
) | |
public | |
returns (uint256) | |
{ | |
uint256 transactionId = addTransaction(destination, value, data); | |
confirmTransaction(transactionId); | |
return transactionId; | |
} | |
/** | |
* Allows an owner to confirm a transaction. | |
* | |
* @param transactionId Transaction ID. | |
*/ | |
function confirmTransaction( | |
uint256 transactionId | |
) | |
public | |
ownerExists(msg.sender) | |
transactionExists(transactionId) | |
notConfirmed(transactionId, msg.sender) | |
{ | |
confirmations[transactionId][msg.sender] = true; | |
emit Confirmation(msg.sender, transactionId); | |
executeTransaction(transactionId); | |
} | |
/** | |
* Allows an owner to revoke a confirmation for a transaction. | |
* | |
* @param transactionId Transaction ID. | |
*/ | |
function revokeConfirmation( | |
uint256 transactionId | |
) | |
public | |
ownerExists(msg.sender) | |
confirmed(transactionId, msg.sender) | |
notExecuted(transactionId) | |
{ | |
confirmations[transactionId][msg.sender] = false; | |
emit Revocation(msg.sender, transactionId); | |
} | |
/** | |
* Allows an owner to execute a confirmed transaction. | |
* | |
* @param transactionId Transaction ID. | |
*/ | |
function executeTransaction( | |
uint256 transactionId | |
) | |
public | |
ownerExists(msg.sender) | |
confirmed(transactionId, msg.sender) | |
notExecuted(transactionId) | |
{ | |
if (isConfirmed(transactionId)) { | |
Transaction storage txn = transactions[transactionId]; | |
txn.executed = true; | |
if (externalCall( | |
txn.destination, | |
txn.value, | |
txn.data.length, | |
txn.data) | |
) { | |
emit Execution(transactionId); | |
} else { | |
emit ExecutionFailure(transactionId); | |
txn.executed = false; | |
} | |
} | |
} | |
// ============ Getter Functions ============ | |
/** | |
* Returns the confirmation status of a transaction. | |
* | |
* @param transactionId Transaction ID. | |
* @return Confirmation status. | |
*/ | |
function isConfirmed( | |
uint256 transactionId | |
) | |
public | |
view | |
returns (bool) | |
{ | |
uint256 count = 0; | |
for (uint256 i = 0; i < owners.length; i++) { | |
if (confirmations[transactionId][owners[i]]) { | |
count += 1; | |
} | |
if (count == required) { | |
return true; | |
} | |
} | |
} | |
/** | |
* Returns number of confirmations of a transaction. | |
* | |
* @param transactionId Transaction ID. | |
* @return Number of confirmations. | |
*/ | |
function getConfirmationCount( | |
uint256 transactionId | |
) | |
public | |
view | |
returns (uint256) | |
{ | |
uint256 count = 0; | |
for (uint256 i = 0; i < owners.length; i++) { | |
if (confirmations[transactionId][owners[i]]) { | |
count += 1; | |
} | |
} | |
return count; | |
} | |
/** | |
* Returns total number of transactions after filers are applied. | |
* | |
* @param pending Include pending transactions. | |
* @param executed Include executed transactions. | |
* @return Total number of transactions after filters are applied. | |
*/ | |
function getTransactionCount( | |
bool pending, | |
bool executed | |
) | |
public | |
view | |
returns (uint256) | |
{ | |
uint256 count = 0; | |
for (uint256 i = 0; i < transactionCount; i++) { | |
if ( | |
pending && !transactions[i].executed | |
|| executed && transactions[i].executed | |
) { | |
count += 1; | |
} | |
} | |
return count; | |
} | |
/** | |
* Returns array of owners. | |
* | |
* @return Array of owner addresses. | |
*/ | |
function getOwners() | |
public | |
view | |
returns (address[] memory) | |
{ | |
return owners; | |
} | |
/** | |
* Returns array with owner addresses, which confirmed transaction. | |
* | |
* @param transactionId Transaction ID. | |
* @return Array of owner addresses. | |
*/ | |
function getConfirmations( | |
uint256 transactionId | |
) | |
public | |
view | |
returns (address[] memory) | |
{ | |
address[] memory confirmationsTemp = new address[](owners.length); | |
uint256 count = 0; | |
uint256 i; | |
for (i = 0; i < owners.length; i++) { | |
if (confirmations[transactionId][owners[i]]) { | |
confirmationsTemp[count] = owners[i]; | |
count += 1; | |
} | |
} | |
address[] memory _confirmations = new address[](count); | |
for (i = 0; i < count; i++) { | |
_confirmations[i] = confirmationsTemp[i]; | |
} | |
return _confirmations; | |
} | |
/** | |
* Returns list of transaction IDs in defined range. | |
* | |
* @param from Index start position of transaction array. | |
* @param to Index end position of transaction array. | |
* @param pending Include pending transactions. | |
* @param executed Include executed transactions. | |
* @return Array of transaction IDs. | |
*/ | |
function getTransactionIds( | |
uint256 from, | |
uint256 to, | |
bool pending, | |
bool executed | |
) | |
public | |
view | |
returns (uint256[] memory) | |
{ | |
uint256[] memory transactionIdsTemp = new uint256[](transactionCount); | |
uint256 count = 0; | |
uint256 i; | |
for (i = 0; i < transactionCount; i++) { | |
if ( | |
pending && !transactions[i].executed | |
|| executed && transactions[i].executed | |
) { | |
transactionIdsTemp[count] = i; | |
count += 1; | |
} | |
} | |
uint256[] memory _transactionIds = new uint256[](to - from); | |
for (i = from; i < to; i++) { | |
_transactionIds[i - from] = transactionIdsTemp[i]; | |
} | |
return _transactionIds; | |
} | |
// ============ Helper Functions ============ | |
// call has been separated into its own function in order to take advantage | |
// of the Solidity's code generator to produce a loop that copies tx.data into memory. | |
function externalCall( | |
address destination, | |
uint256 value, | |
uint256 dataLength, | |
bytes memory data | |
) | |
internal | |
returns (bool) | |
{ | |
bool result; | |
/* solium-disable-next-line security/no-inline-assembly */ | |
assembly { | |
let x := mload(0x40) // "Allocate" memory for output (0x40 is where "free memory" pointer is stored by convention) | |
let d := add(data, 32) // First 32 bytes are the padded length of data, so exclude that | |
result := call( | |
sub(gas, 34710), // 34710 is the value that solidity is currently emitting | |
// It includes callGas (700) + callVeryLow (3, to pay for SUB) + callValueTransferGas (9000) + | |
// callNewAccountGas (25000, in case the destination address does not exist and needs creating) | |
destination, | |
value, | |
d, | |
dataLength, // Size of the input (in bytes) - this is what fixes the padding problem | |
x, | |
0 // Output is ignored, therefore the output size is zero | |
) | |
} | |
return result; | |
} | |
/** | |
* Adds a new transaction to the transaction mapping, if transaction does not exist yet. | |
* | |
* @param destination Transaction target address. | |
* @param value Transaction ether value. | |
* @param data Transaction data payload. | |
* @return Transaction ID. | |
*/ | |
function addTransaction( | |
address destination, | |
uint256 value, | |
bytes memory data | |
) | |
internal | |
notNull(destination) | |
returns (uint256) | |
{ | |
uint256 transactionId = transactionCount; | |
transactions[transactionId] = Transaction({ | |
destination: destination, | |
value: value, | |
data: data, | |
executed: false | |
}); | |
transactionCount += 1; | |
emit Submission(transactionId); | |
return transactionId; | |
} | |
} | |
contract TokenC is TestToken { | |
function decimals() public pure returns (uint8) { | |
return 33; | |
} | |
function symbol() public pure returns (string memory) { | |
return "CCC"; | |
} | |
function name() public pure returns (string memory) { | |
return "Test Token C"; | |
} | |
} | |
contract UsdcPriceOracle is | |
IPriceOracle | |
{ | |
// ============ Constants ============ | |
uint256 constant DECIMALS = 6; | |
uint256 constant EXPECTED_PRICE = ONE_DOLLAR / (10 ** DECIMALS); | |
// ============ IPriceOracle Functions ============= | |
function getPrice( | |
address /* token */ | |
) | |
public | |
view | |
returns (Monetary.Price memory) | |
{ | |
return Monetary.Price({ value: EXPECTED_PRICE }); | |
} | |
} | |
contract TestMakerOracle is | |
IMakerOracle | |
{ | |
uint256 public price; | |
bool public valid; | |
function setValues( | |
uint256 _price, | |
bool _valid | |
) | |
external | |
{ | |
price = _price; | |
valid = _valid; | |
} | |
function peek() | |
external | |
view | |
returns (bytes32, bool) | |
{ | |
return (bytes32(price), valid); | |
} | |
function read() | |
external | |
view | |
returns (bytes32) | |
{ | |
require(valid); | |
return bytes32(price); | |
} | |
} | |
library OperationImpl { | |
using Cache for Cache.MarketCache; | |
using SafeMath for uint256; | |
using Storage for Storage.State; | |
using Types for Types.Par; | |
using Types for Types.Wei; | |
// ============ Constants ============ | |
bytes32 constant FILE = "OperationImpl"; | |
// ============ Public Functions ============ | |
function operate( | |
Storage.State storage state, | |
Account.Info[] memory accounts, | |
Actions.ActionArgs[] memory actions | |
) | |
public | |
{ | |
Events.logOperation(); | |
_verifyInputs(accounts, actions); | |
( | |
bool[] memory primaryAccounts, | |
Cache.MarketCache memory cache | |
) = _runPreprocessing( | |
state, | |
accounts, | |
actions | |
); | |
_runActions( | |
state, | |
accounts, | |
actions, | |
cache | |
); | |
_verifyFinalState( | |
state, | |
accounts, | |
primaryAccounts, | |
cache | |
); | |
} | |
// ============ Helper Functions ============ | |
function _verifyInputs( | |
Account.Info[] memory accounts, | |
Actions.ActionArgs[] memory actions | |
) | |
private | |
pure | |
{ | |
Require.that( | |
actions.length != 0, | |
FILE, | |
"Cannot have zero actions" | |
); | |
Require.that( | |
accounts.length != 0, | |
FILE, | |
"Cannot have zero accounts" | |
); | |
for (uint256 a = 0; a < accounts.length; a++) { | |
for (uint256 b = a + 1; b < accounts.length; b++) { | |
Require.that( | |
!Account.equals(accounts[a], accounts[b]), | |
FILE, | |
"Cannot duplicate accounts", | |
a, | |
b | |
); | |
} | |
} | |
} | |
function _runPreprocessing( | |
Storage.State storage state, | |
Account.Info[] memory accounts, | |
Actions.ActionArgs[] memory actions | |
) | |
private | |
returns ( | |
bool[] memory, | |
Cache.MarketCache memory | |
) | |
{ | |
uint256 numMarkets = state.numMarkets; | |
bool[] memory primaryAccounts = new bool[](accounts.length); | |
Cache.MarketCache memory cache = Cache.create(numMarkets); | |
// keep track of primary accounts and indexes that need updating | |
for (uint256 i = 0; i < actions.length; i++) { | |
Actions.ActionArgs memory arg = actions[i]; | |
Actions.ActionType actionType = arg.actionType; | |
Actions.MarketLayout marketLayout = Actions.getMarketLayout(actionType); | |
Actions.AccountLayout accountLayout = Actions.getAccountLayout(actionType); | |
// parse out primary accounts | |
if (accountLayout != Actions.AccountLayout.OnePrimary) { | |
Require.that( | |
arg.accountId != arg.otherAccountId, | |
FILE, | |
"Duplicate accounts in action", | |
i | |
); | |
if (accountLayout == Actions.AccountLayout.TwoPrimary) { | |
primaryAccounts[arg.otherAccountId] = true; | |
} else { | |
assert(accountLayout == Actions.AccountLayout.PrimaryAndSecondary); | |
Require.that( | |
!primaryAccounts[arg.otherAccountId], | |
FILE, | |
"Requires non-primary account", | |
arg.otherAccountId | |
); | |
} | |
} | |
primaryAccounts[arg.accountId] = true; | |
// keep track of indexes to update | |
if (marketLayout == Actions.MarketLayout.OneMarket) { | |
_updateMarket(state, cache, arg.primaryMarketId); | |
} else if (marketLayout == Actions.MarketLayout.TwoMarkets) { | |
Require.that( | |
arg.primaryMarketId != arg.secondaryMarketId, | |
FILE, | |
"Duplicate markets in action", | |
i | |
); | |
_updateMarket(state, cache, arg.primaryMarketId); | |
_updateMarket(state, cache, arg.secondaryMarketId); | |
} else { | |
assert(marketLayout == Actions.MarketLayout.ZeroMarkets); | |
} | |
} | |
// get any other markets for which an account has a balance | |
for (uint256 m = 0; m < numMarkets; m++) { | |
if (cache.hasMarket(m)) { | |
continue; | |
} | |
for (uint256 a = 0; a < accounts.length; a++) { | |
if (!state.getPar(accounts[a], m).isZero()) { | |
_updateMarket(state, cache, m); | |
break; | |
} | |
} | |
} | |
return (primaryAccounts, cache); | |
} | |
function _updateMarket( | |
Storage.State storage state, | |
Cache.MarketCache memory cache, | |
uint256 marketId | |
) | |
private | |
{ | |
bool updated = cache.addMarket(state, marketId); | |
if (updated) { | |
Events.logIndexUpdate(marketId, state.updateIndex(marketId)); | |
} | |
} | |
function _runActions( | |
Storage.State storage state, | |
Account.Info[] memory accounts, | |
Actions.ActionArgs[] memory actions, | |
Cache.MarketCache memory cache | |
) | |
private | |
{ | |
for (uint256 i = 0; i < actions.length; i++) { | |
Actions.ActionArgs memory action = actions[i]; | |
Actions.ActionType actionType = action.actionType; | |
if (actionType == Actions.ActionType.Deposit) { | |
_deposit(state, Actions.parseDepositArgs(accounts, action)); | |
} | |
else if (actionType == Actions.ActionType.Withdraw) { | |
_withdraw(state, Actions.parseWithdrawArgs(accounts, action)); | |
} | |
else if (actionType == Actions.ActionType.Transfer) { | |
_transfer(state, Actions.parseTransferArgs(accounts, action)); | |
} | |
else if (actionType == Actions.ActionType.Buy) { | |
_buy(state, Actions.parseBuyArgs(accounts, action)); | |
} | |
else if (actionType == Actions.ActionType.Sell) { | |
_sell(state, Actions.parseSellArgs(accounts, action)); | |
} | |
else if (actionType == Actions.ActionType.Trade) { | |
_trade(state, Actions.parseTradeArgs(accounts, action)); | |
} | |
else if (actionType == Actions.ActionType.Liquidate) { | |
_liquidate(state, Actions.parseLiquidateArgs(accounts, action), cache); | |
} | |
else if (actionType == Actions.ActionType.Vaporize) { | |
_vaporize(state, Actions.parseVaporizeArgs(accounts, action), cache); | |
} | |
else { | |
assert(actionType == Actions.ActionType.Call); | |
_call(state, Actions.parseCallArgs(accounts, action)); | |
} | |
} | |
} | |
function _verifyFinalState( | |
Storage.State storage state, | |
Account.Info[] memory accounts, | |
bool[] memory primaryAccounts, | |
Cache.MarketCache memory cache | |
) | |
private | |
{ | |
// verify no increase in borrowPar for closing markets | |
uint256 numMarkets = cache.getNumMarkets(); | |
for (uint256 m = 0; m < numMarkets; m++) { | |
if (cache.getIsClosing(m)) { | |
Require.that( | |
state.getTotalPar(m).borrow <= cache.getBorrowPar(m), | |
FILE, | |
"Market is closing", | |
m | |
); | |
} | |
} | |
// verify account collateralization | |
for (uint256 a = 0; a < accounts.length; a++) { | |
Account.Info memory account = accounts[a]; | |
// validate minBorrowedValue | |
bool collateralized = state.isCollateralized(account, cache, true); | |
// don't check collateralization for non-primary accounts | |
if (!primaryAccounts[a]) { | |
continue; | |
} | |
// check collateralization for primary accounts | |
Require.that( | |
collateralized, | |
FILE, | |
"Undercollateralized account", | |
account.owner, | |
account.number | |
); | |
// ensure status is normal for primary accounts | |
if (state.getStatus(account) != Account.Status.Normal) { | |
state.setStatus(account, Account.Status.Normal); | |
} | |
} | |
} | |
// ============ Action Functions ============ | |
function _deposit( | |
Storage.State storage state, | |
Actions.DepositArgs memory args | |
) | |
private | |
{ | |
state.requireIsOperator(args.account, msg.sender); | |
Require.that( | |
args.from == msg.sender || args.from == args.account.owner, | |
FILE, | |
"Invalid deposit source", | |
args.from | |
); | |
( | |
Types.Par memory newPar, | |
Types.Wei memory deltaWei | |
) = state.getNewParAndDeltaWei( | |
args.account, | |
args.market, | |
args.amount | |
); | |
state.setPar( | |
args.account, | |
args.market, | |
newPar | |
); | |
// requires a positive deltaWei | |
Exchange.transferIn( | |
state.getToken(args.market), | |
args.from, | |
deltaWei | |
); | |
Events.logDeposit( | |
state, | |
args, | |
deltaWei | |
); | |
} | |
function _withdraw( | |
Storage.State storage state, | |
Actions.WithdrawArgs memory args | |
) | |
private | |
{ | |
state.requireIsOperator(args.account, msg.sender); | |
( | |
Types.Par memory newPar, | |
Types.Wei memory deltaWei | |
) = state.getNewParAndDeltaWei( | |
args.account, | |
args.market, | |
args.amount | |
); | |
state.setPar( | |
args.account, | |
args.market, | |
newPar | |
); | |
// requires a negative deltaWei | |
Exchange.transferOut( | |
state.getToken(args.market), | |
args.to, | |
deltaWei | |
); | |
Events.logWithdraw( | |
state, | |
args, | |
deltaWei | |
); | |
} | |
function _transfer( | |
Storage.State storage state, | |
Actions.TransferArgs memory args | |
) | |
private | |
{ | |
state.requireIsOperator(args.accountOne, msg.sender); | |
state.requireIsOperator(args.accountTwo, msg.sender); | |
( | |
Types.Par memory newPar, | |
Types.Wei memory deltaWei | |
) = state.getNewParAndDeltaWei( | |
args.accountOne, | |
args.market, | |
args.amount | |
); | |
state.setPar( | |
args.accountOne, | |
args.market, | |
newPar | |
); | |
state.setParFromDeltaWei( | |
args.accountTwo, | |
args.market, | |
deltaWei.negative() | |
); | |
Events.logTransfer( | |
state, | |
args, | |
deltaWei | |
); | |
} | |
function _buy( | |
Storage.State storage state, | |
Actions.BuyArgs memory args | |
) | |
private | |
{ | |
state.requireIsOperator(args.account, msg.sender); | |
address takerToken = state.getToken(args.takerMarket); | |
address makerToken = state.getToken(args.makerMarket); | |
( | |
Types.Par memory makerPar, | |
Types.Wei memory makerWei | |
) = state.getNewParAndDeltaWei( | |
args.account, | |
args.makerMarket, | |
args.amount | |
); | |
Types.Wei memory takerWei = Exchange.getCost( | |
args.exchangeWrapper, | |
makerToken, | |
takerToken, | |
makerWei, | |
args.orderData | |
); | |
Types.Wei memory tokensReceived = Exchange.exchange( | |
args.exchangeWrapper, | |
args.account.owner, | |
makerToken, | |
takerToken, | |
takerWei, | |
args.orderData | |
); | |
Require.that( | |
tokensReceived.value >= makerWei.value, | |
FILE, | |
"Buy amount less than promised", | |
tokensReceived.value, | |
makerWei.value | |
); | |
state.setPar( | |
args.account, | |
args.makerMarket, | |
makerPar | |
); | |
state.setParFromDeltaWei( | |
args.account, | |
args.takerMarket, | |
takerWei | |
); | |
Events.logBuy( | |
state, | |
args, | |
takerWei, | |
makerWei | |
); | |
} | |
function _sell( | |
Storage.State storage state, | |
Actions.SellArgs memory args | |
) | |
private | |
{ | |
state.requireIsOperator(args.account, msg.sender); | |
address takerToken = state.getToken(args.takerMarket); | |
address makerToken = state.getToken(args.makerMarket); | |
( | |
Types.Par memory takerPar, | |
Types.Wei memory takerWei | |
) = state.getNewParAndDeltaWei( | |
args.account, | |
args.takerMarket, | |
args.amount | |
); | |
Types.Wei memory makerWei = Exchange.exchange( | |
args.exchangeWrapper, | |
args.account.owner, | |
makerToken, | |
takerToken, | |
takerWei, | |
args.orderData | |
); | |
state.setPar( | |
args.account, | |
args.takerMarket, | |
takerPar | |
); | |
state.setParFromDeltaWei( | |
args.account, | |
args.makerMarket, | |
makerWei | |
); | |
Events.logSell( | |
state, | |
args, | |
takerWei, | |
makerWei | |
); | |
} | |
function _trade( | |
Storage.State storage state, | |
Actions.TradeArgs memory args | |
) | |
private | |
{ | |
state.requireIsOperator(args.takerAccount, msg.sender); | |
state.requireIsOperator(args.makerAccount, args.autoTrader); | |
Types.Par memory oldInputPar = state.getPar( | |
args.makerAccount, | |
args.inputMarket | |
); | |
( | |
Types.Par memory newInputPar, | |
Types.Wei memory inputWei | |
) = state.getNewParAndDeltaWei( | |
args.makerAccount, | |
args.inputMarket, | |
args.amount | |
); | |
Types.AssetAmount memory outputAmount = IAutoTrader(args.autoTrader).getTradeCost( | |
args.inputMarket, | |
args.outputMarket, | |
args.makerAccount, | |
args.takerAccount, | |
oldInputPar, | |
newInputPar, | |
inputWei, | |
args.tradeData | |
); | |
( | |
Types.Par memory newOutputPar, | |
Types.Wei memory outputWei | |
) = state.getNewParAndDeltaWei( | |
args.makerAccount, | |
args.outputMarket, | |
outputAmount | |
); | |
Require.that( | |
outputWei.isZero() || inputWei.isZero() || outputWei.sign != inputWei.sign, | |
FILE, | |
"Trades cannot be one-sided" | |
); | |
// set the balance for the maker | |
state.setPar( | |
args.makerAccount, | |
args.inputMarket, | |
newInputPar | |
); | |
state.setPar( | |
args.makerAccount, | |
args.outputMarket, | |
newOutputPar | |
); | |
// set the balance for the taker | |
state.setParFromDeltaWei( | |
args.takerAccount, | |
args.inputMarket, | |
inputWei.negative() | |
); | |
state.setParFromDeltaWei( | |
args.takerAccount, | |
args.outputMarket, | |
outputWei.negative() | |
); | |
Events.logTrade( | |
state, | |
args, | |
inputWei, | |
outputWei | |
); | |
} | |
function _liquidate( | |
Storage.State storage state, | |
Actions.LiquidateArgs memory args, | |
Cache.MarketCache memory cache | |
) | |
private | |
{ | |
state.requireIsOperator(args.solidAccount, msg.sender); | |
// verify liquidatable | |
if (Account.Status.Liquid != state.getStatus(args.liquidAccount)) { | |
Require.that( | |
!state.isCollateralized(args.liquidAccount, cache, /* requireMinBorrow = */ false), | |
FILE, | |
"Unliquidatable account", | |
args.liquidAccount.owner, | |
args.liquidAccount.number | |
); | |
state.setStatus(args.liquidAccount, Account.Status.Liquid); | |
} | |
Types.Wei memory maxHeldWei = state.getWei( | |
args.liquidAccount, | |
args.heldMarket | |
); | |
Require.that( | |
!maxHeldWei.isNegative(), | |
FILE, | |
"Collateral cannot be negative", | |
args.liquidAccount.owner, | |
args.liquidAccount.number, | |
args.heldMarket | |
); | |
( | |
Types.Par memory owedPar, | |
Types.Wei memory owedWei | |
) = state.getNewParAndDeltaWeiForLiquidation( | |
args.liquidAccount, | |
args.owedMarket, | |
args.amount | |
); | |
( | |
Monetary.Price memory heldPrice, | |
Monetary.Price memory owedPrice | |
) = _getLiquidationPrices( | |
state, | |
cache, | |
args.heldMarket, | |
args.owedMarket | |
); | |
Types.Wei memory heldWei = _owedWeiToHeldWei(owedWei, heldPrice, owedPrice); | |
// if attempting to over-borrow the held asset, bound it by the maximum | |
if (heldWei.value > maxHeldWei.value) { | |
heldWei = maxHeldWei.negative(); | |
owedWei = _heldWeiToOwedWei(heldWei, heldPrice, owedPrice); | |
state.setPar( | |
args.liquidAccount, | |
args.heldMarket, | |
Types.zeroPar() | |
); | |
state.setParFromDeltaWei( | |
args.liquidAccount, | |
args.owedMarket, | |
owedWei | |
); | |
} else { | |
state.setPar( | |
args.liquidAccount, | |
args.owedMarket, | |
owedPar | |
); | |
state.setParFromDeltaWei( | |
args.liquidAccount, | |
args.heldMarket, | |
heldWei | |
); | |
} | |
// set the balances for the solid account | |
state.setParFromDeltaWei( | |
args.solidAccount, | |
args.owedMarket, | |
owedWei.negative() | |
); | |
state.setParFromDeltaWei( | |
args.solidAccount, | |
args.heldMarket, | |
heldWei.negative() | |
); | |
Events.logLiquidate( | |
state, | |
args, | |
heldWei, | |
owedWei | |
); | |
} | |
function _vaporize( | |
Storage.State storage state, | |
Actions.VaporizeArgs memory args, | |
Cache.MarketCache memory cache | |
) | |
private | |
{ | |
state.requireIsOperator(args.solidAccount, msg.sender); | |
// verify vaporizable | |
if (Account.Status.Vapor != state.getStatus(args.vaporAccount)) { | |
Require.that( | |
state.isVaporizable(args.vaporAccount, cache), | |
FILE, | |
"Unvaporizable account", | |
args.vaporAccount.owner, | |
args.vaporAccount.number | |
); | |
state.setStatus(args.vaporAccount, Account.Status.Vapor); | |
} | |
// First, attempt to refund using the same token | |
( | |
bool fullyRepaid, | |
Types.Wei memory excessWei | |
) = _vaporizeUsingExcess(state, args); | |
if (fullyRepaid) { | |
Events.logVaporize( | |
state, | |
args, | |
Types.zeroWei(), | |
Types.zeroWei(), | |
excessWei | |
); | |
return; | |
} | |
Types.Wei memory maxHeldWei = state.getNumExcessTokens(args.heldMarket); | |
Require.that( | |
!maxHeldWei.isNegative(), | |
FILE, | |
"Excess cannot be negative", | |
args.heldMarket | |
); | |
( | |
Types.Par memory owedPar, | |
Types.Wei memory owedWei | |
) = state.getNewParAndDeltaWeiForLiquidation( | |
args.vaporAccount, | |
args.owedMarket, | |
args.amount | |
); | |
( | |
Monetary.Price memory heldPrice, | |
Monetary.Price memory owedPrice | |
) = _getLiquidationPrices( | |
state, | |
cache, | |
args.heldMarket, | |
args.owedMarket | |
); | |
Types.Wei memory heldWei = _owedWeiToHeldWei(owedWei, heldPrice, owedPrice); | |
// if attempting to over-borrow the held asset, bound it by the maximum | |
if (heldWei.value > maxHeldWei.value) { | |
heldWei = maxHeldWei.negative(); | |
owedWei = _heldWeiToOwedWei(heldWei, heldPrice, owedPrice); | |
state.setParFromDeltaWei( | |
args.vaporAccount, | |
args.owedMarket, | |
owedWei | |
); | |
} else { | |
state.setPar( | |
args.vaporAccount, | |
args.owedMarket, | |
owedPar | |
); | |
} | |
// set the balances for the solid account | |
state.setParFromDeltaWei( | |
args.solidAccount, | |
args.owedMarket, | |
owedWei.negative() | |
); | |
state.setParFromDeltaWei( | |
args.solidAccount, | |
args.heldMarket, | |
heldWei.negative() | |
); | |
Events.logVaporize( | |
state, | |
args, | |
heldWei, | |
owedWei, | |
excessWei | |
); | |
} | |
function _call( | |
Storage.State storage state, | |
Actions.CallArgs memory args | |
) | |
private | |
{ | |
state.requireIsOperator(args.account, msg.sender); | |
ICallee(args.callee).callFunction( | |
msg.sender, | |
args.account, | |
args.data | |
); | |
Events.logCall(args); | |
} | |
// ============ Private Functions ============ | |
/** | |
* For the purposes of liquidation or vaporization, get the value-equivalent amount of heldWei | |
* given owedWei and the (spread-adjusted) prices of each asset. | |
*/ | |
function _owedWeiToHeldWei( | |
Types.Wei memory owedWei, | |
Monetary.Price memory heldPrice, | |
Monetary.Price memory owedPrice | |
) | |
private | |
pure | |
returns (Types.Wei memory) | |
{ | |
return Types.Wei({ | |
sign: false, | |
value: Math.getPartial(owedWei.value, owedPrice.value, heldPrice.value) | |
}); | |
} | |
/** | |
* For the purposes of liquidation or vaporization, get the value-equivalent amount of owedWei | |
* given heldWei and the (spread-adjusted) prices of each asset. | |
*/ | |
function _heldWeiToOwedWei( | |
Types.Wei memory heldWei, | |
Monetary.Price memory heldPrice, | |
Monetary.Price memory owedPrice | |
) | |
private | |
pure | |
returns (Types.Wei memory) | |
{ | |
return Types.Wei({ | |
sign: true, | |
value: Math.getPartialRoundUp(heldWei.value, heldPrice.value, owedPrice.value) | |
}); | |
} | |
/** | |
* Attempt to vaporize an account's balance using the excess tokens in the protocol. Return a | |
* bool and a wei value. The boolean is true if and only if the balance was fully vaporized. The | |
* Wei value is how many excess tokens were used to partially or fully vaporize the account's | |
* negative balance. | |
*/ | |
function _vaporizeUsingExcess( | |
Storage.State storage state, | |
Actions.VaporizeArgs memory args | |
) | |
internal | |
returns (bool, Types.Wei memory) | |
{ | |
Types.Wei memory excessWei = state.getNumExcessTokens(args.owedMarket); | |
// There are no excess funds, return zero | |
if (!excessWei.isPositive()) { | |
return (false, Types.zeroWei()); | |
} | |
Types.Wei memory maxRefundWei = state.getWei(args.vaporAccount, args.owedMarket); | |
maxRefundWei.sign = true; | |
// The account is fully vaporizable using excess funds | |
if (excessWei.value >= maxRefundWei.value) { | |
state.setPar( | |
args.vaporAccount, | |
args.owedMarket, | |
Types.zeroPar() | |
); | |
return (true, maxRefundWei); | |
} | |
// The account is only partially vaporizable using excess funds | |
else { | |
state.setParFromDeltaWei( | |
args.vaporAccount, | |
args.owedMarket, | |
excessWei | |
); | |
return (false, excessWei); | |
} | |
} | |
/** | |
* Return the (spread-adjusted) prices of two assets for the purposes of liquidation or | |
* vaporization. | |
*/ | |
function _getLiquidationPrices( | |
Storage.State storage state, | |
Cache.MarketCache memory cache, | |
uint256 heldMarketId, | |
uint256 owedMarketId | |
) | |
internal | |
view | |
returns ( | |
Monetary.Price memory, | |
Monetary.Price memory | |
) | |
{ | |
uint256 originalPrice = cache.getPrice(owedMarketId).value; | |
Decimal.D256 memory spread = state.getLiquidationSpreadForPair( | |
heldMarketId, | |
owedMarketId | |
); | |
Monetary.Price memory owedPrice = Monetary.Price({ | |
value: originalPrice.add(Decimal.mul(originalPrice, spread)) | |
}); | |
return (cache.getPrice(heldMarketId), owedPrice); | |
} | |
} | |
library Account { | |
// ============ Enums ============ | |
/* | |
* Most-recently-cached account status. | |
* | |
* Normal: Can only be liquidated if the account values are violating the global margin-ratio. | |
* Liquid: Can be liquidated no matter the account values. | |
* Can be vaporized if there are no more positive account values. | |
* Vapor: Has only negative (or zeroed) account values. Can be vaporized. | |
* | |
*/ | |
enum Status { | |
Normal, | |
Liquid, | |
Vapor | |
} | |
// ============ Structs ============ | |
// Represents the unique key that specifies an account | |
struct Info { | |
address owner; // The address that owns the account | |
uint256 number; // A nonce that allows a single address to control many accounts | |
} | |
// The complete storage for any account | |
struct Storage { | |
mapping (uint256 => Types.Par) balances; // Mapping from marketId to principal | |
Status status; | |
} | |
// ============ Library Functions ============ | |
function equals( | |
Info memory a, | |
Info memory b | |
) | |
internal | |
pure | |
returns (bool) | |
{ | |
return a.owner == b.owner && a.number == b.number; | |
} | |
} | |
contract ReentrancyGuard { | |
/// @dev counter to allow mutex lock with only one SSTORE operation | |
uint256 private _guardCounter; | |
constructor () internal { | |
// The counter starts at one to prevent changing it from zero to a non-zero | |
// value, which is a more expensive operation. | |
_guardCounter = 1; | |
} | |
/** | |
* @dev Prevents a contract from calling itself, directly or indirectly. | |
* Calling a `nonReentrant` function from another `nonReentrant` | |
* function is not supported. It is possible to prevent this from happening | |
* by making the `nonReentrant` function external, and make it call a | |
* `private` function that does the actual work. | |
*/ | |
modifier nonReentrant() { | |
_guardCounter += 1; | |
uint256 localCounter = _guardCounter; | |
_; | |
require(localCounter == _guardCounter); | |
} | |
} | |
library Time { | |
// ============ Library Functions ============ | |
function currentTime() | |
internal | |
view | |
returns (uint32) | |
{ | |
return Math.to32(block.timestamp); | |
} | |
} | |
library Cache { | |
using Cache for MarketCache; | |
using Storage for Storage.State; | |
// ============ Structs ============ | |
struct MarketInfo { | |
bool isClosing; | |
uint128 borrowPar; | |
Monetary.Price price; | |
} | |
struct MarketCache { | |
MarketInfo[] markets; | |
} | |
// ============ Setter Functions ============ | |
/** | |
* Initialize an empty cache for some given number of total markets. | |
*/ | |
function create( | |
uint256 numMarkets | |
) | |
internal | |
pure | |
returns (MarketCache memory) | |
{ | |
return MarketCache({ | |
markets: new MarketInfo[](numMarkets) | |
}); | |
} | |
/** | |
* Add market information (price and total borrowed par if the market is closing) to the cache. | |
* Return true if the market information did not previously exist in the cache. | |
*/ | |
function addMarket( | |
MarketCache memory cache, | |
Storage.State storage state, | |
uint256 marketId | |
) | |
internal | |
view | |
returns (bool) | |
{ | |
if (cache.hasMarket(marketId)) { | |
return false; | |
} | |
cache.markets[marketId].price = state.fetchPrice(marketId); | |
if (state.markets[marketId].isClosing) { | |
cache.markets[marketId].isClosing = true; | |
cache.markets[marketId].borrowPar = state.getTotalPar(marketId).borrow; | |
} | |
return true; | |
} | |
// ============ Getter Functions ============ | |
function getNumMarkets( | |
MarketCache memory cache | |
) | |
internal | |
pure | |
returns (uint256) | |
{ | |
return cache.markets.length; | |
} | |
function hasMarket( | |
MarketCache memory cache, | |
uint256 marketId | |
) | |
internal | |
pure | |
returns (bool) | |
{ | |
return cache.markets[marketId].price.value != 0; | |
} | |
function getIsClosing( | |
MarketCache memory cache, | |
uint256 marketId | |
) | |
internal | |
pure | |
returns (bool) | |
{ | |
return cache.markets[marketId].isClosing; | |
} | |
function getPrice( | |
MarketCache memory cache, | |
uint256 marketId | |
) | |
internal | |
pure | |
returns (Monetary.Price memory) | |
{ | |
return cache.markets[marketId].price; | |
} | |
function getBorrowPar( | |
MarketCache memory cache, | |
uint256 marketId | |
) | |
internal | |
pure | |
returns (uint128) | |
{ | |
return cache.markets[marketId].borrowPar; | |
} | |
} | |
library Decimal { | |
using SafeMath for uint256; | |
// ============ Constants ============ | |
uint256 constant BASE = 10**18; | |
// ============ Structs ============ | |
struct D256 { | |
uint256 value; | |
} | |
// ============ Functions ============ | |
function one() | |
internal | |
pure | |
returns (D256 memory) | |
{ | |
return D256({ value: BASE }); | |
} | |
function onePlus( | |
D256 memory d | |
) | |
internal | |
pure | |
returns (D256 memory) | |
{ | |
return D256({ value: d.value.add(BASE) }); | |
} | |
function mul( | |
uint256 target, | |
D256 memory d | |
) | |
internal | |
pure | |
returns (uint256) | |
{ | |
return Math.getPartial(target, d.value, BASE); | |
} | |
function div( | |
uint256 target, | |
D256 memory d | |
) | |
internal | |
pure | |
returns (uint256) | |
{ | |
return Math.getPartial(target, BASE, d.value); | |
} | |
} | |
library Monetary { | |
/* | |
* The price of a base-unit of an asset. | |
*/ | |
struct Price { | |
uint256 value; | |
} | |
/* | |
* Total value of an some amount of an asset. Equal to (price * amount). | |
*/ | |
struct Value { | |
uint256 value; | |
} | |
} | |
library Interest { | |
using Math for uint256; | |
using SafeMath for uint256; | |
// ============ Constants ============ | |
bytes32 constant FILE = "Interest"; | |
uint64 constant BASE = 10**18; | |
// ============ Structs ============ | |
struct Rate { | |
uint256 value; | |
} | |
struct Index { | |
uint96 borrow; | |
uint96 supply; | |
uint32 lastUpdate; | |
} | |
// ============ Library Functions ============ | |
/** | |
* Get a new market Index based on the old index and market interest rate. | |
* Calculate interest for borrowers by using the formula rate * time. Approximates | |
* continuously-compounded interest when called frequently, but is much more | |
* gas-efficient to calculate. For suppliers, the interest rate is adjusted by the earningsRate, | |
* then prorated the across all suppliers. | |
* | |
* @param index The old index for a market | |
* @param rate The current interest rate of the market | |
* @param totalPar The total supply and borrow par values of the market | |
* @param earningsRate The portion of the interest that is forwarded to the suppliers | |
* @return The updated index for a market | |
*/ | |
function calculateNewIndex( | |
Index memory index, | |
Rate memory rate, | |
Types.TotalPar memory totalPar, | |
Decimal.D256 memory earningsRate | |
) | |
internal | |
view | |
returns (Index memory) | |
{ | |
( | |
Types.Wei memory supplyWei, | |
Types.Wei memory borrowWei | |
) = totalParToWei(totalPar, index); | |
// get interest increase for borrowers | |
uint32 currentTime = Time.currentTime(); | |
uint256 borrowInterest = rate.value.mul(uint256(currentTime).sub(index.lastUpdate)); | |
// get interest increase for suppliers | |
uint256 supplyInterest; | |
if (Types.isZero(supplyWei)) { | |
supplyInterest = 0; | |
} else { | |
supplyInterest = Decimal.mul(borrowInterest, earningsRate); | |
if (borrowWei.value < supplyWei.value) { | |
supplyInterest = Math.getPartial(supplyInterest, borrowWei.value, supplyWei.value); | |
} | |
} | |
assert(supplyInterest <= borrowInterest); | |
return Index({ | |
borrow: Math.getPartial(index.borrow, borrowInterest, BASE).add(index.borrow).to96(), | |
supply: Math.getPartial(index.supply, supplyInterest, BASE).add(index.supply).to96(), | |
lastUpdate: currentTime | |
}); | |
} | |
function newIndex() | |
internal | |
view | |
returns (Index memory) | |
{ | |
return Index({ | |
borrow: BASE, | |
supply: BASE, | |
lastUpdate: Time.currentTime() | |
}); | |
} | |
/* | |
* Convert a principal amount to a token amount given an index. | |
*/ | |
function parToWei( | |
Types.Par memory input, | |
Index memory index | |
) | |
internal | |
pure | |
returns (Types.Wei memory) | |
{ | |
uint256 inputValue = uint256(input.value); | |
if (input.sign) { | |
return Types.Wei({ | |
sign: true, | |
value: inputValue.getPartial(index.supply, BASE) | |
}); | |
} else { | |
return Types.Wei({ | |
sign: false, | |
value: inputValue.getPartialRoundUp(index.borrow, BASE) | |
}); | |
} | |
} | |
/* | |
* Convert a token amount to a principal amount given an index. | |
*/ | |
function weiToPar( | |
Types.Wei memory input, | |
Index memory index | |
) | |
internal | |
pure | |
returns (Types.Par memory) | |
{ | |
if (input.sign) { | |
return Types.Par({ | |
sign: true, | |
value: input.value.getPartial(BASE, index.supply).to128() | |
}); | |
} else { | |
return Types.Par({ | |
sign: false, | |
value: input.value.getPartialRoundUp(BASE, index.borrow).to128() | |
}); | |
} | |
} | |
/* | |
* Convert the total supply and borrow principal amounts of a market to total supply and borrow | |
* token amounts. | |
*/ | |
function totalParToWei( | |
Types.TotalPar memory totalPar, | |
Index memory index | |
) | |
internal | |
pure | |
returns (Types.Wei memory, Types.Wei memory) | |
{ | |
Types.Par memory supplyPar = Types.Par({ | |
sign: true, | |
value: totalPar.supply | |
}); | |
Types.Par memory borrowPar = Types.Par({ | |
sign: false, | |
value: totalPar.borrow | |
}); | |
Types.Wei memory supplyWei = parToWei(supplyPar, index); | |
Types.Wei memory borrowWei = parToWei(borrowPar, index); | |
return (supplyWei, borrowWei); | |
} | |
} | |
contract WETH9 { | |
string public name = "Wrapped Ether"; | |
string public symbol = "WETH"; | |
uint8 public decimals = 18; | |
event Approval(address indexed src, address indexed guy, uint wad); | |
event Transfer(address indexed src, address indexed dst, uint wad); | |
event Deposit(address indexed dst, uint wad); | |
event Withdrawal(address indexed src, uint wad); | |
mapping (address => uint) public balanceOf; | |
mapping (address => mapping (address => uint)) public allowance; | |
function() external payable { | |
deposit(); | |
} | |
function deposit() public payable { | |
balanceOf[msg.sender] += msg.value; | |
emit Deposit(msg.sender, msg.value); | |
} | |
function withdraw(uint wad) public { | |
require(balanceOf[msg.sender] >= wad); | |
balanceOf[msg.sender] -= wad; | |
msg.sender.transfer(wad); | |
emit Withdrawal(msg.sender, wad); | |
} | |
function totalSupply() public view returns (uint) { | |
return address(this).balance; | |
} | |
function approve(address guy, uint wad) public returns (bool) { | |
allowance[msg.sender][guy] = wad; | |
emit Approval(msg.sender, guy, wad); | |
return true; | |
} | |
function transfer(address dst, uint wad) public returns (bool) { | |
return transferFrom(msg.sender, dst, wad); | |
} | |
function transferFrom(address src, address dst, uint wad) | |
public | |
returns (bool) | |
{ | |
require(balanceOf[src] >= wad); | |
if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) { | |
require(allowance[src][msg.sender] >= wad); | |
allowance[src][msg.sender] -= wad; | |
} | |
balanceOf[src] -= wad; | |
balanceOf[dst] += wad; | |
emit Transfer(src, dst, wad); | |
return true; | |
} | |
} | |
contract Operation is | |
State, | |
ReentrancyGuard | |
{ | |
// ============ Public Functions ============ | |
/** | |
* 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. | |
* One call to operate() is considered a singular "operation". Account collateralization is | |
* ensured only after the completion of the entire operation. | |
* | |
* @param accounts A list of all accounts that will be used in this operation. Cannot contain | |
* duplicates. In each action, the relevant account will be referred-to by its | |
* index in the list. | |
* @param actions An ordered list of all actions that will be taken in this operation. The | |
* actions will be processed in order. | |
*/ | |
function operate( | |
Account.Info[] memory accounts, | |
Actions.ActionArgs[] memory actions | |
) | |
public | |
nonReentrant | |
{ | |
OperationImpl.operate( | |
g_state, | |
accounts, | |
actions | |
); | |
} | |
} | |
contract TokenA is TestToken { | |
function decimals() public pure returns (uint8) { | |
return 11; | |
} | |
function symbol() public pure returns (string memory) { | |
return "AAA"; | |
} | |
function name() public pure returns (string memory) { | |
return "Test Token A"; | |
} | |
} | |
library TypedSignature { | |
// ============ Constants ============ | |
bytes32 constant private FILE = "TypedSignature"; | |
// prepended message with the length of the signed hash in decimal | |
bytes constant private PREPEND_DEC = "\x19Ethereum Signed Message:\n32"; | |
// prepended message with the length of the signed hash in hexadecimal | |
bytes constant private PREPEND_HEX = "\x19Ethereum Signed Message:\n\x20"; | |
// Number of bytes in a typed signature | |
uint256 constant private NUM_SIGNATURE_BYTES = 66; | |
// ============ Enums ============ | |
// Different RPC providers may implement signing methods differently, so we allow different | |
// signature types depending on the string prepended to a hash before it was signed. | |
enum SignatureType { | |
NoPrepend, // No string was prepended. | |
Decimal, // PREPEND_DEC was prepended. | |
Hexadecimal, // PREPEND_HEX was prepended. | |
Invalid // Not a valid type. Used for bound-checking. | |
} | |
// ============ Functions ============ | |
/** | |
* Gives the address of the signer of a hash. Also allows for the commonly prepended string of | |
* '\x19Ethereum Signed Message:\n' + message.length | |
* | |
* @param hash Hash that was signed (does not include prepended message) | |
* @param signatureWithType Type and ECDSA signature with structure: {32:r}{32:s}{1:v}{1:type} | |
* @return address of the signer of the hash | |
*/ | |
function recover( | |
bytes32 hash, | |
bytes memory signatureWithType | |
) | |
internal | |
pure | |
returns (address) | |
{ | |
Require.that( | |
signatureWithType.length == NUM_SIGNATURE_BYTES, | |
FILE, | |
"Invalid signature length" | |
); | |
bytes32 r; | |
bytes32 s; | |
uint8 v; | |
uint8 rawSigType; | |
/* solium-disable-next-line security/no-inline-assembly */ | |
assembly { | |
r := mload(add(signatureWithType, 0x20)) | |
s := mload(add(signatureWithType, 0x40)) | |
let lastSlot := mload(add(signatureWithType, 0x60)) | |
v := byte(0, lastSlot) | |
rawSigType := byte(1, lastSlot) | |
} | |
Require.that( | |
rawSigType < uint8(SignatureType.Invalid), | |
FILE, | |
"Invalid signature type" | |
); | |
SignatureType sigType = SignatureType(rawSigType); | |
bytes32 signedHash; | |
if (sigType == SignatureType.NoPrepend) { | |
signedHash = hash; | |
} else if (sigType == SignatureType.Decimal) { | |
signedHash = keccak256(abi.encodePacked(PREPEND_DEC, hash)); | |
} else { | |
assert(sigType == SignatureType.Hexadecimal); | |
signedHash = keccak256(abi.encodePacked(PREPEND_HEX, hash)); | |
} | |
return ecrecover( | |
signedHash, | |
v, | |
r, | |
s | |
); | |
} | |
} | |
library Require { | |
// ============ Constants ============ | |
uint256 constant ASCII_ZERO = 48; // '0' | |
uint256 constant ASCII_RELATIVE_ZERO = 87; // 'a' - 10 | |
uint256 constant ASCII_LOWER_EX = 120; // 'x' | |
bytes2 constant COLON = 0x3a20; // ': ' | |
bytes2 constant COMMA = 0x2c20; // ', ' | |
bytes2 constant LPAREN = 0x203c; // ' <' | |
byte constant RPAREN = 0x3e; // '>' | |
uint256 constant FOUR_BIT_MASK = 0xf; | |
// ============ Library Functions ============ | |
function that( | |
bool must, | |
bytes32 file, | |
bytes32 reason | |
) | |
internal | |
pure | |
{ | |
if (!must) { | |
revert( | |
string( | |
abi.encodePacked( | |
stringifyTruncated(file), | |
COLON, | |
stringifyTruncated(reason) | |
) | |
) | |
); | |
} | |
} | |
function that( | |
bool must, | |
bytes32 file, | |
bytes32 reason, | |
uint256 payloadA | |
) | |
internal | |
pure | |
{ | |
if (!must) { | |
revert( | |
string( | |
abi.encodePacked( | |
stringifyTruncated(file), | |
COLON, | |
stringifyTruncated(reason), | |
LPAREN, | |
stringify(payloadA), | |
RPAREN | |
) | |
) | |
); | |
} | |
} | |
function that( | |
bool must, | |
bytes32 file, | |
bytes32 reason, | |
uint256 payloadA, | |
uint256 payloadB | |
) | |
internal | |
pure | |
{ | |
if (!must) { | |
revert( | |
string( | |
abi.encodePacked( | |
stringifyTruncated(file), | |
COLON, | |
stringifyTruncated(reason), | |
LPAREN, | |
stringify(payloadA), | |
COMMA, | |
stringify(payloadB), | |
RPAREN | |
) | |
) | |
); | |
} | |
} | |
function that( | |
bool must, | |
bytes32 file, | |
bytes32 reason, | |
address payloadA | |
) | |
internal | |
pure | |
{ | |
if (!must) { | |
revert( | |
string( | |
abi.encodePacked( | |
stringifyTruncated(file), | |
COLON, | |
stringifyTruncated(reason), | |
LPAREN, | |
stringify(payloadA), | |
RPAREN | |
) | |
) | |
); | |
} | |
} | |
function that( | |
bool must, | |
bytes32 file, | |
bytes32 reason, | |
address payloadA, | |
uint256 payloadB | |
) | |
internal | |
pure | |
{ | |
if (!must) { | |
revert( | |
string( | |
abi.encodePacked( | |
stringifyTruncated(file), | |
COLON, | |
stringifyTruncated(reason), | |
LPAREN, | |
stringify(payloadA), | |
COMMA, | |
stringify(payloadB), | |
RPAREN | |
) | |
) | |
); | |
} | |
} | |
function that( | |
bool must, | |
bytes32 file, | |
bytes32 reason, | |
address payloadA, | |
uint256 payloadB, | |
uint256 payloadC | |
) | |
internal | |
pure | |
{ | |
if (!must) { | |
revert( | |
string( | |
abi.encodePacked( | |
stringifyTruncated(file), | |
COLON, | |
stringifyTruncated(reason), | |
LPAREN, | |
stringify(payloadA), | |
COMMA, | |
stringify(payloadB), | |
COMMA, | |
stringify(payloadC), | |
RPAREN | |
) | |
) | |
); | |
} | |
} | |
function that( | |
bool must, | |
bytes32 file, | |
bytes32 reason, | |
bytes32 payloadA | |
) | |
internal | |
pure | |
{ | |
if (!must) { | |
revert( | |
string( | |
abi.encodePacked( | |
stringifyTruncated(file), | |
COLON, | |
stringifyTruncated(reason), | |
LPAREN, | |
stringify(payloadA), | |
RPAREN | |
) | |
) | |
); | |
} | |
} | |
function that( | |
bool must, | |
bytes32 file, | |
bytes32 reason, | |
bytes32 payloadA, | |
uint256 payloadB, | |
uint256 payloadC | |
) | |
internal | |
pure | |
{ | |
if (!must) { | |
revert( | |
string( | |
abi.encodePacked( | |
stringifyTruncated(file), | |
COLON, | |
stringifyTruncated(reason), | |
LPAREN, | |
stringify(payloadA), | |
COMMA, | |
stringify(payloadB), | |
COMMA, | |
stringify(payloadC), | |
RPAREN | |
) | |
) | |
); | |
} | |
} | |
// ============ Private Functions ============ | |
function stringifyTruncated( | |
bytes32 input | |
) | |
private | |
pure | |
returns (bytes memory) | |
{ | |
// put the input bytes into the result | |
bytes memory result = abi.encodePacked(input); | |
// determine the length of the input by finding the location of the last non-zero byte | |
for (uint256 i = 32; i > 0; ) { | |
// reverse-for-loops with unsigned integer | |
/* solium-disable-next-line security/no-modify-for-iter-var */ | |
i--; | |
// find the last non-zero byte in order to determine the length | |
if (result[i] != 0) { | |
uint256 length = i + 1; | |
/* solium-disable-next-line security/no-inline-assembly */ | |
assembly { | |
mstore(result, length) // r.length = length; | |
} | |
return result; | |
} | |
} | |
// all bytes are zero | |
return new bytes(0); | |
} | |
function stringify( | |
uint256 input | |
) | |
private | |
pure | |
returns (bytes memory) | |
{ | |
if (input == 0) { | |
return "0"; | |
} | |
// get the final string length | |
uint256 j = input; | |
uint256 length; | |
while (j != 0) { | |
length++; | |
j /= 10; | |
} | |
// allocate the string | |
bytes memory bstr = new bytes(length); | |
// populate the string starting with the least-significant character | |
j = input; | |
for (uint256 i = length; i > 0; ) { | |
// reverse-for-loops with unsigned integer | |
/* solium-disable-next-line security/no-modify-for-iter-var */ | |
i--; | |
// take last decimal digit | |
bstr[i] = byte(uint8(ASCII_ZERO + (j % 10))); | |
// remove the last decimal digit | |
j /= 10; | |
} | |
return bstr; | |
} | |
function stringify( | |
address input | |
) | |
private | |
pure | |
returns (bytes memory) | |
{ | |
uint256 z = uint256(input); | |
// addresses are "0x" followed by 20 bytes of data which take up 2 characters each | |
bytes memory result = new bytes(42); | |
// populate the result with "0x" | |
result[0] = byte(uint8(ASCII_ZERO)); | |
result[1] = byte(uint8(ASCII_LOWER_EX)); | |
// for each byte (starting from the lowest byte), populate the result with two characters | |
for (uint256 i = 0; i < 20; i++) { | |
// each byte takes two characters | |
uint256 shift = i * 2; | |
// populate the least-significant character | |
result[41 - shift] = char(z & FOUR_BIT_MASK); | |
z = z >> 4; | |
// populate the most-significant character | |
result[40 - shift] = char(z & FOUR_BIT_MASK); | |
z = z >> 4; | |
} | |
return result; | |
} | |
function stringify( | |
bytes32 input | |
) | |
private | |
pure | |
returns (bytes memory) | |
{ | |
uint256 z = uint256(input); | |
// bytes32 are "0x" followed by 32 bytes of data which take up 2 characters each | |
bytes memory result = new bytes(66); | |
// populate the result with "0x" | |
result[0] = byte(uint8(ASCII_ZERO)); | |
result[1] = byte(uint8(ASCII_LOWER_EX)); | |
// for each byte (starting from the lowest byte), populate the result with two characters | |
for (uint256 i = 0; i < 32; i++) { | |
// each byte takes two characters | |
uint256 shift = i * 2; | |
// populate the least-significant character | |
result[65 - shift] = char(z & FOUR_BIT_MASK); | |
z = z >> 4; | |
// populate the most-significant character | |
result[64 - shift] = char(z & FOUR_BIT_MASK); | |
z = z >> 4; | |
} | |
return result; | |
} | |
function char( | |
uint256 input | |
) | |
private | |
pure | |
returns (byte) | |
{ | |
// return ASCII digit (0-9) | |
if (input < 10) { | |
return byte(uint8(input + ASCII_ZERO)); | |
} | |
// return ASCII letter (a-f) | |
return byte(uint8(input + ASCII_RELATIVE_ZERO)); | |
} | |
} | |
contract ICallee { | |
// ============ Public Functions ============ | |
/** | |
* Allows users to send this contract arbitrary data. | |
* | |
* @param sender The msg.sender to Solo | |
* @param accountInfo The account from which the data is being sent | |
* @param data Arbitrary data given by the sender | |
*/ | |
function callFunction( | |
address sender, | |
Account.Info memory accountInfo, | |
bytes memory data | |
) | |
public; | |
} | |
contract WethPriceOracle is | |
IPriceOracle | |
{ | |
// ============ Storage ============ | |
IMakerOracle public MEDIANIZER; | |
// ============ Constructor ============= | |
constructor( | |
address medianizer | |
) | |
public | |
{ | |
MEDIANIZER = IMakerOracle(medianizer); | |
} | |
// ============ IPriceOracle Functions ============= | |
function getPrice( | |
address /* token */ | |
) | |
public | |
view | |
returns (Monetary.Price memory) | |
{ | |
(bytes32 value, /* bool fresh */) = MEDIANIZER.peek(); | |
return Monetary.Price({ value: uint256(value) }); | |
} | |
} | |
interface IOasisDex { | |
// ============ Structs ================ | |
struct OfferInfo { | |
uint256 pay_amt; | |
address pay_gem; | |
uint256 buy_amt; | |
address buy_gem; | |
address owner; | |
uint64 timestamp; | |
} | |
struct SortInfo { | |
uint256 next; //points to id of next higher offer | |
uint256 prev; //points to id of previous lower offer | |
uint256 delb; //the blocknumber where this entry was marked for delete | |
} | |
// ============ Storage Getters ================ | |
function last_offer_id() | |
external | |
view | |
returns (uint256); | |
function offers( | |
uint256 id | |
) | |
external | |
view | |
returns (OfferInfo memory); | |
function close_time() | |
external | |
view | |
returns (uint64); | |
function stopped() | |
external | |
view | |
returns (bool); | |
function buyEnabled() | |
external | |
view | |
returns (bool); | |
function matchingEnabled() | |
external | |
view | |
returns (bool); | |
function _rank( | |
uint256 id | |
) | |
external | |
view | |
returns (SortInfo memory); | |
function _best( | |
address sell_gem, | |
address buy_gem | |
) | |
external | |
view | |
returns (uint256); | |
function _span( | |
address sell_gem, | |
address buy_gem | |
) | |
external | |
view | |
returns (uint256); | |
function _dust( | |
address gem | |
) | |
external | |
view | |
returns (uint256); | |
function _near( | |
uint256 id | |
) | |
external | |
view | |
returns (uint256); | |
// ============ Constant Functions ================ | |
function isActive( | |
uint256 id | |
) | |
external | |
view | |
returns (bool); | |
function getOwner( | |
uint256 id | |
) | |
external | |
view | |
returns (address); | |
function getOffer( | |
uint256 id | |
) | |
external | |
view | |
returns (uint256, address, uint256, address); | |
function getMinSell( | |
address pay_gem | |
) | |
external | |
view | |
returns (uint256); | |
function getBestOffer( | |
address sell_gem, | |
address buy_gem | |
) | |
external | |
view | |
returns (uint256); | |
function getWorseOffer( | |
uint256 id | |
) | |
external | |
view | |
returns (uint256); | |
function getBetterOffer( | |
uint256 id | |
) | |
external | |
view | |
returns (uint256); | |
function getOfferCount( | |
address sell_gem, | |
address buy_gem | |
) | |
external | |
view | |
returns (uint256); | |
function getFirstUnsortedOffer() | |
external | |
view | |
returns (uint256); | |
function getNextUnsortedOffer( | |
uint256 id | |
) | |
external | |
view | |
returns (uint256); | |
function isOfferSorted( | |
uint256 id | |
) | |
external | |
view | |
returns (bool); | |
function getBuyAmount( | |
address buy_gem, | |
address pay_gem, | |
uint256 pay_amt | |
) | |
external | |
view | |
returns (uint256); | |
function getPayAmount( | |
address pay_gem, | |
address buy_gem, | |
uint256 buy_amt | |
) | |
external | |
view | |
returns (uint256); | |
function isClosed() | |
external | |
view | |
returns (bool); | |
function getTime() | |
external | |
view | |
returns (uint64); | |
// ============ Non-Constant Functions ================ | |
function bump( | |
bytes32 id_ | |
) | |
external; | |
function buy( | |
uint256 id, | |
uint256 quantity | |
) | |
external | |
returns (bool); | |
function cancel( | |
uint256 id | |
) | |
external | |
returns (bool); | |
function kill( | |
bytes32 id | |
) | |
external; | |
function make( | |
address pay_gem, | |
address buy_gem, | |
uint128 pay_amt, | |
uint128 buy_amt | |
) | |
external | |
returns (bytes32); | |
function take( | |
bytes32 id, | |
uint128 maxTakeAmount | |
) | |
external; | |
function offer( | |
uint256 pay_amt, | |
address pay_gem, | |
uint256 buy_amt, | |
address buy_gem | |
) | |
external | |
returns (uint256); | |
function offer( | |
uint256 pay_amt, | |
address pay_gem, | |
uint256 buy_amt, | |
address buy_gem, | |
uint256 pos | |
) | |
external | |
returns (uint256); | |
function offer( | |
uint256 pay_amt, | |
address pay_gem, | |
uint256 buy_amt, | |
address buy_gem, | |
uint256 pos, | |
bool rounding | |
) | |
external | |
returns (uint256); | |
function insert( | |
uint256 id, | |
uint256 pos | |
) | |
external | |
returns (bool); | |
function del_rank( | |
uint256 id | |
) | |
external | |
returns (bool); | |
function sellAllAmount( | |
address pay_gem, | |
uint256 pay_amt, | |
address buy_gem, | |
uint256 min_fill_amount | |
) | |
external | |
returns (uint256); | |
function buyAllAmount( | |
address buy_gem, | |
uint256 buy_amt, | |
address pay_gem, | |
uint256 max_fill_amount | |
) | |
external | |
returns (uint256); | |
} | |
contract Migrations { | |
address public owner; | |
uint256 public last_completed_migration; | |
modifier restricted() { | |
if (msg.sender == owner) { | |
_; | |
} | |
} | |
constructor() public { | |
owner = msg.sender; | |
} | |
function setCompleted(uint256 completed) public restricted { | |
last_completed_migration = completed; | |
} | |
function upgrade(address newAddress) public restricted { | |
Migrations upgraded = Migrations(newAddress); | |
upgraded.setCompleted(last_completed_migration); | |
} | |
} | |
contract ErroringToken is TestToken { | |
function transfer(address, uint256) public returns (bool) { | |
return false; | |
} | |
function transferFrom(address, address, uint256) public returns (bool) { | |
return false; | |
} | |
function approve(address, uint256) public returns (bool) { | |
return false; | |
} | |
} | |
library Actions { | |
// ============ Constants ============ | |
bytes32 constant FILE = "Actions"; | |
// ============ Enums ============ | |
enum ActionType { | |
Deposit, // supply tokens | |
Withdraw, // borrow tokens | |
Transfer, // transfer balance between accounts | |
Buy, // buy an amount of some token (externally) | |
Sell, // sell an amount of some token (externally) | |
Trade, // trade tokens against another account | |
Liquidate, // liquidate an undercollateralized or expiring account | |
Vaporize, // use excess tokens to zero-out a completely negative account | |
Call // send arbitrary data to an address | |
} | |
enum AccountLayout { | |
OnePrimary, | |
TwoPrimary, | |
PrimaryAndSecondary | |
} | |
enum MarketLayout { | |
ZeroMarkets, | |
OneMarket, | |
TwoMarkets | |
} | |
// ============ Structs ============ | |
/* | |
* Arguments that are passed to Solo in an ordered list as part of a single operation. | |
* Each ActionArgs has an actionType which specifies which action struct that this data will be | |
* parsed into before being processed. | |
*/ | |
struct ActionArgs { | |
ActionType actionType; | |
uint256 accountId; | |
Types.AssetAmount amount; | |
uint256 primaryMarketId; | |
uint256 secondaryMarketId; | |
address otherAddress; | |
uint256 otherAccountId; | |
bytes data; | |
} | |
// ============ Action Types ============ | |
/* | |
* Moves tokens from an address to Solo. Can either repay a borrow or provide additional supply. | |
*/ | |
struct DepositArgs { | |
Types.AssetAmount amount; | |
Account.Info account; | |
uint256 market; | |
address from; | |
} | |
/* | |
* Moves tokens from Solo to another address. Can either borrow tokens or reduce the amount | |
* previously supplied. | |
*/ | |
struct WithdrawArgs { | |
Types.AssetAmount amount; | |
Account.Info account; | |
uint256 market; | |
address to; | |
} | |
/* | |
* Transfers balance between two accounts. The msg.sender must be an operator for both accounts. | |
* The amount field applies to accountOne. | |
* This action does not require any token movement since the trade is done internally to Solo. | |
*/ | |
struct TransferArgs { | |
Types.AssetAmount amount; | |
Account.Info accountOne; | |
Account.Info accountTwo; | |
uint256 market; | |
} | |
/* | |
* Acquires a certain amount of tokens by spending other tokens. Sends takerMarket tokens to the | |
* specified exchangeWrapper contract and expects makerMarket tokens in return. The amount field | |
* applies to the makerMarket. | |
*/ | |
struct BuyArgs { | |
Types.AssetAmount amount; | |
Account.Info account; | |
uint256 makerMarket; | |
uint256 takerMarket; | |
address exchangeWrapper; | |
bytes orderData; | |
} | |
/* | |
* Spends a certain amount of tokens to acquire other tokens. Sends takerMarket tokens to the | |
* specified exchangeWrapper and expects makerMarket tokens in return. The amount field applies | |
* to the takerMarket. | |
*/ | |
struct SellArgs { | |
Types.AssetAmount amount; | |
Account.Info account; | |
uint256 takerMarket; | |
uint256 makerMarket; | |
address exchangeWrapper; | |
bytes orderData; | |
} | |
/* | |
* Trades balances between two accounts using any external contract that implements the | |
* AutoTrader interface. The AutoTrader contract must be an operator for the makerAccount (for | |
* which it is trading on-behalf-of). The amount field applies to the makerAccount and the | |
* inputMarket. This proposed change to the makerAccount is passed to the AutoTrader which will | |
* quote a change for the makerAccount in the outputMarket (or will disallow the trade). | |
* This action does not require any token movement since the trade is done internally to Solo. | |
*/ | |
struct TradeArgs { | |
Types.AssetAmount amount; | |
Account.Info takerAccount; | |
Account.Info makerAccount; | |
uint256 inputMarket; | |
uint256 outputMarket; | |
address autoTrader; | |
bytes tradeData; | |
} | |
/* | |
* Each account must maintain a certain margin-ratio (specified globally). If the account falls | |
* below this margin-ratio, it can be liquidated by any other account. This allows anyone else | |
* (arbitrageurs) to repay any borrowed asset (owedMarket) of the liquidating account in | |
* exchange for any collateral asset (heldMarket) of the liquidAccount. The ratio is determined | |
* by the price ratio (given by the oracles) plus a spread (specified globally). Liquidating an | |
* account also sets a flag on the account that the account is being liquidated. This allows | |
* anyone to continue liquidating the account until there are no more borrows being taken by the | |
* liquidating account. Liquidators do not have to liquidate the entire account all at once but | |
* can liquidate as much as they choose. The liquidating flag allows liquidators to continue | |
* liquidating the account even if it becomes collateralized through partial liquidation or | |
* price movement. | |
*/ | |
struct LiquidateArgs { | |
Types.AssetAmount amount; | |
Account.Info solidAccount; | |
Account.Info liquidAccount; | |
uint256 owedMarket; | |
uint256 heldMarket; | |
} | |
/* | |
* Similar to liquidate, but vaporAccounts are accounts that have only negative balances | |
* remaining. The arbitrageur pays back the negative asset (owedMarket) of the vaporAccount in | |
* exchange for a collateral asset (heldMarket) at a favorable spread. However, since the | |
* liquidAccount has no collateral assets, the collateral must come from Solo's excess tokens. | |
*/ | |
struct VaporizeArgs { | |
Types.AssetAmount amount; | |
Account.Info solidAccount; | |
Account.Info vaporAccount; | |
uint256 owedMarket; | |
uint256 heldMarket; | |
} | |
/* | |
* Passes arbitrary bytes of data to an external contract that implements the Callee interface. | |
* Does not change any asset amounts. This function may be useful for setting certain variables | |
* on layer-two contracts for certain accounts without having to make a separate Ethereum | |
* transaction for doing so. Also, the second-layer contracts can ensure that the call is coming | |
* from an operator of the particular account. | |
*/ | |
struct CallArgs { | |
Account.Info account; | |
address callee; | |
bytes data; | |
} | |
// ============ Helper Functions ============ | |
function getMarketLayout( | |
ActionType actionType | |
) | |
internal | |
pure | |
returns (MarketLayout) | |
{ | |
if ( | |
actionType == Actions.ActionType.Deposit | |
|| actionType == Actions.ActionType.Withdraw | |
|| actionType == Actions.ActionType.Transfer | |
) { | |
return MarketLayout.OneMarket; | |
} | |
else if (actionType == Actions.ActionType.Call) { | |
return MarketLayout.ZeroMarkets; | |
} | |
return MarketLayout.TwoMarkets; | |
} | |
function getAccountLayout( | |
ActionType actionType | |
) | |
internal | |
pure | |
returns (AccountLayout) | |
{ | |
if ( | |
actionType == Actions.ActionType.Transfer | |
|| actionType == Actions.ActionType.Trade | |
) { | |
return AccountLayout.TwoPrimary; | |
} else if ( | |
actionType == Actions.ActionType.Liquidate | |
|| actionType == Actions.ActionType.Vaporize | |
) { | |
return AccountLayout.PrimaryAndSecondary; | |
} | |
return AccountLayout.OnePrimary; | |
} | |
// ============ Parsing Functions ============ | |
function parseDepositArgs( | |
Account.Info[] memory accounts, | |
ActionArgs memory args | |
) | |
internal | |
pure | |
returns (DepositArgs memory) | |
{ | |
assert(args.actionType == ActionType.Deposit); | |
return DepositArgs({ | |
amount: args.amount, | |
account: accounts[args.accountId], | |
market: args.primaryMarketId, | |
from: args.otherAddress | |
}); | |
} | |
function parseWithdrawArgs( | |
Account.Info[] memory accounts, | |
ActionArgs memory args | |
) | |
internal | |
pure | |
returns (WithdrawArgs memory) | |
{ | |
assert(args.actionType == ActionType.Withdraw); | |
return WithdrawArgs({ | |
amount: args.amount, | |
account: accounts[args.accountId], | |
market: args.primaryMarketId, | |
to: args.otherAddress | |
}); | |
} | |
function parseTransferArgs( | |
Account.Info[] memory accounts, | |
ActionArgs memory args | |
) | |
internal | |
pure | |
returns (TransferArgs memory) | |
{ | |
assert(args.actionType == ActionType.Transfer); | |
return TransferArgs({ | |
amount: args.amount, | |
accountOne: accounts[args.accountId], | |
accountTwo: accounts[args.otherAccountId], | |
market: args.primaryMarketId | |
}); | |
} | |
function parseBuyArgs( | |
Account.Info[] memory accounts, | |
ActionArgs memory args | |
) | |
internal | |
pure | |
returns (BuyArgs memory) | |
{ | |
assert(args.actionType == ActionType.Buy); | |
return BuyArgs({ | |
amount: args.amount, | |
account: accounts[args.accountId], | |
makerMarket: args.primaryMarketId, | |
takerMarket: args.secondaryMarketId, | |
exchangeWrapper: args.otherAddress, | |
orderData: args.data | |
}); | |
} | |
function parseSellArgs( | |
Account.Info[] memory accounts, | |
ActionArgs memory args | |
) | |
internal | |
pure | |
returns (SellArgs memory) | |
{ | |
assert(args.actionType == ActionType.Sell); | |
return SellArgs({ | |
amount: args.amount, | |
account: accounts[args.accountId], | |
takerMarket: args.primaryMarketId, | |
makerMarket: args.secondaryMarketId, | |
exchangeWrapper: args.otherAddress, | |
orderData: args.data | |
}); | |
} | |
function parseTradeArgs( | |
Account.Info[] memory accounts, | |
ActionArgs memory args | |
) | |
internal | |
pure | |
returns (TradeArgs memory) | |
{ | |
assert(args.actionType == ActionType.Trade); | |
return TradeArgs({ | |
amount: args.amount, | |
takerAccount: accounts[args.accountId], | |
makerAccount: accounts[args.otherAccountId], | |
inputMarket: args.primaryMarketId, | |
outputMarket: args.secondaryMarketId, | |
autoTrader: args.otherAddress, | |
tradeData: args.data | |
}); | |
} | |
function parseLiquidateArgs( | |
Account.Info[] memory accounts, | |
ActionArgs memory args | |
) | |
internal | |
pure | |
returns (LiquidateArgs memory) | |
{ | |
assert(args.actionType == ActionType.Liquidate); | |
return LiquidateArgs({ | |
amount: args.amount, | |
solidAccount: accounts[args.accountId], | |
liquidAccount: accounts[args.otherAccountId], | |
owedMarket: args.primaryMarketId, | |
heldMarket: args.secondaryMarketId | |
}); | |
} | |
function parseVaporizeArgs( | |
Account.Info[] memory accounts, | |
ActionArgs memory args | |
) | |
internal | |
pure | |
returns (VaporizeArgs memory) | |
{ | |
assert(args.actionType == ActionType.Vaporize); | |
return VaporizeArgs({ | |
amount: args.amount, | |
solidAccount: accounts[args.accountId], | |
vaporAccount: accounts[args.otherAccountId], | |
owedMarket: args.primaryMarketId, | |
heldMarket: args.secondaryMarketId | |
}); | |
} | |
function parseCallArgs( | |
Account.Info[] memory accounts, | |
ActionArgs memory args | |
) | |
internal | |
pure | |
returns (CallArgs memory) | |
{ | |
assert(args.actionType == ActionType.Call); | |
return CallArgs({ | |
account: accounts[args.accountId], | |
callee: args.otherAddress, | |
data: args.data | |
}); | |
} | |
} | |
contract Ownable { | |
address private _owner; | |
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); | |
/** | |
* @dev The Ownable constructor sets the original `owner` of the contract to the sender | |
* account. | |
*/ | |
constructor () internal { | |
_owner = msg.sender; | |
emit OwnershipTransferred(address(0), _owner); | |
} | |
/** | |
* @return the address of the owner. | |
*/ | |
function owner() public view returns (address) { | |
return _owner; | |
} | |
/** | |
* @dev Throws if called by any account other than the owner. | |
*/ | |
modifier onlyOwner() { | |
require(isOwner()); | |
_; | |
} | |
/** | |
* @return true if `msg.sender` is the owner of the contract. | |
*/ | |
function isOwner() public view returns (bool) { | |
return msg.sender == _owner; | |
} | |
/** | |
* @dev Allows the current owner to relinquish control of the contract. | |
* @notice Renouncing to ownership will leave the contract without an owner. | |
* It will not be possible to call the functions with the `onlyOwner` | |
* modifier anymore. | |
*/ | |
function renounceOwnership() public onlyOwner { | |
emit OwnershipTransferred(_owner, address(0)); | |
_owner = address(0); | |
} | |
/** | |
* @dev Allows the current owner to transfer control of the contract to a newOwner. | |
* @param newOwner The address to transfer ownership to. | |
*/ | |
function transferOwnership(address newOwner) public onlyOwner { | |
_transferOwnership(newOwner); | |
} | |
/** | |
* @dev Transfers control of the contract to a newOwner. | |
* @param newOwner The address to transfer ownership to. | |
*/ | |
function _transferOwnership(address newOwner) internal { | |
require(newOwner != address(0)); | |
emit OwnershipTransferred(_owner, newOwner); | |
_owner = newOwner; | |
} | |
} | |
library Storage { | |
using Cache for Cache.MarketCache; | |
using Storage for Storage.State; | |
using Math for uint256; | |
using Types for Types.Par; | |
using Types for Types.Wei; | |
using SafeMath for uint256; | |
// ============ Constants ============ | |
bytes32 constant FILE = "Storage"; | |
// ============ Structs ============ | |
// All information necessary for tracking a market | |
struct Market { | |
// Contract address of the associated ERC20 token | |
address token; | |
// Total aggregated supply and borrow amount of the entire market | |
Types.TotalPar totalPar; | |
// Interest index of the market | |
Interest.Index index; | |
// Contract address of the price oracle for this market | |
IPriceOracle priceOracle; | |
// Contract address of the interest setter for this market | |
IInterestSetter interestSetter; | |
// Multiplier on the marginRatio for this market | |
Decimal.D256 marginPremium; | |
// Multiplier on the liquidationSpread for this market | |
Decimal.D256 spreadPremium; | |
// Whether additional borrows are allowed for this market | |
bool isClosing; | |
} | |
// The global risk parameters that govern the health and security of the system | |
struct RiskParams { | |
// Required ratio of over-collateralization | |
Decimal.D256 marginRatio; | |
// Percentage penalty incurred by liquidated accounts | |
Decimal.D256 liquidationSpread; | |
// Percentage of the borrower's interest fee that gets passed to the suppliers | |
Decimal.D256 earningsRate; | |
// The minimum absolute borrow value of an account | |
// There must be sufficient incentivize to liquidate undercollateralized accounts | |
Monetary.Value minBorrowedValue; | |
} | |
// The maximum RiskParam values that can be set | |
struct RiskLimits { | |
uint64 marginRatioMax; | |
uint64 liquidationSpreadMax; | |
uint64 earningsRateMax; | |
uint64 marginPremiumMax; | |
uint64 spreadPremiumMax; | |
uint128 minBorrowedValueMax; | |
} | |
// The entire storage state of Solo | |
struct State { | |
// number of markets | |
uint256 numMarkets; | |
// marketId => Market | |
mapping (uint256 => Market) markets; | |
// owner => account number => Account | |
mapping (address => mapping (uint256 => Account.Storage)) accounts; | |
// Addresses that can control other users accounts | |
mapping (address => mapping (address => bool)) operators; | |
// Addresses that can control all users accounts | |
mapping (address => bool) globalOperators; | |
// mutable risk parameters of the system | |
RiskParams riskParams; | |
// immutable risk limits of the system | |
RiskLimits riskLimits; | |
} | |
// ============ Functions ============ | |
function getToken( | |
Storage.State storage state, | |
uint256 marketId | |
) | |
internal | |
view | |
returns (address) | |
{ | |
return state.markets[marketId].token; | |
} | |
function getTotalPar( | |
Storage.State storage state, | |
uint256 marketId | |
) | |
internal | |
view | |
returns (Types.TotalPar memory) | |
{ | |
return state.markets[marketId].totalPar; | |
} | |
function getIndex( | |
Storage.State storage state, | |
uint256 marketId | |
) | |
internal | |
view | |
returns (Interest.Index memory) | |
{ | |
return state.markets[marketId].index; | |
} | |
function getNumExcessTokens( | |
Storage.State storage state, | |
uint256 marketId | |
) | |
internal | |
view | |
returns (Types.Wei memory) | |
{ | |
Interest.Index memory index = state.getIndex(marketId); | |
Types.TotalPar memory totalPar = state.getTotalPar(marketId); | |
address token = state.getToken(marketId); | |
Types.Wei memory balanceWei = Types.Wei({ | |
sign: true, | |
value: Token.balanceOf(token, address(this)) | |
}); | |
( | |
Types.Wei memory supplyWei, | |
Types.Wei memory borrowWei | |
) = Interest.totalParToWei(totalPar, index); | |
// borrowWei is negative, so subtracting it makes the value more positive | |
return balanceWei.sub(borrowWei).sub(supplyWei); | |
} | |
function getStatus( | |
Storage.State storage state, | |
Account.Info memory account | |
) | |
internal | |
view | |
returns (Account.Status) | |
{ | |
return state.accounts[account.owner][account.number].status; | |
} | |
function getPar( | |
Storage.State storage state, | |
Account.Info memory account, | |
uint256 marketId | |
) | |
internal | |
view | |
returns (Types.Par memory) | |
{ | |
return state.accounts[account.owner][account.number].balances[marketId]; | |
} | |
function getWei( | |
Storage.State storage state, | |
Account.Info memory account, | |
uint256 marketId | |
) | |
internal | |
view | |
returns (Types.Wei memory) | |
{ | |
Types.Par memory par = state.getPar(account, marketId); | |
if (par.isZero()) { | |
return Types.zeroWei(); | |
} | |
Interest.Index memory index = state.getIndex(marketId); | |
return Interest.parToWei(par, index); | |
} | |
function getLiquidationSpreadForPair( | |
Storage.State storage state, | |
uint256 heldMarketId, | |
uint256 owedMarketId | |
) | |
internal | |
view | |
returns (Decimal.D256 memory) | |
{ | |
uint256 result = state.riskParams.liquidationSpread.value; | |
result = Decimal.mul(result, Decimal.onePlus(state.markets[heldMarketId].spreadPremium)); | |
result = Decimal.mul(result, Decimal.onePlus(state.markets[owedMarketId].spreadPremium)); | |
return Decimal.D256({ | |
value: result | |
}); | |
} | |
function fetchNewIndex( | |
Storage.State storage state, | |
uint256 marketId, | |
Interest.Index memory index | |
) | |
internal | |
view | |
returns (Interest.Index memory) | |
{ | |
Interest.Rate memory rate = state.fetchInterestRate(marketId, index); | |
return Interest.calculateNewIndex( | |
index, | |
rate, | |
state.getTotalPar(marketId), | |
state.riskParams.earningsRate | |
); | |
} | |
function fetchInterestRate( | |
Storage.State storage state, | |
uint256 marketId, | |
Interest.Index memory index | |
) | |
internal | |
view | |
returns (Interest.Rate memory) | |
{ | |
Types.TotalPar memory totalPar = state.getTotalPar(marketId); | |
( | |
Types.Wei memory supplyWei, | |
Types.Wei memory borrowWei | |
) = Interest.totalParToWei(totalPar, index); | |
Interest.Rate memory rate = state.markets[marketId].interestSetter.getInterestRate( | |
state.getToken(marketId), | |
borrowWei.value, | |
supplyWei.value | |
); | |
return rate; | |
} | |
function fetchPrice( | |
Storage.State storage state, | |
uint256 marketId | |
) | |
internal | |
view | |
returns (Monetary.Price memory) | |
{ | |
IPriceOracle oracle = IPriceOracle(state.markets[marketId].priceOracle); | |
Monetary.Price memory price = oracle.getPrice(state.getToken(marketId)); | |
Require.that( | |
price.value != 0, | |
FILE, | |
"Price cannot be zero", | |
marketId | |
); | |
return price; | |
} | |
function getAccountValues( | |
Storage.State storage state, | |
Account.Info memory account, | |
Cache.MarketCache memory cache, | |
bool adjustForLiquidity | |
) | |
internal | |
view | |
returns (Monetary.Value memory, Monetary.Value memory) | |
{ | |
Monetary.Value memory supplyValue; | |
Monetary.Value memory borrowValue; | |
uint256 numMarkets = cache.getNumMarkets(); | |
for (uint256 m = 0; m < numMarkets; m++) { | |
if (!cache.hasMarket(m)) { | |
continue; | |
} | |
Types.Wei memory userWei = state.getWei(account, m); | |
if (userWei.isZero()) { | |
continue; | |
} | |
uint256 assetValue = userWei.value.mul(cache.getPrice(m).value); | |
Decimal.D256 memory adjust = Decimal.one(); | |
if (adjustForLiquidity) { | |
adjust = Decimal.onePlus(state.markets[m].marginPremium); | |
} | |
if (userWei.sign) { | |
supplyValue.value = supplyValue.value.add(Decimal.div(assetValue, adjust)); | |
} else { | |
borrowValue.value = borrowValue.value.add(Decimal.mul(assetValue, adjust)); | |
} | |
} | |
return (supplyValue, borrowValue); | |
} | |
function isCollateralized( | |
Storage.State storage state, | |
Account.Info memory account, | |
Cache.MarketCache memory cache, | |
bool requireMinBorrow | |
) | |
internal | |
view | |
returns (bool) | |
{ | |
// get account values (adjusted for liquidity) | |
( | |
Monetary.Value memory supplyValue, | |
Monetary.Value memory borrowValue | |
) = state.getAccountValues(account, cache, /* adjustForLiquidity = */ true); | |
if (borrowValue.value == 0) { | |
return true; | |
} | |
if (requireMinBorrow) { | |
Require.that( | |
borrowValue.value >= state.riskParams.minBorrowedValue.value, | |
FILE, | |
"Borrow value too low", | |
account.owner, | |
account.number, | |
borrowValue.value | |
); | |
} | |
uint256 requiredMargin = Decimal.mul(borrowValue.value, state.riskParams.marginRatio); | |
return supplyValue.value >= borrowValue.value.add(requiredMargin); | |
} | |
function isGlobalOperator( | |
Storage.State storage state, | |
address operator | |
) | |
internal | |
view | |
returns (bool) | |
{ | |
return state.globalOperators[operator]; | |
} | |
function isLocalOperator( | |
Storage.State storage state, | |
address owner, | |
address operator | |
) | |
internal | |
view | |
returns (bool) | |
{ | |
return state.operators[owner][operator]; | |
} | |
function requireIsOperator( | |
Storage.State storage state, | |
Account.Info memory account, | |
address operator | |
) | |
internal | |
view | |
{ | |
bool isValidOperator = | |
operator == account.owner | |
|| state.isGlobalOperator(operator) | |
|| state.isLocalOperator(account.owner, operator); | |
Require.that( | |
isValidOperator, | |
FILE, | |
"Unpermissioned operator", | |
operator | |
); | |
} | |
/** | |
* Determine and set an account's balance based on the intended balance change. Return the | |
* equivalent amount in wei | |
*/ | |
function getNewParAndDeltaWei( | |
Storage.State storage state, | |
Account.Info memory account, | |
uint256 marketId, | |
Types.AssetAmount memory amount | |
) | |
internal | |
view | |
returns (Types.Par memory, Types.Wei memory) | |
{ | |
Types.Par memory oldPar = state.getPar(account, marketId); | |
if (amount.value == 0 && amount.ref == Types.AssetReference.Delta) { | |
return (oldPar, Types.zeroWei()); | |
} | |
Interest.Index memory index = state.getIndex(marketId); | |
Types.Wei memory oldWei = Interest.parToWei(oldPar, index); | |
Types.Par memory newPar; | |
Types.Wei memory deltaWei; | |
if (amount.denomination == Types.AssetDenomination.Wei) { | |
deltaWei = Types.Wei({ | |
sign: amount.sign, | |
value: amount.value | |
}); | |
if (amount.ref == Types.AssetReference.Target) { | |
deltaWei = deltaWei.sub(oldWei); | |
} | |
newPar = Interest.weiToPar(oldWei.add(deltaWei), index); | |
} else { // AssetDenomination.Par | |
newPar = Types.Par({ | |
sign: amount.sign, | |
value: amount.value.to128() | |
}); | |
if (amount.ref == Types.AssetReference.Delta) { | |
newPar = oldPar.add(newPar); | |
} | |
deltaWei = Interest.parToWei(newPar, index).sub(oldWei); | |
} | |
return (newPar, deltaWei); | |
} | |
function getNewParAndDeltaWeiForLiquidation( | |
Storage.State storage state, | |
Account.Info memory account, | |
uint256 marketId, | |
Types.AssetAmount memory amount | |
) | |
internal | |
view | |
returns (Types.Par memory, Types.Wei memory) | |
{ | |
Types.Par memory oldPar = state.getPar(account, marketId); | |
Require.that( | |
!oldPar.isPositive(), | |
FILE, | |
"Owed balance cannot be positive", | |
account.owner, | |
account.number, | |
marketId | |
); | |
( | |
Types.Par memory newPar, | |
Types.Wei memory deltaWei | |
) = state.getNewParAndDeltaWei( | |
account, | |
marketId, | |
amount | |
); | |
// if attempting to over-repay the owed asset, bound it by the maximum | |
if (newPar.isPositive()) { | |
newPar = Types.zeroPar(); | |
deltaWei = state.getWei(account, marketId).negative(); | |
} | |
Require.that( | |
!deltaWei.isNegative() && oldPar.value >= newPar.value, | |
FILE, | |
"Owed balance cannot increase", | |
account.owner, | |
account.number, | |
marketId | |
); | |
// if not paying back enough wei to repay any par, then bound wei to zero | |
if (oldPar.equals(newPar)) { | |
deltaWei = Types.zeroWei(); | |
} | |
return (newPar, deltaWei); | |
} | |
function isVaporizable( | |
Storage.State storage state, | |
Account.Info memory account, | |
Cache.MarketCache memory cache | |
) | |
internal | |
view | |
returns (bool) | |
{ | |
bool hasNegative = false; | |
uint256 numMarkets = cache.getNumMarkets(); | |
for (uint256 m = 0; m < numMarkets; m++) { | |
if (!cache.hasMarket(m)) { | |
continue; | |
} | |
Types.Par memory par = state.getPar(account, m); | |
if (par.isZero()) { | |
continue; | |
} else if (par.sign) { | |
return false; | |
} else { | |
hasNegative = true; | |
} | |
} | |
return hasNegative; | |
} | |
// =============== Setter Functions =============== | |
function updateIndex( | |
Storage.State storage state, | |
uint256 marketId | |
) | |
internal | |
returns (Interest.Index memory) | |
{ | |
Interest.Index memory index = state.getIndex(marketId); | |
if (index.lastUpdate == Time.currentTime()) { | |
return index; | |
} | |
return state.markets[marketId].index = state.fetchNewIndex(marketId, index); | |
} | |
function setStatus( | |
Storage.State storage state, | |
Account.Info memory account, | |
Account.Status status | |
) | |
internal | |
{ | |
state.accounts[account.owner][account.number].status = status; | |
} | |
function setPar( | |
Storage.State storage state, | |
Account.Info memory account, | |
uint256 marketId, | |
Types.Par memory newPar | |
) | |
internal | |
{ | |
Types.Par memory oldPar = state.getPar(account, marketId); | |
if (Types.equals(oldPar, newPar)) { | |
return; | |
} | |
// updateTotalPar | |
Types.TotalPar memory totalPar = state.getTotalPar(marketId); | |
// roll-back oldPar | |
if (oldPar.sign) { | |
totalPar.supply = uint256(totalPar.supply).sub(oldPar.value).to128(); | |
} else { | |
totalPar.borrow = uint256(totalPar.borrow).sub(oldPar.value).to128(); | |
} | |
// roll-forward newPar | |
if (newPar.sign) { | |
totalPar.supply = uint256(totalPar.supply).add(newPar.value).to128(); | |
} else { | |
totalPar.borrow = uint256(totalPar.borrow).add(newPar.value).to128(); | |
} | |
state.markets[marketId].totalPar = totalPar; | |
state.accounts[account.owner][account.number].balances[marketId] = newPar; | |
} | |
/** | |
* Determine and set an account's balance based on a change in wei | |
*/ | |
function setParFromDeltaWei( | |
Storage.State storage state, | |
Account.Info memory account, | |
uint256 marketId, | |
Types.Wei memory deltaWei | |
) | |
internal | |
{ | |
if (deltaWei.isZero()) { | |
return; | |
} | |
Interest.Index memory index = state.getIndex(marketId); | |
Types.Wei memory oldWei = state.getWei(account, marketId); | |
Types.Wei memory newWei = oldWei.add(deltaWei); | |
Types.Par memory newPar = Interest.weiToPar(newWei, index); | |
state.setPar( | |
account, | |
marketId, | |
newPar | |
); | |
} | |
} | |
interface IInterestSetter { | |
// ============ Public Functions ============ | |
/** | |
* Get the interest rate of a token given some borrowed and supplied amounts | |
* | |
* @param token The address of the ERC20 token for the market | |
* @param borrowWei The total borrowed token amount for the market | |
* @param supplyWei The total supplied token amount for the market | |
* @return The interest rate per second | |
*/ | |
function getInterestRate( | |
address token, | |
uint256 borrowWei, | |
uint256 supplyWei | |
) | |
external | |
view | |
returns (Interest.Rate memory); | |
} | |
contract OmiseToken { | |
using SafeMath for uint256; | |
uint256 supply; | |
mapping (address => uint256) balances; | |
mapping (address => mapping (address => uint256)) allowed; | |
event Transfer(address token, address from, address to, uint256 value); | |
event Approval(address token, address owner, address spender, uint256 value); | |
event Issue(address token, address owner, uint256 value); | |
// Allow anyone to get new token | |
function issue(uint256 amount) public { | |
issueTo(msg.sender, amount); | |
} | |
function issueTo(address who, uint256 amount) public { | |
supply = supply.add(amount); | |
balances[who] = balances[who].add(amount); | |
emit Issue(address(this), who, amount); | |
} | |
function totalSupply() public view returns (uint256) { | |
return supply; | |
} | |
function balanceOf(address who) public view returns (uint256) { | |
return balances[who]; | |
} | |
function allowance(address owner, address spender) public view returns (uint256) { | |
return allowed[owner][spender]; | |
} | |
function symbol() public pure returns (string memory) { | |
return "TOMG"; | |
} | |
function name() public pure returns (string memory) { | |
return "Test Omise"; | |
} | |
function decimals() public pure returns (uint8) { | |
return 18; | |
} | |
function transfer(address to, uint256 value) public { | |
require(balances[msg.sender] >= value); | |
balances[msg.sender] -= value; | |
balances[to] = balances[to].add(value); | |
emit Transfer( | |
address(this), | |
msg.sender, | |
to, | |
value | |
); | |
} | |
function transferFrom(address from, address to, uint256 value) public { | |
require(balances[from] >= value && allowed[from][msg.sender] >= value); | |
balances[to] = balances[to].add(value); | |
balances[from] = balances[from].sub(value); | |
allowed[from][msg.sender] = allowed[from][msg.sender].sub(value); | |
emit Transfer( | |
address(this), | |
from, | |
to, | |
value | |
); | |
} | |
function approve(address spender, uint256 value) public { | |
allowed[msg.sender][spender] = value; | |
emit Approval( | |
address(this), | |
msg.sender, | |
spender, | |
value | |
); | |
} | |
} | |
interface IErc20 { | |
event Transfer( | |
address indexed from, | |
address indexed to, | |
uint256 value | |
); | |
event Approval( | |
address indexed owner, | |
address indexed spender, | |
uint256 value | |
); | |
function totalSupply( | |
) | |
external | |
view | |
returns (uint256); | |
function balanceOf( | |
address who | |
) | |
external | |
view | |
returns (uint256); | |
function allowance( | |
address owner, | |
address spender | |
) | |
external | |
view | |
returns (uint256); | |
function transfer( | |
address to, | |
uint256 value | |
) | |
external; | |
function transferFrom( | |
address from, | |
address to, | |
uint256 value | |
) | |
external; | |
function approve( | |
address spender, | |
uint256 value | |
) | |
external; | |
function name() | |
external | |
view | |
returns (string memory); | |
function symbol() | |
external | |
view | |
returns (string memory); | |
function decimals() | |
external | |
view | |
returns (uint8); | |
} | |
contract OnlySolo { | |
// ============ Constants ============ | |
bytes32 constant FILE = "OnlySolo"; | |
// ============ Storage ============ | |
SoloMargin public SOLO_MARGIN; | |
// ============ Constructor ============ | |
constructor ( | |
address soloMargin | |
) | |
public | |
{ | |
SOLO_MARGIN = SoloMargin(soloMargin); | |
} | |
// ============ Modifiers ============ | |
modifier onlySolo(address from) { | |
Require.that( | |
from == address(SOLO_MARGIN), | |
FILE, | |
"Only Solo can call function", | |
from | |
); | |
_; | |
} | |
} | |
interface IExchangeWrapper { | |
// ============ Public Functions ============ | |
/** | |
* Exchange some amount of takerToken for makerToken. | |
* | |
* @param tradeOriginator Address of the initiator of the trade (however, this value | |
* cannot always be trusted as it is set at the discretion of the | |
* msg.sender) | |
* @param receiver Address to set allowance on once the trade has completed | |
* @param makerToken Address of makerToken, the token to receive | |
* @param takerToken Address of takerToken, the token to pay | |
* @param requestedFillAmount Amount of takerToken being paid | |
* @param orderData Arbitrary bytes data for any information to pass to the exchange | |
* @return The amount of makerToken received | |
*/ | |
function exchange( | |
address tradeOriginator, | |
address receiver, | |
address makerToken, | |
address takerToken, | |
uint256 requestedFillAmount, | |
bytes calldata orderData | |
) | |
external | |
returns (uint256); | |
/** | |
* Get amount of takerToken required to buy a certain amount of makerToken for a given trade. | |
* Should match the takerToken amount used in exchangeForAmount. If the order cannot provide | |
* exactly desiredMakerToken, then it must return the price to buy the minimum amount greater | |
* than desiredMakerToken | |
* | |
* @param makerToken Address of makerToken, the token to receive | |
* @param takerToken Address of takerToken, the token to pay | |
* @param desiredMakerToken Amount of makerToken requested | |
* @param orderData Arbitrary bytes data for any information to pass to the exchange | |
* @return Amount of takerToken the needed to complete the exchange | |
*/ | |
function getExchangeCost( | |
address makerToken, | |
address takerToken, | |
uint256 desiredMakerToken, | |
bytes calldata orderData | |
) | |
external | |
view | |
returns (uint256); | |
} | |
contract DelayedMultiSig is | |
MultiSig | |
{ | |
// ============ Events ============ | |
event ConfirmationTimeSet(uint256 indexed transactionId, uint256 confirmationTime); | |
event TimeLockChange(uint32 secondsTimeLocked); | |
// ============ Storage ============ | |
uint32 public secondsTimeLocked; | |
mapping (uint256 => uint256) public confirmationTimes; | |
// ============ Modifiers ============ | |
modifier notFullyConfirmed( | |
uint256 transactionId | |
) { | |
require( | |
!isConfirmed(transactionId), | |
"TX_FULLY_CONFIRMED" | |
); | |
_; | |
} | |
modifier fullyConfirmed( | |
uint256 transactionId | |
) { | |
require( | |
isConfirmed(transactionId), | |
"TX_NOT_FULLY_CONFIRMED" | |
); | |
_; | |
} | |
modifier pastTimeLock( | |
uint256 transactionId | |
) { | |
require( | |
block.timestamp >= confirmationTimes[transactionId] + secondsTimeLocked, | |
"TIME_LOCK_INCOMPLETE" | |
); | |
_; | |
} | |
// ============ Constructor ============ | |
/** | |
* Contract constructor sets initial owners, required number of confirmations, and time lock. | |
* | |
* @param _owners List of initial owners. | |
* @param _required Number of required confirmations. | |
* @param _secondsTimeLocked Duration needed after a transaction is confirmed and before it | |
* becomes executable, in seconds. | |
*/ | |
constructor ( | |
address[] memory _owners, | |
uint256 _required, | |
uint32 _secondsTimeLocked | |
) | |
public | |
MultiSig(_owners, _required) | |
{ | |
secondsTimeLocked = _secondsTimeLocked; | |
} | |
// ============ Wallet-Only Functions ============ | |
/** | |
* Changes the duration of the time lock for transactions. | |
* | |
* @param _secondsTimeLocked Duration needed after a transaction is confirmed and before it | |
* becomes executable, in seconds. | |
*/ | |
function changeTimeLock( | |
uint32 _secondsTimeLocked | |
) | |
public | |
onlyWallet | |
{ | |
secondsTimeLocked = _secondsTimeLocked; | |
emit TimeLockChange(_secondsTimeLocked); | |
} | |
// ============ Admin Functions ============ | |
/** | |
* Allows an owner to confirm a transaction. | |
* Overrides the function in MultiSig. | |
* | |
* @param transactionId Transaction ID. | |
*/ | |
function confirmTransaction( | |
uint256 transactionId | |
) | |
public | |
ownerExists(msg.sender) | |
transactionExists(transactionId) | |
notConfirmed(transactionId, msg.sender) | |
notFullyConfirmed(transactionId) | |
{ | |
confirmations[transactionId][msg.sender] = true; | |
emit Confirmation(msg.sender, transactionId); | |
if (isConfirmed(transactionId)) { | |
setConfirmationTime(transactionId, block.timestamp); | |
} | |
} | |
/** | |
* Allows an owner to execute a confirmed transaction. | |
* Overrides the function in MultiSig. | |
* | |
* @param transactionId Transaction ID. | |
*/ | |
function executeTransaction( | |
uint256 transactionId | |
) | |
public | |
ownerExists(msg.sender) | |
notExecuted(transactionId) | |
fullyConfirmed(transactionId) | |
pastTimeLock(transactionId) | |
{ | |
Transaction storage txn = transactions[transactionId]; | |
txn.executed = true; | |
bool success = externalCall( | |
txn.destination, | |
txn.value, | |
txn.data.length, | |
txn.data | |
); | |
require( | |
success, | |
"TX_REVERTED" | |
); | |
emit Execution(transactionId); | |
} | |
/** | |
* Allows an owner to execute multiple confirmed transactions. | |
* | |
* @param transactionIds List of transaction IDs. | |
*/ | |
function executeMultipleTransactions( | |
uint256[] memory transactionIds | |
) | |
public | |
ownerExists(msg.sender) | |
{ | |
for (uint256 i = 0; i < transactionIds.length; i++) { | |
executeTransaction(transactionIds[i]); | |
} | |
} | |
// ============ Helper Functions ============ | |
/** | |
* Sets the time of when a submission first passed. | |
*/ | |
function setConfirmationTime( | |
uint256 transactionId, | |
uint256 confirmationTime | |
) | |
internal | |
{ | |
confirmationTimes[transactionId] = confirmationTime; | |
emit ConfirmationTimeSet(transactionId, confirmationTime); | |
} | |
} | |
contract TestAutoTrader is | |
IAutoTrader | |
{ | |
// ============ Constants ============ | |
bytes32 constant FILE = "TestAutoTrader"; | |
// ============ Events ============ | |
event DataSet( | |
uint256 indexed input, | |
Types.AssetAmount output | |
); | |
event DataDeleted( | |
uint256 indexed input | |
); | |
// ============ Storage ============ | |
// input => output | |
mapping (uint256 => Types.AssetAmount) public data; | |
mapping (uint256 => bool) public valid; | |
uint256 public requireInputMarketId; | |
uint256 public requireOutputMarketId; | |
Account.Info public requireMakerAccount; | |
Account.Info public requireTakerAccount; | |
Types.Par public requireOldInputPar; | |
Types.Par public requireNewInputPar; | |
Types.Wei public requireInputWei; | |
// ============ Testing Functions ============ | |
function setData( | |
uint256 input, | |
Types.AssetAmount memory output | |
) | |
public | |
{ | |
setDataInternal(input, output); | |
} | |
function setRequireInputMarketId( | |
uint256 inputMarketId | |
) | |
public | |
{ | |
requireInputMarketId = inputMarketId; | |
} | |
function setRequireOutputMarketId( | |
uint256 outputMarketId | |
) | |
public | |
{ | |
requireOutputMarketId = outputMarketId; | |
} | |
function setRequireMakerAccount( | |
Account.Info memory account | |
) | |
public | |
{ | |
requireMakerAccount = account; | |
} | |
function setRequireTakerAccount( | |
Account.Info memory account | |
) | |
public | |
{ | |
requireTakerAccount = account; | |
} | |
function setRequireOldInputPar( | |
Types.Par memory oldInputPar | |
) | |
public | |
{ | |
requireOldInputPar = oldInputPar; | |
} | |
function setRequireNewInputPar( | |
Types.Par memory newInputPar | |
) | |
public | |
{ | |
requireNewInputPar = newInputPar; | |
} | |
function setRequireInputWei( | |
Types.Wei memory inputWei | |
) | |
public | |
{ | |
requireInputWei = inputWei; | |
} | |
// ============ AutoTrader Functions ============ | |
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 tradeData | |
) | |
public | |
returns (Types.AssetAmount memory) | |
{ | |
if (requireInputMarketId != 0) { | |
Require.that( | |
requireInputMarketId == inputMarketId, | |
FILE, | |
"input market mismatch" | |
); | |
} | |
if (requireOutputMarketId != 0) { | |
Require.that( | |
requireOutputMarketId == outputMarketId, | |
FILE, | |
"output market mismatch" | |
); | |
} | |
if (requireMakerAccount.owner != address(0)) { | |
Require.that( | |
requireMakerAccount.owner == makerAccount.owner, | |
FILE, | |
"maker account owner mismatch" | |
); | |
Require.that( | |
requireMakerAccount.number == makerAccount.number, | |
FILE, | |
"maker account number mismatch" | |
); | |
} | |
if (requireTakerAccount.owner != address(0)) { | |
Require.that( | |
requireTakerAccount.owner == takerAccount.owner, | |
FILE, | |
"taker account owner mismatch" | |
); | |
Require.that( | |
requireTakerAccount.number == takerAccount.number, | |
FILE, | |
"taker account number mismatch" | |
); | |
} | |
if (requireOldInputPar.value != 0) { | |
Require.that( | |
requireOldInputPar.sign == oldInputPar.sign, | |
FILE, | |
"oldInputPar sign mismatch" | |
); | |
Require.that( | |
requireOldInputPar.value == oldInputPar.value, | |
FILE, | |
"oldInputPar value mismatch" | |
); | |
} | |
if (requireNewInputPar.value != 0) { | |
Require.that( | |
requireNewInputPar.sign == newInputPar.sign, | |
FILE, | |
"newInputPar sign mismatch" | |
); | |
Require.that( | |
requireNewInputPar.value == newInputPar.value, | |
FILE, | |
"newInputPar value mismatch" | |
); | |
} | |
if (requireInputWei.value != 0) { | |
Require.that( | |
requireInputWei.sign == inputWei.sign, | |
FILE, | |
"inputWei sign mismatch" | |
); | |
Require.that( | |
requireInputWei.value == inputWei.value, | |
FILE, | |
"inputWei value mismatch" | |
); | |
} | |
uint256 input = parseTradeData(tradeData); | |
return deleteDataInternal(input); | |
} | |
// ============ Private Functions ============ | |
function setDataInternal( | |
uint256 input, | |
Types.AssetAmount memory output | |
) | |
private | |
{ | |
data[input] = output; | |
valid[input] = true; | |
emit DataSet(input, output); | |
} | |
function deleteDataInternal( | |
uint256 input | |
) | |
private | |
returns (Types.AssetAmount memory) | |
{ | |
Require.that( | |
valid[input], | |
FILE, | |
"Trade does not exist" | |
); | |
Types.AssetAmount memory output = data[input]; | |
delete data[input]; | |
delete valid[input]; | |
emit DataDeleted(input); | |
return output; | |
} | |
function parseTradeData( | |
bytes memory tradeData | |
) | |
private | |
pure | |
returns (uint256) | |
{ | |
Require.that( | |
tradeData.length == 32, | |
FILE, | |
"Call data invalid length" | |
); | |
uint256 input; | |
/* solium-disable-next-line security/no-inline-assembly */ | |
assembly { | |
input := mload(add(tradeData, 32)) | |
} | |
return input; | |
} | |
} | |
contract PayableProxyForSoloMargin is | |
OnlySolo, | |
ReentrancyGuard | |
{ | |
// ============ Constants ============ | |
bytes32 constant FILE = "PayableProxyForSoloMargin"; | |
// ============ Storage ============ | |
WETH9 public WETH; | |
// ============ Constructor ============ | |
constructor ( | |
address soloMargin, | |
address payable weth | |
) | |
public | |
OnlySolo(soloMargin) | |
{ | |
WETH = WETH9(weth); | |
WETH.approve(soloMargin, uint256(-1)); | |
} | |
// ============ Public Functions ============ | |
/** | |
* Fallback function. Disallows ether to be sent to this contract without data except when | |
* unwrapping WETH. | |
*/ | |
function () | |
external | |
payable | |
{ | |
require( // coverage-disable-line | |
msg.sender == address(WETH), | |
"Cannot receive ETH" | |
); | |
} | |
function operate( | |
Account.Info[] memory accounts, | |
Actions.ActionArgs[] memory actions, | |
address payable sendEthTo | |
) | |
public | |
payable | |
nonReentrant | |
{ | |
WETH9 weth = WETH; | |
// create WETH from ETH | |
if (msg.value != 0) { | |
weth.deposit.value(msg.value)(); | |
} | |
// validate the input | |
for (uint256 i = 0; i < actions.length; i++) { | |
Actions.ActionArgs memory action = actions[i]; | |
// Can only operate on accounts owned by msg.sender | |
address owner1 = accounts[action.accountId].owner; | |
Require.that( | |
owner1 == msg.sender, | |
FILE, | |
"Sender must be primary account", | |
owner1 | |
); | |
// For a transfer both accounts must be owned by msg.sender | |
if (action.actionType == Actions.ActionType.Transfer) { | |
address owner2 = accounts[action.otherAccountId].owner; | |
Require.that( | |
owner2 == msg.sender, | |
FILE, | |
"Sender must be secondary account", | |
owner2 | |
); | |
} | |
} | |
SOLO_MARGIN.operate(accounts, actions); | |
// return all remaining WETH to the sendEthTo as ETH | |
uint256 remainingWeth = weth.balanceOf(address(this)); | |
if (remainingWeth != 0) { | |
Require.that( | |
sendEthTo != address(0), | |
FILE, | |
"Must set sendEthTo" | |
); | |
weth.withdraw(remainingWeth); | |
sendEthTo.transfer(remainingWeth); | |
} | |
} | |
} | |
contract Refunder is | |
Ownable, | |
OnlySolo, | |
IAutoTrader | |
{ | |
using Types for Types.Wei; | |
// ============ Constants ============ | |
bytes32 constant FILE = "Refunder"; | |
// ============ Events ============ | |
event LogGiverAdded( | |
address giver | |
); | |
event LogGiverRemoved( | |
address giver | |
); | |
event LogRefund( | |
Account.Info account, | |
uint256 marketId, | |
uint256 amount | |
); | |
// ============ Storage ============ | |
// the addresses that are able to give funds | |
mapping (address => bool) public g_givers; | |
// ============ Constructor ============ | |
constructor ( | |
address soloMargin, | |
address[] memory givers | |
) | |
public | |
OnlySolo(soloMargin) | |
{ | |
for (uint256 i = 0; i < givers.length; i++) { | |
g_givers[givers[i]] = true; | |
} | |
} | |
// ============ Admin Functions ============ | |
function addGiver( | |
address giver | |
) | |
external | |
onlyOwner | |
{ | |
emit LogGiverAdded(giver); | |
g_givers[giver] = true; | |
} | |
function removeGiver( | |
address giver | |
) | |
external | |
onlyOwner | |
{ | |
emit LogGiverRemoved(giver); | |
g_givers[giver] = false; | |
} | |
// ============ Only-Solo Functions ============ | |
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) | |
{ | |
Require.that( | |
g_givers[takerAccount.owner], | |
FILE, | |
"Giver not approved", | |
takerAccount.owner | |
); | |
Require.that( | |
inputWei.isPositive(), | |
FILE, | |
"Refund must be positive" | |
); | |
emit LogRefund( | |
makerAccount, | |
inputMarketId, | |
inputWei.value | |
); | |
return Types.AssetAmount({ | |
sign: false, | |
denomination: Types.AssetDenomination.Par, | |
ref: Types.AssetReference.Delta, | |
value: 0 | |
}); | |
} | |
} | |
contract Getters is | |
State | |
{ | |
using Cache for Cache.MarketCache; | |
using Storage for Storage.State; | |
using Types for Types.Par; | |
// ============ Constants ============ | |
bytes32 FILE = "Getters"; | |
// ============ Getters for Risk ============ | |
/** | |
* Get the global minimum margin-ratio that every position must maintain to prevent being | |
* liquidated. | |
* | |
* @return The global margin-ratio | |
*/ | |
function getMarginRatio() | |
public | |
view | |
returns (Decimal.D256 memory) | |
{ | |
return g_state.riskParams.marginRatio; | |
} | |
/** | |
* Get the global liquidation spread. This is the spread between oracle prices that incentivizes | |
* the liquidation of risky positions. | |
* | |
* @return The global liquidation spread | |
*/ | |
function getLiquidationSpread() | |
public | |
view | |
returns (Decimal.D256 memory) | |
{ | |
return g_state.riskParams.liquidationSpread; | |
} | |
/** | |
* Get the global earnings-rate variable that determines what percentage of the interest paid | |
* by borrowers gets passed-on to suppliers. | |
* | |
* @return The global earnings rate | |
*/ | |
function getEarningsRate() | |
public | |
view | |
returns (Decimal.D256 memory) | |
{ | |
return g_state.riskParams.earningsRate; | |
} | |
/** | |
* Get the global minimum-borrow value which is the minimum value of any new borrow on Solo. | |
* | |
* @return The global minimum borrow value | |
*/ | |
function getMinBorrowedValue() | |
public | |
view | |
returns (Monetary.Value memory) | |
{ | |
return g_state.riskParams.minBorrowedValue; | |
} | |
/** | |
* Get all risk parameters in a single struct. | |
* | |
* @return All global risk parameters | |
*/ | |
function getRiskParams() | |
public | |
view | |
returns (Storage.RiskParams memory) | |
{ | |
return g_state.riskParams; | |
} | |
/** | |
* Get all risk parameter limits in a single struct. These are the maximum limits at which the | |
* risk parameters can be set by the admin of Solo. | |
* | |
* @return All global risk parameter limnits | |
*/ | |
function getRiskLimits() | |
public | |
view | |
returns (Storage.RiskLimits memory) | |
{ | |
return g_state.riskLimits; | |
} | |
// ============ Getters for Markets ============ | |
/** | |
* Get the total number of markets. | |
* | |
* @return The number of markets | |
*/ | |
function getNumMarkets() | |
public | |
view | |
returns (uint256) | |
{ | |
return g_state.numMarkets; | |
} | |
/** | |
* Get the ERC20 token address for a market. | |
* | |
* @param marketId The market to query | |
* @return The token address | |
*/ | |
function getMarketTokenAddress( | |
uint256 marketId | |
) | |
public | |
view | |
returns (address) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.getToken(marketId); | |
} | |
/** | |
* Get the total principal amounts (borrowed and supplied) for a market. | |
* | |
* @param marketId The market to query | |
* @return The total principal amounts | |
*/ | |
function getMarketTotalPar( | |
uint256 marketId | |
) | |
public | |
view | |
returns (Types.TotalPar memory) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.getTotalPar(marketId); | |
} | |
/** | |
* Get the most recently cached interest index for a market. | |
* | |
* @param marketId The market to query | |
* @return The most recent index | |
*/ | |
function getMarketCachedIndex( | |
uint256 marketId | |
) | |
public | |
view | |
returns (Interest.Index memory) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.getIndex(marketId); | |
} | |
/** | |
* Get the interest index for a market if it were to be updated right now. | |
* | |
* @param marketId The market to query | |
* @return The estimated current index | |
*/ | |
function getMarketCurrentIndex( | |
uint256 marketId | |
) | |
public | |
view | |
returns (Interest.Index memory) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.fetchNewIndex(marketId, g_state.getIndex(marketId)); | |
} | |
/** | |
* Get the price oracle address for a market. | |
* | |
* @param marketId The market to query | |
* @return The price oracle address | |
*/ | |
function getMarketPriceOracle( | |
uint256 marketId | |
) | |
public | |
view | |
returns (IPriceOracle) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.markets[marketId].priceOracle; | |
} | |
/** | |
* Get the interest-setter address for a market. | |
* | |
* @param marketId The market to query | |
* @return The interest-setter address | |
*/ | |
function getMarketInterestSetter( | |
uint256 marketId | |
) | |
public | |
view | |
returns (IInterestSetter) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.markets[marketId].interestSetter; | |
} | |
/** | |
* Get the margin premium for a market. A margin premium makes it so that any positions that | |
* include the market require a higher collateralization to avoid being liquidated. | |
* | |
* @param marketId The market to query | |
* @return The market's margin premium | |
*/ | |
function getMarketMarginPremium( | |
uint256 marketId | |
) | |
public | |
view | |
returns (Decimal.D256 memory) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.markets[marketId].marginPremium; | |
} | |
/** | |
* Get the spread premium for a market. A spread premium makes it so that any liquidations | |
* that include the market have a higher spread than the global default. | |
* | |
* @param marketId The market to query | |
* @return The market's spread premium | |
*/ | |
function getMarketSpreadPremium( | |
uint256 marketId | |
) | |
public | |
view | |
returns (Decimal.D256 memory) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.markets[marketId].spreadPremium; | |
} | |
/** | |
* Return true if a particular market is in closing mode. Additional borrows cannot be taken | |
* from a market that is closing. | |
* | |
* @param marketId The market to query | |
* @return True if the market is closing | |
*/ | |
function getMarketIsClosing( | |
uint256 marketId | |
) | |
public | |
view | |
returns (bool) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.markets[marketId].isClosing; | |
} | |
/** | |
* Get the price of the token for a market. | |
* | |
* @param marketId The market to query | |
* @return The price of each atomic unit of the token | |
*/ | |
function getMarketPrice( | |
uint256 marketId | |
) | |
public | |
view | |
returns (Monetary.Price memory) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.fetchPrice(marketId); | |
} | |
/** | |
* Get the current borrower interest rate for a market. | |
* | |
* @param marketId The market to query | |
* @return The current interest rate | |
*/ | |
function getMarketInterestRate( | |
uint256 marketId | |
) | |
public | |
view | |
returns (Interest.Rate memory) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.fetchInterestRate( | |
marketId, | |
g_state.getIndex(marketId) | |
); | |
} | |
/** | |
* Get the adjusted liquidation spread for some market pair. This is equal to the global | |
* liquidation spread multiplied by (1 + spreadPremium) for each of the two markets. | |
* | |
* @param heldMarketId The market for which the account has collateral | |
* @param owedMarketId The market for which the account has borrowed tokens | |
* @return The adjusted liquidation spread | |
*/ | |
function getLiquidationSpreadForPair( | |
uint256 heldMarketId, | |
uint256 owedMarketId | |
) | |
public | |
view | |
returns (Decimal.D256 memory) | |
{ | |
_requireValidMarket(heldMarketId); | |
_requireValidMarket(owedMarketId); | |
return g_state.getLiquidationSpreadForPair(heldMarketId, owedMarketId); | |
} | |
/** | |
* Get basic information about a particular market. | |
* | |
* @param marketId The market to query | |
* @return A Storage.Market struct with the current state of the market | |
*/ | |
function getMarket( | |
uint256 marketId | |
) | |
public | |
view | |
returns (Storage.Market memory) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.markets[marketId]; | |
} | |
/** | |
* Get comprehensive information about a particular market. | |
* | |
* @param marketId The market to query | |
* @return A tuple containing the values: | |
* - A Storage.Market struct with the current state of the market | |
* - The current estimated interest index | |
* - The current token price | |
* - The current market interest rate | |
*/ | |
function getMarketWithInfo( | |
uint256 marketId | |
) | |
public | |
view | |
returns ( | |
Storage.Market memory, | |
Interest.Index memory, | |
Monetary.Price memory, | |
Interest.Rate memory | |
) | |
{ | |
_requireValidMarket(marketId); | |
return ( | |
getMarket(marketId), | |
getMarketCurrentIndex(marketId), | |
getMarketPrice(marketId), | |
getMarketInterestRate(marketId) | |
); | |
} | |
/** | |
* Get the number of excess tokens for a market. The number of excess tokens is calculated | |
* by taking the current number of tokens held in Solo, adding the number of tokens owed to Solo | |
* by borrowers, and subtracting the number of tokens owed to suppliers by Solo. | |
* | |
* @param marketId The market to query | |
* @return The number of excess tokens | |
*/ | |
function getNumExcessTokens( | |
uint256 marketId | |
) | |
public | |
view | |
returns (Types.Wei memory) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.getNumExcessTokens(marketId); | |
} | |
// ============ Getters for Accounts ============ | |
/** | |
* Get the principal value for a particular account and market. | |
* | |
* @param account The account to query | |
* @param marketId The market to query | |
* @return The principal value | |
*/ | |
function getAccountPar( | |
Account.Info memory account, | |
uint256 marketId | |
) | |
public | |
view | |
returns (Types.Par memory) | |
{ | |
_requireValidMarket(marketId); | |
return g_state.getPar(account, marketId); | |
} | |
/** | |
* Get the token balance for a particular account and market. | |
* | |
* @param account The account to query | |
* @param marketId The market to query | |
* @return The token amount | |
*/ | |
function getAccountWei( | |
Account.Info memory account, | |
uint256 marketId | |
) | |
public | |
view | |
returns (Types.Wei memory) | |
{ | |
_requireValidMarket(marketId); | |
return Interest.parToWei( | |
g_state.getPar(account, marketId), | |
g_state.fetchNewIndex(marketId, g_state.getIndex(marketId)) | |
); | |
} | |
/** | |
* Get the status of an account (Normal, Liquidating, or Vaporizing). | |
* | |
* @param account The account to query | |
* @return The account's status | |
*/ | |
function getAccountStatus( | |
Account.Info memory account | |
) | |
public | |
view | |
returns (Account.Status) | |
{ | |
return g_state.getStatus(account); | |
} | |
/** | |
* Get the total supplied and total borrowed value of an account. | |
* | |
* @param account The account to query | |
* @return The following values: | |
* - The supplied value of the account | |
* - The borrowed value of the account | |
*/ | |
function getAccountValues( | |
Account.Info memory account | |
) | |
public | |
view | |
returns (Monetary.Value memory, Monetary.Value memory) | |
{ | |
return getAccountValuesInternal(account, /* adjustForLiquidity = */ false); | |
} | |
/** | |
* 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 The following values: | |
* - The supplied value of the account (adjusted for marginPremium) | |
* - The borrowed value of the account (adjusted for marginPremium) | |
*/ | |
function getAdjustedAccountValues( | |
Account.Info memory account | |
) | |
public | |
view | |
returns (Monetary.Value memory, Monetary.Value memory) | |
{ | |
return getAccountValuesInternal(account, /* adjustForLiquidity = */ true); | |
} | |
/** | |
* Get an account's summary for each market. | |
* | |
* @param account The account to query | |
* @return The following values: | |
* - The ERC20 token address for each market | |
* - The account's principal value for each market | |
* - The account's (supplied or borrowed) number of tokens for each market | |
*/ | |
function getAccountBalances( | |
Account.Info memory account | |
) | |
public | |
view | |
returns ( | |
address[] memory, | |
Types.Par[] memory, | |
Types.Wei[] memory | |
) | |
{ | |
uint256 numMarkets = g_state.numMarkets; | |
address[] memory tokens = new address[](numMarkets); | |
Types.Par[] memory pars = new Types.Par[](numMarkets); | |
Types.Wei[] memory weis = new Types.Wei[](numMarkets); | |
for (uint256 m = 0; m < numMarkets; m++) { | |
tokens[m] = getMarketTokenAddress(m); | |
pars[m] = getAccountPar(account, m); | |
weis[m] = getAccountWei(account, m); | |
} | |
return ( | |
tokens, | |
pars, | |
weis | |
); | |
} | |
// ============ Getters for Permissions ============ | |
/** | |
* 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 True if operator is approved for owner's accounts | |
*/ | |
function getIsLocalOperator( | |
address owner, | |
address operator | |
) | |
public | |
view | |
returns (bool) | |
{ | |
return g_state.isLocalOperator(owner, operator); | |
} | |
/** | |
* Return true if a particular address is approved as a global operator. Such an address can | |
* act on any account as if it were the operator's own. | |
* | |
* @param operator The address to query | |
* @return True if operator is a global operator | |
*/ | |
function getIsGlobalOperator( | |
address operator | |
) | |
public | |
view | |
returns (bool) | |
{ | |
return g_state.isGlobalOperator(operator); | |
} | |
// ============ Private Helper Functions ============ | |
/** | |
* Revert if marketId is invalid. | |
*/ | |
function _requireValidMarket( | |
uint256 marketId | |
) | |
private | |
view | |
{ | |
Require.that( | |
marketId < g_state.numMarkets, | |
FILE, | |
"Market OOB" | |
); | |
} | |
/** | |
* Private helper for getting the monetary values of an account. | |
*/ | |
function getAccountValuesInternal( | |
Account.Info memory account, | |
bool adjustForLiquidity | |
) | |
private | |
view | |
returns (Monetary.Value memory, Monetary.Value memory) | |
{ | |
uint256 numMarkets = g_state.numMarkets; | |
// populate cache | |
Cache.MarketCache memory cache = Cache.create(numMarkets); | |
for (uint256 m = 0; m < numMarkets; m++) { | |
if (!g_state.getPar(account, m).isZero()) { | |
cache.addMarket(g_state, m); | |
} | |
} | |
return g_state.getAccountValues(account, cache, adjustForLiquidity); | |
} | |
} | |
contract DaiPriceOracle is | |
Ownable, | |
IPriceOracle | |
{ | |
using SafeMath for uint256; | |
// ============ Constants ============ | |
bytes32 constant FILE = "DaiPriceOracle"; | |
uint256 constant DECIMALS = 18; | |
uint256 constant EXPECTED_PRICE = ONE_DOLLAR / (10 ** DECIMALS); | |
// ============ Structs ============ | |
struct PriceInfo { | |
uint128 price; | |
uint32 lastUpdate; | |
} | |
struct DeviationParams { | |
uint64 denominator; | |
uint64 maximumPerSecond; | |
uint64 maximumAbsolute; | |
} | |
// ============ Events ============ | |
event PriceSet( | |
PriceInfo newPriceInfo | |
); | |
// ============ Storage ============ | |
PriceInfo public g_priceInfo; | |
address public g_poker; | |
DeviationParams public DEVIATION_PARAMS; | |
uint256 public OASIS_ETH_AMOUNT; | |
IErc20 public WETH; | |
IErc20 public DAI; | |
IMakerOracle public MEDIANIZER; | |
IOasisDex public OASIS; | |
address public UNISWAP; | |
// ============ Constructor ============= | |
constructor( | |
address poker, | |
address weth, | |
address dai, | |
address medianizer, | |
address oasis, | |
address uniswap, | |
uint256 oasisEthAmount, | |
DeviationParams memory deviationParams | |
) | |
public | |
{ | |
g_poker = poker; | |
MEDIANIZER = IMakerOracle(medianizer); | |
WETH = IErc20(weth); | |
DAI = IErc20(dai); | |
OASIS = IOasisDex(oasis); | |
UNISWAP = uniswap; | |
DEVIATION_PARAMS = deviationParams; | |
OASIS_ETH_AMOUNT = oasisEthAmount; | |
g_priceInfo = PriceInfo({ | |
lastUpdate: uint32(block.timestamp), | |
price: uint128(EXPECTED_PRICE) | |
}); | |
} | |
// ============ Admin Functions ============ | |
function ownerSetPokerAddress( | |
address newPoker | |
) | |
external | |
onlyOwner | |
{ | |
g_poker = newPoker; | |
} | |
// ============ Public Functions ============ | |
function updatePrice( | |
Monetary.Price memory minimum, | |
Monetary.Price memory maximum | |
) | |
public | |
returns (PriceInfo memory) | |
{ | |
Require.that( | |
msg.sender == g_poker, | |
FILE, | |
"Only poker can call updatePrice", | |
msg.sender | |
); | |
Monetary.Price memory newPrice = getBoundedTargetPrice(); | |
Require.that( | |
newPrice.value >= minimum.value, | |
FILE, | |
"newPrice below minimum", | |
newPrice.value, | |
minimum.value | |
); | |
Require.that( | |
newPrice.value <= maximum.value, | |
FILE, | |
"newPrice above maximum", | |
newPrice.value, | |
maximum.value | |
); | |
g_priceInfo = PriceInfo({ | |
price: Math.to128(newPrice.value), | |
lastUpdate: Time.currentTime() | |
}); | |
emit PriceSet(g_priceInfo); | |
return g_priceInfo; | |
} | |
// ============ IPriceOracle Functions ============ | |
function getPrice( | |
address /* token */ | |
) | |
public | |
view | |
returns (Monetary.Price memory) | |
{ | |
return Monetary.Price({ | |
value: g_priceInfo.price | |
}); | |
} | |
// ============ Price-Query Functions ============ | |
/** | |
* Get the new price that would be stored if updated right now. | |
*/ | |
function getBoundedTargetPrice() | |
public | |
view | |
returns (Monetary.Price memory) | |
{ | |
Monetary.Price memory targetPrice = getTargetPrice(); | |
PriceInfo memory oldInfo = g_priceInfo; | |
uint256 timeDelta = uint256(Time.currentTime()).sub(oldInfo.lastUpdate); | |
(uint256 minPrice, uint256 maxPrice) = getPriceBounds(oldInfo.price, timeDelta); | |
uint256 boundedTargetPrice = boundValue(targetPrice.value, minPrice, maxPrice); | |
return Monetary.Price({ | |
value: boundedTargetPrice | |
}); | |
} | |
/** | |
* Get the USD price of DAI that this contract will move towards when updated. This price is | |
* not bounded by the variables governing the maximum deviation from the old price. | |
*/ | |
function getTargetPrice() | |
public | |
view | |
returns (Monetary.Price memory) | |
{ | |
Monetary.Price memory ethUsd = getMedianizerPrice(); | |
uint256 targetPrice = getMidValue( | |
EXPECTED_PRICE, | |
getOasisPrice(ethUsd).value, | |
getUniswapPrice(ethUsd).value | |
); | |
return Monetary.Price({ | |
value: targetPrice | |
}); | |
} | |
/** | |
* Get the USD price of ETH according the Maker Medianizer contract. | |
*/ | |
function getMedianizerPrice() | |
public | |
view | |
returns (Monetary.Price memory) | |
{ | |
// throws if the price is not fresh | |
return Monetary.Price({ | |
value: uint256(MEDIANIZER.read()) | |
}); | |
} | |
/** | |
* Get the USD price of DAI according to OasisDEX given the USD price of ETH. | |
*/ | |
function getOasisPrice( | |
Monetary.Price memory ethUsd | |
) | |
public | |
view | |
returns (Monetary.Price memory) | |
{ | |
IOasisDex oasis = OASIS; | |
// If exchange is not operational, return old value. | |
// This allows the price to move only towards 1 USD | |
if ( | |
oasis.isClosed() | |
|| !oasis.buyEnabled() | |
|| !oasis.matchingEnabled() | |
) { | |
return Monetary.Price({ | |
value: g_priceInfo.price | |
}); | |
} | |
uint256 numWei = OASIS_ETH_AMOUNT; | |
address dai = address(DAI); | |
address weth = address(WETH); | |
// Assumes at least `numWei` of depth on both sides of the book if the exchange is active. | |
// Will revert if not enough depth. | |
uint256 daiAmt1 = oasis.getBuyAmount(dai, weth, numWei); | |
uint256 daiAmt2 = oasis.getPayAmount(dai, weth, numWei); | |
uint256 num = numWei.mul(daiAmt2).add(numWei.mul(daiAmt1)); | |
uint256 den = daiAmt1.mul(daiAmt2).mul(2); | |
uint256 oasisPrice = Math.getPartial(ethUsd.value, num, den); | |
return Monetary.Price({ | |
value: oasisPrice | |
}); | |
} | |
/** | |
* Get the USD price of DAI according to Uniswap given the USD price of ETH. | |
*/ | |
function getUniswapPrice( | |
Monetary.Price memory ethUsd | |
) | |
public | |
view | |
returns (Monetary.Price memory) | |
{ | |
address uniswap = address(UNISWAP); | |
uint256 ethAmt = uniswap.balance; | |
uint256 daiAmt = DAI.balanceOf(uniswap); | |
uint256 uniswapPrice = Math.getPartial(ethUsd.value, ethAmt, daiAmt); | |
return Monetary.Price({ | |
value: uniswapPrice | |
}); | |
} | |
// ============ Helper Functions ============ | |
function getPriceBounds( | |
uint256 oldPrice, | |
uint256 timeDelta | |
) | |
private | |
view | |
returns (uint256, uint256) | |
{ | |
DeviationParams memory deviation = DEVIATION_PARAMS; | |
uint256 maxDeviation = Math.getPartial( | |
oldPrice, | |
Math.min(deviation.maximumAbsolute, timeDelta.mul(deviation.maximumPerSecond)), | |
deviation.denominator | |
); | |
return ( | |
oldPrice.sub(maxDeviation), | |
oldPrice.add(maxDeviation) | |
); | |
} | |
function getMidValue( | |
uint256 valueA, | |
uint256 valueB, | |
uint256 valueC | |
) | |
private | |
pure | |
returns (uint256) | |
{ | |
uint256 maximum = Math.max(valueA, Math.max(valueB, valueC)); | |
if (maximum == valueA) { | |
return Math.max(valueB, valueC); | |
} | |
if (maximum == valueB) { | |
return Math.max(valueA, valueC); | |
} | |
return Math.max(valueA, valueB); | |
} | |
function boundValue( | |
uint256 value, | |
uint256 minimum, | |
uint256 maximum | |
) | |
private | |
pure | |
returns (uint256) | |
{ | |
assert(minimum <= maximum); | |
return Math.max(minimum, Math.min(maximum, value)); | |
} | |
} | |
contract StopLimitOrders is | |
Ownable, | |
OnlySolo, | |
IAutoTrader, | |
ICallee | |
{ | |
using Math for uint256; | |
using SafeMath for uint256; | |
using Types for Types.Par; | |
using Types for Types.Wei; | |
// ============ Constants ============ | |
bytes32 constant private FILE = "StopLimitOrders"; | |
// EIP191 header for EIP712 prefix | |
bytes2 constant private EIP191_HEADER = 0x1901; | |
// EIP712 Domain Name value | |
string constant private EIP712_DOMAIN_NAME = "StopLimitOrders"; | |
// EIP712 Domain Version value | |
string constant private EIP712_DOMAIN_VERSION = "1.1"; | |
// Hash of the EIP712 Domain Separator Schema | |
/* solium-disable-next-line indentation */ | |
bytes32 constant private EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH = keccak256(abi.encodePacked( | |
"EIP712Domain(", | |
"string name,", | |
"string version,", | |
"uint256 chainId,", | |
"address verifyingContract", | |
")" | |
)); | |
// Hash of the EIP712 StopLimitOrder struct | |
/* solium-disable-next-line indentation */ | |
bytes32 constant private EIP712_ORDER_STRUCT_SCHEMA_HASH = keccak256(abi.encodePacked( | |
"StopLimitOrder(", | |
"uint256 makerMarket,", | |
"uint256 takerMarket,", | |
"uint256 makerAmount,", | |
"uint256 takerAmount,", | |
"address makerAccountOwner,", | |
"uint256 makerAccountNumber,", | |
"address takerAccountOwner,", | |
"uint256 takerAccountNumber,", | |
"uint256 triggerPrice,", | |
"bool decreaseOnly,", | |
"uint256 expiration,", | |
"uint256 salt", | |
")" | |
)); | |
// Number of bytes in an Order struct | |
uint256 constant private NUM_ORDER_BYTES = 384; | |
// Number of bytes in a typed signature | |
uint256 constant private NUM_SIGNATURE_BYTES = 66; | |
// Number of bytes in a CallFunctionData struct | |
uint256 constant private NUM_CALLFUNCTIONDATA_BYTES = 32 + NUM_ORDER_BYTES; | |
// The number of decimal places of precision in the price ratio of a triggerPrice | |
uint256 PRICE_BASE = 10 ** 18; | |
// ============ Enums ============ | |
enum OrderStatus { | |
Null, | |
Approved, | |
Canceled | |
} | |
enum CallFunctionType { | |
Approve, | |
Cancel | |
} | |
// ============ Structs ============ | |
struct Order { | |
uint256 makerMarket; | |
uint256 takerMarket; | |
uint256 makerAmount; | |
uint256 takerAmount; | |
address makerAccountOwner; | |
uint256 makerAccountNumber; | |
address takerAccountOwner; | |
uint256 takerAccountNumber; | |
uint256 triggerPrice; | |
bool decreaseOnly; | |
uint256 expiration; | |
uint256 salt; | |
} | |
struct OrderInfo { | |
Order order; | |
bytes32 orderHash; | |
} | |
struct CallFunctionData { | |
CallFunctionType callType; | |
Order order; | |
} | |
struct OrderQueryOutput { | |
OrderStatus orderStatus; | |
uint256 orderMakerFilledAmount; | |
} | |
// ============ Events ============ | |
event ContractStatusSet( | |
bool operational | |
); | |
event LogStopLimitOrderCanceled( | |
bytes32 indexed orderHash, | |
address indexed canceler, | |
uint256 makerMarket, | |
uint256 takerMarket | |
); | |
event LogStopLimitOrderApproved( | |
bytes32 indexed orderHash, | |
address indexed approver, | |
uint256 makerMarket, | |
uint256 takerMarket | |
); | |
event LogStopLimitOrderFilled( | |
bytes32 indexed orderHash, | |
address indexed orderMaker, | |
uint256 makerFillAmount, | |
uint256 totalMakerFilledAmount | |
); | |
// ============ Immutable Storage ============ | |
// Hash of the EIP712 Domain Separator data | |
bytes32 public EIP712_DOMAIN_HASH; | |
// ============ Mutable Storage ============ | |
// true if this contract can process orders | |
bool public g_isOperational; | |
// order hash => filled amount (in makerAmount) | |
mapping (bytes32 => uint256) public g_makerFilledAmount; | |
// order hash => status | |
mapping (bytes32 => OrderStatus) public g_status; | |
// ============ Constructor ============ | |
constructor ( | |
address soloMargin, | |
uint256 chainId | |
) | |
public | |
OnlySolo(soloMargin) | |
{ | |
g_isOperational = true; | |
/* solium-disable-next-line indentation */ | |
EIP712_DOMAIN_HASH = keccak256(abi.encode( | |
EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH, | |
keccak256(bytes(EIP712_DOMAIN_NAME)), | |
keccak256(bytes(EIP712_DOMAIN_VERSION)), | |
chainId, | |
address(this) | |
)); | |
} | |
// ============ Admin Functions ============ | |
/** | |
* The owner can shut down the exchange. | |
*/ | |
function shutDown() | |
external | |
onlyOwner | |
{ | |
g_isOperational = false; | |
emit ContractStatusSet(false); | |
} | |
/** | |
* The owner can start back up the exchange. | |
*/ | |
function startUp() | |
external | |
onlyOwner | |
{ | |
g_isOperational = true; | |
emit ContractStatusSet(true); | |
} | |
// ============ External Functions ============ | |
/** | |
* Cancels an order. Cannot already be canceled. | |
* | |
* @param order The order to cancel | |
*/ | |
function cancelOrder( | |
Order memory order | |
) | |
public | |
{ | |
cancelOrderInternal(msg.sender, order); | |
} | |
/** | |
* Approves an order. Cannot already be approved or canceled. | |
* | |
* @param order The order to approve | |
*/ | |
function approveOrder( | |
Order memory order | |
) | |
public | |
{ | |
approveOrderInternal(msg.sender, order); | |
} | |
// ============ Only-Solo Functions ============ | |
/** | |
* Allows traders to make trades approved by this smart contract. The active trader's account is | |
* the takerAccount and the passive account (for which this contract approves trades | |
* on-behalf-of) is the makerAccount. | |
* | |
* @param inputMarketId The market for which the trader specified the original amount | |
* @param outputMarketId The market for which the trader wants the resulting amount specified | |
* @param makerAccount The account for which this contract is making trades | |
* @param takerAccount The account requesting the trade | |
* @param oldInputPar The par balance of the makerAccount for inputMarketId pre-trade | |
* @param newInputPar The par balance of the makerAccount for inputMarketId post-trade | |
* @param inputWei The change in token amount for the makerAccount for the inputMarketId | |
* @param data Arbitrary data passed in by the trader | |
* @return The AssetAmount for the makerAccount for the outputMarketId | |
*/ | |
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) | |
{ | |
Require.that( | |
g_isOperational, | |
FILE, | |
"Contract is not operational" | |
); | |
OrderInfo memory orderInfo = getOrderAndValidateSignature(data); | |
verifyOrderAndAccountsAndMarkets( | |
orderInfo, | |
makerAccount, | |
takerAccount, | |
inputMarketId, | |
outputMarketId, | |
inputWei | |
); | |
Types.AssetAmount memory assetAmount = getOutputAssetAmount( | |
inputMarketId, | |
outputMarketId, | |
inputWei, | |
orderInfo | |
); | |
if (orderInfo.order.decreaseOnly) { | |
verifyDecreaseOnly( | |
oldInputPar, | |
newInputPar, | |
assetAmount, | |
makerAccount, | |
outputMarketId | |
); | |
} | |
return assetAmount; | |
} | |
/** | |
* Allows users to send this contract arbitrary data. | |
* | |
* param sender (unused) | |
* @param accountInfo The account from which the data is being sent | |
* @param data Arbitrary data given by the sender | |
*/ | |
function callFunction( | |
address /* sender */, | |
Account.Info memory accountInfo, | |
bytes memory data | |
) | |
public | |
onlySolo(msg.sender) | |
{ | |
Require.that( | |
data.length == NUM_CALLFUNCTIONDATA_BYTES, | |
FILE, | |
"Cannot parse CallFunctionData" | |
); | |
CallFunctionData memory cfd = abi.decode(data, (CallFunctionData)); | |
if (cfd.callType == CallFunctionType.Approve) { | |
approveOrderInternal(accountInfo.owner, cfd.order); | |
} else { | |
assert(cfd.callType == CallFunctionType.Cancel); | |
cancelOrderInternal(accountInfo.owner, cfd.order); | |
} | |
} | |
// ============ Getters ============ | |
/** | |
* Returns the status and the filled amount (in makerAmount) of several orders. | |
*/ | |
function getOrderStates( | |
bytes32[] memory orderHashes | |
) | |
public | |
view | |
returns(OrderQueryOutput[] memory) | |
{ | |
uint256 numOrders = orderHashes.length; | |
OrderQueryOutput[] memory output = new OrderQueryOutput[](numOrders); | |
// for each order | |
for (uint256 i = 0; i < numOrders; i++) { | |
bytes32 orderHash = orderHashes[i]; | |
output[i] = OrderQueryOutput({ | |
orderStatus: g_status[orderHash], | |
orderMakerFilledAmount: g_makerFilledAmount[orderHash] | |
}); | |
} | |
return output; | |
} | |
// ============ Private Storage Functions ============ | |
/** | |
* Cancels an order as long as it is not already canceled. | |
*/ | |
function cancelOrderInternal( | |
address canceler, | |
Order memory order | |
) | |
private | |
{ | |
Require.that( | |
canceler == order.makerAccountOwner, | |
FILE, | |
"Canceler must be maker" | |
); | |
bytes32 orderHash = getOrderHash(order); | |
g_status[orderHash] = OrderStatus.Canceled; | |
emit LogStopLimitOrderCanceled( | |
orderHash, | |
canceler, | |
order.makerMarket, | |
order.takerMarket | |
); | |
} | |
/** | |
* Approves an order as long as it is not already approved or canceled. | |
*/ | |
function approveOrderInternal( | |
address approver, | |
Order memory order | |
) | |
private | |
{ | |
Require.that( | |
approver == order.makerAccountOwner, | |
FILE, | |
"Approver must be maker" | |
); | |
bytes32 orderHash = getOrderHash(order); | |
Require.that( | |
g_status[orderHash] != OrderStatus.Canceled, | |
FILE, | |
"Cannot approve canceled order", | |
orderHash | |
); | |
g_status[orderHash] = OrderStatus.Approved; | |
emit LogStopLimitOrderApproved( | |
orderHash, | |
approver, | |
order.makerMarket, | |
order.takerMarket | |
); | |
} | |
// ============ Private Helper Functions ============ | |
/** | |
* Verifies that the order is still fillable for the particular accounts and markets specified. | |
*/ | |
function verifyOrderAndAccountsAndMarkets( | |
OrderInfo memory orderInfo, | |
Account.Info memory makerAccount, | |
Account.Info memory takerAccount, | |
uint256 inputMarketId, | |
uint256 outputMarketId, | |
Types.Wei memory inputWei | |
) | |
private | |
view | |
{ | |
// verify triggerPrice | |
if (orderInfo.order.triggerPrice > 0) { | |
uint256 currentPrice = getCurrentPrice( | |
orderInfo.order.makerMarket, | |
orderInfo.order.takerMarket | |
); | |
Require.that( | |
currentPrice >= orderInfo.order.triggerPrice, | |
FILE, | |
"Order triggerPrice not triggered", | |
currentPrice | |
); | |
} | |
// verify expriy | |
Require.that( | |
orderInfo.order.expiration == 0 || orderInfo.order.expiration >= block.timestamp, | |
FILE, | |
"Order expired", | |
orderInfo.orderHash | |
); | |
// verify maker | |
Require.that( | |
makerAccount.owner == orderInfo.order.makerAccountOwner && | |
makerAccount.number == orderInfo.order.makerAccountNumber, | |
FILE, | |
"Order maker account mismatch", | |
orderInfo.orderHash | |
); | |
// verify taker | |
Require.that( | |
( | |
orderInfo.order.takerAccountOwner == address(0) && | |
orderInfo.order.takerAccountNumber == 0 | |
) || ( | |
orderInfo.order.takerAccountOwner == takerAccount.owner && | |
orderInfo.order.takerAccountNumber == takerAccount.number | |
), | |
FILE, | |
"Order taker account mismatch", | |
orderInfo.orderHash | |
); | |
// verify markets | |
Require.that( | |
( | |
orderInfo.order.makerMarket == outputMarketId && | |
orderInfo.order.takerMarket == inputMarketId | |
) || ( | |
orderInfo.order.takerMarket == outputMarketId && | |
orderInfo.order.makerMarket == inputMarketId | |
), | |
FILE, | |
"Market mismatch", | |
orderInfo.orderHash | |
); | |
// verify inputWei | |
Require.that( | |
!inputWei.isZero(), | |
FILE, | |
"InputWei is zero", | |
orderInfo.orderHash | |
); | |
Require.that( | |
inputWei.sign == (orderInfo.order.takerMarket == inputMarketId), | |
FILE, | |
"InputWei sign mismatch", | |
orderInfo.orderHash | |
); | |
} | |
/** | |
* Verifies that the order is decreasing the size of the maker's position. | |
*/ | |
function verifyDecreaseOnly( | |
Types.Par memory oldInputPar, | |
Types.Par memory newInputPar, | |
Types.AssetAmount memory assetAmount, | |
Account.Info memory makerAccount, | |
uint256 outputMarketId | |
) | |
private | |
view | |
{ | |
// verify that the balance of inputMarketId is not increased | |
Require.that( | |
newInputPar.isZero() | |
|| (newInputPar.value <= oldInputPar.value && newInputPar.sign == oldInputPar.sign), | |
FILE, | |
"inputMarket not decreased" | |
); | |
// verify that the balance of outputMarketId is not increased | |
Types.Wei memory oldOutputWei = SOLO_MARGIN.getAccountWei(makerAccount, outputMarketId); | |
Require.that( | |
assetAmount.value == 0 | |
|| (assetAmount.value <= oldOutputWei.value && assetAmount.sign != oldOutputWei.sign), | |
FILE, | |
"outputMarket not decreased" | |
); | |
} | |
/** | |
* Returns the AssetAmount for the outputMarketId given the order and the inputs. Updates the | |
* filled amount of the order in storage. | |
*/ | |
function getOutputAssetAmount( | |
uint256 inputMarketId, | |
uint256 outputMarketId, | |
Types.Wei memory inputWei, | |
OrderInfo memory orderInfo | |
) | |
private | |
returns (Types.AssetAmount memory) | |
{ | |
uint256 outputAmount; | |
uint256 makerFillAmount; | |
if (orderInfo.order.takerMarket == inputMarketId) { | |
outputAmount = inputWei.value.getPartial( | |
orderInfo.order.makerAmount, | |
orderInfo.order.takerAmount | |
); | |
makerFillAmount = outputAmount; | |
} else { | |
assert(orderInfo.order.takerMarket == outputMarketId); | |
outputAmount = inputWei.value.getPartialRoundUp( | |
orderInfo.order.takerAmount, | |
orderInfo.order.makerAmount | |
); | |
makerFillAmount = inputWei.value; | |
} | |
updateMakerFilledAmount(orderInfo, makerFillAmount); | |
return Types.AssetAmount({ | |
sign: orderInfo.order.takerMarket == outputMarketId, | |
denomination: Types.AssetDenomination.Wei, | |
ref: Types.AssetReference.Delta, | |
value: outputAmount | |
}); | |
} | |
/** | |
* Increases the stored filled amount (in makerAmount) of the order by makerFillAmount. | |
* Returns the new total filled amount (in makerAmount). | |
*/ | |
function updateMakerFilledAmount( | |
OrderInfo memory orderInfo, | |
uint256 makerFillAmount | |
) | |
private | |
{ | |
uint256 oldMakerFilledAmount = g_makerFilledAmount[orderInfo.orderHash]; | |
uint256 totalMakerFilledAmount = oldMakerFilledAmount.add(makerFillAmount); | |
Require.that( | |
totalMakerFilledAmount <= orderInfo.order.makerAmount, | |
FILE, | |
"Cannot overfill order", | |
orderInfo.orderHash, | |
oldMakerFilledAmount, | |
makerFillAmount | |
); | |
g_makerFilledAmount[orderInfo.orderHash] = totalMakerFilledAmount; | |
emit LogStopLimitOrderFilled( | |
orderInfo.orderHash, | |
orderInfo.order.makerAccountOwner, | |
makerFillAmount, | |
totalMakerFilledAmount | |
); | |
} | |
/** | |
* Returns the current price of makerMarket divided by the current price of takerMarket. This | |
* value is multiplied by 10^18. | |
*/ | |
function getCurrentPrice( | |
uint256 makerMarket, | |
uint256 takerMarket | |
) | |
private | |
view | |
returns (uint256) | |
{ | |
Monetary.Price memory takerPrice = SOLO_MARGIN.getMarketPrice(takerMarket); | |
Monetary.Price memory makerPrice = SOLO_MARGIN.getMarketPrice(makerMarket); | |
return takerPrice.value.mul(PRICE_BASE).div(makerPrice.value); | |
} | |
/** | |
* Parses the order, verifies that it is not expired or canceled, and verifies the signature. | |
*/ | |
function getOrderAndValidateSignature( | |
bytes memory data | |
) | |
private | |
view | |
returns (OrderInfo memory) | |
{ | |
Require.that( | |
( | |
data.length == NUM_ORDER_BYTES || | |
data.length == NUM_ORDER_BYTES + NUM_SIGNATURE_BYTES | |
), | |
FILE, | |
"Cannot parse order from data" | |
); | |
OrderInfo memory orderInfo; | |
orderInfo.order = abi.decode(data, (Order)); | |
orderInfo.orderHash = getOrderHash(orderInfo.order); | |
OrderStatus orderStatus = g_status[orderInfo.orderHash]; | |
// verify valid signature or is pre-approved | |
if (orderStatus == OrderStatus.Null) { | |
bytes memory signature = parseSignature(data); | |
address signer = TypedSignature.recover(orderInfo.orderHash, signature); | |
Require.that( | |
orderInfo.order.makerAccountOwner == signer, | |
FILE, | |
"Order invalid signature", | |
orderInfo.orderHash | |
); | |
} else { | |
Require.that( | |
orderStatus != OrderStatus.Canceled, | |
FILE, | |
"Order canceled", | |
orderInfo.orderHash | |
); | |
assert(orderStatus == OrderStatus.Approved); | |
} | |
return orderInfo; | |
} | |
// ============ Private Parsing Functions ============ | |
/** | |
* Returns the EIP712 hash of an order. | |
*/ | |
function getOrderHash( | |
Order memory order | |
) | |
private | |
view | |
returns (bytes32) | |
{ | |
// compute the overall signed struct hash | |
/* solium-disable-next-line indentation */ | |
bytes32 structHash = keccak256(abi.encode( | |
EIP712_ORDER_STRUCT_SCHEMA_HASH, | |
order | |
)); | |
// compute eip712 compliant hash | |
/* solium-disable-next-line indentation */ | |
return keccak256(abi.encodePacked( | |
EIP191_HEADER, | |
EIP712_DOMAIN_HASH, | |
structHash | |
)); | |
} | |
/** | |
* Parses out a signature from call data. | |
*/ | |
function parseSignature( | |
bytes memory data | |
) | |
private | |
pure | |
returns (bytes memory) | |
{ | |
Require.that( | |
data.length == NUM_ORDER_BYTES + NUM_SIGNATURE_BYTES, | |
FILE, | |
"Cannot parse signature from data" | |
); | |
bytes memory signature = new bytes(NUM_SIGNATURE_BYTES); | |
uint256 sigOffset = NUM_ORDER_BYTES; | |
/* solium-disable-next-line security/no-inline-assembly */ | |
assembly { | |
let sigStart := add(data, sigOffset) | |
mstore(add(signature, 0x020), mload(add(sigStart, 0x20))) | |
mstore(add(signature, 0x040), mload(add(sigStart, 0x40))) | |
mstore(add(signature, 0x042), mload(add(sigStart, 0x42))) | |
} | |
return signature; | |
} | |
} | |
contract LiquidatorProxyV1ForSoloMargin is | |
OnlySolo, | |
ReentrancyGuard | |
{ | |
using Math for uint256; | |
using SafeMath for uint256; | |
using Types for Types.Par; | |
using Types for Types.Wei; | |
// ============ Constants ============ | |
bytes32 constant FILE = "LiquidatorProxyV1ForSoloMargin"; | |
// ============ Structs ============ | |
struct Constants { | |
Account.Info fromAccount; | |
Account.Info liquidAccount; | |
Decimal.D256 minLiquidatorRatio; | |
MarketInfo[] markets; | |
} | |
struct MarketInfo { | |
Monetary.Price price; | |
Interest.Index index; | |
} | |
struct Cache { | |
// mutable | |
uint256 toLiquidate; | |
Types.Wei heldWei; | |
Types.Wei owedWei; | |
uint256 supplyValue; | |
uint256 borrowValue; | |
// immutable | |
Decimal.D256 spread; | |
uint256 heldMarket; | |
uint256 owedMarket; | |
uint256 heldPrice; | |
uint256 owedPrice; | |
uint256 owedPriceAdj; | |
} | |
// ============ Constructor ============ | |
constructor ( | |
address soloMargin | |
) | |
public | |
OnlySolo(soloMargin) | |
{} /* solium-disable-line no-empty-blocks */ | |
// ============ Public Functions ============ | |
/** | |
* Liquidate liquidAccount using fromAccount. This contract and the msg.sender to this contract | |
* must both be operators for the fromAccount. | |
* | |
* @param fromAccount The account that will do the liquidating | |
* @param liquidAccount The account that will be liquidated | |
* @param minLiquidatorRatio The minimum collateralization ratio to leave the fromAccount at | |
* @param owedPreferences Ordered list of markets to repay first | |
* @param heldPreferences Ordered list of markets to recieve payout for first | |
*/ | |
function liquidate( | |
Account.Info memory fromAccount, | |
Account.Info memory liquidAccount, | |
Decimal.D256 memory minLiquidatorRatio, | |
uint256 minValueLiquidated, | |
uint256[] memory owedPreferences, | |
uint256[] memory heldPreferences | |
) | |
public | |
nonReentrant | |
{ | |
// put all values that will not change into a single struct | |
Constants memory constants = Constants({ | |
fromAccount: fromAccount, | |
liquidAccount: liquidAccount, | |
minLiquidatorRatio: minLiquidatorRatio, | |
markets: getMarketsInfo() | |
}); | |
// validate the msg.sender and that the liquidAccount can be liquidated | |
checkRequirements(constants); | |
// keep a running tally of how much value will be attempted to be liquidated | |
uint256 totalValueLiquidated = 0; | |
// for each owedMarket | |
for (uint256 owedIndex = 0; owedIndex < owedPreferences.length; owedIndex++) { | |
uint256 owedMarket = owedPreferences[owedIndex]; | |
// for each heldMarket | |
for (uint256 heldIndex = 0; heldIndex < heldPreferences.length; heldIndex++) { | |
uint256 heldMarket = heldPreferences[heldIndex]; | |
// cannot use the same market | |
if (heldMarket == owedMarket) { | |
continue; | |
} | |
// cannot liquidate non-negative markets | |
if (!SOLO_MARGIN.getAccountPar(liquidAccount, owedMarket).isNegative()) { | |
break; | |
} | |
// cannot use non-positive markets as collateral | |
if (!SOLO_MARGIN.getAccountPar(liquidAccount, heldMarket).isPositive()) { | |
continue; | |
} | |
// get all relevant values | |
Cache memory cache = initializeCache( | |
constants, | |
heldMarket, | |
owedMarket | |
); | |
// get the liquidation amount (before liquidator decreases in collateralization) | |
calculateSafeLiquidationAmount(cache); | |
// get the max liquidation amount (before liquidator reaches minLiquidatorRatio) | |
calculateMaxLiquidationAmount(constants, cache); | |
// if nothing to liquidate, do nothing | |
if (cache.toLiquidate == 0) { | |
continue; | |
} | |
// execute the liquidations | |
SOLO_MARGIN.operate( | |
constructAccountsArray(constants), | |
constructActionsArray(cache) | |
); | |
// increment the total value liquidated | |
totalValueLiquidated = | |
totalValueLiquidated.add(cache.toLiquidate.mul(cache.owedPrice)); | |
} | |
} | |
// revert if liquidator account does not have a lot of overhead to liquidate these pairs | |
Require.that( | |
totalValueLiquidated >= minValueLiquidated, | |
FILE, | |
"Not enough liquidatable value", | |
totalValueLiquidated, | |
minValueLiquidated | |
); | |
} | |
// ============ Calculation Functions ============ | |
/** | |
* Calculate the owedAmount that can be liquidated until the liquidator account will be left | |
* with BOTH a non-negative balance of heldMarket AND a non-positive balance of owedMarket. | |
* This is the amount that can be liquidated until the collateralization of the liquidator | |
* account will begin to decrease. | |
*/ | |
function calculateSafeLiquidationAmount( | |
Cache memory cache | |
) | |
private | |
pure | |
{ | |
bool negOwed = !cache.owedWei.isPositive(); | |
bool posHeld = !cache.heldWei.isNegative(); | |
// owedWei is already negative and heldWei is already positive | |
if (negOwed && posHeld) { | |
return; | |
} | |
// true if it takes longer for the liquidator owed balance to become negative than it takes | |
// the liquidator held balance to become positive. | |
bool owedGoesToZeroLast; | |
if (negOwed) { | |
owedGoesToZeroLast = false; | |
} else if (posHeld) { | |
owedGoesToZeroLast = true; | |
} else { | |
// owed is still positive and held is still negative | |
owedGoesToZeroLast = | |
cache.owedWei.value.mul(cache.owedPriceAdj) > | |
cache.heldWei.value.mul(cache.heldPrice); | |
} | |
if (owedGoesToZeroLast) { | |
// calculate the change in heldWei to get owedWei to zero | |
Types.Wei memory heldWeiDelta = Types.Wei({ | |
sign: cache.owedWei.sign, | |
value: cache.owedWei.value.getPartial(cache.owedPriceAdj, cache.heldPrice) | |
}); | |
setCacheWeiValues( | |
cache, | |
cache.heldWei.add(heldWeiDelta), | |
Types.zeroWei() | |
); | |
} else { | |
// calculate the change in owedWei to get heldWei to zero | |
Types.Wei memory owedWeiDelta = Types.Wei({ | |
sign: cache.heldWei.sign, | |
value: cache.heldWei.value.getPartial(cache.heldPrice, cache.owedPriceAdj) | |
}); | |
setCacheWeiValues( | |
cache, | |
Types.zeroWei(), | |
cache.owedWei.add(owedWeiDelta) | |
); | |
} | |
} | |
/** | |
* Calculate the additional owedAmount that can be liquidated until the collateralization of the | |
* liquidator account reaches the minLiquidatorRatio. By this point, the cache will be set such | |
* that the amount of owedMarket is non-positive and the amount of heldMarket is non-negative. | |
*/ | |
function calculateMaxLiquidationAmount( | |
Constants memory constants, | |
Cache memory cache | |
) | |
private | |
pure | |
{ | |
assert(!cache.heldWei.isNegative()); | |
assert(!cache.owedWei.isPositive()); | |
// if the liquidator account is already not above the collateralization requirement, return | |
bool liquidatorAboveCollateralization = isCollateralized( | |
cache.supplyValue, | |
cache.borrowValue, | |
constants.minLiquidatorRatio | |
); | |
if (!liquidatorAboveCollateralization) { | |
cache.toLiquidate = 0; | |
return; | |
} | |
// find the value difference between the current margin and the margin at minLiquidatorRatio | |
uint256 requiredOverhead = Decimal.mul(cache.borrowValue, constants.minLiquidatorRatio); | |
uint256 requiredSupplyValue = cache.borrowValue.add(requiredOverhead); | |
uint256 remainingValueBuffer = cache.supplyValue.sub(requiredSupplyValue); | |
// get the absolute difference between the minLiquidatorRatio and the liquidation spread | |
Decimal.D256 memory spreadMarginDiff = Decimal.D256({ | |
value: constants.minLiquidatorRatio.value.sub(cache.spread.value) | |
}); | |
// get the additional value of owedToken I can borrow to liquidate this position | |
uint256 owedValueToTakeOn = Decimal.div(remainingValueBuffer, spreadMarginDiff); | |
// get the additional amount of owedWei to liquidate | |
uint256 owedWeiToLiquidate = owedValueToTakeOn.div(cache.owedPrice); | |
// store the additional amount in the cache | |
cache.toLiquidate = cache.toLiquidate.add(owedWeiToLiquidate); | |
} | |
// ============ Helper Functions ============ | |
/** | |
* Make some basic checks before attempting to liquidate an account. | |
* - Require that the msg.sender is permissioned to use the liquidator account | |
* - Require that the liquid account is liquidatable | |
*/ | |
function checkRequirements( | |
Constants memory constants | |
) | |
private | |
view | |
{ | |
// check credentials for msg.sender | |
Require.that( | |
constants.fromAccount.owner == msg.sender | |
|| SOLO_MARGIN.getIsLocalOperator(constants.fromAccount.owner, msg.sender), | |
FILE, | |
"Sender not operator", | |
constants.fromAccount.owner | |
); | |
// require that the liquidAccount is liquidatable | |
( | |
Monetary.Value memory liquidSupplyValue, | |
Monetary.Value memory liquidBorrowValue | |
) = getCurrentAccountValues(constants, constants.liquidAccount); | |
Require.that( | |
liquidSupplyValue.value != 0, | |
FILE, | |
"Liquid account no supply" | |
); | |
Require.that( | |
SOLO_MARGIN.getAccountStatus(constants.liquidAccount) == Account.Status.Liquid | |
|| !isCollateralized( | |
liquidSupplyValue.value, | |
liquidBorrowValue.value, | |
SOLO_MARGIN.getMarginRatio() | |
), | |
FILE, | |
"Liquid account not liquidatable", | |
liquidSupplyValue.value, | |
liquidBorrowValue.value | |
); | |
} | |
/** | |
* Changes the cache values to reflect changing the heldWei and owedWei of the liquidator | |
* account. Changes toLiquidate, heldWei, owedWei, supplyValue, and borrowValue. | |
*/ | |
function setCacheWeiValues( | |
Cache memory cache, | |
Types.Wei memory newHeldWei, | |
Types.Wei memory newOwedWei | |
) | |
private | |
pure | |
{ | |
// roll-back the old held value | |
uint256 oldHeldValue = cache.heldWei.value.mul(cache.heldPrice); | |
if (cache.heldWei.sign) { | |
cache.supplyValue = cache.supplyValue.sub(oldHeldValue); | |
} else { | |
cache.borrowValue = cache.borrowValue.sub(oldHeldValue); | |
} | |
// add the new held value | |
uint256 newHeldValue = newHeldWei.value.mul(cache.heldPrice); | |
if (newHeldWei.sign) { | |
cache.supplyValue = cache.supplyValue.add(newHeldValue); | |
} else { | |
cache.borrowValue = cache.borrowValue.add(newHeldValue); | |
} | |
// roll-back the old owed value | |
uint256 oldOwedValue = cache.owedWei.value.mul(cache.owedPrice); | |
if (cache.owedWei.sign) { | |
cache.supplyValue = cache.supplyValue.sub(oldOwedValue); | |
} else { | |
cache.borrowValue = cache.borrowValue.sub(oldOwedValue); | |
} | |
// add the new owed value | |
uint256 newOwedValue = newOwedWei.value.mul(cache.owedPrice); | |
if (newOwedWei.sign) { | |
cache.supplyValue = cache.supplyValue.add(newOwedValue); | |
} else { | |
cache.borrowValue = cache.borrowValue.add(newOwedValue); | |
} | |
// update toLiquidate, heldWei, and owedWei | |
Types.Wei memory delta = cache.owedWei.sub(newOwedWei); | |
assert(!delta.isNegative()); | |
cache.toLiquidate = cache.toLiquidate.add(delta.value); | |
cache.heldWei = newHeldWei; | |
cache.owedWei = newOwedWei; | |
} | |
/** | |
* Returns true if the supplyValue over-collateralizes the borrowValue by the ratio. | |
*/ | |
function isCollateralized( | |
uint256 supplyValue, | |
uint256 borrowValue, | |
Decimal.D256 memory ratio | |
) | |
private | |
pure | |
returns(bool) | |
{ | |
uint256 requiredMargin = Decimal.mul(borrowValue, ratio); | |
return supplyValue >= borrowValue.add(requiredMargin); | |
} | |
// ============ Getter Functions ============ | |
/** | |
* Gets the current total supplyValue and borrowValue for some account. Takes into account what | |
* the current index will be once updated. | |
*/ | |
function getCurrentAccountValues( | |
Constants memory constants, | |
Account.Info memory account | |
) | |
private | |
view | |
returns ( | |
Monetary.Value memory, | |
Monetary.Value memory | |
) | |
{ | |
Monetary.Value memory supplyValue; | |
Monetary.Value memory borrowValue; | |
for (uint256 m = 0; m < constants.markets.length; m++) { | |
Types.Par memory par = SOLO_MARGIN.getAccountPar(account, m); | |
if (par.isZero()) { | |
continue; | |
} | |
Types.Wei memory userWei = Interest.parToWei(par, constants.markets[m].index); | |
uint256 assetValue = userWei.value.mul(constants.markets[m].price.value); | |
if (userWei.sign) { | |
supplyValue.value = supplyValue.value.add(assetValue); | |
} else { | |
borrowValue.value = borrowValue.value.add(assetValue); | |
} | |
} | |
return (supplyValue, borrowValue); | |
} | |
/** | |
* Get the updated index and price for every market. | |
*/ | |
function getMarketsInfo() | |
private | |
view | |
returns (MarketInfo[] memory) | |
{ | |
uint256 numMarkets = SOLO_MARGIN.getNumMarkets(); | |
MarketInfo[] memory markets = new MarketInfo[](numMarkets); | |
for (uint256 m = 0; m < numMarkets; m++) { | |
markets[m] = MarketInfo({ | |
price: SOLO_MARGIN.getMarketPrice(m), | |
index: SOLO_MARGIN.getMarketCurrentIndex(m) | |
}); | |
} | |
return markets; | |
} | |
/** | |
* Pre-populates cache values for some pair of markets. | |
*/ | |
function initializeCache( | |
Constants memory constants, | |
uint256 heldMarket, | |
uint256 owedMarket | |
) | |
private | |
view | |
returns (Cache memory) | |
{ | |
( | |
Monetary.Value memory supplyValue, | |
Monetary.Value memory borrowValue | |
) = getCurrentAccountValues(constants, constants.fromAccount); | |
uint256 heldPrice = constants.markets[heldMarket].price.value; | |
uint256 owedPrice = constants.markets[owedMarket].price.value; | |
Decimal.D256 memory spread = | |
SOLO_MARGIN.getLiquidationSpreadForPair(heldMarket, owedMarket); | |
return Cache({ | |
heldWei: Interest.parToWei( | |
SOLO_MARGIN.getAccountPar(constants.fromAccount, heldMarket), | |
constants.markets[heldMarket].index | |
), | |
owedWei: Interest.parToWei( | |
SOLO_MARGIN.getAccountPar(constants.fromAccount, owedMarket), | |
constants.markets[owedMarket].index | |
), | |
toLiquidate: 0, | |
supplyValue: supplyValue.value, | |
borrowValue: borrowValue.value, | |
heldMarket: heldMarket, | |
owedMarket: owedMarket, | |
spread: spread, | |
heldPrice: heldPrice, | |
owedPrice: owedPrice, | |
owedPriceAdj: Decimal.mul(owedPrice, Decimal.onePlus(spread)) | |
}); | |
} | |
// ============ Operation-Construction Functions ============ | |
function constructAccountsArray( | |
Constants memory constants | |
) | |
private | |
pure | |
returns (Account.Info[] memory) | |
{ | |
Account.Info[] memory accounts = new Account.Info[](2); | |
accounts[0] = constants.fromAccount; | |
accounts[1] = constants.liquidAccount; | |
return accounts; | |
} | |
function constructActionsArray( | |
Cache memory cache | |
) | |
private | |
pure | |
returns (Actions.ActionArgs[] memory) | |
{ | |
Actions.ActionArgs[] memory actions = new Actions.ActionArgs[](1); | |
actions[0] = Actions.ActionArgs({ | |
actionType: Actions.ActionType.Liquidate, | |
accountId: 0, | |
amount: Types.AssetAmount({ | |
sign: true, | |
denomination: Types.AssetDenomination.Wei, | |
ref: Types.AssetReference.Delta, | |
value: cache.toLiquidate | |
}), | |
primaryMarketId: cache.owedMarket, | |
secondaryMarketId: cache.heldMarket, | |
otherAddress: address(0), | |
otherAccountId: 1, | |
data: new bytes(0) | |
}); | |
return actions; | |
} | |
} | |
contract PolynomialInterestSetter is | |
IInterestSetter | |
{ | |
using Math for uint256; | |
using SafeMath for uint256; | |
// ============ Constants ============ | |
uint256 constant PERCENT = 100; | |
uint256 constant BASE = 10 ** 18; | |
uint256 constant SECONDS_IN_A_YEAR = 60 * 60 * 24 * 365; | |
uint256 constant BYTE = 8; | |
// ============ Structs ============ | |
struct PolyStorage { | |
uint128 maxAPR; | |
uint128 coefficients; | |
} | |
// ============ Storage ============ | |
PolyStorage g_storage; | |
// ============ Constructor ============ | |
constructor( | |
PolyStorage memory params | |
) | |
public | |
{ | |
// verify that all coefficients add up to 100% | |
uint256 sumOfCoefficients = 0; | |
for ( | |
uint256 coefficients = params.coefficients; | |
coefficients != 0; | |
coefficients >>= BYTE | |
) { | |
sumOfCoefficients += coefficients % 256; | |
} | |
require( | |
sumOfCoefficients == PERCENT, | |
"Coefficients must sum to 100" | |
); | |
// store the params | |
g_storage = params; | |
} | |
// ============ Public Functions ============ | |
/** | |
* Get the interest rate given some borrowed and supplied amounts. The interest function is a | |
* polynomial function of the utilization (borrowWei / supplyWei) of the market. | |
* | |
* - If borrowWei > supplyWei then the utilization is considered to be equal to 1. | |
* - If both are zero, then the utilization is considered to be equal to 0. | |
* | |
* @return The interest rate per second (times 10 ** 18) | |
*/ | |
function getInterestRate( | |
address /* token */, | |
uint256 borrowWei, | |
uint256 supplyWei | |
) | |
external | |
view | |
returns (Interest.Rate memory) | |
{ | |
if (borrowWei == 0) { | |
return Interest.Rate({ | |
value: 0 | |
}); | |
} | |
PolyStorage memory s = g_storage; | |
uint256 maxAPR = s.maxAPR; | |
if (borrowWei >= supplyWei) { | |
return Interest.Rate({ | |
value: maxAPR / SECONDS_IN_A_YEAR | |
}); | |
} | |
uint256 result = 0; | |
uint256 polynomial = BASE; | |
// for each non-zero coefficient... | |
uint256 coefficients = s.coefficients; | |
while (true) { | |
// gets the lowest-order byte | |
uint256 coefficient = coefficients % 256; | |
// if non-zero, add to result | |
if (coefficient != 0) { | |
// no safeAdd since there are at most 16 coefficients | |
// no safeMul since (coefficient < 256 && polynomial <= 10**18) | |
result += coefficient * polynomial; | |
// break if this is the last non-zero coefficient | |
if (coefficient == coefficients) { | |
break; | |
} | |
} | |
// increase the order of the polynomial term | |
// no safeDiv since supplyWei must be stricly larger than borrowWei | |
polynomial = polynomial.mul(borrowWei) / supplyWei; | |
// move to next coefficient | |
coefficients >>= BYTE; | |
} | |
// normalize the result | |
// no safeMul since result fits within 72 bits and maxAPR fits within 128 bits | |
// no safeDiv since the divisor is a non-zero constant | |
return Interest.Rate({ | |
value: result * maxAPR / (SECONDS_IN_A_YEAR * BASE * PERCENT) | |
}); | |
} | |
/** | |
* Get the maximum APR that this interestSetter will return. The actual APY may be higher | |
* depending on how often the interest is compounded. | |
* | |
* @return The maximum APR | |
*/ | |
function getMaxAPR() | |
external | |
view | |
returns (uint256) | |
{ | |
return g_storage.maxAPR; | |
} | |
/** | |
* Get all of the coefficients of the interest calculation, starting from the coefficient for | |
* the first-order utilization variable. | |
* | |
* @return The coefficients | |
*/ | |
function getCoefficients() | |
external | |
view | |
returns (uint256[] memory) | |
{ | |
// allocate new array with maximum of 16 coefficients | |
uint256[] memory result = new uint256[](16); | |
// add the coefficients to the array | |
uint256 numCoefficients = 0; | |
for ( | |
uint256 coefficients = g_storage.coefficients; | |
coefficients != 0; | |
coefficients >>= BYTE | |
) { | |
result[numCoefficients] = coefficients % 256; | |
numCoefficients++; | |
} | |
// modify result.length to match numCoefficients | |
/* solium-disable-next-line security/no-inline-assembly */ | |
assembly { | |
mstore(result, numCoefficients) | |
} | |
return result; | |
} | |
} | |
contract Expiry 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; | |
} | |
// ============ Admin Functions ============ | |
function ownerSetExpiryRampTime( | |
uint256 newExpiryRampTime | |
) | |
external | |
onlyOwner | |
{ | |
emit LogExpiryRampTimeSet(newExpiryRampTime); | |
g_expiryRampTime = newExpiryRampTime; | |
} | |
// ============ 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 | |
); | |
} | |
// ============ 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); | |
} | |
// ============ 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) | |
); | |
} | |
} | |
contract DoubleExponentInterestSetter is | |
IInterestSetter | |
{ | |
using Math for uint256; | |
using SafeMath for uint256; | |
// ============ Constants ============ | |
uint256 constant PERCENT = 100; | |
uint256 constant BASE = 10 ** 18; | |
uint256 constant SECONDS_IN_A_YEAR = 60 * 60 * 24 * 365; | |
uint256 constant BYTE = 8; | |
// ============ Structs ============ | |
struct PolyStorage { | |
uint128 maxAPR; | |
uint128 coefficients; | |
} | |
// ============ Storage ============ | |
PolyStorage g_storage; | |
// ============ Constructor ============ | |
constructor( | |
PolyStorage memory params | |
) | |
public | |
{ | |
// verify that all coefficients add up to 100% | |
uint256 sumOfCoefficients = 0; | |
for ( | |
uint256 coefficients = params.coefficients; | |
coefficients != 0; | |
coefficients >>= BYTE | |
) { | |
sumOfCoefficients += coefficients % 256; | |
} | |
require( | |
sumOfCoefficients == PERCENT, | |
"Coefficients must sum to 100" | |
); | |
// store the params | |
g_storage = params; | |
} | |
// ============ Public Functions ============ | |
/** | |
* Get the interest rate given some borrowed and supplied amounts. The interest function is a | |
* polynomial function of the utilization (borrowWei / supplyWei) of the market. | |
* | |
* - If borrowWei > supplyWei then the utilization is considered to be equal to 1. | |
* - If both are zero, then the utilization is considered to be equal to 0. | |
* | |
* @return The interest rate per second (times 10 ** 18) | |
*/ | |
function getInterestRate( | |
address /* token */, | |
uint256 borrowWei, | |
uint256 supplyWei | |
) | |
external | |
view | |
returns (Interest.Rate memory) | |
{ | |
if (borrowWei == 0) { | |
return Interest.Rate({ | |
value: 0 | |
}); | |
} | |
PolyStorage memory s = g_storage; | |
uint256 maxAPR = s.maxAPR; | |
if (borrowWei >= supplyWei) { | |
return Interest.Rate({ | |
value: maxAPR / SECONDS_IN_A_YEAR | |
}); | |
} | |
// process the first coefficient | |
uint256 coefficients = s.coefficients; | |
uint256 result = uint8(coefficients) * BASE; | |
coefficients >>= BYTE; | |
// initialize polynomial as the utilization | |
// no safeDiv since supplyWei must be non-zero at this point | |
uint256 polynomial = BASE.mul(borrowWei) / supplyWei; | |
// for each non-zero coefficient... | |
while (true) { | |
// gets the lowest-order byte | |
uint256 coefficient = uint256(uint8(coefficients)); | |
// if non-zero, add to result | |
if (coefficient != 0) { | |
// no safeAdd since there are at most 16 coefficients | |
// no safeMul since (coefficient < 256 && polynomial <= 10**18) | |
result += coefficient * polynomial; | |
// break if this is the last non-zero coefficient | |
if (coefficient == coefficients) { | |
break; | |
} | |
} | |
// double the order of the polynomial term | |
// no safeMul since polynomial <= 10^18 | |
// no safeDiv since the divisor is a non-zero constant | |
polynomial = polynomial * polynomial / BASE; | |
// move to next coefficient | |
coefficients >>= BYTE; | |
} | |
// normalize the result | |
// no safeMul since result fits within 72 bits and maxAPR fits within 128 bits | |
// no safeDiv since the divisor is a non-zero constant | |
return Interest.Rate({ | |
value: result * maxAPR / (SECONDS_IN_A_YEAR * BASE * PERCENT) | |
}); | |
} | |
/** | |
* Get the maximum APR that this interestSetter will return. The actual APY may be higher | |
* depending on how often the interest is compounded. | |
* | |
* @return The maximum APR | |
*/ | |
function getMaxAPR() | |
external | |
view | |
returns (uint256) | |
{ | |
return g_storage.maxAPR; | |
} | |
/** | |
* Get all of the coefficients of the interest calculation, starting from the coefficient for | |
* the first-order utilization variable. | |
* | |
* @return The coefficients | |
*/ | |
function getCoefficients() | |
external | |
view | |
returns (uint256[] memory) | |
{ | |
// allocate new array with maximum of 16 coefficients | |
uint256[] memory result = new uint256[](16); | |
// add the coefficients to the array | |
uint256 numCoefficients = 0; | |
for ( | |
uint256 coefficients = g_storage.coefficients; | |
coefficients != 0; | |
coefficients >>= BYTE | |
) { | |
result[numCoefficients] = coefficients % 256; | |
numCoefficients++; | |
} | |
// modify result.length to match numCoefficients | |
/* solium-disable-next-line security/no-inline-assembly */ | |
assembly { | |
mstore(result, numCoefficients) | |
} | |
return result; | |
} | |
} | |
contract ErroringOmiseToken is OmiseToken { | |
function transfer(address, uint256) public { | |
require(false); | |
} | |
function transferFrom(address, address, uint256) public { | |
require(false); | |
} | |
function approve(address, uint256) public { | |
require(false); | |
} | |
} | |
contract LimitOrders is | |
Ownable, | |
OnlySolo, | |
IAutoTrader, | |
ICallee | |
{ | |
using Math for uint256; | |
using SafeMath for uint256; | |
using Types for Types.Par; | |
using Types for Types.Wei; | |
// ============ Constants ============ | |
bytes32 constant private FILE = "LimitOrders"; | |
// EIP191 header for EIP712 prefix | |
bytes2 constant private EIP191_HEADER = 0x1901; | |
// EIP712 Domain Name value | |
string constant private EIP712_DOMAIN_NAME = "LimitOrders"; | |
// EIP712 Domain Version value | |
string constant private EIP712_DOMAIN_VERSION = "1.1"; | |
// Hash of the EIP712 Domain Separator Schema | |
/* solium-disable-next-line indentation */ | |
bytes32 constant private EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH = keccak256(abi.encodePacked( | |
"EIP712Domain(", | |
"string name,", | |
"string version,", | |
"uint256 chainId,", | |
"address verifyingContract", | |
")" | |
)); | |
// Hash of the EIP712 LimitOrder struct | |
/* solium-disable-next-line indentation */ | |
bytes32 constant private EIP712_LIMIT_ORDER_STRUCT_SCHEMA_HASH = keccak256(abi.encodePacked( | |
"LimitOrder(", | |
"uint256 makerMarket,", | |
"uint256 takerMarket,", | |
"uint256 makerAmount,", | |
"uint256 takerAmount,", | |
"address makerAccountOwner,", | |
"uint256 makerAccountNumber,", | |
"address takerAccountOwner,", | |
"uint256 takerAccountNumber,", | |
"uint256 expiration,", | |
"uint256 salt", | |
")" | |
)); | |
// Number of bytes in an Order struct | |
uint256 constant private NUM_ORDER_BYTES = 320; | |
// Number of bytes in a typed signature | |
uint256 constant private NUM_SIGNATURE_BYTES = 66; | |
// Number of bytes in a CallFunctionData struct | |
uint256 constant private NUM_CALLFUNCTIONDATA_BYTES = 32 + NUM_ORDER_BYTES; | |
// ============ Enums ============ | |
enum OrderStatus { | |
Null, | |
Approved, | |
Canceled | |
} | |
enum CallFunctionType { | |
Approve, | |
Cancel | |
} | |
// ============ Structs ============ | |
struct Order { | |
uint256 makerMarket; | |
uint256 takerMarket; | |
uint256 makerAmount; | |
uint256 takerAmount; | |
address makerAccountOwner; | |
uint256 makerAccountNumber; | |
address takerAccountOwner; | |
uint256 takerAccountNumber; | |
uint256 expiration; | |
uint256 salt; | |
} | |
struct OrderInfo { | |
Order order; | |
bytes32 orderHash; | |
} | |
struct CallFunctionData { | |
CallFunctionType callType; | |
Order order; | |
} | |
struct OrderQueryOutput { | |
OrderStatus orderStatus; | |
uint256 orderMakerFilledAmount; | |
} | |
// ============ Events ============ | |
event ContractStatusSet( | |
bool operational | |
); | |
event LogLimitOrderCanceled( | |
bytes32 indexed orderHash, | |
address indexed canceler, | |
uint256 makerMarket, | |
uint256 takerMarket | |
); | |
event LogLimitOrderApproved( | |
bytes32 indexed orderHash, | |
address indexed approver, | |
uint256 makerMarket, | |
uint256 takerMarket | |
); | |
event LogLimitOrderFilled( | |
bytes32 indexed orderHash, | |
address indexed orderMaker, | |
uint256 makerFillAmount, | |
uint256 totalMakerFilledAmount | |
); | |
// ============ Immutable Storage ============ | |
// Hash of the EIP712 Domain Separator data | |
bytes32 public EIP712_DOMAIN_HASH; | |
// ============ Mutable Storage ============ | |
// true if this contract can process orders | |
bool public g_isOperational; | |
// order hash => filled amount (in makerAmount) | |
mapping (bytes32 => uint256) public g_makerFilledAmount; | |
// order hash => status | |
mapping (bytes32 => OrderStatus) public g_status; | |
// ============ Constructor ============ | |
constructor ( | |
address soloMargin, | |
uint256 chainId | |
) | |
public | |
OnlySolo(soloMargin) | |
{ | |
g_isOperational = true; | |
/* solium-disable-next-line indentation */ | |
EIP712_DOMAIN_HASH = keccak256(abi.encode( | |
EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH, | |
keccak256(bytes(EIP712_DOMAIN_NAME)), | |
keccak256(bytes(EIP712_DOMAIN_VERSION)), | |
chainId, | |
address(this) | |
)); | |
} | |
// ============ Admin Functions ============ | |
/** | |
* The owner can shut down the exchange. | |
*/ | |
function shutDown() | |
external | |
onlyOwner | |
{ | |
g_isOperational = false; | |
emit ContractStatusSet(false); | |
} | |
/** | |
* The owner can start back up the exchange. | |
*/ | |
function startUp() | |
external | |
onlyOwner | |
{ | |
g_isOperational = true; | |
emit ContractStatusSet(true); | |
} | |
// ============ External Functions ============ | |
/** | |
* Cancels an order. Cannot already be canceled. | |
* | |
* @param order The order to cancel | |
*/ | |
function cancelOrder( | |
Order memory order | |
) | |
public | |
{ | |
cancelOrderInternal(msg.sender, order); | |
} | |
/** | |
* Approves an order. Cannot already be approved or canceled. | |
* | |
* @param order The order to approve | |
*/ | |
function approveOrder( | |
Order memory order | |
) | |
public | |
{ | |
approveOrderInternal(msg.sender, order); | |
} | |
// ============ Only-Solo Functions ============ | |
/** | |
* Allows traders to make trades approved by this smart contract. The active trader's account is | |
* the takerAccount and the passive account (for which this contract approves trades | |
* on-behalf-of) is the makerAccount. | |
* | |
* @param inputMarketId The market for which the trader specified the original amount | |
* @param outputMarketId The market for which the trader wants the resulting amount specified | |
* @param makerAccount The account for which this contract is making trades | |
* @param takerAccount The account requesting the trade | |
* param oldInputPar (unused) | |
* param newInputPar (unused) | |
* @param inputWei The change in token amount for the makerAccount for the inputMarketId | |
* @param data Arbitrary data passed in by the trader | |
* @return The AssetAmount for the makerAccount for the outputMarketId | |
*/ | |
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) | |
{ | |
Require.that( | |
g_isOperational, | |
FILE, | |
"Contract is not operational" | |
); | |
OrderInfo memory orderInfo = getOrderAndValidateSignature(data); | |
verifyOrderAndAccountsAndMarkets( | |
orderInfo, | |
makerAccount, | |
takerAccount, | |
inputMarketId, | |
outputMarketId, | |
inputWei | |
); | |
return getOutputAssetAmount( | |
inputMarketId, | |
outputMarketId, | |
inputWei, | |
orderInfo | |
); | |
} | |
/** | |
* Allows users to send this contract arbitrary data. | |
* | |
* param sender (unused) | |
* @param accountInfo The account from which the data is being sent | |
* @param data Arbitrary data given by the sender | |
*/ | |
function callFunction( | |
address /* sender */, | |
Account.Info memory accountInfo, | |
bytes memory data | |
) | |
public | |
onlySolo(msg.sender) | |
{ | |
Require.that( | |
data.length == NUM_CALLFUNCTIONDATA_BYTES, | |
FILE, | |
"Cannot parse CallFunctionData" | |
); | |
CallFunctionData memory cfd = abi.decode(data, (CallFunctionData)); | |
if (cfd.callType == CallFunctionType.Approve) { | |
approveOrderInternal(accountInfo.owner, cfd.order); | |
} else { | |
assert(cfd.callType == CallFunctionType.Cancel); | |
cancelOrderInternal(accountInfo.owner, cfd.order); | |
} | |
} | |
// ============ Getters ============ | |
/** | |
* Returns the status and the filled amount (in makerAmount) of several orders. | |
*/ | |
function getOrderStates( | |
bytes32[] memory orderHashes | |
) | |
public | |
view | |
returns(OrderQueryOutput[] memory) | |
{ | |
uint256 numOrders = orderHashes.length; | |
OrderQueryOutput[] memory output = new OrderQueryOutput[](numOrders); | |
// for each order | |
for (uint256 i = 0; i < numOrders; i++) { | |
bytes32 orderHash = orderHashes[i]; | |
output[i] = OrderQueryOutput({ | |
orderStatus: g_status[orderHash], | |
orderMakerFilledAmount: g_makerFilledAmount[orderHash] | |
}); | |
} | |
return output; | |
} | |
// ============ Private Storage Functions ============ | |
/** | |
* Cancels an order as long as it is not already canceled. | |
*/ | |
function cancelOrderInternal( | |
address canceler, | |
Order memory order | |
) | |
private | |
{ | |
Require.that( | |
canceler == order.makerAccountOwner, | |
FILE, | |
"Canceler must be maker" | |
); | |
bytes32 orderHash = getOrderHash(order); | |
g_status[orderHash] = OrderStatus.Canceled; | |
emit LogLimitOrderCanceled( | |
orderHash, | |
canceler, | |
order.makerMarket, | |
order.takerMarket | |
); | |
} | |
/** | |
* Approves an order as long as it is not already approved or canceled. | |
*/ | |
function approveOrderInternal( | |
address approver, | |
Order memory order | |
) | |
private | |
{ | |
Require.that( | |
approver == order.makerAccountOwner, | |
FILE, | |
"Approver must be maker" | |
); | |
bytes32 orderHash = getOrderHash(order); | |
Require.that( | |
g_status[orderHash] != OrderStatus.Canceled, | |
FILE, | |
"Cannot approve canceled order", | |
orderHash | |
); | |
g_status[orderHash] = OrderStatus.Approved; | |
emit LogLimitOrderApproved( | |
orderHash, | |
approver, | |
order.makerMarket, | |
order.takerMarket | |
); | |
} | |
// ============ Private Helper Functions ============ | |
/** | |
* Verifies that the order is still fillable for the particular accounts and markets specified. | |
*/ | |
function verifyOrderAndAccountsAndMarkets( | |
OrderInfo memory orderInfo, | |
Account.Info memory makerAccount, | |
Account.Info memory takerAccount, | |
uint256 inputMarketId, | |
uint256 outputMarketId, | |
Types.Wei memory inputWei | |
) | |
private | |
view | |
{ | |
// verify expriy | |
Require.that( | |
orderInfo.order.expiration == 0 || orderInfo.order.expiration >= block.timestamp, | |
FILE, | |
"Order expired", | |
orderInfo.orderHash | |
); | |
// verify maker | |
Require.that( | |
makerAccount.owner == orderInfo.order.makerAccountOwner && | |
makerAccount.number == orderInfo.order.makerAccountNumber, | |
FILE, | |
"Order maker account mismatch", | |
orderInfo.orderHash | |
); | |
// verify taker | |
Require.that( | |
( | |
orderInfo.order.takerAccountOwner == address(0) && | |
orderInfo.order.takerAccountNumber == 0 | |
) || ( | |
orderInfo.order.takerAccountOwner == takerAccount.owner && | |
orderInfo.order.takerAccountNumber == takerAccount.number | |
), | |
FILE, | |
"Order taker account mismatch", | |
orderInfo.orderHash | |
); | |
// verify markets | |
Require.that( | |
( | |
orderInfo.order.makerMarket == outputMarketId && | |
orderInfo.order.takerMarket == inputMarketId | |
) || ( | |
orderInfo.order.takerMarket == outputMarketId && | |
orderInfo.order.makerMarket == inputMarketId | |
), | |
FILE, | |
"Market mismatch", | |
orderInfo.orderHash | |
); | |
// verify inputWei | |
Require.that( | |
!inputWei.isZero(), | |
FILE, | |
"InputWei is zero", | |
orderInfo.orderHash | |
); | |
Require.that( | |
inputWei.sign == (orderInfo.order.takerMarket == inputMarketId), | |
FILE, | |
"InputWei sign mismatch", | |
orderInfo.orderHash | |
); | |
} | |
/** | |
* Returns the AssetAmount for the outputMarketId given the order and the inputs. Updates the | |
* filled amount of the order in storage. | |
*/ | |
function getOutputAssetAmount( | |
uint256 inputMarketId, | |
uint256 outputMarketId, | |
Types.Wei memory inputWei, | |
OrderInfo memory orderInfo | |
) | |
private | |
returns (Types.AssetAmount memory) | |
{ | |
uint256 outputAmount; | |
uint256 makerFillAmount; | |
if (orderInfo.order.takerMarket == inputMarketId) { | |
outputAmount = inputWei.value.getPartial( | |
orderInfo.order.makerAmount, | |
orderInfo.order.takerAmount | |
); | |
makerFillAmount = outputAmount; | |
} else { | |
assert(orderInfo.order.takerMarket == outputMarketId); | |
outputAmount = inputWei.value.getPartialRoundUp( | |
orderInfo.order.takerAmount, | |
orderInfo.order.makerAmount | |
); | |
makerFillAmount = inputWei.value; | |
} | |
updateMakerFilledAmount(orderInfo, makerFillAmount); | |
return Types.AssetAmount({ | |
sign: orderInfo.order.takerMarket == outputMarketId, | |
denomination: Types.AssetDenomination.Wei, | |
ref: Types.AssetReference.Delta, | |
value: outputAmount | |
}); | |
} | |
/** | |
* Increases the stored filled amount (in makerAmount) of the order by makerFillAmount. | |
* Returns the new total filled amount (in makerAmount). | |
*/ | |
function updateMakerFilledAmount( | |
OrderInfo memory orderInfo, | |
uint256 makerFillAmount | |
) | |
private | |
{ | |
uint256 oldMakerFilledAmount = g_makerFilledAmount[orderInfo.orderHash]; | |
uint256 totalMakerFilledAmount = oldMakerFilledAmount.add(makerFillAmount); | |
Require.that( | |
totalMakerFilledAmount <= orderInfo.order.makerAmount, | |
FILE, | |
"Cannot overfill order", | |
orderInfo.orderHash, | |
oldMakerFilledAmount, | |
makerFillAmount | |
); | |
g_makerFilledAmount[orderInfo.orderHash] = totalMakerFilledAmount; | |
emit LogLimitOrderFilled( | |
orderInfo.orderHash, | |
orderInfo.order.makerAccountOwner, | |
makerFillAmount, | |
totalMakerFilledAmount | |
); | |
} | |
/** | |
* Parses the order, verifies that it is not expired or canceled, and verifies the signature. | |
*/ | |
function getOrderAndValidateSignature( | |
bytes memory data | |
) | |
private | |
view | |
returns (OrderInfo memory) | |
{ | |
Require.that( | |
( | |
data.length == NUM_ORDER_BYTES || | |
data.length == NUM_ORDER_BYTES + NUM_SIGNATURE_BYTES | |
), | |
FILE, | |
"Cannot parse order from data" | |
); | |
OrderInfo memory orderInfo; | |
orderInfo.order = abi.decode(data, (Order)); | |
orderInfo.orderHash = getOrderHash(orderInfo.order); | |
OrderStatus orderStatus = g_status[orderInfo.orderHash]; | |
// verify valid signature or is pre-approved | |
if (orderStatus == OrderStatus.Null) { | |
bytes memory signature = parseSignature(data); | |
address signer = TypedSignature.recover(orderInfo.orderHash, signature); | |
Require.that( | |
orderInfo.order.makerAccountOwner == signer, | |
FILE, | |
"Order invalid signature", | |
orderInfo.orderHash | |
); | |
} else { | |
Require.that( | |
orderStatus != OrderStatus.Canceled, | |
FILE, | |
"Order canceled", | |
orderInfo.orderHash | |
); | |
assert(orderStatus == OrderStatus.Approved); | |
} | |
return orderInfo; | |
} | |
// ============ Private Parsing Functions ============ | |
/** | |
* Returns the EIP712 hash of an order. | |
*/ | |
function getOrderHash( | |
Order memory order | |
) | |
private | |
view | |
returns (bytes32) | |
{ | |
// compute the overall signed struct hash | |
/* solium-disable-next-line indentation */ | |
bytes32 structHash = keccak256(abi.encode( | |
EIP712_LIMIT_ORDER_STRUCT_SCHEMA_HASH, | |
order | |
)); | |
// compute eip712 compliant hash | |
/* solium-disable-next-line indentation */ | |
return keccak256(abi.encodePacked( | |
EIP191_HEADER, | |
EIP712_DOMAIN_HASH, | |
structHash | |
)); | |
} | |
/** | |
* Parses out a signature from call data. | |
*/ | |
function parseSignature( | |
bytes memory data | |
) | |
private | |
pure | |
returns (bytes memory) | |
{ | |
Require.that( | |
data.length == NUM_ORDER_BYTES + NUM_SIGNATURE_BYTES, | |
FILE, | |
"Cannot parse signature from data" | |
); | |
bytes memory signature = new bytes(NUM_SIGNATURE_BYTES); | |
uint256 sigOffset = NUM_ORDER_BYTES; | |
/* solium-disable-next-line security/no-inline-assembly */ | |
assembly { | |
let sigStart := add(data, sigOffset) | |
mstore(add(signature, 0x020), mload(add(sigStart, 0x20))) | |
mstore(add(signature, 0x040), mload(add(sigStart, 0x40))) | |
mstore(add(signature, 0x042), mload(add(sigStart, 0x42))) | |
} | |
return signature; | |
} | |
} | |
contract TestInterestSetter is | |
IInterestSetter | |
{ | |
mapping (address => Interest.Rate) public g_interestRates; | |
function setInterestRate( | |
address token, | |
Interest.Rate memory rate | |
) | |
public | |
{ | |
g_interestRates[token] = rate; | |
} | |
function getInterestRate( | |
address token, | |
uint256 /* borrowWei */, | |
uint256 /* supplyWei */ | |
) | |
public | |
view | |
returns (Interest.Rate memory) | |
{ | |
return g_interestRates[token]; | |
} | |
} | |
contract CanonicalOrders is | |
Ownable, | |
OnlySolo, | |
IAutoTrader, | |
ICallee | |
{ | |
using Math for uint256; | |
using SafeMath for uint256; | |
using Types for Types.Par; | |
using Types for Types.Wei; | |
// ============ Constants ============ | |
bytes32 constant private FILE = "CanonicalOrders"; | |
// EIP191 header for EIP712 prefix | |
bytes2 constant private EIP191_HEADER = 0x1901; | |
// EIP712 Domain Name value | |
string constant private EIP712_DOMAIN_NAME = "CanonicalOrders"; | |
// EIP712 Domain Version value | |
string constant private EIP712_DOMAIN_VERSION = "1.1"; | |
// Hash of the EIP712 Domain Separator Schema | |
/* solium-disable-next-line indentation */ | |
bytes32 constant private EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH = keccak256(abi.encodePacked( | |
"EIP712Domain(", | |
"string name,", | |
"string version,", | |
"uint256 chainId,", | |
"address verifyingContract", | |
")" | |
)); | |
// Hash of the EIP712 CanonicalOrder struct | |
/* solium-disable-next-line indentation */ | |
bytes32 constant private EIP712_ORDER_STRUCT_SCHEMA_HASH = keccak256(abi.encodePacked( | |
"CanonicalOrder(", | |
"bytes32 flags,", | |
"uint256 baseMarket,", | |
"uint256 quoteMarket,", | |
"uint256 amount,", | |
"uint256 limitPrice,", | |
"uint256 triggerPrice,", | |
"uint256 limitFee,", | |
"address makerAccountOwner,", | |
"uint256 makerAccountNumber,", | |
"uint256 expiration", | |
")" | |
)); | |
// Number of bytes in an Order struct plus number of bytes in a FillArgs struct | |
uint256 constant private NUM_ORDER_AND_FILL_BYTES = 416; | |
// Number of bytes in a typed signature | |
uint256 constant private NUM_SIGNATURE_BYTES = 66; | |
// The number of decimal places of precision in the price ratio of a triggerPrice | |
uint256 constant private PRICE_BASE = 10 ** 18; | |
// Bitmasks for the order.flag argument | |
bytes32 constant private IS_BUY_FLAG = bytes32(uint256(1)); | |
bytes32 constant private IS_DECREASE_ONLY_FLAG = bytes32(uint256(1 << 1)); | |
bytes32 constant private IS_NEGATIVE_FEE_FLAG = bytes32(uint256(1 << 2)); | |
// ============ Enums ============ | |
enum OrderStatus { | |
Null, | |
Approved, | |
Canceled | |
} | |
enum CallFunctionType { | |
Approve, | |
Cancel, | |
SetFillArgs | |
} | |
// ============ Structs ============ | |
struct Order { | |
bytes32 flags; // salt, negativeFee, decreaseOnly, isBuy | |
uint256 baseMarket; | |
uint256 quoteMarket; | |
uint256 amount; | |
uint256 limitPrice; | |
uint256 triggerPrice; | |
uint256 limitFee; | |
address makerAccountOwner; | |
uint256 makerAccountNumber; | |
uint256 expiration; | |
} | |
struct FillArgs { | |
uint256 price; | |
uint128 fee; | |
bool isNegativeFee; | |
} | |
struct OrderInfo { | |
Order order; | |
FillArgs fill; | |
bytes32 orderHash; | |
} | |
struct OrderQueryOutput { | |
OrderStatus orderStatus; | |
uint256 filledAmount; | |
} | |
// ============ Events ============ | |
event LogContractStatusSet( | |
bool operational | |
); | |
event LogTakerSet( | |
address taker | |
); | |
event LogCanonicalOrderCanceled( | |
bytes32 indexed orderHash, | |
address indexed canceler, | |
uint256 baseMarket, | |
uint256 quoteMarket | |
); | |
event LogCanonicalOrderApproved( | |
bytes32 indexed orderHash, | |
address indexed approver, | |
uint256 baseMarket, | |
uint256 quoteMarket | |
); | |
event LogCanonicalOrderFilled( | |
bytes32 indexed orderHash, | |
address indexed orderMaker, | |
uint256 fillAmount, | |
uint256 triggerPrice, | |
bytes32 orderFlags, | |
FillArgs fill | |
); | |
// ============ Immutable Storage ============ | |
// Hash of the EIP712 Domain Separator data | |
bytes32 public EIP712_DOMAIN_HASH; | |
// ============ Mutable Storage ============ | |
// true if this contract can process orders | |
bool public g_isOperational; | |
// order hash => filled amount (in baseAmount) | |
mapping (bytes32 => uint256) public g_filledAmount; | |
// order hash => status | |
mapping (bytes32 => OrderStatus) public g_status; | |
// stored fillArgs | |
FillArgs public g_fillArgs; | |
// required taker address | |
address public g_taker; | |
// ============ Constructor ============ | |
constructor ( | |
address soloMargin, | |
address taker, | |
uint256 chainId | |
) | |
public | |
OnlySolo(soloMargin) | |
{ | |
g_isOperational = true; | |
g_taker = taker; | |
/* solium-disable-next-line indentation */ | |
EIP712_DOMAIN_HASH = keccak256(abi.encode( | |
EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH, | |
keccak256(bytes(EIP712_DOMAIN_NAME)), | |
keccak256(bytes(EIP712_DOMAIN_VERSION)), | |
chainId, | |
address(this) | |
)); | |
} | |
// ============ Admin Functions ============ | |
/** | |
* The owner can shut down the exchange. | |
*/ | |
function shutDown() | |
external | |
onlyOwner | |
{ | |
g_isOperational = false; | |
emit LogContractStatusSet(false); | |
} | |
/** | |
* The owner can start back up the exchange. | |
*/ | |
function startUp() | |
external | |
onlyOwner | |
{ | |
g_isOperational = true; | |
emit LogContractStatusSet(true); | |
} | |
/** | |
* The owner can set the taker address. | |
*/ | |
function setTakerAddress( | |
address taker | |
) | |
external | |
onlyOwner | |
{ | |
g_taker = taker; | |
emit LogTakerSet(taker); | |
} | |
// ============ External Functions ============ | |
/** | |
* Cancels an order. | |
* | |
* @param order The order to cancel | |
*/ | |
function cancelOrder( | |
Order memory order | |
) | |
public | |
{ | |
cancelOrderInternal(msg.sender, order); | |
} | |
/** | |
* Approves an order. Cannot already be canceled. | |
* | |
* @param order The order to approve | |
*/ | |
function approveOrder( | |
Order memory order | |
) | |
public | |
{ | |
approveOrderInternal(msg.sender, order); | |
} | |
// ============ Only-Solo Functions ============ | |
/** | |
* Allows traders to make trades approved by this smart contract. The active trader's account is | |
* the takerAccount and the passive account (for which this contract approves trades | |
* on-behalf-of) is the makerAccount. | |
* | |
* @param inputMarketId The market for which the trader specified the original amount | |
* @param outputMarketId The market for which the trader wants the resulting amount specified | |
* @param makerAccount The account for which this contract is making trades | |
* @param takerAccount The account requesting the trade | |
* @param oldInputPar The par balance of the makerAccount for inputMarketId pre-trade | |
* @param newInputPar The par balance of the makerAccount for inputMarketId post-trade | |
* @param inputWei The change in token amount for the makerAccount for the inputMarketId | |
* @param data Arbitrary data passed in by the trader | |
* @return The AssetAmount for the makerAccount for the outputMarketId | |
*/ | |
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) | |
{ | |
Require.that( | |
g_isOperational, | |
FILE, | |
"Contract is not operational" | |
); | |
OrderInfo memory orderInfo = getOrderInfo(data); | |
verifySignature(orderInfo, data); | |
verifyOrderInfo( | |
orderInfo, | |
makerAccount, | |
takerAccount, | |
inputMarketId, | |
outputMarketId, | |
inputWei | |
); | |
Types.AssetAmount memory assetAmount = getOutputAssetAmount( | |
inputMarketId, | |
outputMarketId, | |
inputWei, | |
orderInfo | |
); | |
if (isDecreaseOnly(orderInfo.order)) { | |
verifyDecreaseOnly( | |
oldInputPar, | |
newInputPar, | |
assetAmount, | |
makerAccount, | |
outputMarketId | |
); | |
} | |
return assetAmount; | |
} | |
/** | |
* Allows users to send this contract arbitrary data. | |
* | |
* param sender (unused) | |
* @param accountInfo The account from which the data is being sent | |
* @param data Arbitrary data given by the sender | |
*/ | |
function callFunction( | |
address /* sender */, | |
Account.Info memory accountInfo, | |
bytes memory data | |
) | |
public | |
onlySolo(msg.sender) | |
{ | |
CallFunctionType cft = abi.decode(data, (CallFunctionType)); | |
if (cft == CallFunctionType.SetFillArgs) { | |
FillArgs memory fillArgs; | |
(cft, fillArgs) = abi.decode(data, (CallFunctionType, FillArgs)); | |
g_fillArgs = fillArgs; | |
} else { | |
Order memory order; | |
(cft, order) = abi.decode(data, (CallFunctionType, Order)); | |
if (cft == CallFunctionType.Approve) { | |
approveOrderInternal(accountInfo.owner, order); | |
} else { | |
assert(cft == CallFunctionType.Cancel); | |
cancelOrderInternal(accountInfo.owner, order); | |
} | |
} | |
} | |
// ============ Getters ============ | |
/** | |
* Returns the status and the filled amount of several orders. | |
*/ | |
function getOrderStates( | |
bytes32[] memory orderHashes | |
) | |
public | |
view | |
returns(OrderQueryOutput[] memory) | |
{ | |
uint256 numOrders = orderHashes.length; | |
OrderQueryOutput[] memory output = new OrderQueryOutput[](numOrders); | |
// for each order | |
for (uint256 i = 0; i < numOrders; i++) { | |
bytes32 orderHash = orderHashes[i]; | |
output[i] = OrderQueryOutput({ | |
orderStatus: g_status[orderHash], | |
filledAmount: g_filledAmount[orderHash] | |
}); | |
} | |
return output; | |
} | |
// ============ Private Storage Functions ============ | |
/** | |
* Cancels an order as long as it is not already canceled. | |
*/ | |
function cancelOrderInternal( | |
address canceler, | |
Order memory order | |
) | |
private | |
{ | |
Require.that( | |
canceler == order.makerAccountOwner, | |
FILE, | |
"Canceler must be maker" | |
); | |
bytes32 orderHash = getOrderHash(order); | |
g_status[orderHash] = OrderStatus.Canceled; | |
emit LogCanonicalOrderCanceled( | |
orderHash, | |
canceler, | |
order.baseMarket, | |
order.quoteMarket | |
); | |
} | |
/** | |
* Approves an order as long as it is not already approved or canceled. | |
*/ | |
function approveOrderInternal( | |
address approver, | |
Order memory order | |
) | |
private | |
{ | |
Require.that( | |
approver == order.makerAccountOwner, | |
FILE, | |
"Approver must be maker" | |
); | |
bytes32 orderHash = getOrderHash(order); | |
Require.that( | |
g_status[orderHash] != OrderStatus.Canceled, | |
FILE, | |
"Cannot approve canceled order", | |
orderHash | |
); | |
g_status[orderHash] = OrderStatus.Approved; | |
emit LogCanonicalOrderApproved( | |
orderHash, | |
approver, | |
order.baseMarket, | |
order.quoteMarket | |
); | |
} | |
// ============ Private Helper Functions ============ | |
/** | |
* Parses the order, verifies that it is not expired or canceled, and verifies the signature. | |
*/ | |
function getOrderInfo( | |
bytes memory data | |
) | |
private | |
returns (OrderInfo memory) | |
{ | |
Require.that( | |
( | |
data.length == NUM_ORDER_AND_FILL_BYTES || | |
data.length == NUM_ORDER_AND_FILL_BYTES + NUM_SIGNATURE_BYTES | |
), | |
FILE, | |
"Cannot parse order from data" | |
); | |
// load orderInfo from calldata | |
OrderInfo memory orderInfo; | |
( | |
orderInfo.order, | |
orderInfo.fill | |
) = abi.decode(data, (Order, FillArgs)); | |
// load fillArgs from storage if price is zero | |
if (orderInfo.fill.price == 0) { | |
orderInfo.fill = g_fillArgs; | |
g_fillArgs = FillArgs({ | |
price: 0, | |
fee: 0, | |
isNegativeFee: false | |
}); | |
} | |
Require.that( | |
orderInfo.fill.price != 0, | |
FILE, | |
"FillArgs loaded price is zero" | |
); | |
orderInfo.orderHash = getOrderHash(orderInfo.order); | |
return orderInfo; | |
} | |
function verifySignature( | |
OrderInfo memory orderInfo, | |
bytes memory data | |
) | |
private | |
view | |
{ | |
OrderStatus orderStatus = g_status[orderInfo.orderHash]; | |
// verify valid signature or is pre-approved | |
if (orderStatus == OrderStatus.Null) { | |
bytes memory signature = parseSignature(data); | |
address signer = TypedSignature.recover(orderInfo.orderHash, signature); | |
Require.that( | |
orderInfo.order.makerAccountOwner == signer, | |
FILE, | |
"Order invalid signature", | |
orderInfo.orderHash | |
); | |
} else { | |
Require.that( | |
orderStatus != OrderStatus.Canceled, | |
FILE, | |
"Order canceled", | |
orderInfo.orderHash | |
); | |
assert(orderStatus == OrderStatus.Approved); | |
} | |
} | |
/** | |
* Verifies that the order is still fillable for the particular accounts and markets specified. | |
*/ | |
function verifyOrderInfo( | |
OrderInfo memory orderInfo, | |
Account.Info memory makerAccount, | |
Account.Info memory takerAccount, | |
uint256 inputMarketId, | |
uint256 outputMarketId, | |
Types.Wei memory inputWei | |
) | |
private | |
view | |
{ | |
// verify fill price | |
FillArgs memory fill = orderInfo.fill; | |
bool validPrice = isBuy(orderInfo.order) | |
? fill.price <= orderInfo.order.limitPrice | |
: fill.price >= orderInfo.order.limitPrice; | |
Require.that( | |
validPrice, | |
FILE, | |
"Fill invalid price" | |
); | |
// verify fill fee | |
bool validFee = isNegativeLimitFee(orderInfo.order) | |
? (fill.fee >= orderInfo.order.limitFee) && fill.isNegativeFee | |
: (fill.fee <= orderInfo.order.limitFee) || fill.isNegativeFee; | |
Require.that( | |
validFee, | |
FILE, | |
"Fill invalid fee" | |
); | |
// verify triggerPrice | |
if (orderInfo.order.triggerPrice > 0) { | |
uint256 currentPrice = getCurrentPrice( | |
orderInfo.order.baseMarket, | |
orderInfo.order.quoteMarket | |
); | |
Require.that( | |
isBuy(orderInfo.order) | |
? currentPrice >= orderInfo.order.triggerPrice | |
: currentPrice <= orderInfo.order.triggerPrice, | |
FILE, | |
"Order triggerPrice not triggered", | |
currentPrice | |
); | |
} | |
// verify expriy | |
Require.that( | |
orderInfo.order.expiration == 0 || orderInfo.order.expiration >= block.timestamp, | |
FILE, | |
"Order expired", | |
orderInfo.orderHash | |
); | |
// verify maker | |
Require.that( | |
makerAccount.owner == orderInfo.order.makerAccountOwner && | |
makerAccount.number == orderInfo.order.makerAccountNumber, | |
FILE, | |
"Order maker account mismatch", | |
orderInfo.orderHash | |
); | |
// verify taker | |
Require.that( | |
takerAccount.owner == g_taker, | |
FILE, | |
"Order taker mismatch", | |
orderInfo.orderHash | |
); | |
// verify markets | |
Require.that( | |
( | |
orderInfo.order.baseMarket == outputMarketId && | |
orderInfo.order.quoteMarket == inputMarketId | |
) || ( | |
orderInfo.order.quoteMarket == outputMarketId && | |
orderInfo.order.baseMarket == inputMarketId | |
), | |
FILE, | |
"Market mismatch", | |
orderInfo.orderHash | |
); | |
// verify inputWei is non-zero | |
Require.that( | |
!inputWei.isZero(), | |
FILE, | |
"InputWei is zero", | |
orderInfo.orderHash | |
); | |
// verify inputWei is positive if-and-only-if: | |
// 1) inputMarket is the baseMarket and the order is a buy order | |
// 2) inputMarket is the quoteMarket and the order is a sell order | |
Require.that( | |
inputWei.sign == | |
((orderInfo.order.baseMarket == inputMarketId) == isBuy(orderInfo.order)), | |
FILE, | |
"InputWei sign mismatch", | |
orderInfo.orderHash | |
); | |
} | |
/** | |
* Verifies that the order is decreasing the size of the maker's position. | |
*/ | |
function verifyDecreaseOnly( | |
Types.Par memory oldInputPar, | |
Types.Par memory newInputPar, | |
Types.AssetAmount memory assetAmount, | |
Account.Info memory makerAccount, | |
uint256 outputMarketId | |
) | |
private | |
view | |
{ | |
// verify that the balance of inputMarketId is not increased | |
Require.that( | |
newInputPar.isZero() | |
|| (newInputPar.value <= oldInputPar.value && newInputPar.sign == oldInputPar.sign), | |
FILE, | |
"inputMarket not decreased" | |
); | |
// verify that the balance of outputMarketId is not increased | |
Types.Wei memory oldOutputWei = SOLO_MARGIN.getAccountWei(makerAccount, outputMarketId); | |
Require.that( | |
assetAmount.value == 0 | |
|| (assetAmount.value <= oldOutputWei.value && assetAmount.sign != oldOutputWei.sign), | |
FILE, | |
"outputMarket not decreased" | |
); | |
} | |
/** | |
* Returns the AssetAmount for the outputMarketId given the order and the inputs. Updates the | |
* filled amount of the order in storage. | |
*/ | |
function getOutputAssetAmount( | |
uint256 inputMarketId, | |
uint256 outputMarketId, | |
Types.Wei memory inputWei, | |
OrderInfo memory orderInfo | |
) | |
private | |
returns (Types.AssetAmount memory) | |
{ | |
uint256 fee = orderInfo.fill.price.getPartial(orderInfo.fill.fee, PRICE_BASE); | |
uint256 adjustedPrice = (isBuy(orderInfo.order) == orderInfo.fill.isNegativeFee) | |
? orderInfo.fill.price.sub(fee) | |
: orderInfo.fill.price.add(fee); | |
uint256 outputAmount; | |
uint256 fillAmount; | |
if (orderInfo.order.quoteMarket == inputMarketId) { | |
outputAmount = inputWei.value.getPartial(PRICE_BASE, adjustedPrice); | |
fillAmount = outputAmount; | |
} else { | |
assert(orderInfo.order.quoteMarket == outputMarketId); | |
outputAmount = inputWei.value.getPartial(adjustedPrice, PRICE_BASE); | |
fillAmount = inputWei.value; | |
} | |
updateFilledAmount(orderInfo, fillAmount); | |
return Types.AssetAmount({ | |
sign: !inputWei.sign, | |
denomination: Types.AssetDenomination.Wei, | |
ref: Types.AssetReference.Delta, | |
value: outputAmount | |
}); | |
} | |
/** | |
* Increases the stored filled amount of the order by fillAmount. | |
* Returns the new total filled amount. | |
*/ | |
function updateFilledAmount( | |
OrderInfo memory orderInfo, | |
uint256 fillAmount | |
) | |
private | |
{ | |
uint256 oldFilledAmount = g_filledAmount[orderInfo.orderHash]; | |
uint256 totalFilledAmount = oldFilledAmount.add(fillAmount); | |
Require.that( | |
totalFilledAmount <= orderInfo.order.amount, | |
FILE, | |
"Cannot overfill order", | |
orderInfo.orderHash, | |
oldFilledAmount, | |
fillAmount | |
); | |
g_filledAmount[orderInfo.orderHash] = totalFilledAmount; | |
emit LogCanonicalOrderFilled( | |
orderInfo.orderHash, | |
orderInfo.order.makerAccountOwner, | |
fillAmount, | |
orderInfo.order.triggerPrice, | |
orderInfo.order.flags, | |
orderInfo.fill | |
); | |
} | |
/** | |
* Returns the current price of baseMarket divided by the current price of quoteMarket. This | |
* value is multiplied by 10^18. | |
*/ | |
function getCurrentPrice( | |
uint256 baseMarket, | |
uint256 quoteMarket | |
) | |
private | |
view | |
returns (uint256) | |
{ | |
Monetary.Price memory basePrice = SOLO_MARGIN.getMarketPrice(baseMarket); | |
Monetary.Price memory quotePrice = SOLO_MARGIN.getMarketPrice(quoteMarket); | |
return basePrice.value.mul(PRICE_BASE).div(quotePrice.value); | |
} | |
// ============ Private Parsing Functions ============ | |
/** | |
* Returns the EIP712 hash of an order. | |
*/ | |
function getOrderHash( | |
Order memory order | |
) | |
private | |
view | |
returns (bytes32) | |
{ | |
// compute the overall signed struct hash | |
/* solium-disable-next-line indentation */ | |
bytes32 structHash = keccak256(abi.encode( | |
EIP712_ORDER_STRUCT_SCHEMA_HASH, | |
order | |
)); | |
// compute eip712 compliant hash | |
/* solium-disable-next-line indentation */ | |
return keccak256(abi.encodePacked( | |
EIP191_HEADER, | |
EIP712_DOMAIN_HASH, | |
structHash | |
)); | |
} | |
/** | |
* Parses out a signature from call data. | |
*/ | |
function parseSignature( | |
bytes memory data | |
) | |
private | |
pure | |
returns (bytes memory) | |
{ | |
Require.that( | |
data.length == NUM_ORDER_AND_FILL_BYTES + NUM_SIGNATURE_BYTES, | |
FILE, | |
"Cannot parse signature from data" | |
); | |
bytes memory signature = new bytes(NUM_SIGNATURE_BYTES); | |
uint256 sigOffset = NUM_ORDER_AND_FILL_BYTES; | |
/* solium-disable-next-line security/no-inline-assembly */ | |
assembly { | |
let sigStart := add(data, sigOffset) | |
mstore(add(signature, 0x020), mload(add(sigStart, 0x20))) | |
mstore(add(signature, 0x040), mload(add(sigStart, 0x40))) | |
mstore(add(signature, 0x042), mload(add(sigStart, 0x42))) | |
} | |
return signature; | |
} | |
/** | |
* Returns true if the order is a buy order. | |
*/ | |
function isBuy( | |
Order memory order | |
) | |
private | |
pure | |
returns (bool) | |
{ | |
return (order.flags & IS_BUY_FLAG) != bytes32(0); | |
} | |
/** | |
* Returns true if the order is a decrease-only order. | |
*/ | |
function isDecreaseOnly( | |
Order memory order | |
) | |
private | |
pure | |
returns (bool) | |
{ | |
return (order.flags & IS_DECREASE_ONLY_FLAG) != bytes32(0); | |
} | |
/** | |
* Returns true if the order's limitFee is negative. | |
*/ | |
function isNegativeLimitFee( | |
Order memory order | |
) | |
private | |
pure | |
returns (bool) | |
{ | |
return (order.flags & IS_NEGATIVE_FEE_FLAG) != bytes32(0); | |
} | |
} | |
contract PartiallyDelayedMultiSig is | |
DelayedMultiSig | |
{ | |
// ============ Events ============ | |
event SelectorSet(address destination, bytes4 selector, bool approved); | |
// ============ Constants ============ | |
bytes4 constant internal BYTES_ZERO = bytes4(0x0); | |
// ============ Storage ============ | |
// destination => function selector => can bypass timelock | |
mapping (address => mapping (bytes4 => bool)) public instantData; | |
// ============ Modifiers ============ | |
// Overrides old modifier that requires a timelock for every transaction | |
modifier pastTimeLock( | |
uint256 transactionId | |
) { | |
// if the function selector is not exempt from timelock, then require timelock | |
require( | |
block.timestamp >= confirmationTimes[transactionId] + secondsTimeLocked | |
|| txCanBeExecutedInstantly(transactionId), | |
"TIME_LOCK_INCOMPLETE" | |
); | |
_; | |
} | |
// ============ Constructor ============ | |
/** | |
* Contract constructor sets initial owners, required number of confirmations, and time lock. | |
* | |
* @param _owners List of initial owners. | |
* @param _required Number of required confirmations. | |
* @param _secondsTimeLocked Duration needed after a transaction is confirmed and before it | |
* becomes executable, in seconds. | |
* @param _noDelayDestinations List of destinations that correspond with the selectors. | |
* Zero address allows the function selector to be used with any | |
* address. | |
* @param _noDelaySelectors All function selectors that do not require a delay to execute. | |
* Fallback function is 0x00000000. | |
*/ | |
constructor ( | |
address[] memory _owners, | |
uint256 _required, | |
uint32 _secondsTimeLocked, | |
address[] memory _noDelayDestinations, | |
bytes4[] memory _noDelaySelectors | |
) | |
public | |
DelayedMultiSig(_owners, _required, _secondsTimeLocked) | |
{ | |
require( | |
_noDelayDestinations.length == _noDelaySelectors.length, | |
"ADDRESS_AND_SELECTOR_MISMATCH" | |
); | |
for (uint256 i = 0; i < _noDelaySelectors.length; i++) { | |
address destination = _noDelayDestinations[i]; | |
bytes4 selector = _noDelaySelectors[i]; | |
instantData[destination][selector] = true; | |
emit SelectorSet(destination, selector, true); | |
} | |
} | |
// ============ Wallet-Only Functions ============ | |
/** | |
* Adds or removes functions that can be executed instantly. Transaction must be sent by wallet. | |
* | |
* @param destination Destination address of function. Zero address allows the function to be | |
* sent to any address. | |
* @param selector 4-byte selector of the function. Fallback function is 0x00000000. | |
* @param approved True if adding approval, false if removing approval. | |
*/ | |
function setSelector( | |
address destination, | |
bytes4 selector, | |
bool approved | |
) | |
public | |
onlyWallet | |
{ | |
instantData[destination][selector] = approved; | |
emit SelectorSet(destination, selector, approved); | |
} | |
// ============ Helper Functions ============ | |
/** | |
* Returns true if transaction can be executed instantly (without timelock). | |
*/ | |
function txCanBeExecutedInstantly( | |
uint256 transactionId | |
) | |
internal | |
view | |
returns (bool) | |
{ | |
// get transaction from storage | |
Transaction memory txn = transactions[transactionId]; | |
address dest = txn.destination; | |
bytes memory data = txn.data; | |
// fallback function | |
if (data.length == 0) { | |
return selectorCanBeExecutedInstantly(dest, BYTES_ZERO); | |
} | |
// invalid function selector | |
if (data.length < 4) { | |
return false; | |
} | |
// check first four bytes (function selector) | |
bytes32 rawData; | |
/* solium-disable-next-line security/no-inline-assembly */ | |
assembly { | |
rawData := mload(add(data, 32)) | |
} | |
bytes4 selector = bytes4(rawData); | |
return selectorCanBeExecutedInstantly(dest, selector); | |
} | |
/** | |
* Function selector is in instantData for address dest (or for address zero). | |
*/ | |
function selectorCanBeExecutedInstantly( | |
address destination, | |
bytes4 selector | |
) | |
internal | |
view | |
returns (bool) | |
{ | |
return instantData[destination][selector] | |
|| instantData[ADDRESS_ZERO][selector]; | |
} | |
} | |
contract DaiMigrator is | |
Ownable, | |
IAutoTrader | |
{ | |
using Types for Types.Wei; | |
using Types for Types.Par; | |
// ============ Constants ============ | |
bytes32 constant FILE = "DaiMigrator"; | |
uint256 constant SAI_MARKET = 1; | |
uint256 constant DAI_MARKET = 3; | |
// ============ Events ============ | |
event LogMigratorAdded( | |
address migrator | |
); | |
event LogMigratorRemoved( | |
address migrator | |
); | |
// ============ Storage ============ | |
// the addresses that are able to migrate positions | |
mapping (address => bool) public g_migrators; | |
// ============ Constructor ============ | |
constructor ( | |
address[] memory migrators | |
) | |
public | |
{ | |
for (uint256 i = 0; i < migrators.length; i++) { | |
g_migrators[migrators[i]] = true; | |
} | |
} | |
// ============ Admin Functions ============ | |
function addMigrator( | |
address migrator | |
) | |
external | |
onlyOwner | |
{ | |
emit LogMigratorAdded(migrator); | |
g_migrators[migrator] = true; | |
} | |
function removeMigrator( | |
address migrator | |
) | |
external | |
onlyOwner | |
{ | |
emit LogMigratorRemoved(migrator); | |
g_migrators[migrator] = false; | |
} | |
// ============ Only-Solo Functions ============ | |
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 | |
/* view */ | |
returns (Types.AssetAmount memory) | |
{ | |
Require.that( | |
g_migrators[takerAccount.owner], | |
FILE, | |
"Migrator not approved", | |
takerAccount.owner | |
); | |
Require.that( | |
inputMarketId == SAI_MARKET && outputMarketId == DAI_MARKET, | |
FILE, | |
"Invalid markets" | |
); | |
// require that SAI amount is getting smaller (closer to zero) | |
if (oldInputPar.isPositive()) { | |
Require.that( | |
inputWei.isNegative(), | |
FILE, | |
"inputWei must be negative" | |
); | |
Require.that( | |
!newInputPar.isNegative(), | |
FILE, | |
"newInputPar cannot be negative" | |
); | |
} else if (oldInputPar.isNegative()) { | |
Require.that( | |
inputWei.isPositive(), | |
FILE, | |
"inputWei must be positive" | |
); | |
Require.that( | |
!newInputPar.isPositive(), | |
FILE, | |
"newInputPar cannot be positive" | |
); | |
} else { | |
Require.that( | |
inputWei.isZero() && newInputPar.isZero(), | |
FILE, | |
"inputWei must be zero" | |
); | |
} | |
/* return the exact opposite amount of SAI in DAI */ | |
return Types.AssetAmount ({ | |
sign: !inputWei.sign, | |
denomination: Types.AssetDenomination.Wei, | |
ref: Types.AssetReference.Delta, | |
value: inputWei.value | |
}); | |
} | |
} | |
contract TestDoubleExponentInterestSetter is | |
DoubleExponentInterestSetter | |
{ | |
constructor( | |
PolyStorage memory parameters | |
) | |
public | |
DoubleExponentInterestSetter(parameters) | |
{ | |
} | |
function setParameters( | |
PolyStorage memory parameters | |
) | |
public | |
{ | |
g_storage = parameters; | |
} | |
function createNew( | |
PolyStorage memory parameters | |
) | |
public | |
returns (DoubleExponentInterestSetter) | |
{ | |
return new DoubleExponentInterestSetter(parameters); | |
} | |
} | |
contract TestSimpleCallee is | |
ICallee, | |
OnlySolo | |
{ | |
// ============ Constants ============ | |
bytes32 constant FILE = "TestSimpleCallee"; | |
// ============ Events ============ | |
event Called( | |
address indexed sender, | |
address indexed accountOwner, | |
uint256 accountNumber, | |
bytes data | |
); | |
// ============ Constructor ============ | |
constructor( | |
address soloMargin | |
) | |
public | |
OnlySolo(soloMargin) | |
{} | |
// ============ ICallee Functions ============ | |
function callFunction( | |
address sender, | |
Account.Info memory account, | |
bytes memory data | |
) | |
public | |
onlySolo(msg.sender) | |
{ | |
emit Called( | |
sender, | |
account.owner, | |
account.number, | |
data | |
); | |
} | |
} | |
contract TestPolynomialInterestSetter is | |
PolynomialInterestSetter | |
{ | |
constructor( | |
PolyStorage memory parameters | |
) | |
public | |
PolynomialInterestSetter(parameters) | |
{ | |
} | |
function setParameters( | |
PolyStorage memory parameters | |
) | |
public | |
{ | |
g_storage = parameters; | |
} | |
function createNew( | |
PolyStorage memory parameters | |
) | |
public | |
returns (PolynomialInterestSetter) | |
{ | |
return new PolynomialInterestSetter(parameters); | |
} | |
} | |
contract TestCallee is | |
ICallee, | |
OnlySolo | |
{ | |
// ============ Constants ============ | |
bytes32 constant FILE = "TestCallee"; | |
// ============ Events ============ | |
event Called( | |
address indexed sender, | |
address indexed accountOwner, | |
uint256 accountNumber, | |
uint256 accountData, | |
uint256 senderData | |
); | |
// ============ Storage ============ | |
// owner => number => data | |
mapping (address => mapping (uint256 => uint256)) public accountData; | |
// sender => data | |
mapping (address => uint256) public senderData; | |
// ============ Constructor ============ | |
constructor( | |
address soloMargin | |
) | |
public | |
OnlySolo(soloMargin) | |
{} | |
// ============ ICallee Functions ============ | |
function callFunction( | |
address sender, | |
Account.Info memory account, | |
bytes memory data | |
) | |
public | |
onlySolo(msg.sender) | |
{ | |
( | |
uint256 aData, | |
uint256 sData | |
) = parseData(data); | |
emit Called( | |
sender, | |
account.owner, | |
account.number, | |
aData, | |
sData | |
); | |
accountData[account.owner][account.number] = aData; | |
senderData[sender] = sData; | |
} | |
// ============ Private Functions ============ | |
function parseData( | |
bytes memory data | |
) | |
private | |
pure | |
returns ( | |
uint256, | |
uint256 | |
) | |
{ | |
Require.that( | |
data.length == 64, | |
FILE, | |
"Call data invalid length" | |
); | |
uint256 aData; | |
uint256 sData; | |
/* solium-disable-next-line security/no-inline-assembly */ | |
assembly { | |
aData := mload(add(data, 32)) | |
sData := mload(add(data, 64)) | |
} | |
return ( | |
aData, | |
sData | |
); | |
} | |
} | |
contract Admin is | |
State, | |
Ownable, | |
ReentrancyGuard | |
{ | |
// ============ Token Functions ============ | |
/** | |
* Withdraw an ERC20 token for which there is an associated market. Only excess tokens can be | |
* withdrawn. The number of excess tokens is calculated by taking the current number of tokens | |
* held in Solo, adding the number of tokens owed to Solo by borrowers, and subtracting the | |
* number of tokens owed to suppliers by Solo. | |
*/ | |
function ownerWithdrawExcessTokens( | |
uint256 marketId, | |
address recipient | |
) | |
public | |
onlyOwner | |
nonReentrant | |
returns (uint256) | |
{ | |
return AdminImpl.ownerWithdrawExcessTokens( | |
g_state, | |
marketId, | |
recipient | |
); | |
} | |
/** | |
* Withdraw an ERC20 token for which there is no associated market. | |
*/ | |
function ownerWithdrawUnsupportedTokens( | |
address token, | |
address recipient | |
) | |
public | |
onlyOwner | |
nonReentrant | |
returns (uint256) | |
{ | |
return AdminImpl.ownerWithdrawUnsupportedTokens( | |
g_state, | |
token, | |
recipient | |
); | |
} | |
// ============ Market Functions ============ | |
/** | |
* Add a new market to Solo. Must be for a previously-unsupported ERC20 token. | |
*/ | |
function ownerAddMarket( | |
address token, | |
IPriceOracle priceOracle, | |
IInterestSetter interestSetter, | |
Decimal.D256 memory marginPremium, | |
Decimal.D256 memory spreadPremium | |
) | |
public | |
onlyOwner | |
nonReentrant | |
{ | |
AdminImpl.ownerAddMarket( | |
g_state, | |
token, | |
priceOracle, | |
interestSetter, | |
marginPremium, | |
spreadPremium | |
); | |
} | |
/** | |
* Set (or unset) the status of a market to "closing". The borrowedValue of a market cannot | |
* increase while its status is "closing". | |
*/ | |
function ownerSetIsClosing( | |
uint256 marketId, | |
bool isClosing | |
) | |
public | |
onlyOwner | |
nonReentrant | |
{ | |
AdminImpl.ownerSetIsClosing( | |
g_state, | |
marketId, | |
isClosing | |
); | |
} | |
/** | |
* Set the price oracle for a market. | |
*/ | |
function ownerSetPriceOracle( | |
uint256 marketId, | |
IPriceOracle priceOracle | |
) | |
public | |
onlyOwner | |
nonReentrant | |
{ | |
AdminImpl.ownerSetPriceOracle( | |
g_state, | |
marketId, | |
priceOracle | |
); | |
} | |
/** | |
* Set the interest-setter for a market. | |
*/ | |
function ownerSetInterestSetter( | |
uint256 marketId, | |
IInterestSetter interestSetter | |
) | |
public | |
onlyOwner | |
nonReentrant | |
{ | |
AdminImpl.ownerSetInterestSetter( | |
g_state, | |
marketId, | |
interestSetter | |
); | |
} | |
/** | |
* Set a premium on the minimum margin-ratio for a market. This makes it so that any positions | |
* that include this market require a higher collateralization to avoid being liquidated. | |
*/ | |
function ownerSetMarginPremium( | |
uint256 marketId, | |
Decimal.D256 memory marginPremium | |
) | |
public | |
onlyOwner | |
nonReentrant | |
{ | |
AdminImpl.ownerSetMarginPremium( | |
g_state, | |
marketId, | |
marginPremium | |
); | |
} | |
/** | |
* Set a premium on the liquidation spread for a market. This makes it so that any liquidations | |
* that include this market have a higher spread than the global default. | |
*/ | |
function ownerSetSpreadPremium( | |
uint256 marketId, | |
Decimal.D256 memory spreadPremium | |
) | |
public | |
onlyOwner | |
nonReentrant | |
{ | |
AdminImpl.ownerSetSpreadPremium( | |
g_state, | |
marketId, | |
spreadPremium | |
); | |
} | |
// ============ Risk Functions ============ | |
/** | |
* Set the global minimum margin-ratio that every position must maintain to prevent being | |
* liquidated. | |
*/ | |
function ownerSetMarginRatio( | |
Decimal.D256 memory ratio | |
) | |
public | |
onlyOwner | |
nonReentrant | |
{ | |
AdminImpl.ownerSetMarginRatio( | |
g_state, | |
ratio | |
); | |
} | |
/** | |
* Set the global liquidation spread. This is the spread between oracle prices that incentivizes | |
* the liquidation of risky positions. | |
*/ | |
function ownerSetLiquidationSpread( | |
Decimal.D256 memory spread | |
) | |
public | |
onlyOwner | |
nonReentrant | |
{ | |
AdminImpl.ownerSetLiquidationSpread( | |
g_state, | |
spread | |
); | |
} | |
/** | |
* Set the global earnings-rate variable that determines what percentage of the interest paid | |
* by borrowers gets passed-on to suppliers. | |
*/ | |
function ownerSetEarningsRate( | |
Decimal.D256 memory earningsRate | |
) | |
public | |
onlyOwner | |
nonReentrant | |
{ | |
AdminImpl.ownerSetEarningsRate( | |
g_state, | |
earningsRate | |
); | |
} | |
/** | |
* Set the global minimum-borrow value which is the minimum value of any new borrow on Solo. | |
*/ | |
function ownerSetMinBorrowedValue( | |
Monetary.Value memory minBorrowedValue | |
) | |
public | |
onlyOwner | |
nonReentrant | |
{ | |
AdminImpl.ownerSetMinBorrowedValue( | |
g_state, | |
minBorrowedValue | |
); | |
} | |
// ============ Global Operator Functions ============ | |
/** | |
* Approve (or disapprove) an address that is permissioned to be an operator for all accounts in | |
* Solo. Intended only to approve smart-contracts. | |
*/ | |
function ownerSetGlobalOperator( | |
address operator, | |
bool approved | |
) | |
public | |
onlyOwner | |
nonReentrant | |
{ | |
AdminImpl.ownerSetGlobalOperator( | |
g_state, | |
operator, | |
approved | |
); | |
} | |
} | |
contract ExpiryV2 is | |
Ownable, | |
OnlySolo, | |
ICallee, | |
IAutoTrader | |
{ | |
using Math for uint256; | |
using SafeMath for uint32; | |
using SafeMath for uint256; | |
using Types for Types.Par; | |
using Types for Types.Wei; | |
// ============ Constants ============ | |
bytes32 constant FILE = "ExpiryV2"; | |
// ============ Enums ============ | |
enum CallFunctionType { | |
SetExpiry, | |
SetApproval | |
} | |
// ============ Structs ============ | |
struct SetExpiryArg { | |
Account.Info account; | |
uint256 marketId; | |
uint32 timeDelta; | |
bool forceUpdate; | |
} | |
struct SetApprovalArg { | |
address sender; | |
uint32 minTimeDelta; | |
} | |
// ============ Events ============ | |
event ExpirySet( | |
address owner, | |
uint256 number, | |
uint256 marketId, | |
uint32 time | |
); | |
event LogExpiryRampTimeSet( | |
uint256 expiryRampTime | |
); | |
event LogSenderApproved( | |
address approver, | |
address sender, | |
uint32 minTimeDelta | |
); | |
// ============ Storage ============ | |
// owner => number => market => time | |
mapping (address => mapping (uint256 => mapping (uint256 => uint32))) g_expiries; | |
// owner => sender => minimum time delta | |
mapping (address => mapping (address => uint32)) public g_approvedSender; | |
// 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; | |
} | |
// ============ Admin Functions ============ | |
function ownerSetExpiryRampTime( | |
uint256 newExpiryRampTime | |
) | |
external | |
onlyOwner | |
{ | |
emit LogExpiryRampTimeSet(newExpiryRampTime); | |
g_expiryRampTime = newExpiryRampTime; | |
} | |
// ============ Approval Functions ============ | |
function approveSender( | |
address sender, | |
uint32 minTimeDelta | |
) | |
external | |
{ | |
setApproval(msg.sender, sender, minTimeDelta); | |
} | |
// ============ Only-Solo Functions ============ | |
function callFunction( | |
address /* sender */, | |
Account.Info memory account, | |
bytes memory data | |
) | |
public | |
onlySolo(msg.sender) | |
{ | |
CallFunctionType callType = abi.decode(data, (CallFunctionType)); | |
if (callType == CallFunctionType.SetExpiry) { | |
callFunctionSetExpiry(account.owner, data); | |
} else { | |
callFunctionSetApproval(account.owner, data); | |
} | |
} | |
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) = abi.decode(data, (uint256, uint32)); | |
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 | |
); | |
} | |
// ============ 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); | |
} | |
// ============ Private Functions ============ | |
function callFunctionSetExpiry( | |
address sender, | |
bytes memory data | |
) | |
private | |
{ | |
( | |
CallFunctionType callType, | |
SetExpiryArg[] memory expiries | |
) = abi.decode(data, (CallFunctionType, SetExpiryArg[])); | |
assert(callType == CallFunctionType.SetExpiry); | |
for (uint256 i = 0; i < expiries.length; i++) { | |
SetExpiryArg memory exp = expiries[i]; | |
if (exp.account.owner != sender) { | |
// don't do anything if sender is not approved for this action | |
uint32 minApprovedTimeDelta = g_approvedSender[exp.account.owner][sender]; | |
if (minApprovedTimeDelta == 0 || exp.timeDelta < minApprovedTimeDelta) { | |
continue; | |
} | |
} | |
// if timeDelta is zero, interpret it as unset expiry | |
if ( | |
exp.timeDelta != 0 && | |
SOLO_MARGIN.getAccountPar(exp.account, exp.marketId).isNegative() | |
) { | |
// only change non-zero values if forceUpdate is true | |
if (exp.forceUpdate || getExpiry(exp.account, exp.marketId) == 0) { | |
uint32 newExpiryTime = Time.currentTime().add(exp.timeDelta).to32(); | |
setExpiry(exp.account, exp.marketId, newExpiryTime); | |
} | |
} else { | |
// timeDelta is zero or account has non-negative balance | |
setExpiry(exp.account, exp.marketId, 0); | |
} | |
} | |
} | |
function callFunctionSetApproval( | |
address sender, | |
bytes memory data | |
) | |
private | |
{ | |
( | |
CallFunctionType callType, | |
SetApprovalArg memory approvalArg | |
) = abi.decode(data, (CallFunctionType, SetApprovalArg)); | |
assert(callType == CallFunctionType.SetApproval); | |
setApproval(sender, approvalArg.sender, approvalArg.minTimeDelta); | |
} | |
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 setApproval( | |
address approver, | |
address sender, | |
uint32 minTimeDelta | |
) | |
private | |
{ | |
g_approvedSender[approver][sender] = minTimeDelta; | |
emit LogSenderApproved(approver, sender, minTimeDelta); | |
} | |
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 | |
}); | |
} | |
} | |
contract SignedOperationProxy is | |
OnlySolo, | |
Ownable | |
{ | |
using SafeMath for uint256; | |
// ============ Constants ============ | |
bytes32 constant private FILE = "SignedOperationProxy"; | |
// EIP191 header for EIP712 prefix | |
bytes2 constant private EIP191_HEADER = 0x1901; | |
// EIP712 Domain Name value | |
string constant private EIP712_DOMAIN_NAME = "SignedOperationProxy"; | |
// EIP712 Domain Version value | |
string constant private EIP712_DOMAIN_VERSION = "1.1"; | |
// EIP712 encodeType of EIP712Domain | |
bytes constant private EIP712_DOMAIN_STRING = abi.encodePacked( | |
"EIP712Domain(", | |
"string name,", | |
"string version,", | |
"uint256 chainId,", | |
"address verifyingContract", | |
")" | |
); | |
// EIP712 encodeType of Operation | |
bytes constant private EIP712_OPERATION_STRING = abi.encodePacked( | |
"Operation(", | |
"Action[] actions,", | |
"uint256 expiration,", | |
"uint256 salt,", | |
"address sender,", | |
"address signer", | |
")" | |
); | |
// EIP712 encodeType of Action | |
bytes constant private EIP712_ACTION_STRING = abi.encodePacked( | |
"Action(", | |
"uint8 actionType,", | |
"address accountOwner,", | |
"uint256 accountNumber,", | |
"AssetAmount assetAmount,", | |
"uint256 primaryMarketId,", | |
"uint256 secondaryMarketId,", | |
"address otherAddress,", | |
"address otherAccountOwner,", | |
"uint256 otherAccountNumber,", | |
"bytes data", | |
")" | |
); | |
// EIP712 encodeType of AssetAmount | |
bytes constant private EIP712_ASSET_AMOUNT_STRING = abi.encodePacked( | |
"AssetAmount(", | |
"bool sign,", | |
"uint8 denomination,", | |
"uint8 ref,", | |
"uint256 value", | |
")" | |
); | |
// EIP712 typeHash of EIP712Domain | |
/* solium-disable-next-line indentation */ | |
bytes32 constant private EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH = keccak256(abi.encodePacked( | |
EIP712_DOMAIN_STRING | |
)); | |
// EIP712 typeHash of Operation | |
/* solium-disable-next-line indentation */ | |
bytes32 constant private EIP712_OPERATION_HASH = keccak256(abi.encodePacked( | |
EIP712_OPERATION_STRING, | |
EIP712_ACTION_STRING, | |
EIP712_ASSET_AMOUNT_STRING | |
)); | |
// EIP712 typeHash of Action | |
/* solium-disable-next-line indentation */ | |
bytes32 constant private EIP712_ACTION_HASH = keccak256(abi.encodePacked( | |
EIP712_ACTION_STRING, | |
EIP712_ASSET_AMOUNT_STRING | |
)); | |
// EIP712 typeHash of AssetAmount | |
/* solium-disable-next-line indentation */ | |
bytes32 constant private EIP712_ASSET_AMOUNT_HASH = keccak256(abi.encodePacked( | |
EIP712_ASSET_AMOUNT_STRING | |
)); | |
// ============ Structs ============ | |
struct OperationHeader { | |
uint256 expiration; | |
uint256 salt; | |
address sender; | |
address signer; | |
} | |
struct Authorization { | |
uint256 numActions; | |
OperationHeader header; | |
bytes signature; | |
} | |
// ============ Events ============ | |
event ContractStatusSet( | |
bool operational | |
); | |
event LogOperationExecuted( | |
bytes32 indexed operationHash, | |
address indexed signer, | |
address indexed sender | |
); | |
event LogOperationCanceled( | |
bytes32 indexed operationHash, | |
address indexed canceler | |
); | |
// ============ Immutable Storage ============ | |
// Hash of the EIP712 Domain Separator data | |
bytes32 public EIP712_DOMAIN_HASH; | |
// ============ Mutable Storage ============ | |
// true if this contract can process operationss | |
bool public g_isOperational; | |
// operation hash => was executed (or canceled) | |
mapping (bytes32 => bool) public g_invalidated; | |
// ============ Constructor ============ | |
constructor ( | |
address soloMargin, | |
uint256 chainId | |
) | |
public | |
OnlySolo(soloMargin) | |
{ | |
g_isOperational = true; | |
/* solium-disable-next-line indentation */ | |
EIP712_DOMAIN_HASH = keccak256(abi.encode( | |
EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH, | |
keccak256(bytes(EIP712_DOMAIN_NAME)), | |
keccak256(bytes(EIP712_DOMAIN_VERSION)), | |
chainId, | |
address(this) | |
)); | |
} | |
// ============ Admin Functions ============ | |
/** | |
* The owner can shut down the exchange. | |
*/ | |
function shutDown() | |
external | |
onlyOwner | |
{ | |
g_isOperational = false; | |
emit ContractStatusSet(false); | |
} | |
/** | |
* The owner can start back up the exchange. | |
*/ | |
function startUp() | |
external | |
onlyOwner | |
{ | |
g_isOperational = true; | |
emit ContractStatusSet(true); | |
} | |
// ============ Public Functions ============ | |
/** | |
* Allows a signer to permanently cancel an operation on-chain. | |
* | |
* @param accounts The accounts involved in the operation | |
* @param actions The actions involved in the operation | |
* @param auth The unsigned authorization of the operation | |
*/ | |
function cancel( | |
Account.Info[] memory accounts, | |
Actions.ActionArgs[] memory actions, | |
Authorization memory auth | |
) | |
public | |
{ | |
bytes32 operationHash = getOperationHash( | |
accounts, | |
actions, | |
auth, | |
0 | |
); | |
Require.that( | |
auth.header.signer == msg.sender, | |
FILE, | |
"Canceler must be signer" | |
); | |
g_invalidated[operationHash] = true; | |
emit LogOperationCanceled(operationHash, msg.sender); | |
} | |
/** | |
* Submits an operation to SoloMargin. Actions for accounts that the msg.sender does not control | |
* must be authorized by a signed message. Each authorization can apply to multiple actions at | |
* once which must occur in-order next to each other. An empty authorization must be supplied | |
* explicitly for each group of actions that do not require a signed message. | |
* | |
* @param accounts The accounts to forward to SoloMargin.operate() | |
* @param actions The actions to forward to SoloMargin.operate() | |
* @param auths The signed authorizations for each group of actions | |
* (or unsigned if msg.sender is already authorized) | |
*/ | |
function operate( | |
Account.Info[] memory accounts, | |
Actions.ActionArgs[] memory actions, | |
Authorization[] memory auths | |
) | |
public | |
{ | |
Require.that( | |
g_isOperational, | |
FILE, | |
"Contract is not operational" | |
); | |
// cache the index of the first action for this auth | |
uint256 actionStartIdx = 0; | |
// loop over all auths | |
for (uint256 authIdx = 0; authIdx < auths.length; authIdx++) { | |
Authorization memory auth = auths[authIdx]; | |
// require that the message is not expired | |
Require.that( | |
auth.header.expiration == 0 || auth.header.expiration >= block.timestamp, | |
FILE, | |
"Signed operation is expired", | |
authIdx | |
); | |
// require that the sender matches the authorization | |
Require.that( | |
auth.header.sender == address(0) || auth.header.sender == msg.sender, | |
FILE, | |
"Operation sender mismatch", | |
authIdx | |
); | |
// consider the signer to be msg.sender unless there is a signature | |
address signer = msg.sender; | |
// if there is a signature, then validate it | |
if (auth.signature.length != 0) { | |
// get the hash of the operation | |
bytes32 operationHash = getOperationHash( | |
accounts, | |
actions, | |
auth, | |
actionStartIdx | |
); | |
// require that this message is still valid | |
Require.that( | |
!g_invalidated[operationHash], | |
FILE, | |
"Hash already used or canceled", | |
operationHash | |
); | |
// get the signer | |
signer = TypedSignature.recover(operationHash, auth.signature); | |
// require that this signer matches the authorization | |
Require.that( | |
auth.header.signer == signer, | |
FILE, | |
"Invalid signature" | |
); | |
// consider this operationHash to be used (and therefore no longer valid) | |
g_invalidated[operationHash] = true; | |
emit LogOperationExecuted(operationHash, signer, msg.sender); | |
} | |
// cache the index of the first action after this auth | |
uint256 actionEndIdx = actionStartIdx.add(auth.numActions); | |
// loop over all actions for which this auth applies | |
for (uint256 actionIdx = actionStartIdx; actionIdx < actionEndIdx; actionIdx++) { | |
// validate primary account | |
Actions.ActionArgs memory action = actions[actionIdx]; | |
validateAccountOwner(accounts[action.accountId].owner, signer); | |
// validate second account in the case of a transfer | |
if (action.actionType == Actions.ActionType.Transfer) { | |
validateAccountOwner(accounts[action.otherAccountId].owner, signer); | |
} | |
} | |
// update actionStartIdx | |
actionStartIdx = actionEndIdx; | |
} | |
// require that all actions are signed or from msg.sender | |
Require.that( | |
actionStartIdx == actions.length, | |
FILE, | |
"Not all actions are signed" | |
); | |
// send the operation | |
SOLO_MARGIN.operate(accounts, actions); | |
} | |
// ============ Getters ============ | |
/** | |
* Returns a bool for each operation. True if the operation is invalid (from being canceled or | |
* previously executed). | |
*/ | |
function getOperationsAreInvalid( | |
bytes32[] memory operationHashes | |
) | |
public | |
view | |
returns(bool[] memory) | |
{ | |
uint256 numOperations = operationHashes.length; | |
bool[] memory output = new bool[](numOperations); | |
for (uint256 i = 0; i < numOperations; i++) { | |
output[i] = g_invalidated[operationHashes[i]]; | |
} | |
return output; | |
} | |
// ============ Private Helper Functions ============ | |
/** | |
* Validates that either the signer or the msg.sender are the accountOwner (or that either are | |
* localOperators of the accountOwner). | |
*/ | |
function validateAccountOwner( | |
address accountOwner, | |
address signer | |
) | |
private | |
view | |
{ | |
bool valid = | |
msg.sender == accountOwner | |
|| signer == accountOwner | |
|| SOLO_MARGIN.getIsLocalOperator(accountOwner, msg.sender) | |
|| SOLO_MARGIN.getIsLocalOperator(accountOwner, signer); | |
Require.that( | |
valid, | |
FILE, | |
"Signer not authorized", | |
signer | |
); | |
} | |
/** | |
* Returns the EIP712 hash of an Operation message. | |
*/ | |
function getOperationHash( | |
Account.Info[] memory accounts, | |
Actions.ActionArgs[] memory actions, | |
Authorization memory auth, | |
uint256 startIdx | |
) | |
private | |
view | |
returns (bytes32) | |
{ | |
// get the bytes32 hash of each action, then packed together | |
bytes32 actionsEncoding = getActionsEncoding( | |
accounts, | |
actions, | |
auth, | |
startIdx | |
); | |
// compute the EIP712 hashStruct of an Operation struct | |
/* solium-disable-next-line indentation */ | |
bytes32 structHash = keccak256(abi.encode( | |
EIP712_OPERATION_HASH, | |
actionsEncoding, | |
auth.header | |
)); | |
// compute eip712 compliant hash | |
/* solium-disable-next-line indentation */ | |
return keccak256(abi.encodePacked( | |
EIP191_HEADER, | |
EIP712_DOMAIN_HASH, | |
structHash | |
)); | |
} | |
/** | |
* Returns the EIP712 encodeData of an Action struct array. | |
*/ | |
function getActionsEncoding( | |
Account.Info[] memory accounts, | |
Actions.ActionArgs[] memory actions, | |
Authorization memory auth, | |
uint256 startIdx | |
) | |
private | |
pure | |
returns (bytes32) | |
{ | |
// store hash of each action | |
bytes32[] memory actionsBytes = new bytes32[](auth.numActions); | |
// for each action that corresponds to the auth | |
for (uint256 i = 0; i < auth.numActions; i++) { | |
Actions.ActionArgs memory action = actions[startIdx + i]; | |
// if action type has no second account, assume null account | |
Account.Info memory otherAccount = | |
(Actions.getAccountLayout(action.actionType) == Actions.AccountLayout.OnePrimary) | |
? Account.Info({ owner: address(0), number: 0 }) | |
: accounts[action.otherAccountId]; | |
// compute the individual hash for the action | |
/* solium-disable-next-line indentation */ | |
actionsBytes[i] = getActionHash( | |
action, | |
accounts[action.accountId], | |
otherAccount | |
); | |
} | |
return keccak256(abi.encodePacked(actionsBytes)); | |
} | |
/** | |
* Returns the EIP712 hashStruct of an Action struct. | |
*/ | |
function getActionHash( | |
Actions.ActionArgs memory action, | |
Account.Info memory primaryAccount, | |
Account.Info memory secondaryAccount | |
) | |
private | |
pure | |
returns (bytes32) | |
{ | |
/* solium-disable-next-line indentation */ | |
return keccak256(abi.encode( | |
EIP712_ACTION_HASH, | |
action.actionType, | |
primaryAccount.owner, | |
primaryAccount.number, | |
getAssetAmountHash(action.amount), | |
action.primaryMarketId, | |
action.secondaryMarketId, | |
action.otherAddress, | |
secondaryAccount.owner, | |
secondaryAccount.number, | |
keccak256(action.data) | |
)); | |
} | |
/** | |
* Returns the EIP712 hashStruct of an AssetAmount struct. | |
*/ | |
function getAssetAmountHash( | |
Types.AssetAmount memory amount | |
) | |
private | |
pure | |
returns (bytes32) | |
{ | |
/* solium-disable-next-line indentation */ | |
return keccak256(abi.encode( | |
EIP712_ASSET_AMOUNT_HASH, | |
amount.sign, | |
amount.denomination, | |
amount.ref, | |
amount.value | |
)); | |
} | |
} | |
contract SoloMargin is | |
State, | |
Admin, | |
Getters, | |
Operation, | |
Permission | |
{ | |
// ============ Constructor ============ | |
constructor( | |
Storage.RiskParams memory riskParams, | |
Storage.RiskLimits memory riskLimits | |
) | |
public | |
{ | |
g_state.riskParams = riskParams; | |
g_state.riskLimits = riskLimits; | |
} | |
} | |
contract TestSoloMargin is | |
SoloMargin | |
{ | |
using Math for uint256; | |
using SafeMath for uint256; | |
// ============ Constructor ============ | |
constructor ( | |
Storage.RiskParams memory rp, | |
Storage.RiskLimits memory rl | |
) | |
public | |
SoloMargin(rp, rl) | |
{} | |
// ============ Testing Functions ============ | |
function setAccountBalance( | |
Account.Info memory account, | |
uint256 market, | |
Types.Par memory newPar | |
) | |
public | |
{ | |
Types.Par memory oldPar = g_state.accounts[account.owner][account.number].balances[market]; | |
Types.TotalPar memory totalPar = g_state.markets[market].totalPar; | |
// roll-back oldPar | |
if (oldPar.sign) { | |
totalPar.supply = uint256(totalPar.supply).sub(oldPar.value).to128(); | |
} else { | |
totalPar.borrow = uint256(totalPar.borrow).sub(oldPar.value).to128(); | |
} | |
// roll-forward newPar | |
if (newPar.sign) { | |
totalPar.supply = uint256(totalPar.supply).add(newPar.value).to128(); | |
} else { | |
totalPar.borrow = uint256(totalPar.borrow).add(newPar.value).to128(); | |
} | |
g_state.markets[market].totalPar = totalPar; | |
g_state.accounts[account.owner][account.number].balances[market] = newPar; | |
} | |
function setAccountStatus( | |
Account.Info memory account, | |
Account.Status status | |
) | |
public | |
{ | |
g_state.accounts[account.owner][account.number].status = status; | |
} | |
function setMarketIndex( | |
uint256 market, | |
Interest.Index memory index | |
) | |
public | |
{ | |
Interest.Index memory oldIndex = g_state.markets[market].index; | |
if (index.borrow == 0) { | |
index.borrow = oldIndex.borrow; | |
} | |
if (index.supply == 0) { | |
index.supply = oldIndex.supply; | |
} | |
g_state.markets[market].index = index; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment