@dolomite-exchange/dolomite-margin
Version:
Ethereum Smart Contracts and TypeScript library used for the DolomiteMargin trading protocol
311 lines (310 loc) • 2.57 MB
JSON
{
"contractName": "Storage",
"abi": [],
"metadata": "{\"compiler\":{\"version\":\"0.5.16+commit.9c3226ce\"},\"language\":\"Solidity\",\"output\":{\"abi\":[],\"devdoc\":{\"author\":\"dYdX * Functions for reading, writing, and verifying state in DolomiteMargin\",\"methods\":{},\"title\":\"Storage\"},\"userdoc\":{\"methods\":{}}},\"settings\":{\"compilationTarget\":{\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Storage.sol\":\"Storage\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"optimizer\":{\"enabled\":true,\"runs\":10000},\"remappings\":[]},\"sources\":{\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/interfaces/IAccountRiskOverrideSetter.sol\":{\"keccak256\":\"0x5b9fde139ddfdaae7b650b3c3f6699e0e44605f0d9d3daadc1622eb06ad4c3f8\",\"urls\":[\"bzz-raw://2dfd5af24b6de63f94519c99fd8777a264d7cd11266fa2d525c7d72d96a9c7cf\",\"dweb:/ipfs/QmPg9fjhbnmBayZMh6yTG79xsK1PWSoAHA5pfwSXfnKKjF\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/interfaces/IERC20Detailed.sol\":{\"keccak256\":\"0x22a62e2a8155e70300773721902f4b9118e17aa0bb5d9d1b046050a014cfb223\",\"urls\":[\"bzz-raw://03af737a33c552fbe58f6f1fbd5496854fdc28dd57be4365d56663725c083a18\",\"dweb:/ipfs/QmbgLqGAQEd6tXsZB8QaPNCAMiU6nNzP75wNbKdS65BuUF\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/interfaces/IInterestSetter.sol\":{\"keccak256\":\"0x546825f65490a30e29d0f6446acec49c1e895ce6b33d48ca5c05092725a63a57\",\"urls\":[\"bzz-raw://6ad3013aed985bdf6e6234ae0f7f37e6bba76c3470c33bfa24678cf19b3b412c\",\"dweb:/ipfs/QmfYaSBzxGpgiHoR7ontMJY66S5L26XwkLYoEuzsEjpbQH\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/interfaces/IOracleSentinel.sol\":{\"keccak256\":\"0xa94499fda55ef112640daed2cb1d06062e72c51c7b925c323b4e0ffa9b456ce5\",\"urls\":[\"bzz-raw://18b4a16acac577590a57f34f7ec6583e3cb35f575c948abb928b308d29cac1ab\",\"dweb:/ipfs/QmR1jLkzj6w7SjrdZ7GVW4V2vofSdMJwzjTqKpeUKABvxu\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/interfaces/IPriceOracle.sol\":{\"keccak256\":\"0x3f7c3d1397390ea44c6d5e5730d49476431c3df3b28334fe38218de5edc31138\",\"urls\":[\"bzz-raw://63ebf926fca9ce33aaabaf93805686acc25daded1648d6dab59c8c4d05116918\",\"dweb:/ipfs/QmeGkDp7ycYstpfqCYBiqoCYwy876gabx8r42o5koj3H5A\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Account.sol\":{\"keccak256\":\"0x2059276ea5e478bd5ac7f2712e7b1b6c85d291c647a54b264909044844828e67\",\"urls\":[\"bzz-raw://f153777051d12131b13a57e5c3af10a2670927cfc7d22c9519ab20c9b4b9bc89\",\"dweb:/ipfs/QmQMP3qS5xDAVGYKDfNkbkSqBKozQGQsGqX4e8VyjRtxPS\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Bits.sol\":{\"keccak256\":\"0x6d9a641163b2dd25e648c65f5d9e4949a57938e29ddc4c9dddefd74a9964aeef\",\"urls\":[\"bzz-raw://67e5a04ed4055faafbc100d6ccca9c54448ed378a1a8c71171562edda7bcbf7c\",\"dweb:/ipfs/QmYRikK3Xbgiq3UBJkgoo2zNUSSrShVabpRHmQj6iQHhrc\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Cache.sol\":{\"keccak256\":\"0xe57c80a7621a77fcb2acb9f883fb5248e71a6dce03bc2db56df0db3fee332950\",\"urls\":[\"bzz-raw://28e221c04629674490b9a0041d4f9a5f15e669dc0cc192a5df6e2c701c36369f\",\"dweb:/ipfs/QmaL3oh8c7v9cx91pB175pyCEuzxnhNTab4UxmBMsDYfUx\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Decimal.sol\":{\"keccak256\":\"0xd9a348c2b1eec076609ba12bbec6b9c2e4ea135790a7ef4f5411feb050af2144\",\"urls\":[\"bzz-raw://d5bcff29e47d5cd02d7a9966e9f44c46ccbb4f0e4adfc4b0b86646880677db39\",\"dweb:/ipfs/QmRKfJD4FFM1AvTynSFNx1NcDg94o9SAcjz7dvMaJCfecp\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/DolomiteMarginMath.sol\":{\"keccak256\":\"0x53f205f6a779d579be29faa9412c806a892b3e605fff092dfd9d14d936fa019f\",\"urls\":[\"bzz-raw://0ff936f89602a63b8e6a4eb4ce8f87673c0f45b7ce29694920905a3f0b5a6a69\",\"dweb:/ipfs/QmbYME9BeEvPC1ndV8EaUDX5fDMghLc3rocNUxFtgHmg8m\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/EnumerableSet.sol\":{\"keccak256\":\"0x41a3460e34b7b8635d936e35e6568ceb5354b638f53b884530e0d4875fa0f151\",\"urls\":[\"bzz-raw://d1a5ba866ee95d455afe5075dadd91fc71c2806371f2c1b358a1f67c632bb0df\",\"dweb:/ipfs/QmW4qCjmsG8J3UJv97GwDLDM7wt1K4yLDKAwzWMY7TFYi5\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Interest.sol\":{\"keccak256\":\"0x141a0ecb4a746f0e5767df4b99dbe33c6362df507ae9d5c02da1c1c17bc26df3\",\"urls\":[\"bzz-raw://9ff03e64a8ce5231acde07788624bf8c7a8ff55d90b157fa32f62fa887a0eff0\",\"dweb:/ipfs/QmUVxxWeE954RDXbmHJAYnHsXcit6rSXGqZUigu8t9oH2z\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Monetary.sol\":{\"keccak256\":\"0x5257274f2d4d3605f3e97ec45fb902111fe9a22506f0b4cf308ea933bc81bfe2\",\"urls\":[\"bzz-raw://85b8023b78805063a2ec8d376f1f5983994eabd96efd8ceadb0224dbb71c5678\",\"dweb:/ipfs/QmaDhzCzRqsuHuJfhToEC4pATQHwhh7yWWsjwfuRzFT1kG\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Require.sol\":{\"keccak256\":\"0x05a2a90b41b6a5f42f0a72da63d015fb0b406a9ba2172823352e522e8bf3a606\",\"urls\":[\"bzz-raw://19883f0c6d33266f756ec5c3d17539524aa24b993c46c33f8400801d09373a6c\",\"dweb:/ipfs/QmYX2fwK3vQQDSZLMrc5wMfeb8RWrcC9CGX8XECLty8QDk\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Storage.sol\":{\"keccak256\":\"0x9c27f3cd52af28c9b6bca18097b1793f114d08284d78417467d0d05f67e9e2a9\",\"urls\":[\"bzz-raw://d1ed0e7a7f76d59f30572996d4e164ab8fb5b2ec4bfca127983f9e936b904813\",\"dweb:/ipfs/QmePVYbgrY8Yw3Vf76pmEvE9pX916Jaa9we4kjM8tGM4J1\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Time.sol\":{\"keccak256\":\"0x87ee8d3c1d6e315a116426d8c8fc6f083e220fd14dda27fad4e8d7cab3dc7305\",\"urls\":[\"bzz-raw://7c20f2c52da682c3b5917fb6a19e81f43699b1d13c62b569ed723f34da72e766\",\"dweb:/ipfs/QmbXWYXi67nHBVNDKYUpo2E9ZaEVcHLvmXQkpYLMm1d5Vm\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Token.sol\":{\"keccak256\":\"0x095b5f8eb3665f6e246200311efc8a654fa3e90f9a1a522e27a4d089e8ab3917\",\"urls\":[\"bzz-raw://5db812b752a03d601d424e6e21be38153477082af0acf51b5c98eefa5553ed18\",\"dweb:/ipfs/QmbE549QUXYTurpx8hSMLBJvuBpfYqyNE6FBCgHJDY8PLf\"]},\"/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Types.sol\":{\"keccak256\":\"0xe1bab8c8799d83e86c7a49aaf815d8e3bc8d09562d46d44d921500db5e98de0e\",\"urls\":[\"bzz-raw://8ea3d0cc4fdab6dd9bba545a792170f939b342d8d1a7051c1c6365009af72658\",\"dweb:/ipfs/QmaLJgbavu88eEit6JPqfWnw6NmGBDZfveMXxumgWywBFG\"]},\"@openzeppelin/contracts/math/SafeMath.sol\":{\"keccak256\":\"0x640b6dee7a4b830bdfd52b5031a07fc2b12209f5b2e29e5d364a7d37f69d8076\",\"urls\":[\"bzz-raw://31113152e1ddb78fe7a4197f247591ca894e93f916867beb708d8e747b6cc74f\",\"dweb:/ipfs/QmbZaJyXdpsYGykVhHH9qpVGQg9DGCxE2QufbCUy3daTgq\"]},\"@openzeppelin/contracts/token/ERC20/IERC20.sol\":{\"keccak256\":\"0xe5bb0f57cff3e299f360052ba50f1ea0fff046df2be070b6943e0e3c3fdad8a9\",\"urls\":[\"bzz-raw://59fd025151435da35faa8093a5c7a17de02de9d08ad27275c5cdf05050820d91\",\"dweb:/ipfs/QmQMvwEcPhoRXzbXyrdoeRtvLoifUW9Qh7Luho7bmUPRkc\"]}},\"version\":1}",
"bytecode": "0x60636023600b82828239805160001a607314601657fe5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea365627a7a723158202cab56a0144624927194908c280a787ebedd61a64cd468ab74d291b6a3e08da76c6578706572696d656e74616cf564736f6c63430005100040",
"deployedBytecode": "0x73000000000000000000000000000000000000000030146080604052600080fdfea365627a7a723158202cab56a0144624927194908c280a787ebedd61a64cd468ab74d291b6a3e08da76c6578706572696d656e74616cf564736f6c63430005100040",
"sourceMap": "1719:31090:101:-;;132:2:-1;166:7;155:9;146:7;137:37;255:7;249:14;246:1;241:23;235:4;232:33;222:2;;269:9;222:2;293:9;290:1;283:20;323:4;314:7;306:22;347:7;338;331:24",
"deployedSourceMap": "1719:31090:101:-;;;;;;;;",
"source": "/*\n\n Copyright 2019 dYdX Trading Inc.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n*/\n\npragma solidity ^0.5.7;\npragma experimental ABIEncoderV2;\n\nimport { SafeMath } from \"@openzeppelin/contracts/math/SafeMath.sol\";\n\nimport { Account } from \"./Account.sol\";\nimport { Bits } from \"./Bits.sol\";\nimport { Cache } from \"./Cache.sol\";\nimport { Decimal } from \"./Decimal.sol\";\nimport { Interest } from \"./Interest.sol\";\nimport { EnumerableSet } from \"./EnumerableSet.sol\";\nimport { DolomiteMarginMath } from \"./DolomiteMarginMath.sol\";\nimport { Monetary } from \"./Monetary.sol\";\nimport { Require } from \"./Require.sol\";\nimport { Time } from \"./Time.sol\";\nimport { Token } from \"./Token.sol\";\nimport { Types } from \"./Types.sol\";\n\nimport { IAccountRiskOverrideSetter } from \"../interfaces/IAccountRiskOverrideSetter.sol\";\nimport { IERC20Detailed } from \"../interfaces/IERC20Detailed.sol\";\nimport { IInterestSetter } from \"../interfaces/IInterestSetter.sol\";\nimport { IOracleSentinel } from \"../interfaces/IOracleSentinel.sol\";\nimport { IPriceOracle } from \"../interfaces/IPriceOracle.sol\";\n\n\n/**\n * @title Storage\n * @author dYdX\n *\n * Functions for reading, writing, and verifying state in DolomiteMargin\n */\nlibrary Storage {\n using Cache for Cache.MarketCache;\n using Storage for Storage.State;\n using DolomiteMarginMath for uint256;\n using Types for Types.Par;\n using Types for Types.Wei;\n using SafeMath for uint256;\n using EnumerableSet for EnumerableSet.Set;\n\n // ============ Constants ============\n\n bytes32 private constant FILE = \"Storage\";\n\n // ============ Structs ============\n\n // All information necessary for tracking a market\n struct Market {\n // Contract address of the associated ERC20 token\n address token;\n\n // Whether additional borrows are allowed for this market\n bool isClosing;\n\n // Total aggregated supply and borrow amount of the entire market\n Types.TotalPar totalPar;\n\n // Interest index of the market\n Interest.Index index;\n\n // Contract address of the price oracle for this market\n IPriceOracle priceOracle;\n\n // Contract address of the interest setter for this market\n IInterestSetter interestSetter;\n\n // Multiplier on the marginRatio for this market, IE 5% (0.05 * 1e18). This number increases the market's\n // required collateralization by: reducing the user's supplied value (in terms of dollars) for this market and\n // increasing its borrowed value. This is done through the following operation:\n // `suppliedWei = suppliedWei + (assetValueForThisMarket / (1 + marginPremium))`\n // This number increases the user's borrowed wei by multiplying it by:\n // `borrowedWei = borrowedWei + (assetValueForThisMarket * (1 + marginPremium))`\n Decimal.D256 marginPremium;\n\n // Multiplier on the liquidationSpread for this market, IE 20% (0.2 * 1e18). This number increases the\n // `liquidationSpread` using the following formula:\n // `liquidationSpread = liquidationSpread * (1 + spreadPremium)`\n // NOTE: This formula is applied up to two times - one for each market whose spreadPremium is greater than 0\n // (when performing a liquidation between two markets)\n Decimal.D256 liquidationSpreadPremium;\n\n // The maximum amount that can be held by the protocol. This allows the protocol to cap any additional risk\n // that is inferred by allowing borrowing against low-cap or assets with increased volatility. Setting this\n // value to 0 is analogous to having no limit. This value can never be below 0.\n Types.Wei maxSupplyWei;\n\n // The maximum amount that can be borrowed by the protocol. This allows the protocol to cap any additional risk\n // that is inferred by allowing borrowing against low-cap or assets with increased volatility. Setting this\n // value to 0 is analogous to having no limit. This value can never be greater than 0.\n Types.Wei maxBorrowWei;\n\n // The percentage of interest paid that is passed along from borrowers to suppliers. Setting this to 0 will\n // default to RiskParams.earningsRate.\n Decimal.D256 earningsRateOverride;\n }\n\n // The global risk parameters that govern the health and security of the system\n struct RiskParams {\n // Required ratio of over-collateralization\n Decimal.D256 marginRatio;\n\n // Percentage penalty incurred by liquidated accounts\n Decimal.D256 liquidationSpread;\n\n // Percentage of the borrower's interest fee that gets passed to the suppliers\n Decimal.D256 earningsRate;\n\n // The minimum absolute borrow value of an account\n // There must be sufficient incentivize to liquidate undercollateralized accounts\n Monetary.Value minBorrowedValue;\n\n // The maximum number of markets a user can have a non-zero balance for a given account.\n uint256 accountMaxNumberOfMarketsWithBalances;\n\n // The oracle sentinel used to disable borrowing/liquidations if the sequencer goes down\n IOracleSentinel oracleSentinel;\n\n // The gas limit used for making callbacks via `IExternalCallback::onInternalBalanceChange` to smart contract\n // wallets. Setting to 0 will effectively disable callbacks; setting it super large is not desired since it\n // could lead to DOS attacks on the protocol; however, hard coding a max value isn't preferred since some chains\n // can calculate gas usage differently (like ArbGas before Arbitrum rolled out nitro)\n uint256 callbackGasLimit;\n\n // The default account risk override setter. By default, we ping this for any overrides in risk controls,\n // if the `accountRiskOverrideSetterMap` resolves to 0x0. If this value is set to `0x0` there is no default.\n IAccountRiskOverrideSetter defaultAccountRiskOverrideSetter;\n\n // Certain addresses are allowed to borrow with different LTV requirements. When an account's risk is overrode,\n // the global risk parameters are ignored and the account's risk parameters are used instead.\n mapping(address => IAccountRiskOverrideSetter) accountRiskOverrideSetterMap;\n }\n\n // The maximum RiskParam values that can be set\n struct RiskLimits {\n // The highest that the ratio can be for liquidating under-water accounts\n uint64 marginRatioMax;\n // The highest that the liquidation rewards can be when a liquidator liquidates an account\n uint64 liquidationSpreadMax;\n // The highest that the supply APR can be for a market, as a proportion of the borrow rate. Meaning, a rate of\n // 100% (1e18) would give suppliers all of the interest that borrowers are paying. A rate of 90% would give\n // suppliers 90% of the interest that borrowers pay.\n uint64 earningsRateMax;\n // The highest min margin ratio premium that can be applied to a particular market. Meaning, a value of 100%\n // (1e18) would require borrowers to maintain an extra 100% collateral to maintain a healthy margin ratio. This\n // value works by increasing the debt owed and decreasing the supply held for the particular market by this\n // amount, plus 1e18 (since a value of 10% needs to be applied as `decimal.plusOne`)\n uint64 marginPremiumMax;\n // The highest liquidation reward that can be applied to a particular market. This percentage is applied\n // in addition to the liquidation spread in `RiskParams`. Meaning a value of 1e18 is 100%. It is calculated as:\n // `liquidationSpread * Decimal.onePlus(spreadPremium)`\n uint64 liquidationSpreadPremiumMax;\n // The highest that the borrow interest rate can ever be. If the rate returned is ever higher, the rate is\n // capped at this value instead of reverting. The goal is to keep Dolomite operational under all circumstances\n // instead of inadvertently DOS'ing the protocol.\n uint96 interestRateMax;\n // The highest that the minBorrowedValue can be. This is the minimum amount of value that must be borrowed.\n // Typically a value of $100 (100 * 1e18) is more than sufficient.\n uint128 minBorrowedValueMax;\n }\n\n // The entire storage state of DolomiteMargin\n struct State {\n // number of markets\n uint256 numMarkets;\n\n // marketId => Market\n mapping (uint256 => Market) markets;\n\n // token address => marketId\n mapping (address => uint256) tokenToMarketId;\n\n // owner => account number => Account\n mapping (address => mapping (uint256 => Account.Storage)) accounts;\n\n // Addresses that can control other users accounts\n mapping (address => mapping (address => uint256)) operators;\n\n // Addresses that can control all users accounts\n mapping (address => uint256) globalOperators;\n\n // Addresses of auto traders that can only be called by global operators. IE for expirations\n mapping (address => uint256) specialAutoTraders;\n\n // mutable risk parameters of the system\n RiskParams riskParams;\n\n // immutable risk limits of the system\n RiskLimits riskLimits;\n }\n\n // ============ Functions ============\n\n function getToken(\n Storage.State storage state,\n uint256 marketId\n )\n internal\n view\n returns (address)\n {\n return state.markets[marketId].token;\n }\n\n function getTotalPar(\n Storage.State storage state,\n uint256 marketId\n )\n internal\n view\n returns (Types.TotalPar memory)\n {\n return state.markets[marketId].totalPar;\n }\n\n function getMaxSupplyWei(\n Storage.State storage state,\n uint256 marketId\n )\n internal\n view\n returns (Types.Wei memory)\n {\n return state.markets[marketId].maxSupplyWei;\n }\n\n function getMaxBorrowWei(\n Storage.State storage state,\n uint256 marketId\n )\n internal\n view\n returns (Types.Wei memory)\n {\n return state.markets[marketId].maxBorrowWei;\n }\n\n function getIndex(\n Storage.State storage state,\n uint256 marketId\n )\n internal\n view\n returns (Interest.Index memory)\n {\n return state.markets[marketId].index;\n }\n\n function getNumExcessTokens(\n Storage.State storage state,\n uint256 marketId\n )\n internal\n view\n returns (Types.Wei memory)\n {\n Interest.Index memory index = state.getIndex(marketId);\n Types.TotalPar memory totalPar = state.getTotalPar(marketId);\n\n address token = state.getToken(marketId);\n\n Types.Wei memory balanceWei = Types.Wei({\n sign: true,\n value: IERC20Detailed(token).balanceOf(address(this))\n });\n\n (\n Types.Wei memory supplyWei,\n Types.Wei memory borrowWei\n ) = Interest.totalParToWei(totalPar, index);\n\n // borrowWei is negative, so subtracting it makes the value more positive\n return balanceWei.sub(borrowWei).sub(supplyWei);\n }\n\n function getStatus(\n Storage.State storage state,\n Account.Info memory account\n )\n internal\n view\n returns (Account.Status)\n {\n return state.accounts[account.owner][account.number].status;\n }\n\n function getPar(\n Storage.State storage state,\n Account.Info memory account,\n uint256 marketId\n )\n internal\n view\n returns (Types.Par memory)\n {\n return state.accounts[account.owner][account.number].balances[marketId];\n }\n\n function getWei(\n Storage.State storage state,\n Account.Info memory account,\n uint256 marketId,\n Interest.Index memory index\n )\n internal\n view\n returns (Types.Wei memory)\n {\n Types.Par memory par = state.getPar(account, marketId);\n\n if (par.isZero()) {\n return Types.zeroWei();\n }\n\n return Interest.parToWei(par, index);\n }\n\n function getMarketsWithBalances(\n Storage.State storage state,\n Account.Info memory account\n )\n internal\n view\n returns (uint256[] memory)\n {\n return state.accounts[account.owner][account.number].marketsWithNonZeroBalanceSet.values();\n }\n\n function getAccountMarketWithBalanceAtIndex(\n Storage.State storage state,\n Account.Info memory account,\n uint256 index\n )\n internal\n view\n returns (uint256)\n {\n return state.accounts[account.owner][account.number].marketsWithNonZeroBalanceSet.getAtIndex(index);\n }\n\n function getNumberOfMarketsWithBalances(\n Storage.State storage state,\n Account.Info memory account\n )\n internal\n view\n returns (uint256)\n {\n return state.accounts[account.owner][account.number].marketsWithNonZeroBalanceSet.length();\n }\n\n function getAccountNumberOfMarketsWithDebt(\n Storage.State storage state,\n Account.Info memory account\n )\n internal\n view\n returns (uint256)\n {\n return state.accounts[account.owner][account.number].numberOfMarketsWithDebt;\n }\n\n function getLiquidationSpreadForAccountAndPair(\n Storage.State storage state,\n Account.Info memory account,\n uint256 heldMarketId,\n uint256 owedMarketId\n )\n internal\n view\n returns (Decimal.D256 memory)\n {\n (, Decimal.D256 memory liquidationSpreadOverride) = getAccountRiskOverride(state, account);\n if (liquidationSpreadOverride.value != 0) {\n return liquidationSpreadOverride;\n }\n\n uint256 result = state.riskParams.liquidationSpread.value;\n result = Decimal.mul(result, Decimal.onePlus(state.markets[heldMarketId].liquidationSpreadPremium));\n result = Decimal.mul(result, Decimal.onePlus(state.markets[owedMarketId].liquidationSpreadPremium));\n return Decimal.D256({\n value: result\n });\n }\n\n function fetchNewIndex(\n Storage.State storage state,\n uint256 marketId,\n Interest.Index memory index\n )\n internal\n view\n returns (Interest.Index memory)\n {\n Interest.Rate memory rate = state.fetchInterestRate(marketId, index);\n\n Decimal.D256 memory earningsRate = state.markets[marketId].earningsRateOverride;\n if (earningsRate.value == 0) {\n // The earnings rate was not override, fall back to the global one\n earningsRate = state.riskParams.earningsRate;\n }\n\n return Interest.calculateNewIndex(\n index,\n rate,\n state.getTotalPar(marketId),\n earningsRate\n );\n }\n\n function fetchInterestRate(\n Storage.State storage state,\n uint256 marketId,\n Interest.Index memory index\n )\n internal\n view\n returns (Interest.Rate memory)\n {\n Types.TotalPar memory totalPar = state.getTotalPar(marketId);\n (\n Types.Wei memory supplyWei,\n Types.Wei memory borrowWei\n ) = Interest.totalParToWei(totalPar, index);\n\n Interest.Rate memory rate = state.markets[marketId].interestSetter.getInterestRate(\n state.getToken(marketId),\n borrowWei.value,\n supplyWei.value\n );\n\n if (rate.value > state.riskLimits.interestRateMax) {\n // Cap the interest rate at the max instead of reverting. We don't want to DOS the protocol\n rate.value = state.riskLimits.interestRateMax;\n }\n\n return rate;\n }\n\n function fetchPrice(\n Storage.State storage state,\n uint256 marketId,\n address token\n )\n internal\n view\n returns (Monetary.Price memory)\n {\n IPriceOracle oracle = IPriceOracle(state.markets[marketId].priceOracle);\n Monetary.Price memory price = oracle.getPrice(token);\n Require.that(\n price.value != 0,\n FILE,\n \"Price cannot be zero\",\n marketId\n );\n return price;\n }\n\n // solium-disable-next-line security/no-assign-params\n function getAccountValues(\n Storage.State storage state,\n Account.Info memory account,\n Cache.MarketCache memory cache,\n bool adjustForLiquidity,\n Decimal.D256 memory marginRatioOverride\n )\n internal\n view\n returns (Monetary.Value memory, Monetary.Value memory)\n {\n Monetary.Value memory supplyValue;\n Monetary.Value memory borrowValue;\n\n // Only adjust for liquidity if prompted AND if there is no override\n adjustForLiquidity = adjustForLiquidity && marginRatioOverride.value == 0;\n\n uint256 numMarkets = cache.getNumMarkets();\n for (uint256 i; i < numMarkets; ++i) {\n Types.Wei memory userWei = state.getWei(account, cache.getAtIndex(i).marketId, cache.getAtIndex(i).index);\n\n if (userWei.isZero()) {\n continue;\n }\n\n Decimal.D256 memory adjust = Decimal.one();\n if (adjustForLiquidity) {\n adjust = Decimal.onePlus(state.markets[cache.getAtIndex(i).marketId].marginPremium);\n }\n\n uint256 assetValue = userWei.value.mul(cache.getAtIndex(i).price.value);\n if (userWei.sign) {\n supplyValue.value = supplyValue.value.add(Decimal.div(assetValue, adjust));\n } else {\n borrowValue.value = borrowValue.value.add(Decimal.mul(assetValue, adjust));\n }\n }\n\n return (supplyValue, borrowValue);\n }\n\n function isCollateralized(\n Storage.State storage state,\n Account.Info memory account,\n Cache.MarketCache memory cache,\n bool requireMinBorrow\n )\n internal\n view\n returns (bool)\n {\n if (state.getAccountNumberOfMarketsWithDebt(account) == 0) {\n // The user does not have a balance with a borrow amount, so they must be collateralized\n return true;\n }\n\n // get account values (adjusted for liquidity, if there isn't a margin ratio override)\n (Decimal.D256 memory marginRatio,) = getAccountRiskOverride(state, account);\n (\n Monetary.Value memory supplyValue,\n Monetary.Value memory borrowValue\n ) = state.getAccountValues(\n account,\n cache,\n /* adjustForLiquidity = */ true,\n marginRatio\n );\n\n if (requireMinBorrow) {\n Require.that(\n borrowValue.value >= state.riskParams.minBorrowedValue.value,\n FILE,\n \"Borrow value too low\",\n account.owner,\n account.number\n );\n }\n\n if (marginRatio.value == 0) {\n marginRatio = state.riskParams.marginRatio;\n }\n\n uint256 requiredMargin = Decimal.mul(borrowValue.value, marginRatio);\n\n return supplyValue.value >= borrowValue.value.add(requiredMargin);\n }\n\n function isGlobalOperator(\n Storage.State storage state,\n address operator\n )\n internal\n view\n returns (bool)\n {\n return state.globalOperators[operator] == 1;\n }\n\n function isAutoTraderSpecial(\n Storage.State storage state,\n address autoTrader\n )\n internal\n view\n returns (bool)\n {\n return state.specialAutoTraders[autoTrader] == 1;\n }\n\n function isLocalOperator(\n Storage.State storage state,\n address owner,\n address operator\n )\n internal\n view\n returns (bool)\n {\n return state.operators[owner][operator] == 1;\n }\n\n function requireIsGlobalOperator(\n Storage.State storage state,\n address operator\n )\n internal\n view\n {\n bool isValidOperator = state.isGlobalOperator(operator);\n\n Require.that(\n isValidOperator,\n FILE,\n \"Unpermissioned global operator\",\n operator\n );\n }\n\n function requireIsOperator(\n Storage.State storage state,\n Account.Info memory account,\n address operator\n )\n internal\n view\n {\n bool isValidOperator =\n operator == account.owner\n || state.isGlobalOperator(operator)\n || state.isLocalOperator(account.owner, operator);\n\n Require.that(\n isValidOperator,\n FILE,\n \"Unpermissioned operator\",\n operator\n );\n }\n\n function getAccountRiskOverride(\n Storage.State storage state,\n Account.Info memory account\n )\n internal\n view\n returns (Decimal.D256 memory marginRatioOverride, Decimal.D256 memory liquidationSpreadOverride)\n {\n IAccountRiskOverrideSetter riskOverrideSetter = state.riskParams.accountRiskOverrideSetterMap[account.owner];\n if (address(riskOverrideSetter) != address(0)) {\n (marginRatioOverride, liquidationSpreadOverride) = riskOverrideSetter.getAccountRiskOverride(account);\n validateAccountRiskOverrideValues(state, marginRatioOverride, liquidationSpreadOverride);\n return (marginRatioOverride, liquidationSpreadOverride);\n }\n\n riskOverrideSetter = state.riskParams.defaultAccountRiskOverrideSetter;\n if (address(riskOverrideSetter) != address(0)) {\n (marginRatioOverride, liquidationSpreadOverride) = riskOverrideSetter.getAccountRiskOverride(account);\n validateAccountRiskOverrideValues(state, marginRatioOverride, liquidationSpreadOverride);\n return (marginRatioOverride, liquidationSpreadOverride);\n } else {\n marginRatioOverride = Decimal.zero();\n liquidationSpreadOverride = Decimal.zero();\n return (marginRatioOverride, liquidationSpreadOverride);\n }\n }\n\n /**\n * Determine and set an account's balance based on the intended balance change. Return the\n * equivalent amount in wei\n */\n function getNewParAndDeltaWei(\n Storage.State storage state,\n Account.Info memory account,\n uint256 marketId,\n Interest.Index memory index,\n Types.AssetAmount memory amount\n )\n internal\n view\n returns (Types.Par memory, Types.Wei memory)\n {\n Types.Par memory oldPar = state.getPar(account, marketId);\n\n if (amount.value == 0 && amount.ref == Types.AssetReference.Delta) {\n return (oldPar, Types.zeroWei());\n }\n\n Types.Wei memory oldWei = Interest.parToWei(oldPar, index);\n Types.Par memory newPar;\n Types.Wei memory deltaWei;\n\n if (amount.denomination == Types.AssetDenomination.Wei) {\n deltaWei = Types.Wei({\n sign: amount.sign,\n value: amount.value\n });\n if (amount.ref == Types.AssetReference.Target) {\n deltaWei = deltaWei.sub(oldWei);\n }\n newPar = Interest.weiToPar(oldWei.add(deltaWei), index);\n } else { // AssetDenomination.Par\n newPar = Types.Par({\n sign: amount.sign,\n value: amount.value.to128()\n });\n if (amount.ref == Types.AssetReference.Delta) {\n newPar = oldPar.add(newPar);\n }\n deltaWei = Interest.parToWei(newPar, index).sub(oldWei);\n }\n\n return (newPar, deltaWei);\n }\n\n function getNewParAndDeltaWeiForLiquidation(\n Storage.State storage state,\n Account.Info memory account,\n uint256 marketId,\n Interest.Index memory index,\n Types.AssetAmount memory amount\n )\n internal\n view\n returns (Types.Par memory, Types.Wei memory)\n {\n Types.Par memory oldPar = state.getPar(account, marketId);\n\n Require.that(\n !oldPar.isPositive(),\n FILE,\n \"Owed balance cannot be positive\",\n account.owner,\n account.number\n );\n\n (\n Types.Par memory newPar,\n Types.Wei memory deltaWei\n ) = state.getNewParAndDeltaWei(\n account,\n marketId,\n index,\n amount\n );\n\n // if attempting to over-repay the owed asset, bound it by the maximum\n if (newPar.isPositive()) {\n newPar = Types.zeroPar();\n deltaWei = state.getWei(account, marketId, index).negative();\n }\n\n Require.that(\n !deltaWei.isNegative() && oldPar.value >= newPar.value,\n FILE,\n \"Owed balance cannot increase\",\n account.owner,\n account.number\n );\n\n // if not paying back enough wei to repay any par, then bound wei to zero\n if (oldPar.equals(newPar)) {\n deltaWei = Types.zeroWei();\n }\n\n return (newPar, deltaWei);\n }\n\n function isVaporizable(\n Storage.State storage state,\n Account.Info memory account,\n Cache.MarketCache memory cache\n )\n internal\n view\n returns (bool)\n {\n bool hasNegative = false;\n uint256 numMarkets = cache.getNumMarkets();\n for (uint256 i; i < numMarkets; ++i) {\n Types.Par memory par = state.getPar(account, cache.getAtIndex(i).marketId);\n if (par.isZero()) {\n continue;\n } else if (par.sign) {\n return false;\n } else {\n hasNegative = true;\n }\n }\n return hasNegative;\n }\n\n function validateAccountRiskOverrideValues(\n Storage.State storage state,\n Decimal.D256 memory marginRatioOverride,\n Decimal.D256 memory liquidationSpreadOverride\n ) internal view {\n Require.that(\n marginRatioOverride.value <= state.riskLimits.marginRatioMax,\n FILE,\n \"Ratio too high\"\n );\n Require.that(\n liquidationSpreadOverride.value <= state.riskLimits.liquidationSpreadMax,\n FILE,\n \"Spread too high\"\n );\n\n if (marginRatioOverride.value != 0 && liquidationSpreadOverride.value != 0) {\n Require.that(\n liquidationSpreadOverride.value < marginRatioOverride.value,\n FILE,\n \"Spread cannot be >= ratio\"\n );\n } else {\n Require.that(\n liquidationSpreadOverride.value == 0 && marginRatioOverride.value == 0,\n FILE,\n \"Spread and ratio must both be 0\"\n );\n }\n }\n\n // =============== Setter Functions ===============\n\n function updateIndex(\n Storage.State storage state,\n uint256 marketId\n )\n internal\n returns (Interest.Index memory)\n {\n Interest.Index memory index = state.getIndex(marketId);\n if (index.lastUpdate == Time.currentTime()) {\n return index;\n }\n return state.markets[marketId].index = state.fetchNewIndex(marketId, index);\n }\n\n function setStatus(\n Storage.State storage state,\n Account.Info memory account,\n Account.Status status\n )\n internal\n {\n state.accounts[account.owner][account.number].status = status;\n }\n\n function setPar(\n Storage.State storage state,\n Account.Info memory account,\n uint256 marketId,\n Types.Par memory newPar\n )\n internal\n {\n Types.Par memory oldPar = state.getPar(account, marketId);\n\n if (Types.equals(oldPar, newPar)) {\n // GUARD statement\n return;\n }\n\n // updateTotalPar\n Types.TotalPar memory totalPar = state.getTotalPar(marketId);\n\n // roll-back oldPar\n if (oldPar.sign) {\n totalPar.supply = uint256(totalPar.supply).sub(oldPar.value).to128();\n } else {\n totalPar.borrow = uint256(totalPar.borrow).sub(oldPar.value).to128();\n }\n\n // roll-forward newPar\n if (newPar.sign) {\n totalPar.supply = uint256(totalPar.supply).add(newPar.value).to128();\n } else {\n totalPar.borrow = uint256(totalPar.borrow).add(newPar.value).to128();\n }\n\n if (oldPar.isLessThanZero() && newPar.isGreaterThanOrEqualToZero()) {\n // user went from borrowing to repaying or positive\n state.accounts[account.owner][account.number].numberOfMarketsWithDebt -= 1;\n } else if (oldPar.isGreaterThanOrEqualToZero() && newPar.isLessThanZero()) {\n // user went from zero or positive to borrowing\n state.accounts[account.owner][account.number].numberOfMarketsWithDebt += 1;\n }\n\n if (newPar.isZero() && (!oldPar.isZero())) {\n // User went from a non-zero balance to zero. Remove the market from the set.\n state.accounts[account.owner][account.number].marketsWithNonZeroBalanceSet.remove(marketId);\n } else if ((!newPar.isZero()) && oldPar.isZero()) {\n // User went from zero to non-zero. Add the market to the set.\n state.accounts[account.owner][account.number].marketsWithNonZeroBalanceSet.add(marketId);\n }\n\n state.markets[marketId].totalPar = totalPar;\n state.accounts[account.owner][account.number].balances[marketId] = newPar;\n }\n\n /**\n * Determine and set an account's balance based on a change in wei\n */\n function setParFromDeltaWei(\n Storage.State storage state,\n Account.Info memory account,\n uint256 marketId,\n Interest.Index memory index,\n Types.Wei memory deltaWei\n )\n internal\n {\n if (deltaWei.isZero()) {\n return;\n }\n Types.Wei memory oldWei = state.getWei(account, marketId, index);\n Types.Wei memory newWei = oldWei.add(deltaWei);\n Types.Par memory newPar = Interest.weiToPar(newWei, index);\n state.setPar(\n account,\n marketId,\n newPar\n );\n }\n\n /**\n * Initializes the cache using the set bits\n */\n function initializeCache(\n Storage.State storage state,\n Cache.MarketCache memory cache,\n bool fetchFreshIndex\n ) internal view {\n cache.markets = new Cache.MarketInfo[](cache.marketsLength);\n\n // Really neat byproduct of iterating through a bitmap using the least significant bit, where each set flag\n // represents the marketId, --> the initialized `cache.markets` array is sorted in O(n)!\n // Meaning, this function call is O(n) where `n` is the number of markets in the cache\n uint256 marketBitmapsLength = cache.marketBitmaps.length;\n for (uint256 i; i < marketBitmapsLength; ++i) {\n uint256 bitmap = cache.marketBitmaps[i];\n while (bitmap != 0) {\n uint256 nextSetBit = Bits.getLeastSignificantBit(bitmap);\n uint256 marketId = Bits.getMarketIdFromBit(i, nextSetBit);\n address token = state.getToken(marketId);\n Types.TotalPar memory totalPar = state.getTotalPar(marketId);\n Interest.Index memory index = state.getIndex(marketId);\n cache.markets[cache.counter++] = Cache.MarketInfo({\n marketId: marketId,\n token: token,\n isClosing: state.markets[marketId].isClosing,\n borrowPar: totalPar.borrow,\n supplyPar: totalPar.supply,\n index: fetchFreshIndex ? state.fetchNewIndex(marketId, index) : index,\n price: state.fetchPrice(marketId, token)\n });\n\n // unset the set bit\n bitmap = Bits.unsetBit(bitmap, nextSetBit);\n }\n if (cache.counter == cache.marketsLength) {\n break;\n }\n }\n\n assert(cache.marketsLength == cache.counter);\n }\n}\n",
"sourcePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Storage.sol",
"ast": {
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Storage.sol",
"exportedSymbols": {
"Storage": [
30711
]
},
"id": 30712,
"nodeType": "SourceUnit",
"nodes": [
{
"id": 28605,
"literals": [
"solidity",
"^",
"0.5",
".7"
],
"nodeType": "PragmaDirective",
"src": "603:23:101"
},
{
"id": 28606,
"literals": [
"experimental",
"ABIEncoderV2"
],
"nodeType": "PragmaDirective",
"src": "627:33:101"
},
{
"absolutePath": "@openzeppelin/contracts/math/SafeMath.sol",
"file": "@openzeppelin/contracts/math/SafeMath.sol",
"id": 28608,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 37366,
"src": "662:69:101",
"symbolAliases": [
{
"foreign": 28607,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Account.sol",
"file": "./Account.sol",
"id": 28610,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 24886,
"src": "733:40:101",
"symbolAliases": [
{
"foreign": 28609,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Bits.sol",
"file": "./Bits.sol",
"id": 28612,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 25657,
"src": "774:34:101",
"symbolAliases": [
{
"foreign": 28611,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Cache.sol",
"file": "./Cache.sol",
"id": 28614,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 25933,
"src": "809:36:101",
"symbolAliases": [
{
"foreign": 28613,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Decimal.sol",
"file": "./Decimal.sol",
"id": 28616,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 26024,
"src": "846:40:101",
"symbolAliases": [
{
"foreign": 28615,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Interest.sol",
"file": "./Interest.sol",
"id": 28618,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 27751,
"src": "887:42:101",
"symbolAliases": [
{
"foreign": 28617,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/EnumerableSet.sol",
"file": "./EnumerableSet.sol",
"id": 28620,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 26429,
"src": "930:52:101",
"symbolAliases": [
{
"foreign": 28619,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/DolomiteMarginMath.sol",
"file": "./DolomiteMarginMath.sol",
"id": 28622,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 26239,
"src": "983:62:101",
"symbolAliases": [
{
"foreign": 28621,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Monetary.sol",
"file": "./Monetary.sol",
"id": 28624,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 27761,
"src": "1046:42:101",
"symbolAliases": [
{
"foreign": 28623,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Require.sol",
"file": "./Require.sol",
"id": 28626,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 28453,
"src": "1089:40:101",
"symbolAliases": [
{
"foreign": 28625,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Time.sol",
"file": "./Time.sol",
"id": 28628,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 30730,
"src": "1130:34:101",
"symbolAliases": [
{
"foreign": 28627,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Token.sol",
"file": "./Token.sol",
"id": 28630,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 30851,
"src": "1165:36:101",
"symbolAliases": [
{
"foreign": 28629,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/lib/Types.sol",
"file": "./Types.sol",
"id": 28632,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 31362,
"src": "1202:36:101",
"symbolAliases": [
{
"foreign": 28631,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/interfaces/IAccountRiskOverrideSetter.sol",
"file": "../interfaces/IAccountRiskOverrideSetter.sol",
"id": 28634,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 24030,
"src": "1240:90:101",
"symbolAliases": [
{
"foreign": 28633,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/interfaces/IERC20Detailed.sol",
"file": "../interfaces/IERC20Detailed.sol",
"id": 28636,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 24716,
"src": "1331:66:101",
"symbolAliases": [
{
"foreign": 28635,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/interfaces/IInterestSetter.sol",
"file": "../interfaces/IInterestSetter.sol",
"id": 28638,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 24790,
"src": "1398:68:101",
"symbolAliases": [
{
"foreign": 28637,
"local": null
}
],
"unitAlias": ""
},
{
"absolutePath": "/home/cdc218/projects/dolomite-protocol-v2/contracts/protocol/interfaces/IOracleSentinel.sol",
"file": "../interfaces/IOracleSentinel.sol",
"id": 28640,
"nodeType": "ImportDirective",
"scope": 30712,
"sourceUnit": 24818,
"src": "1467:68:101",
"symbolAliases": [
{
"foreign": 28639,
"local": null
}
],