diff --git a/README.md b/README.md index bb68f6d5..3527505f 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ This repo contains the smart contracts and tests for the [Euler Protocol](https: ## Docs * [General Euler Docs](https://docs.euler.finance/) -* [Contract Architecture](https://docs.euler.finance/developers/architecture) -* [Contract Reference](https://docs.euler.finance/developers/contract-reference) -* [IEuler.sol Solidity Interface](https://github.com/euler-xyz/euler-interfaces/blob/master/IEuler.sol) +* [Contract Architecturel](https://docs.euler.finance/developers/getting-started/architecture) +* [Contract Reference](https://docs.euler.finance/developers/getting-started/contract-reference) +* [IEuler.sol Solidity Interface](https://github.com/euler-xyz/euler-interfaces/blob/master/contracts/IEuler.sol) ## License diff --git a/contracts/BaseLogic.sol b/contracts/BaseLogic.sol index f586ae32..fe3e72c9 100644 --- a/contracts/BaseLogic.sol +++ b/contracts/BaseLogic.sol @@ -639,4 +639,67 @@ 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(true, origTotalBalances, 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"); + + assetSnapshots[assetCache.underlying] = AssetSnapshot(false, 0, 0); + } + + function assetPolicyCleanAllEntered(address account) internal { + AssetStorage storage assetStorage; + AssetCache memory assetCache; + + address[] memory underlyings = getEnteredMarketsArray(account); + + for (uint i = 0; i < underlyings.length; ++i) { + address underlying = underlyings[i]; + if (!assetSnapshots[underlying].dirty) continue; + + assetStorage = eTokenLookup[underlyingLookup[underlying].eTokenAddress]; + initAssetCache(underlying, assetStorage, assetCache); + + assetPolicyClean(assetCache, account, false); + } + } } diff --git a/contracts/Constants.sol b/contracts/Constants.sol index 0a524352..a579df9a 100644 --- a/contracts/Constants.sol +++ b/contracts/Constants.sol @@ -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 diff --git a/contracts/Events.sol b/contracts/Events.sol index dc295ede..ec0f3bf4 100644 --- a/contracts/Events.sol +++ b/contracts/Events.sol @@ -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); diff --git a/contracts/Storage.sol b/contracts/Storage.sol index 7bd1a290..e759be1e 100644 --- a/contracts/Storage.sol +++ b/contracts/Storage.sol @@ -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 } diff --git a/contracts/modules/DToken.sol b/contracts/modules/DToken.sol index d539cc7b..3242c5a8 100644 --- a/contracts/modules/DToken.sol +++ b/contracts/modules/DToken.sol @@ -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; @@ -116,6 +117,7 @@ contract DToken is BaseLogic { increaseBorrow(assetStorage, assetCache, proxyAddr, account, amount); + assetPolicyClean(assetCache, account, true); checkLiquidity(account); logAssetStatus(assetCache); } @@ -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) { @@ -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"); @@ -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); diff --git a/contracts/modules/EToken.sol b/contracts/modules/EToken.sol index 8ab8d85a..a16175ea 100644 --- a/contracts/modules/EToken.sol +++ b/contracts/modules/EToken.sol @@ -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); @@ -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); @@ -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; @@ -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); @@ -224,6 +229,7 @@ contract EToken is BaseLogic { increaseBorrow(assetStorage, assetCache, assetStorage.dTokenAddress, account, amount); + assetPolicyClean(assetCache, account, true); checkLiquidity(account); logAssetStatus(assetCache); } @@ -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); @@ -323,8 +330,6 @@ 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"); @@ -332,6 +337,9 @@ contract EToken is BaseLogic { 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) { diff --git a/contracts/modules/Exec.sol b/contracts/modules/Exec.sol index dbc3a40e..288c40ad 100644 --- a/contracts/modules/Exec.sol +++ b/contracts/modules/Exec.sol @@ -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); } @@ -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); } } diff --git a/contracts/modules/Governance.sol b/contracts/modules/Governance.sol index c5b7f539..a6c33ca5 100644 --- a/contracts/modules/Governance.sol +++ b/contracts/modules/Governance.sol @@ -117,6 +117,14 @@ contract Governance is BaseLogic { emit GovSetChainlinkPriceFeed(underlying, chainlinkAggregator); } + function setAssetPolicy(address underlying, AssetPolicy memory newPolicy) external nonReentrant governorOnly { + require(underlyingLookup[underlying].eTokenAddress != address(0), "e/gov/underlying-not-activated"); + + assetPolicies[underlying] = newPolicy; + + emit GovSetAssetPolicy(underlying, newPolicy); + } + // getters diff --git a/contracts/modules/Markets.sol b/contracts/modules/Markets.sol index b0bd366c..8336f241 100644 --- a/contracts/modules/Markets.sol +++ b/contracts/modules/Markets.sol @@ -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 @@ -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) { diff --git a/contracts/modules/Swap.sol b/contracts/modules/Swap.sol index 63f6240b..77891913 100644 --- a/contracts/modules/Swap.sol +++ b/contracts/modules/Swap.sol @@ -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); @@ -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); } diff --git a/contracts/modules/SwapHub.sol b/contracts/modules/SwapHub.sol index eb301c77..ddf81f17 100644 --- a/contracts/modules/SwapHub.sol +++ b/contracts/modules/SwapHub.sol @@ -39,8 +39,12 @@ contract SwapHub is BaseLogic { /// @param swapHandler address of a swap handler to use /// @param params struct defining the requested trade function swap(uint subAccountIdIn, uint subAccountIdOut, address swapHandler, ISwapHandler.SwapParams memory params) external nonReentrant { + assetPolicyCheck(params.underlyingIn, PAUSETYPE__WITHDRAW); + SwapCache memory cache = initSwap(subAccountIdIn, subAccountIdOut, params); + assetPolicyDirty(cache.assetCacheOut, PAUSETYPE__DEPOSIT); + emit RequestSwapHub( cache.accountIn, cache.accountOut, @@ -62,6 +66,8 @@ contract SwapHub is BaseLogic { increaseBalance(assetStorageOut, cache.assetCacheOut, cache.eTokenOut, cache.accountOut, amountOutInternal); logAssetStatus(cache.assetCacheOut); + assetPolicyClean(cache.assetCacheOut, cache.accountOut, true); + // Check liquidity checkLiquidity(cache.accountIn); @@ -78,6 +84,9 @@ contract SwapHub is BaseLogic { /// @param params struct defining the requested trade /// @param targetDebt how much debt should remain after calling the function function swapAndRepay(uint subAccountIdIn, uint subAccountIdOut, address swapHandler, ISwapHandler.SwapParams memory params, uint targetDebt) external nonReentrant { + assetPolicyCheck(params.underlyingIn, PAUSETYPE__WITHDRAW); + assetPolicyCheck(params.underlyingOut, PAUSETYPE__REPAY); + SwapCache memory cache = initSwap(subAccountIdIn, subAccountIdOut, params); emit RequestSwapHubRepay( diff --git a/contracts/test/MockSwapHandler.sol b/contracts/test/MockSwapHandler.sol new file mode 100644 index 00000000..fa19f087 --- /dev/null +++ b/contracts/test/MockSwapHandler.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import "../swapHandlers/ISwapHandler.sol"; +import "../Utils.sol"; + +/// @notice Base contract for swap handlers +contract MockSwapHandler is ISwapHandler { + function executeSwap(SwapParams calldata params) override external { + Utils.safeTransfer(params.underlyingOut, msg.sender, params.amountOut); + } +} diff --git a/contracts/views/EulerGeneralView.sol b/contracts/views/EulerGeneralView.sol index 1cf4d1fd..cdaa62fe 100644 --- a/contracts/views/EulerGeneralView.sol +++ b/contracts/views/EulerGeneralView.sol @@ -57,6 +57,8 @@ contract EulerGeneralView is Constants { uint borrowAPY; uint supplyAPY; + Storage.AssetPolicy assetPolicy; + // Pricing uint twap; @@ -165,6 +167,11 @@ contract EulerGeneralView is Constants { (m.borrowAPY, m.supplyAPY) = computeAPYs(borrowSPY, m.totalBorrows, m.totalBalances, m.reserveFee); } + { + Storage.AssetPolicy memory p = marketsProxy.getAssetPolicy(m.underlying); + m.assetPolicy = p; + } + (m.twap, m.twapPeriod, m.currPrice) = execProxy.getPriceFull(m.underlying); (m.pricingType, m.pricingParameters, m.pricingForwarded) = marketsProxy.getPricingConfig(m.underlying); diff --git a/docs/asset-policies.md b/docs/asset-policies.md new file mode 100644 index 00000000..ea024a0b --- /dev/null +++ b/docs/asset-policies.md @@ -0,0 +1,33 @@ +## Asset Policies + +### Supply/Borrow caps + +For some assets it is desired to limit the amount of assets supplied and/or borrowed on the platform. This could be done in order to phase-in a collateral asset while minimising exposure to the protocol. An asset may also have a limited amount of on-chain liquidity, which could merit capping the lending/borrowing activity. + +While it is possible for governance to lower a cap to below the current supply/borrow level, caps are not intended to function as emergency pause mechanisms. It should still be possible to withdraw/repay, even when an asset is in violation of the cap. This should be true even if your withdraw/repay is insufficient to solve the violation, and the asset's supply/borrow remains above the cap after your operation. + +Furthermore, it should be possible to temporarily exceed the cap, as long as the supply/borrows are brought back down to a non-violating amount in the same transaction, or at least to the level they were when the transaction was initiated. + +The caps are specified in terms of the underlying asset's units and are not converted to their ETH equivalents (for example). + +### Operation pausing + +Additionally, in order to give governance the ability to quickly react to market conditions, contract bugs, and other unpredictable events, assets can have operations "paused". This is intended to temporarily prevent a deposit/withdraw/etc operation from being performed on the asset. + +### Mechanism + +Caps and operation pausing are specified inside a new storage mapping `assetPolicies`. This mapping only needs to exist for assets that have policies configured. Whenever an operation is to be performed on an asset, its asset policies should be checked to ensure the operation is not paused, and also that the caps will not be exceeded. + +For operations that cannot result in a cap being exceeded, the `assetPolicyCheck` function is used. This simply looks up the asset's policy and confirms the requested operation is not paused, throwing an exception if it is. + +For operations that can result in a cap being exceeded, the pair of functions `assetPolicyDirty` and `assetPolicyClean` should be used instead. The first checks if the operation is paused as above, but also (if caps are configured) takes a snapshot of the `totalBalances` and `totalBorrows` of the asset and then marks the asset as "dirty", meaning it will need checking to validate it hasn't exceeded the caps. The operation is performed and then `assetPolicyClean` is called. This will either: + +1. Verify the caps have not been exceeded (or at least are no more exceeded than when the snapshot was taken), and mark the asset clean +2. If the user has deferred liquidity checking, ensure that the user is entered into this market and then return, leaving the asset dirty + +In the second case, the asset will have its caps verified and the assets marked dirty when the account's liquidity checking occurs. In this case, it will attempt to clean all markets the user has entered. Because of this, if a user attempts to exit a market, it must be cleaned first. + +### Notes + +* In order to leave space for future asset policy extensions without overflowing into a new storage slot, the supply and borrow caps have been packed uint `uint64` types. These represent the underlying units without decimals. So, a supply cap of 1 million on an 18 decimal place token would simply be stored as `1000000`, not `1000000 * 1e18`. This reduces the granularity of the caps because fractional units cannot be represented, but does not reduce the range, since asset amounts including decimals must fit within a `uint112` on the euler platform (the `MAX_SANE_AMOUNT` constant). +* The supply/borrow caps are effectively "first-come-first-serve" allocations. This means that it is possible for a user to deliberately perform a large deposit and/or borrow in order to max out the cap, thereby denying the ability of other users to participate in the market. This attack can be done in a relatively capital-efficient manner using self-collateralised loans. For this reason, governance should be careful with caps and should monitor activity on capped markets closely. diff --git a/test/assetPolicies.js b/test/assetPolicies.js new file mode 100644 index 00000000..7d2c8e79 --- /dev/null +++ b/test/assetPolicies.js @@ -0,0 +1,825 @@ +const et = require('./lib/eTestLib'); +const scenarios = require('./lib/scenarios'); + +const PAUSETYPE__DEPOSIT = 1 << 0 +const PAUSETYPE__WITHDRAW = 1 << 1 +const PAUSETYPE__BORROW = 1 << 2 +const PAUSETYPE__REPAY = 1 << 3 +const PAUSETYPE__MINT = 1 << 4 +const PAUSETYPE__BURN = 1 << 5 + +et.testSet({ + desc: "asset policies", + + preActions: ctx => { + let actions = scenarios.basicLiquidity()(ctx) + actions.push({ send: 'tokens.TST2.mint', args: [ctx.contracts.swapHandlers.mockSwapHandler.address, et.eth(100)], }) + return actions + } +}) + + + +.test({ + desc: "simple supply cap", + actions: ctx => [ + { call: 'eTokens.eTST.totalSupply', equals: [10, .001], }, + + // Deposit prevented: + + { action: 'setAssetPolicy', tok: 'TST', policy: { supplyCap: 11, }, }, + { send: 'eTokens.eTST.deposit', args: [0, et.eth(2)], expectError: 'e/supply-cap-exceeded', }, + + // Raise Cap and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST', policy: { supplyCap: 13, }, }, + { send: 'eTokens.eTST.deposit', args: [0, et.eth(2)], }, + + // New limit prevents additional deposits: + + { send: 'eTokens.eTST.deposit', args: [0, et.eth(2)], expectError: 'e/supply-cap-exceeded', }, + + // Lower supply cap. Withdrawal still works, even though it's not enough withdrawn to solve the policy violation: + + { action: 'setAssetPolicy', tok: 'TST', policy: { supplyCap: 5, }, }, + { send: 'eTokens.eTST.withdraw', args: [0, et.eth(3)], }, + + { call: 'eTokens.eTST.totalSupply', equals: [9, .001], }, + + // Deposit doesn't work + + { send: 'eTokens.eTST.deposit', args: [0, et.eth(.1)], expectError: 'e/supply-cap-exceeded', }, + ], +}) + + +.test({ + desc: "simple borrow cap", + actions: ctx => [ + { send: 'dTokens.dTST.borrow', args: [0, et.eth(5)], }, + + { call: 'dTokens.dTST.totalSupply', equals: [5, .001], }, + + // Borrow prevented: + + { action: 'setAssetPolicy', tok: 'TST', policy: { borrowCap: 6, }, }, + { send: 'dTokens.dTST.borrow', args: [0, et.eth(2)], expectError: 'e/borrow-cap-exceeded', }, + + // Raise Cap and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST', policy: { borrowCap: 8, }, }, + { send: 'dTokens.dTST.borrow', args: [0, et.eth(2)], }, + + // New limit prevents additional deposits: + + { send: 'dTokens.dTST.borrow', args: [0, et.eth(2)], expectError: 'e/borrow-cap-exceeded', }, + + // Lower borrow cap to the current dToken supply, set IRM to non-zero. + // Jump time so that new dToken supply exceeds the borrow cap due to the interest accrued + + { action: 'setAssetPolicy', tok: 'TST', policy: { borrowCap: 7, }, }, + { action: 'setIRM', underlying: 'TST', irm: 'IRM_FIXED', }, + { call: 'dTokens.dTST.totalSupply', equals: [7, .001], }, + + { action: 'jumpTimeAndMine', time: 2 * 365 * 24 * 60 * 60, }, // 2 years + { call: 'dTokens.dTST.totalSupply', equals: [8.55, .001], }, + + // Repay still works, even though it's not enough repaid to solve the policy violation: + + { send: 'dTokens.dTST.repay', args: [0, et.eth(1)], }, + + { call: 'dTokens.dTST.totalSupply', equals: [7.55, .001], }, + + // Borrow doesn't work + + { send: 'dTokens.dTST.borrow', args: [0, et.eth(.1)], expectError: 'e/borrow-cap-exceeded', }, + ], +}) + + +.test({ + desc: "supply and borrow cap for mint", + actions: ctx => [ + { call: 'eTokens.eTST.totalSupply', equals: [10, .001], }, + { call: 'dTokens.dTST.totalSupply', equals: [0], }, + + // Mint prevented: + + { action: 'setAssetPolicy', tok: 'TST', policy: { supplyCap: 12, borrowCap: 5 }, }, + { send: 'eTokens.eTST.mint', args: [0, et.eth(3)], expectError: 'e/supply-cap-exceeded', }, + + // Mint prevented: + + { action: 'setAssetPolicy', tok: 'TST', policy: { supplyCap: 15, borrowCap: 2 }, }, + { send: 'eTokens.eTST.mint', args: [0, et.eth(3)], expectError: 'e/borrow-cap-exceeded', }, + + // Raise caps and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST', policy: { supplyCap: 15, borrowCap: 5 }, }, + { send: 'eTokens.eTST.mint', args: [0, et.eth(3)], }, + + // New limit prevents additional mints: + + { send: 'eTokens.eTST.mint', args: [0, et.eth(3)], expectError: 'e/supply-cap-exceeded', }, + + // Lower supply cap. Burn still works, even though it's not enough burnt to solve the policy violation: + + { action: 'setAssetPolicy', tok: 'TST', policy: { supplyCap: 1, borrowCap: 1 }, }, + { send: 'eTokens.eTST.burn', args: [0, et.eth(1)], }, + { call: 'eTokens.eTST.totalSupply', equals: [12, .001], }, + { call: 'dTokens.dTST.totalSupply', equals: [2, .001], }, + + { send: 'eTokens.eTST.burn', args: [0, et.eth(1)], }, + { call: 'eTokens.eTST.totalSupply', equals: [11, .001], }, + { call: 'dTokens.dTST.totalSupply', equals: [1, .001], }, + + // Deposit doesn't work + + { send: 'eTokens.eTST.mint', args: [0, et.eth(.1)], expectError: 'e/supply-cap-exceeded', }, + + // Turn off supply cap. Mint still doesn't work because of borrow cap + + { action: 'setAssetPolicy', tok: 'TST', policy: { supplyCap: 0, borrowCap: 1 }, }, + + { send: 'eTokens.eTST.mint', args: [0, et.eth(.1)], expectError: 'e/borrow-cap-exceeded', }, + ], +}) + + +.test({ + desc: "supply cap for swap hub", + actions: ctx => [ + // Current supply 10, supply cap 15 + + { call: 'eTokens.eTST2.totalSupply', equals: [10, .001], }, + { action: 'setAssetPolicy', tok: 'TST2', policy: { supplyCap: 15, }, }, + + // Won't succeed if received tokens would put us over the supply cap + + { send: 'swapHub.swap', args: [0, 0, ctx.contracts.swapHandlers.mockSwapHandler.address, + { + underlyingIn: ctx.contracts.tokens.TST.address, + underlyingOut: ctx.contracts.tokens.TST2.address, + mode: 0, + amountIn: et.eth(1), + amountOut: et.eth(6), + exactOutTolerance: 0, + payload: '0x', + } + ], expectError: 'e/supply-cap-exceeded', }, + + // Succeeds if received tokens would put us below the supply cap + + { send: 'swapHub.swap', args: [0, 0, ctx.contracts.swapHandlers.mockSwapHandler.address, + { + underlyingIn: ctx.contracts.tokens.TST.address, + underlyingOut: ctx.contracts.tokens.TST2.address, + mode: 0, + amountIn: et.eth(1), + amountOut: et.eth(5), + exactOutTolerance: 0, + payload: '0x', + } + ], }, + + { call: 'eTokens.eTST2.totalSupply', equals: [15, .001], }, + ], +}) + + +.test({ + desc: "batch deferral of supply cap check", + actions: ctx => [ + // Current supply 10, supply cap 15 + + { call: 'eTokens.eTST.totalSupply', equals: [10, .001], }, + { action: 'setAssetPolicy', tok: 'TST', policy: { supplyCap: 15, }, }, + + // This won't work because we don't defer liquidity check: + + { action: 'sendBatch', batch: [ + { send: 'eTokens.eTST.deposit', args: [0, et.eth(10)], }, + { send: 'eTokens.eTST.withdraw', args: [0, et.eth(8)], }, + ], + expectError: 'e/supply-cap-exceeded', + }, + + // Deferring doesn't allow us to leave the asset in policy violation: + + { action: 'sendBatch', batch: [ + { send: 'eTokens.eTST.deposit', args: [0, et.eth(10)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + expectError: 'e/supply-cap-exceeded', + }, + + // Even though we exited the market, it will get entered by itself, so transient violations don't fail: + + { send: 'markets.exitMarket', args: [0, ctx.contracts.tokens.TST.address], }, + + { action: 'sendBatch', batch: [ + { send: 'eTokens.eTST.deposit', args: [0, et.eth(10)], }, + { send: 'eTokens.eTST.withdraw', args: [0, et.eth(8)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + }, + + { call: 'eTokens.eTST.totalSupply', equals: [12, .001], }, + + // Same behaviour if we also have a borrow (which causes liquidity check in deposit) + + { from: ctx.wallet, send: 'dTokens.dTST.borrow', args: [0, et.eth(.1)], }, + + { send: 'eTokens.eTST.deposit', args: [0, et.eth(10)], expectError: 'e/supply-cap-exceeded', }, + + // Failures: + + { action: 'sendBatch', batch: [ + { send: 'eTokens.eTST.deposit', args: [0, et.eth(10)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + expectError: 'e/supply-cap-exceeded', + }, + + // Success + + { action: 'sendBatch', batch: [ + { send: 'eTokens.eTST.deposit', args: [0, et.eth(10)], }, + { send: 'eTokens.eTST.withdraw', args: [0, et.eth(8)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + }, + ], +}) + + +.test({ + desc: "batch deferral of borrow cap check", + actions: ctx => [ + // Current borrow 0, borrow cap 5 + + { call: 'dTokens.dTST2.totalSupply', equals: [0, .001], }, + { action: 'setAssetPolicy', tok: 'TST2', policy: { borrowCap: 5, }, }, + + // This won't work because we don't defer liquidity check: + + { action: 'sendBatch', batch: [ + { send: 'dTokens.dTST2.borrow', args: [0, et.eth(6)], }, + { send: 'dTokens.dTST2.repay', args: [0, et.eth(2)], }, + ], + expectError: 'e/borrow-cap-exceeded', + }, + + // Deferring doesn't allow us to leave the asset in policy violation: + + { action: 'sendBatch', batch: [ + { send: 'dTokens.dTST2.borrow', args: [0, et.eth(6)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + expectError: 'e/borrow-cap-exceeded', + }, + + // Being entered into the market allows the policy check to be deferred, so transient violations don't fail: + + { action: 'sendBatch', batch: [ + { send: 'dTokens.dTST2.borrow', args: [0, et.eth(6)], }, + { send: 'dTokens.dTST2.repay', args: [0, et.eth(2)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + }, + + { call: 'dTokens.dTST2.totalSupply', equals: [4, .001], }, + + // This works despite the fact we had exited the market because we enter it again when the asset policy is checked + + { send: 'dTokens.dTST2.repay', args: [0, et.MaxUint256], }, + { send: 'markets.exitMarket', args: [0, ctx.contracts.tokens.TST2.address], }, + + { action: 'sendBatch', batch: [ + { send: 'dTokens.dTST2.borrow', args: [0, et.eth(6)], }, + { send: 'dTokens.dTST2.repay', args: [0, et.eth(2)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + }, + + { call: 'dTokens.dTST2.totalSupply', equals: [4, .001], }, + + + // Failures: + + { action: 'sendBatch', batch: [ + { send: 'dTokens.dTST2.borrow', args: [0, et.eth(1)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + expectError: 'e/borrow-cap-exceeded', + }, + + // Success + + { action: 'sendBatch', batch: [ + { send: 'dTokens.dTST2.borrow', args: [0, et.eth(1)], }, + { send: 'dTokens.dTST2.repay', args: [0, et.eth(0.1)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + }, + ], +}) + + +.test({ + desc: "can't exit market to bypass supply cap checks", + actions: ctx => [ + // Current supply 10, supply cap 15 + + { call: 'eTokens.eTST.totalSupply', equals: [10, .001], }, + { action: 'setAssetPolicy', tok: 'TST', policy: { supplyCap: 15, }, }, + + { send: 'markets.enterMarket', args: [0, ctx.contracts.tokens.TST.address], }, + + // Can't exit the market if the asset is in violation at the point of exit: + + { action: 'sendBatch', batch: [ + { send: 'eTokens.eTST.deposit', args: [0, et.eth(10)], }, + { send: 'markets.exitMarket', args: [0, ctx.contracts.tokens.TST.address], }, + { send: 'eTokens.eTST.withdraw', args: [0, et.eth(8)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + expectError: 'e/supply-cap-exceeded', + }, + + // ... but you can if it is not in violation: + + { action: 'sendBatch', batch: [ + { send: 'eTokens.eTST.deposit', args: [0, et.eth(10)], }, + { send: 'eTokens.eTST.withdraw', args: [0, et.eth(8)], }, + { send: 'markets.exitMarket', args: [0, ctx.contracts.tokens.TST.address], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + }, + ], +}) + + +.test({ + desc: "can't exit market to bypass borrow cap checks", + actions: ctx => [ + // Current borrow 1, borrow cap 5 + + { send: 'dTokens.dTST.borrow', args: [0, et.eth(1)], }, + { call: 'dTokens.dTST.totalSupply', equals: [1, .001], }, + { action: 'setAssetPolicy', tok: 'TST', policy: { borrowCap: 5, }, }, + + // Can't exit the market if the asset is in violation at the point of exit: + { send: 'markets.enterMarket', args: [1, ctx.contracts.tokens.TST.address], }, + + { action: 'sendBatch', batch: [ + { send: 'dTokens.dTST.borrow', args: [0, et.eth(5)], }, + { send: 'markets.exitMarket', args: [1, ctx.contracts.tokens.TST.address], }, + { send: 'dTokens.dTST.repay', args: [0, et.eth(5)], }, + ], + deferLiquidityChecks: [ctx.wallet.address, et.getSubAccount(ctx.wallet.address, 1)], + expectError: 'e/borrow-cap-exceeded', + }, + + // ... but you can if it is not in violation: + + { action: 'sendBatch', batch: [ + { send: 'dTokens.dTST.borrow', args: [0, et.eth(5)], }, + { send: 'dTokens.dTST.repay', args: [0, et.eth(5)], }, + { send: 'markets.exitMarket', args: [1, ctx.contracts.tokens.TST.address], }, + ], + deferLiquidityChecks: [ctx.wallet.address, et.getSubAccount(ctx.wallet.address, 1)], + }, + ], +}) + + +.test({ + desc: "simple actions pausing", + actions: ctx => [ + // Deposit prevented: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__DEPOSIT, }, }, + { send: 'eTokens.eTST.deposit', args: [0, 1], expectError: 'e/market-operation-paused', }, + + // Remove pause and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: 0, }, }, + { send: 'eTokens.eTST.deposit', args: [0, 1], }, + + // Withdrawal prevented: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__WITHDRAW, }, }, + { send: 'eTokens.eTST.withdraw', args: [0, 1], expectError: 'e/market-operation-paused', }, + + // Remove pause and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: 0, }, }, + { send: 'eTokens.eTST.withdraw', args: [0, 1], }, + + // Mint prevented: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__MINT, }, }, + { send: 'eTokens.eTST.mint', args: [0, 1], expectError: 'e/market-operation-paused', }, + + // Remove pause and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: 0, }, }, + { send: 'eTokens.eTST.mint', args: [0, 1], }, + + // Burn prevented: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__BURN, }, }, + { send: 'eTokens.eTST.burn', args: [0, 1], expectError: 'e/market-operation-paused', }, + + // Remove pause and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: 0, }, }, + { send: 'eTokens.eTST.burn', args: [0, 1], }, + + // Borrow prevented: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__BORROW, }, }, + { send: 'dTokens.dTST.borrow', args: [0, 1], expectError: 'e/market-operation-paused', }, + + // Remove pause and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: 0, }, }, + { send: 'dTokens.dTST.borrow', args: [0, 1], }, + + { send: 'dTokens.dTST.borrow', args: [0, et.eth(5)], }, + + // Repay prevented: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__REPAY, }, }, + { send: 'dTokens.dTST.repay', args: [0, et.MaxUint256], expectError: 'e/market-operation-paused', }, + + // Remove pause and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: 0, }, }, + { send: 'dTokens.dTST.repay', args: [0, et.MaxUint256], }, + + // eToken transfer prevented: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__DEPOSIT, }, }, + { send: 'eTokens.eTST.transfer', args: [et.AddressZero, 1], expectError: 'e/market-operation-paused', }, + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__WITHDRAW, }, }, + { send: 'eTokens.eTST.transfer', args: [et.AddressZero, 1], expectError: 'e/market-operation-paused', }, + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__DEPOSIT | PAUSETYPE__WITHDRAW, }, }, + { send: 'eTokens.eTST.transfer', args: [et.AddressZero, 1], expectError: 'e/market-operation-paused', }, + + // Remove pause and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: 0, }, }, + { send: 'eTokens.eTST.transfer', args: [et.AddressZero, 1], }, + + // dToken transfer prevented: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__BORROW, }, }, + { send: 'dTokens.dTST.transfer', args: [et.getSubAccount(ctx.wallet.address, 1), 1], expectError: 'e/market-operation-paused', }, + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__REPAY, }, }, + { send: 'dTokens.dTST.transfer', args: [et.getSubAccount(ctx.wallet.address, 1), 1], expectError: 'e/market-operation-paused', }, + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__BORROW | PAUSETYPE__REPAY, }, }, + { send: 'dTokens.dTST.transfer', args: [et.getSubAccount(ctx.wallet.address, 1), 1], expectError: 'e/market-operation-paused', }, + + // Remove pause and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: 0, }, }, + { send: 'eTokens.eTST.transfer', args: [et.getSubAccount(ctx.wallet.address, 1), 1], }, + + // swap prevented: + + { action: 'setAssetPolicy', tok: 'TST2', policy: { pauseBitmask: PAUSETYPE__DEPOSIT, }, }, + { send: 'swapHub.swap', args: [0, 0, ctx.contracts.swapHandlers.mockSwapHandler.address, + { + underlyingIn: ctx.contracts.tokens.TST.address, + underlyingOut: ctx.contracts.tokens.TST2.address, + mode: 0, + amountIn: 1, + amountOut: 1, + exactOutTolerance: 0, + payload: '0x', + } + ], expectError: 'e/market-operation-paused', }, + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__WITHDRAW, }, }, + { action: 'setAssetPolicy', tok: 'TST2', policy: { pauseBitmask: 0, }, }, + { send: 'swapHub.swap', args: [0, 0, ctx.contracts.swapHandlers.mockSwapHandler.address, + { + underlyingIn: ctx.contracts.tokens.TST.address, + underlyingOut: ctx.contracts.tokens.TST2.address, + mode: 0, + amountIn: 1, + amountOut: 1, + exactOutTolerance: 0, + payload: '0x', + } + ], expectError: 'e/market-operation-paused', }, + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__WITHDRAW, }, }, + { action: 'setAssetPolicy', tok: 'TST2', policy: { pauseBitmask: PAUSETYPE__DEPOSIT, }, }, + { send: 'swapHub.swap', args: [0, 0, ctx.contracts.swapHandlers.mockSwapHandler.address, + { + underlyingIn: ctx.contracts.tokens.TST.address, + underlyingOut: ctx.contracts.tokens.TST2.address, + mode: 0, + amountIn: 1, + amountOut: 1, + exactOutTolerance: 0, + payload: '0x', + } + ], expectError: 'e/market-operation-paused', }, + + // Remove pause and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: 0, }, }, + { action: 'setAssetPolicy', tok: 'TST2', policy: { pauseBitmask: 0, }, }, + { send: 'swapHub.swap', args: [0, 0, ctx.contracts.swapHandlers.mockSwapHandler.address, + { + underlyingIn: ctx.contracts.tokens.TST.address, + underlyingOut: ctx.contracts.tokens.TST2.address, + mode: 0, + amountIn: 1, + amountOut: 1, + exactOutTolerance: 0, + payload: '0x', + } + ], }, + + { send: 'dTokens.dTST2.borrow', args: [0, et.eth(5)], }, + + // swap and repay prevented: + + { action: 'setAssetPolicy', tok: 'TST2', policy: { pauseBitmask: PAUSETYPE__REPAY, }, }, + { send: 'swapHub.swapAndRepay', args: [0, 0, ctx.contracts.swapHandlers.mockSwapHandler.address, + { + underlyingIn: ctx.contracts.tokens.TST.address, + underlyingOut: ctx.contracts.tokens.TST2.address, + mode: 1, + amountIn: 1, + amountOut: 0, + exactOutTolerance: 0, + payload: '0x', + }, + 0, + ], expectError: 'e/market-operation-paused', }, + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__WITHDRAW, }, }, + { action: 'setAssetPolicy', tok: 'TST2', policy: { pauseBitmask: 0, }, }, + { send: 'swapHub.swapAndRepay', args: [0, 0, ctx.contracts.swapHandlers.mockSwapHandler.address, + { + underlyingIn: ctx.contracts.tokens.TST.address, + underlyingOut: ctx.contracts.tokens.TST2.address, + mode: 1, + amountIn: 1, + amountOut: 0, + exactOutTolerance: 0, + payload: '0x', + }, + 0, + ], expectError: 'e/market-operation-paused', }, + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: PAUSETYPE__WITHDRAW, }, }, + { action: 'setAssetPolicy', tok: 'TST2', policy: { pauseBitmask: PAUSETYPE__REPAY, }, }, + { send: 'swapHub.swapAndRepay', args: [0, 0, ctx.contracts.swapHandlers.mockSwapHandler.address, + { + underlyingIn: ctx.contracts.tokens.TST.address, + underlyingOut: ctx.contracts.tokens.TST2.address, + mode: 1, + amountIn: 1, + amountOut: 0, + exactOutTolerance: 0, + payload: '0x', + }, + 0, + ], expectError: 'e/market-operation-paused', }, + + // Remove pause and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST', policy: { pauseBitmask: 0, }, }, + { action: 'setAssetPolicy', tok: 'TST2', policy: { pauseBitmask: 0, }, }, + { send: 'swapHub.swapAndRepay', args: [0, 0, ctx.contracts.swapHandlers.mockSwapHandler.address, + { + underlyingIn: ctx.contracts.tokens.TST.address, + underlyingOut: ctx.contracts.tokens.TST2.address, + mode: 1, + amountIn: 1, + amountOut: 0, + exactOutTolerance: 0, + payload: '0x', + }, + 0, + ], }, + ], +}) + + +.test({ + desc: "complex scenario", + actions: ctx => [ + { call: 'eTokens.eTST.totalSupply', equals: [10, .001], }, + { call: 'eTokens.eTST2.totalSupply', equals: [10, .001], }, + { call: 'dTokens.dTST.totalSupply', equals: [0], }, + { call: 'dTokens.dTST2.totalSupply', equals: [0], }, + + { action: 'setAssetPolicy', tok: 'TST', policy: { supplyCap: 15, pauseBitmask: PAUSETYPE__MINT }, }, + { action: 'setAssetPolicy', tok: 'TST2', policy: { borrowCap: 5, }, }, + + // This won't work because the end state violates asset policies: + + { action: 'sendBatch', batch: [ + { send: 'eTokens.eTST.deposit', args: [0, et.eth(7)], }, + { send: 'dTokens.dTST2.borrow', args: [0, et.eth(7)], }, + + { send: 'eTokens.eTST.withdraw', args: [0, et.eth(1)], }, + { send: 'dTokens.dTST2.repay', args: [0, et.eth(1)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + expectError: 'e/supply-cap-exceeded', + }, + + { action: 'sendBatch', batch: [ + { send: 'eTokens.eTST.deposit', args: [0, et.eth(7)], }, + { send: 'dTokens.dTST2.borrow', args: [0, et.eth(7)], }, + + { send: 'eTokens.eTST.withdraw', args: [0, et.eth(3)], }, + { send: 'dTokens.dTST2.repay', args: [0, et.eth(1)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + expectError: 'e/borrow-cap-exceeded', + }, + + // Succeeeds if there's no violation: + + { action: 'sendBatch', batch: [ + { send: 'eTokens.eTST.deposit', args: [0, et.eth(7)], }, + { send: 'dTokens.dTST2.borrow', args: [0, et.eth(7)], }, + + { send: 'eTokens.eTST.withdraw', args: [0, et.eth(3)], }, + { send: 'dTokens.dTST2.repay', args: [0, et.eth(3)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + }, + + { send: 'eTokens.eTST.withdraw', args: [0, et.eth(4)], }, + { send: 'dTokens.dTST2.repay', args: [0, et.MaxUint256], }, + + // Fails again if mint item added: + + { action: 'sendBatch', batch: [ + { send: 'eTokens.eTST.deposit', args: [0, et.eth(7)], }, + { send: 'dTokens.dTST2.borrow', args: [0, et.eth(7)], }, + + { send: 'eTokens.eTST.mint', args: [0, et.eth(1)], }, + + { send: 'eTokens.eTST.withdraw', args: [0, et.eth(3)], }, + { send: 'dTokens.dTST2.repay', args: [0, et.eth(4)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + expectError: 'e/market-operation-paused', + }, + + // Succeeds again if mint item added for TST2 instead of TST: + + { action: 'sendBatch', batch: [ + { send: 'eTokens.eTST.deposit', args: [0, et.eth(7)], }, + { send: 'dTokens.dTST2.borrow', args: [0, et.eth(7)], }, + + { send: 'eTokens.eTST2.mint', args: [0, et.eth(1)], }, + + { send: 'eTokens.eTST.withdraw', args: [0, et.eth(3)], }, + { send: 'dTokens.dTST2.repay', args: [0, et.eth(4)], }, + ], + deferLiquidityChecks: [ctx.wallet.address], + }, + + // checkpoint: + + { call: 'eTokens.eTST.totalSupply', equals: [14, .001], }, + { call: 'eTokens.eTST2.totalSupply', equals: [11, .001], }, + { call: 'dTokens.dTST.totalSupply', equals: [0], }, + { call: 'dTokens.dTST2.totalSupply', equals: [4, .001], }, + + // set new asset policies: + + { action: 'setAssetPolicy', tok: 'TST', policy: { supplyCap: 10, borrowCap: 1 }, }, + { action: 'setAssetPolicy', tok: 'TST2', policy: { supplyCap: 1, borrowCap: 1, }, }, + + { action: 'sendBatch', batch: [ + // transfer TST2 deposit to sub-account 1. + // do not use 'transfer' function to test entering the market in assetPolicyClean. + // if not entered, it'd fail due to exceeded supply cap + { send: 'eTokens.eTST2.withdraw', args: [0, et.MaxUint256], }, + { send: 'eTokens.eTST2.deposit', args: [1, et.MaxUint256], }, // this exceeds the supply cap temporarily + + { send: 'dTokens.dTST2.transfer', args: [et.getSubAccount(ctx.wallet.address, 1), et.MaxUint256], }, + { send: 'eTokens.eTST.deposit', args: [0, et.eth(1)], }, // this exceeds the supply cap temporarily + + { send: 'swapHub.swap', args: [0, 1, ctx.contracts.swapHandlers.mockSwapHandler.address, + { + underlyingIn: ctx.contracts.tokens.TST.address, + underlyingOut: ctx.contracts.tokens.TST2.address, + mode: 0, + amountIn: et.eth(10), // this should send enough to swap handler not to violate the supply cap any longer + amountOut: et.eth(10), // this exceeds the supply cap temporarily + exactOutTolerance: 0, + payload: '0x', + }, + ], }, + + // this should burn TST2 debt and deposits, leaving the TST2 borrow cap no longer violated + { send: 'eTokens.eTST2.burn', args: [1, et.MaxUint256], }, + + // this should withdraw TST2 deposits, leaving the TST2 supply cap no longer violated. + // despite the total supply will be greater than the supply cap, the total supply was reduced hence the transaction succeeds + { send: 'eTokens.eTST2.withdraw', args: [1, et.MaxUint256], }, + ], + deferLiquidityChecks: [ctx.wallet.address, et.getSubAccount(ctx.wallet.address, 1)], + }, + + { call: 'eTokens.eTST.totalSupply', equals: [5, .001], }, + { call: 'eTokens.eTST2.totalSupply', equals: [10, .001], }, + { call: 'dTokens.dTST.totalSupply', equals: [0], }, + { call: 'dTokens.dTST2.totalSupply', equals: [0, .001], }, + ], +}) + + + +.test({ + desc: "supply/borrow caps, 6 decimals", + actions: ctx => [ + // Deposit prevented: + + { action: 'setAssetPolicy', tok: 'TST9', policy: { supplyCap: 1e6, }, }, + + { send: 'tokens.TST9.mint', args: [ctx.wallet.address, et.units(2e6, 6)], }, + { send: 'tokens.TST9.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { send: 'eTokens.eTST9.deposit', args: [0, et.units('1000000.000001', 6)], expectError: 'e/supply-cap-exceeded', }, + + // Raise Cap and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST9', policy: { supplyCap: 1.1e6, }, }, + { send: 'eTokens.eTST9.deposit', args: [0, et.units('1000000.000001', 6)], }, + + // Set a borrow cap + + { action: 'setAssetPolicy', tok: 'TST9', policy: { supplyCap: 1.1e6, borrowCap: 0.5e6, }, }, + { send: 'dTokens.dTST9.borrow', args: [0, et.units(0.4e6, 6)], }, + { send: 'dTokens.dTST9.borrow', args: [0, et.units(0.2e6, 6)], expectError: 'e/borrow-cap-exceeded', }, + ], +}) + + +.test({ + desc: "supply/borrow caps, 0 decimals", + actions: ctx => [ + // Deposit prevented: + + { action: 'setAssetPolicy', tok: 'TST10', policy: { supplyCap: 8000, }, }, + + { send: 'tokens.TST10.mint', args: [ctx.wallet.address, et.units(100000, 0)], }, + { send: 'tokens.TST10.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { send: 'eTokens.eTST10.deposit', args: [0, et.units(8001, 0)], expectError: 'e/supply-cap-exceeded', }, + + // Raise Cap and it succeeds: + + { action: 'setAssetPolicy', tok: 'TST10', policy: { supplyCap: 8001, }, }, + { send: 'eTokens.eTST10.deposit', args: [0, et.units(8001, 0)], }, + + // Set a borrow cap + + { action: 'setAssetPolicy', tok: 'TST10', policy: { supplyCap: 8001, borrowCap: 2000, }, }, + { send: 'dTokens.dTST10.borrow', args: [0, et.units(1999, 0)], }, + { send: 'dTokens.dTST10.borrow', args: [0, et.units(2, 0)], expectError: 'e/borrow-cap-exceeded', }, + ], +}) + + +.test({ + desc: "exchange rate conversion", + actions: ctx => [ + { call: 'eTokens.eTST.totalSupplyUnderlying', equals: [10, .001], }, + + // Transfer directly to the pool to increase exchange rate + { call: 'eTokens.eTST.convertBalanceToUnderlying', args: [et.eth(1)], equals: ['1', '0.00000001'], }, + { send: 'tokens.TST.mint', args: [ctx.contracts.euler.address, et.eth(40)], }, + { call: 'eTokens.eTST.convertBalanceToUnderlying', args: [et.eth(1)], equals: ['5', '0.00000001'], }, + + { call: 'eTokens.eTST.totalSupplyUnderlying', equals: [50, .001], }, + + // Deposit prevented: + + { send: 'tokens.TST.mint', args: [ctx.wallet.address, et.eth(10000)], }, + { action: 'setAssetPolicy', tok: 'TST', policy: { supplyCap: 100, }, }, + { send: 'eTokens.eTST.deposit', args: [0, et.eth(51)], expectError: 'e/supply-cap-exceeded', }, + { send: 'eTokens.eTST.deposit', args: [0, et.eth(50)], }, + ], +}) + + +.run(); diff --git a/test/lib/eTestLib.js b/test/lib/eTestLib.js index 4c90ae81..dbcd8dbe 100644 --- a/test/lib/eTestLib.js +++ b/test/lib/eTestLib.js @@ -97,6 +97,7 @@ const contractNames = [ 'TestModule', 'MockAggregatorProxy', 'MockStETH', + 'MockSwapHandler', // Custom Oracles @@ -407,6 +408,18 @@ async function buildContext(provider, wallets, tokenSetupName) { await (await ctx.contracts.governance.connect(ctx.wallet).setAssetConfig(underlying, config)).wait(); }; + ctx.setAssetPolicy = async (underlying, newPolicy) => { + let policy = { + supplyCap: 0, + borrowCap: 0, + pauseBitmask: 0, + loanOriginationFee: 0, + ...newPolicy, + }; + + await (await ctx.contracts.governance.connect(ctx.wallet).setAssetPolicy(underlying, policy)).wait(); + }; + // Batch transactions ctx._batchItemToContract = (item) => { @@ -1052,6 +1065,10 @@ async function deployContracts(provider, wallets, tokenSetupName, verify = null) }; if (ctx.tokenSetup.testing) { + // Deploy mock swap handler + + ctx.contracts.swapHandlers.mockSwapHandler = await (await ctx.factories.MockSwapHandler.deploy()).deployed(); + // Setup default ETokens/DTokens for (let tok of ctx.tokenSetup.testing.activated) { @@ -1494,6 +1511,9 @@ class TestSet { } else if (action.action === 'setAssetConfig') { let underlying = ctx.contracts.tokens[action.tok].address; await ctx.setAssetConfig(underlying, action.config); + } else if (action.action === 'setAssetPolicy') { + let underlying = ctx.contracts.tokens[action.tok].address; + await ctx.setAssetPolicy(underlying, action.policy); } else if (action.action === 'setTokenBalanceInStorage') { await ctx.setTokenBalanceInStorage(action.token, action.for, action.amount, action.slot); } else if (action.action === 'doUniswapSwap') {