Skip to content
This repository has been archived by the owner on May 24, 2024. It is now read-only.

Asset policies: Supply/borrow caps and market pausing #191

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
82 changes: 78 additions & 4 deletions contracts/BaseLogic.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ abstract contract BaseLogic is BaseModule {

output[0] = firstMarketEntered;

for (uint i = 1; i < numMarketsEntered; ++i) {
for (uint i = 1; i < numMarketsEntered;) {
output[i] = markets[i];
unchecked { ++i; }
}

return output;
Expand All @@ -56,8 +57,9 @@ abstract contract BaseLogic is BaseModule {

address[MAX_POSSIBLE_ENTERED_MARKETS] storage markets = marketsEntered[account];

for (uint i = 1; i < numMarketsEntered; ++i) {
for (uint i = 1; i < numMarketsEntered;) {
if (markets[i] == underlying) return true;
unchecked { ++i; }
}

return false;
Expand All @@ -71,8 +73,9 @@ abstract contract BaseLogic is BaseModule {

if (numMarketsEntered != 0) {
if (accountStorage.firstMarketEntered == underlying) return; // already entered
for (uint i = 1; i < numMarketsEntered; i++) {
for (uint i = 1; i < numMarketsEntered;) {
if (markets[i] == underlying) return; // already entered
unchecked { ++i; }
}
}

Expand Down Expand Up @@ -100,11 +103,12 @@ abstract contract BaseLogic is BaseModule {
if (accountStorage.firstMarketEntered == underlying) {
searchIndex = 0;
} else {
for (uint i = 1; i < numMarketsEntered; i++) {
for (uint i = 1; i < numMarketsEntered;) {
if (markets[i] == underlying) {
searchIndex = i;
break;
}
unchecked { ++i; }
}

if (searchIndex == type(uint).max) return; // already exited
Expand Down Expand Up @@ -639,4 +643,74 @@ abstract contract BaseLogic is BaseModule {
accountLookup[account].lastAverageLiquidityUpdate = uint40(block.timestamp);
accountLookup[account].averageLiquidity = computeNewAverageLiquidity(account, deltaT);
}


// Asset Policies

function assetPolicyCheck(address underlying, uint16 pauseType) internal view {
require((pauseType & assetPolicies[underlying].pauseBitmask) == 0, "e/market-operation-paused");
}

function assetPolicyDirty(AssetCache memory assetCache, uint16 pauseType) internal {
AssetPolicy memory policy = assetPolicies[assetCache.underlying];

require((pauseType & policy.pauseBitmask) == 0, "e/market-operation-paused");

if (policy.supplyCap == 0 && policy.borrowCap == 0) return;
if (assetSnapshots[assetCache.underlying].dirty) return;

uint112 origTotalBalances = encodeAmount(balanceToUnderlyingAmount(assetCache, assetCache.totalBalances) / assetCache.underlyingDecimalsScaler);
uint112 origTotalBorrows = encodeAmount(assetCache.totalBorrows / INTERNAL_DEBT_PRECISION / assetCache.underlyingDecimalsScaler);

assetSnapshots[assetCache.underlying] = AssetSnapshot({
dirty: true,
origTotalBalances: origTotalBalances,
origTotalBorrows: origTotalBorrows
});
}

function assetPolicyClean(AssetCache memory assetCache, address account, bool allowDefer) internal {
AssetPolicy memory policy = assetPolicies[assetCache.underlying];

if (policy.supplyCap == 0 && policy.borrowCap == 0) return;
if (!assetSnapshots[assetCache.underlying].dirty) return;

if (allowDefer && accountLookup[account].deferLiquidityStatus != DEFERLIQUIDITY__NONE) {
doEnterMarket(account, assetCache.underlying);
return;
}

uint112 newTotalBalances = encodeAmount(balanceToUnderlyingAmount(assetCache, assetCache.totalBalances) / assetCache.underlyingDecimalsScaler);
uint112 newTotalBorrows = encodeAmount(assetCache.totalBorrows / INTERNAL_DEBT_PRECISION / assetCache.underlyingDecimalsScaler);

require(policy.supplyCap == 0
|| newTotalBalances < uint(policy.supplyCap) * 1e18 / assetCache.underlyingDecimalsScaler
|| newTotalBalances <= assetSnapshots[assetCache.underlying].origTotalBalances, "e/supply-cap-exceeded");

require(policy.borrowCap == 0
|| newTotalBorrows < uint(policy.borrowCap) * 1e18 / assetCache.underlyingDecimalsScaler
|| newTotalBorrows <= assetSnapshots[assetCache.underlying].origTotalBorrows, "e/borrow-cap-exceeded");

delete assetSnapshots[assetCache.underlying];
}

function assetPolicyCleanAllEntered(address account) internal {
AssetStorage storage assetStorage;
AssetCache memory assetCache;

address[] memory underlyings = getEnteredMarketsArray(account);

for (uint i = 0; i < underlyings.length;) {
address underlying = underlyings[i];

if (assetSnapshots[underlying].dirty) {
assetStorage = eTokenLookup[underlyingLookup[underlying].eTokenAddress];
initAssetCache(underlying, assetStorage, assetCache);

assetPolicyClean(assetCache, account, false);
}

unchecked { ++i; }
}
}
}
10 changes: 10 additions & 0 deletions contracts/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ abstract contract Constants {
uint16 internal constant PRICINGTYPE__OUT_OF_BOUNDS = 5;


// Pause bitmask

uint16 internal constant PAUSETYPE__DEPOSIT = 1 << 0;
uint16 internal constant PAUSETYPE__WITHDRAW = 1 << 1;
uint16 internal constant PAUSETYPE__BORROW = 1 << 2;
uint16 internal constant PAUSETYPE__REPAY = 1 << 3;
uint16 internal constant PAUSETYPE__MINT = 1 << 4;
uint16 internal constant PAUSETYPE__BURN = 1 << 5;


// Modules

// Public single-proxy modules
Expand Down
1 change: 1 addition & 0 deletions contracts/Events.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ abstract contract Events {
event GovSetReserveFee(address indexed underlying, uint32 newReserveFee);
event GovConvertReserves(address indexed underlying, address indexed recipient, uint amount);
event GovSetChainlinkPriceFeed(address indexed underlying, address chainlinkAggregator);
event GovSetAssetPolicy(address indexed underlying, Storage.AssetPolicy newPolicy);

event RequestSwap(address indexed accountIn, address indexed accountOut, address indexed underlyingIn, address underlyingOut, uint amount, uint swapType);
event RequestSwapHub(address indexed accountIn, address indexed accountOut, address indexed underlyingIn, address underlyingOut, uint amountIn, uint amountOut, uint mode, address swapHandler);
Expand Down
15 changes: 15 additions & 0 deletions contracts/Storage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,19 @@ abstract contract Storage is Constants {
mapping(address => address) internal pTokenLookup; // PToken => underlying
mapping(address => address) internal reversePTokenLookup; // underlying => PToken
mapping(address => address) internal chainlinkPriceFeedLookup; // underlying => chainlinkAggregator

struct AssetPolicy {
uint64 supplyCap; // underlying units without decimals, 0 means no cap
uint64 borrowCap; // underlying units without decimals, 0 means no cap
uint16 pauseBitmask;
}

struct AssetSnapshot {
bool dirty;
uint112 origTotalBalances; // underlying units and decimals
uint112 origTotalBorrows; // underlying units and decimals
}

mapping(address => AssetPolicy) internal assetPolicies; // underlying => AssetPolicy
mapping(address => AssetSnapshot) internal assetSnapshots; // underlying => AssetSnapshot
}
7 changes: 6 additions & 1 deletion contracts/modules/DToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ contract DToken is BaseLogic {
emit RequestBorrow(account, amount);

AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);
assetPolicyDirty(assetCache, PAUSETYPE__BORROW);

if (amount == type(uint).max) {
amount = assetCache.poolSize;
Expand All @@ -116,6 +117,7 @@ contract DToken is BaseLogic {

increaseBorrow(assetStorage, assetCache, proxyAddr, account, amount);

assetPolicyClean(assetCache, account, true);
checkLiquidity(account);
logAssetStatus(assetCache);
}
Expand All @@ -130,6 +132,7 @@ contract DToken is BaseLogic {
updateAverageLiquidity(account);
emit RequestRepay(account, amount);

assetPolicyCheck(underlying, PAUSETYPE__REPAY);
AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);

if (amount != type(uint).max) {
Expand Down Expand Up @@ -210,7 +213,6 @@ contract DToken is BaseLogic {
/// @param amount In underlying units. Use max uint256 for full balance.
function transferFrom(address from, address to, uint amount) public nonReentrant returns (bool) {
(address underlying, AssetStorage storage assetStorage, address proxyAddr, address msgSender) = CALLER();
AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);

if (from == address(0)) from = msgSender;
require(from != to, "e/self-transfer");
Expand All @@ -219,6 +221,9 @@ contract DToken is BaseLogic {
updateAverageLiquidity(to);
emit RequestTransferDToken(from, to, amount);

assetPolicyCheck(underlying, PAUSETYPE__BORROW | PAUSETYPE__REPAY);
AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);

if (amount == type(uint).max) amount = getCurrentOwed(assetStorage, assetCache, from);
else amount = decodeExternalAmount(assetCache, amount);

Expand Down
12 changes: 10 additions & 2 deletions contracts/modules/EToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ contract EToken is BaseLogic {
emit RequestDeposit(account, amount);

AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);
assetPolicyDirty(assetCache, PAUSETYPE__DEPOSIT);

if (amount == type(uint).max) {
amount = callBalanceOf(assetCache, msgSender);
Expand All @@ -167,6 +168,8 @@ contract EToken is BaseLogic {

increaseBalance(assetStorage, assetCache, proxyAddr, account, amountInternal);

assetPolicyClean(assetCache, account, true);

// Depositing a token to an account with pre-existing debt in that token creates a self-collateralized loan
// which may result in borrow isolation violation if other tokens are also borrowed on the account
if (assetStorage.users[account].owed != 0) checkLiquidity(account);
Expand All @@ -184,6 +187,7 @@ contract EToken is BaseLogic {
updateAverageLiquidity(account);
emit RequestWithdraw(account, amount);

assetPolicyCheck(underlying, PAUSETYPE__WITHDRAW);
AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);

uint amountInternal;
Expand Down Expand Up @@ -211,6 +215,7 @@ contract EToken is BaseLogic {
emit RequestMint(account, amount);

AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);
assetPolicyDirty(assetCache, PAUSETYPE__MINT);

amount = decodeExternalAmount(assetCache, amount);
uint amountInternal = underlyingAmountToBalanceRoundUp(assetCache, amount);
Expand All @@ -224,6 +229,7 @@ contract EToken is BaseLogic {

increaseBorrow(assetStorage, assetCache, assetStorage.dTokenAddress, account, amount);

assetPolicyClean(assetCache, account, true);
checkLiquidity(account);
logAssetStatus(assetCache);
}
Expand All @@ -238,6 +244,7 @@ contract EToken is BaseLogic {
updateAverageLiquidity(account);
emit RequestBurn(account, amount);

assetPolicyCheck(underlying, PAUSETYPE__BURN);
AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);

uint owed = getCurrentOwed(assetStorage, assetCache, account);
Expand Down Expand Up @@ -323,15 +330,16 @@ contract EToken is BaseLogic {
function transferFrom(address from, address to, uint amount) public nonReentrant returns (bool) {
(address underlying, AssetStorage storage assetStorage, address proxyAddr, address msgSender) = CALLER();

AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);

if (from == address(0)) from = msgSender;
require(from != to, "e/self-transfer");

updateAverageLiquidity(from);
updateAverageLiquidity(to);
emit RequestTransferEToken(from, to, amount);

assetPolicyCheck(underlying, PAUSETYPE__WITHDRAW | PAUSETYPE__DEPOSIT);
AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);

if (amount == 0) return true;

if (!isSubAccountOf(msgSender, from) && assetStorage.eTokenAllowance[from][msgSender] != type(uint).max) {
Expand Down
2 changes: 2 additions & 0 deletions contracts/modules/Exec.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ contract Exec is BaseLogic {
uint8 status = accountLookup[account].deferLiquidityStatus;
accountLookup[account].deferLiquidityStatus = DEFERLIQUIDITY__NONE;

assetPolicyCleanAllEntered(account);
if (status == DEFERLIQUIDITY__DIRTY) checkLiquidity(account);
}

Expand Down Expand Up @@ -318,6 +319,7 @@ contract Exec is BaseLogic {
uint8 status = accountLookup[account].deferLiquidityStatus;
accountLookup[account].deferLiquidityStatus = DEFERLIQUIDITY__NONE;

assetPolicyCleanAllEntered(account);
if (status == DEFERLIQUIDITY__DIRTY) checkLiquidity(account);
}
}
Expand Down
6 changes: 6 additions & 0 deletions contracts/modules/Governance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ contract Governance is BaseLogic {
emit GovSetChainlinkPriceFeed(underlying, chainlinkAggregator);
}

function setAssetPolicy(address underlying, AssetPolicy memory newPolicy) external nonReentrant governorOnly {
assetPolicies[underlying] = newPolicy;

emit GovSetAssetPolicy(underlying, newPolicy);
}


// getters

Expand Down
11 changes: 11 additions & 0 deletions contracts/modules/Markets.sol
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ contract Markets is BaseLogic {
chainlinkAggregator = chainlinkPriceFeedLookup[underlying];
}

/// @notice Retrieves the Asset Policy config for an asset
/// @param underlying Token address
/// @return assetPolicy Asset Policy config
function getAssetPolicy(address underlying) external view returns (Storage.AssetPolicy memory assetPolicy) {
assetPolicy = assetPolicies[underlying];
}

// Enter/exit markets

Expand Down Expand Up @@ -273,6 +279,11 @@ contract Markets is BaseLogic {

require(owed == 0, "e/outstanding-borrow");

if (assetSnapshots[oldMarket].dirty) {
AssetCache memory assetCache = loadAssetCache(oldMarket, assetStorage);
assetPolicyClean(assetCache, account, false);
}

doExitMarket(account, oldMarket);

if (config.collateralFactor != 0 && balance != 0) {
Expand Down
19 changes: 14 additions & 5 deletions contracts/modules/Swap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -395,14 +395,27 @@ contract Swap is BaseLogic {
function finalizeSwap(SwapCache memory swap) private {
uint balanceIn = checkBalances(swap);

assetPolicyCheck(swap.assetCacheIn.underlying, PAUSETYPE__WITHDRAW);
processWithdraw(eTokenLookup[swap.eTokenIn], swap.assetCacheIn, swap.eTokenIn, swap.accountIn, swap.amountInternalIn, balanceIn);

processDeposit(eTokenLookup[swap.eTokenOut], swap.assetCacheOut, swap.eTokenOut, swap.accountOut, swap.amountOut);
assetPolicyDirty(swap.assetCacheOut, PAUSETYPE__DEPOSIT);
AssetStorage storage assetStorageOut = eTokenLookup[swap.eTokenOut];
processDeposit(assetStorageOut, swap.assetCacheOut, swap.eTokenOut, swap.accountOut, swap.amountOut);

assetPolicyClean(swap.assetCacheOut, swap.accountOut, true);

checkLiquidity(swap.accountIn);

// Depositing a token to the account with a pre-existing debt in that token creates a self-collateralized loan
// which may result in borrow isolation violation if other tokens are also borrowed on the account
if (swap.accountIn != swap.accountOut && assetStorageOut.users[swap.accountOut].owed != 0)
checkLiquidity(swap.accountOut);
}

function finalizeSwapAndRepay(SwapCache memory swap) private {
assetPolicyCheck(swap.assetCacheIn.underlying, PAUSETYPE__WITHDRAW);
assetPolicyCheck(swap.assetCacheOut.underlying, PAUSETYPE__REPAY);

uint balanceIn = checkBalances(swap);

processWithdraw(eTokenLookup[swap.eTokenIn], swap.assetCacheIn, swap.eTokenIn, swap.accountIn, swap.amountInternalIn, balanceIn);
Expand All @@ -429,10 +442,6 @@ contract Swap is BaseLogic {

increaseBalance(assetStorage, assetCache, eTokenAddress, account, amountInternal);

// Depositing a token to an account with pre-existing debt in that token creates a self-collateralized loan
// which may result in borrow isolation violation if other tokens are also borrowed on the account
if (assetStorage.users[account].owed != 0) checkLiquidity(account);

logAssetStatus(assetCache);
}

Expand Down
Loading